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
Source
# 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
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
Source
# 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
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')
Source
# File lib/ronin/support/crypto/cert.rb, line 226 def self.load(buffer) new(buffer) end
Parses the PEM encoded certificate.
@param [String] buffer
The String containing the certificate.
@return [Cert]
The parsed certificate.
Source
# File lib/ronin/support/crypto/cert.rb, line 239 def self.load_file(path) new(File.read(path)) end
Loads the certificate from the file.
@param [String] path
The path to the file.
@return [Cert]
The loaded certificate.
Source
# File lib/ronin/support/crypto/cert.rb, line 213 def self.parse(string) new(string) end
Parses the PEM encoded certificate string.
@param [String] string
The certificate string.
@return [Cert]
The parsed certificate.
Public Instance Methods
Source
# File lib/ronin/support/crypto/cert.rb, line 447 def common_name if (subject = self.subject) subject.common_name end end
The subjects common name (‘CN`) entry.
@return [String, nil]
Source
# File lib/ronin/support/crypto/cert.rb, line 458 def extension_names extensions.map(&:oid) end
The extension OID names.
@return [Array<String>]
Source
# File lib/ronin/support/crypto/cert.rb, line 481 def extension_value(oid) if (ext = find_extension(oid)) ext.value end end
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.
Source
# File lib/ronin/support/crypto/cert.rb, line 468 def extensions_hash extensions.to_h { |ext| [ext.oid, ext] } end
Converts the certificate’s extensions into a Hash.
@return [Hash{String => OpenSSL::X509::Extension}]
The Hash of extension OID names and extension objects.
Source
# File lib/ronin/support/crypto/cert.rb, line 425 def issuer @issuer ||= if (issuer = super) Cert::Name(issuer) end end
The issuer of the certificate.
@return [Name, nil]
Source
# 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
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`.
Source
# File lib/ronin/support/crypto/cert.rb, line 436 def subject @subject ||= if (subject = super) Cert::Name(subject) end end
The subject of the certificate.
@return [Name, nil]
Source
# File lib/ronin/support/crypto/cert.rb, line 494 def subject_alt_name extension_value('subjectAltName') end
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.
Source
# 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
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.