class Ronin::Support::Crypto::Cert

Represents a X509 or TLS certificate.

@api public

@since 1.0.0

Constants

ONE_YEAR

One year in seconds

Public Class Methods

Name(name) click to toggle source

Coerces a value into a {Name} object.

@param [String, Hash, OpenSSL::X509::Name, Name] name

The name value to coerce.

@return [Cert::Name]

The name object.

@api semipublic

# File lib/ronin/support/crypto/cert.rb, line 190
def self.Name(name)
  case name
  when String then Name.parse(name)
  when Hash   then Name.build(**name)
  when Name   then name
  when OpenSSL::X509::Name
    new_name = Name.allocate
    new_name.send(:initialize_copy,name)
    new_name
  else
    raise(ArgumentError,"value must be either a String, Hash, or a OpenSSL::X509::Name object: #{name.inspect}")
  end
end
generate(version: 2, serial: 0, not_before: Time.now, not_after: not_before + ONE_YEAR, subject: nil, extensions: nil, key: , ca_cert: nil, ca_key: nil, ca: false, subject_alt_names: nil, signing_hash: :sha256) click to toggle source

Generates and signs a new certificate.

@param [Integer] version

The version of the encoded certificate.
See [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280).

@param [Integer] serial

The certificate serial number.

@param [String, Hash{Symbol => String,nil}, Name, nil] subject

The subject field for the certificate. If a `Hash` is given it will
be passed to {Name.build}.

@param [Time] not_before

Beginning time when the certificate is valid.

@param [Time] not_after

When the certificate expires and is no longer valid.

@param [Hash{String => Object}] extensions

Additional extensions to add to the new certificate.

@param [Key::RSA] key

The public/private key pair used with the certificate.

@param [Key::RSA, nil] ca_key

The optional Certificate Authority (CA) key to use to sign the new
certificate.

@param [Cert, nil] ca_cert

The optional Certificate Authority (CA) certificate to attach to the
new certificate.

@param [Boolean] ca

Indicates whether to add the basicConstraints extension.

@param [Array<String>, nil] subject_alt_names

List of subject alt names to add into subjectAltName extension.

@param [Symbol] signing_hash

The hashing algorithm to use to sign the new certificate.

@return [Cert]

The newly generated and signed certificate.

@example Generate a self-signed certificate for ‘localhost`:

key  = Ronin::Support::Crypto::Key::RSA.random
cert = Ronin::Support::Crypto::Cert.generate(
  key: key,
  subject: {
    common_name:         'localhost',
    organization:        'Test Co.',
    organizational_unit: 'Test Dept',
    locality:            'Test City',
    state:               'XX',
    country:             'US'
  },
  extensions: {
    'subjectAltName' => 'DNS: localhost, IP: 127.0.0.1'
  }
)
key.save('cert.key')
cert.save('cert.pem')

@example Generate a CA certificate:

ca_key  = Ronin::Support::Crypto::Key::RSA.random
ca_cert = Ronin::Support::Crypto::Cert.generate(
  key: ca_key,
  subject: {
    common_name:         'Test CA',
    organization:        'Test CA, Inc.',
    organizational_unit: 'Test Dept',
    locality:            'Test City',
    state:               'XX',
    country:             'US'
  },
  extensions: {
    'basicConstraints' => ['CA:TRUE', true]
  }
)
key.save('ca.key')
cert.save('ca.pem')

@example Generate a sub-certificate from a CA certificate:

key  = Ronin::Support::Crypto::Key::RSA.random
cert = Ronin::Support::Crypto::Cert.generate(
  key:     key,
  ca_key:  ca_key,
  ca_cert: ca_cert,
  subject: {
    common_name:         'test.com',
    organization:        'Test Co.',
    organizational_unit: 'Test Dept',
    locality:            'Test City',
    state:               'XX',
    country:             'US'
  },
  extensions: {
    'subjectAltName'   => 'DNS: *.test.com',
    'basicConstraints' => ['CA:FALSE', true]
  }
)
key.save('cert.key')
cert.save('cert.pem')
# File lib/ronin/support/crypto/cert.rb, line 352
def self.generate(version:    2,
                  serial:     0,
                  not_before: Time.now,
                  not_after:  not_before + ONE_YEAR,
                  subject:    nil,
                  extensions: nil,
                  # signing arguments
                  key: ,
                  ca_cert: nil,
                  ca_key:  nil,
                  ca:      false,
                  subject_alt_names: nil,
                  signing_hash: :sha256)
  cert = new

  cert.version = version
  cert.serial  = if ca_cert then ca_cert.serial + 1
                 else            serial
                 end

  cert.not_before = not_before
  cert.not_after  = not_after
  cert.public_key = case key
                    when OpenSSL::PKey::EC then key
                    else                        key.public_key
                    end
  cert.subject    = Name(subject) if subject
  cert.issuer     = if ca_cert then ca_cert.subject
                    else            cert.subject
                    end

  if subject_alt_names
    subject_alt_name = subject_alt_names.map { |alt_name|
      if alt_name.match?(Network::IP::REGEX)
        "IP:#{alt_name}"
      else
        "DNS:#{alt_name}"
      end
    }.join(', ')

    extensions ||= {}
    extensions   = extensions.merge('subjectAltName' => subject_alt_name)
  end

  if ca
    extensions ||= {}
    extensions   = extensions.merge('basicConstraints' => ['CA:TRUE', true])
  end

  if extensions
    extension_factory = OpenSSL::X509::ExtensionFactory.new

    extension_factory.subject_certificate = cert
    extension_factory.issuer_certificate  = ca_cert || cert

    extensions.each do |name,(value,critical)|
      ext = extension_factory.create_extension(name,value,critical)
      cert.add_extension(ext)
    end
  end

  signing_key    = ca_key || key
  signing_digest = OpenSSL::Digest.const_get(signing_hash.upcase).new

  cert.sign(signing_key,signing_digest)
  return cert
end
load(buffer) click to toggle source

Parses the PEM encoded certificate.

@param [String] buffer

The String containing the certificate.

@return [Cert]

The parsed certificate.
# File lib/ronin/support/crypto/cert.rb, line 226
def self.load(buffer)
  new(buffer)
end
load_file(path) click to toggle source

Loads the certificate from the file.

@param [String] path

The path to the file.

@return [Cert]

The loaded certificate.
# File lib/ronin/support/crypto/cert.rb, line 239
def self.load_file(path)
  new(File.read(path))
end
parse(string) click to toggle source

Parses the PEM encoded certificate string.

@param [String] string

The certificate string.

@return [Cert]

The parsed certificate.
# File lib/ronin/support/crypto/cert.rb, line 213
def self.parse(string)
  new(string)
end

Public Instance Methods

common_name() click to toggle source

The subjects common name (‘CN`) entry.

