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, ca: false, subject_alt_names: 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.

  • ca (Boolean) (defaults to: false)

    Indicates whether to add the basicConstraints extension.

  • subject_alt_names (Array<String>, nil) (defaults to: nil)

    List of subject alt names to add into subjectAltName extension.

  • 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



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
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# 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) ⇒ Cert

Parses the PEM encoded certificate.

Parameters:

  • buffer (String)

    The String containing the certificate.

Returns:

  • (Cert)

    The parsed certificate.

Since:

  • 1.0.0



226
227
228
# File 'lib/ronin/support/crypto/cert.rb', line 226

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



239
240
241
# File 'lib/ronin/support/crypto/cert.rb', line 239

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



190
191
192
193
194
195
196
197
198
199
200
201
202
# 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

.parse(string) ⇒ Cert

Parses the PEM encoded certificate string.

Parameters:

  • string (String)

    The certificate string.

Returns:

  • (Cert)

    The parsed certificate.

Since:

  • 1.0.0



213
214
215
# File 'lib/ronin/support/crypto/cert.rb', line 213

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

Instance Method Details

#common_nameString?

The subjects common name (CN) entry.

Returns:

Since:

  • 1.0.0



447
448
449
450
451
# File 'lib/ronin/support/crypto/cert.rb', line 447

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

#extension_namesArray<String>

The extension OID names.

Returns:

Since:

  • 1.0.0



458
459
460
# File 'lib/ronin/support/crypto/cert.rb', line 458

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



481
482
483
484
485
# File 'lib/ronin/support/crypto/cert.rb', line 481

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



468
469
470
# File 'lib/ronin/support/crypto/cert.rb', line 468

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

#issuerName?

The issuer of the certificate.

Returns:

Since:

  • 1.0.0



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

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



527
528
529
530
531
532
533
534
535
536
# 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

#subjectName?

The subject of the certificate.

Returns:

Since:

  • 1.0.0



436
437
438
439
440
# File 'lib/ronin/support/crypto/cert.rb', line 436

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



494
495
496
# File 'lib/ronin/support/crypto/cert.rb', line 494

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



505
506
507
508
509
510
511
# 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