Class: Ronin::Support::Crypto::Cert

Inherits:
OpenSSL::X509::Certificate
  • Object
show all
Defined in:
lib/ronin/support/crypto/cert.rb

Overview

Represents a X509 or TLS certificate.

Since:

  • 1.0.0

Defined Under Namespace

Classes: Name

Constant Summary collapse

ONE_YEAR =

One year in seconds

Since:

  • 1.0.0

60 * 60 * 24 * 365

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.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, signing_hash: :sha256) ⇒ Cert

Generates and signs a new certificate.

Examples:

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')

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')

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')

Parameters:

  • version (Integer) (defaults to: 2)

    The version of the encoded certificate. See RFC 5280.

  • serial (Integer) (defaults to: 0)

    The certificate serial number.

  • subject (String, Hash{Symbol => String,nil}, Name, nil) (defaults to: nil)

    The subject field for the certificate. If a Hash is given it will be passed to Ronin::Support::Crypto::Cert::Name.build.

  • not_before (Time) (defaults to: Time.now)

    Beginning time when the certificate is valid.

  • not_after (Time) (defaults to: not_before + ONE_YEAR)

    When the certificate expires and is no longer valid.

  • extensions (Hash{String => Object}) (defaults to: nil)

    Additional extensions to add to the new certificate.

  • key (Key::RSA)

    The public/private key pair used with the certificate.

  • ca_key (Key::RSA, nil) (defaults to: nil)

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

  • ca_cert (Cert, nil) (defaults to: nil)

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

  • signing_hash (Symbol) (defaults to: :sha256)

    The hashing algorithm to use to sign the new certificate.

Returns:

  • (Cert)

    The newly generated and signed certificate.

Since:

  • 1.0.0



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/ronin/support/crypto/cert.rb', line 330

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,
                  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 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) ⇒ Cert

Parses the PEM encoded certificate.

Parameters:

  • buffer (String)

    The String containing the certificate.

Returns:

  • (Cert)

    The parsed certificate.

Since:

  • 1.0.0



210
211
212
# File 'lib/ronin/support/crypto/cert.rb', line 210

def self.load(buffer)
  new(buffer)
end

.load_file(path) ⇒ Cert

Loads the certificate from the file.

Parameters:

  • path (String)

    The path to the file.

Returns:

  • (Cert)

    The loaded certificate.

Since:

  • 1.0.0



223
224
225
# File 'lib/ronin/support/crypto/cert.rb', line 223

def self.load_file(path)
  new(File.read(path))
end

.Name(name) ⇒ Cert::Name

Coerces a value into a Name object.

Parameters:

  • name (String, Hash, OpenSSL::X509::Name, Name)

    The name value to coerce.

Returns:

Since:

  • 1.0.0



174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/ronin/support/crypto/cert.rb', line 174

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

.parse(string) ⇒ Cert

Parses the PEM encoded certificate string.

Parameters:

  • string (String)

    The certificate string.

Returns:

  • (Cert)

    The parsed certificate.

Since:

  • 1.0.0



197
198
199
# File 'lib/ronin/support/crypto/cert.rb', line 197

def self.parse(string)
  new(string)
end

Instance Method Details

#common_nameString?

The subjects common name (CN) entry.

Returns:

Since:

  • 1.0.0



405
406
407
408
409
# File 'lib/ronin/support/crypto/cert.rb', line 405

def common_name
  if (subject = self.subject)
    subject.common_name
  end
end

#extension_namesArray<String>

The extension OID names.

Returns:

Since:

  • 1.0.0



416
417
418
# File 'lib/ronin/support/crypto/cert.rb', line 416

def extension_names
  extensions.map(&:oid)
end

#extension_value(oid) ⇒ String?

Gets the value for the extension with the matching OID.

Parameters:

  • oid (String)

    The OID to search for.

Returns:

  • (String, nil)

    The value of the matching extension.

Since:

  • 1.0.0



439
440
441
442
443
# File 'lib/ronin/support/crypto/cert.rb', line 439

def extension_value(oid)
  if (ext = find_extension(oid))
    ext.value
  end
end

#extensions_hashHash{String => OpenSSL::X509::Extension}

Converts the certificate's extensions into a Hash.

Returns:

  • (Hash{String => OpenSSL::X509::Extension})

    The Hash of extension OID names and extension objects.

Since:

  • 1.0.0



426
427
428
# File 'lib/ronin/support/crypto/cert.rb', line 426

def extensions_hash
  extensions.to_h { |ext| [ext.oid, ext] }
end

#issuerName?

The issuer of the certificate.

Returns:

Since:

  • 1.0.0



383
384
385
386
387
# File 'lib/ronin/support/crypto/cert.rb', line 383

def issuer
  @issuer ||= if (issuer = super)
                Cert::Name(issuer)
              end
end

#save(path, encoding: :pem) ⇒ Object

Saves the certificate to the given path.

Parameters:

  • path (String)

    The path to write the exported certificate to.

  • encoding (:pem, :der) (defaults to: :pem)

    The desired encoding of the exported key.

    • :pem - PEM encoding.
    • :der - DER encoding.

Raises:

  • (ArgumentError)

    The endcoding: value must be either :pem or :der.

Since:

  • 1.0.0



485
486
487
488
489
490
491
492
493
494
# File 'lib/ronin/support/crypto/cert.rb', line 485

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

#subjectName?

The subject of the certificate.

Returns:

Since:

  • 1.0.0



394
395
396
397
398
# File 'lib/ronin/support/crypto/cert.rb', line 394

def subject
  @subject ||= if (subject = super)
                 Cert::Name(subject)
               end
end

#subject_alt_nameString?

Retrieves the subjectAltName extension and parses it's contents.

Returns:

  • (String, nil)

    The subjectAltName value or nil if the certificate does not have the extension.

Since:

  • 1.0.0



452
453
454
# File 'lib/ronin/support/crypto/cert.rb', line 452

def subject_alt_name
  extension_value('subjectAltName')
end

#subject_alt_namesArray<String>?

Retrieves the subjectAltName extension and parses it's value.

Returns:

  • (Array<String>, nil)

    The parsed subjectAltName or nil if the certificate does not have the extension.

Since:

  • 1.0.0



463
464
465
466
467
468
469
# File 'lib/ronin/support/crypto/cert.rb', line 463

def subject_alt_names
  if (value = subject_alt_name)
    value.split(', ').map do |name|
      name.split(':',2).last
    end
  end
end