@return [String, nil]

# File lib/ronin/support/crypto/cert.rb, line 447
def common_name
  if (subject = self.subject)
    subject.common_name
  end
end
extension_names() click to toggle source

The extension OID names.

@return [Array<String>]

# File lib/ronin/support/crypto/cert.rb, line 458
def extension_names
  extensions.map(&:oid)
end
extension_value(oid) click to toggle source

Gets the value for the extension with the matching OID.

@param [String] oid

The OID to search for.

@return [String, nil]

The value of the matching extension.
# File lib/ronin/support/crypto/cert.rb, line 481
def extension_value(oid)
  if (ext = find_extension(oid))
    ext.value
  end
end
extensions_hash() click to toggle source

Converts the certificate’s extensions into a Hash.

@return [Hash{String => OpenSSL::X509::Extension}]

The Hash of extension OID names and extension objects.
# File lib/ronin/support/crypto/cert.rb, line 468
def extensions_hash
  extensions.to_h { |ext| [ext.oid, ext] }
end
issuer() click to toggle source

The issuer of the certificate.

@return [Name, nil]

Calls superclass method
# File lib/ronin/support/crypto/cert.rb, line 425
def issuer
  @issuer ||= if (issuer = super)
                Cert::Name(issuer)
              end
end
save(path, encoding: :pem) click to toggle source

Saves the certificate to the given path.

@param [String] path

The path to write the exported certificate to.

@param [:pem, :der] encoding

The desired encoding of the exported key.
* `:pem` - PEM encoding.
* `:der` - DER encoding.

@raise [ArgumentError]

The `endcoding:` value must be either `:pem` or `:der`.
# File lib/ronin/support/crypto/cert.rb, line 527
def save(path, encoding: :pem)
  exported = case encoding
             when :pem then to_pem
             when :der then to_der
             else
               raise(ArgumentError,"encoding: keyword argument (#{encoding.inspect}) must be either :pem or :der")
             end

  File.write(path,exported)
end
subject() click to toggle source

The subject of the certificate.

@return [Name, nil]

Calls superclass method
# File lib/ronin/support/crypto/cert.rb, line 436
def subject
  @subject ||= if (subject = super)
                 Cert::Name(subject)
               end
end
subject_alt_name() click to toggle source

Retrieves the ‘subjectAltName` extension and parses it’s contents.

@return [String, nil]

The `subjectAltName` value or `nil` if the certificate does not
have the extension.
# File lib/ronin/support/crypto/cert.rb, line 494
def subject_alt_name
  extension_value('subjectAltName')
end
subject_alt_names() click to toggle source

Retrieves the ‘subjectAltName` extension and parses it’s value.

@return [Array<String>, nil]

The parsed `subjectAltName` or `nil` if the certificate does not
have the extension.
# File lib/ronin/support/crypto/cert.rb, line 505
def subject_alt_names
  if (value = subject_alt_name)
    value.split(', ').map do |name|
      name.split(':',2).last
    end
  end
end