class Flores::PKI::CertificateSigningRequest

A certificate signing request.

From here, you can configure a certificate to be created based on your desired configuration.

Example making a root CA:

key = OpenSSL::PKey::RSA.generate(4096, 65537)
csr = Flores::PKI::CertificateSigningRequest.new
csr.subject = "OU=Fancy Pants Inc."
certificate = csr.create_root(key)

Example making an intermediate CA:

root_key = OpenSSL::PKey::RSA.generate(4096, 65537)
root_csr = Flores::PKI::CertificateSigningRequest.new
root_csr.subject = "OU=Fancy Pants Inc."
root_csr.public_key = root_key.public
root_certificate = csr.create_root(root_key)

intermediate_key = OpenSSL::PKey::RSA.generate(4096, 65537)
intermediate_csr = Flores::PKI::CertificateSigningRequest.new
intermediate_csr.public_key = intermediate_key.public
intermediate_csr.subject = "OU=Fancy Pants Inc. Intermediate 1"
intermediate_certificate = csr.create_intermediate(root_certificate, root_key)

Attributes

digest_method[R]
expire_time[R]
public_key[R]
serial[R]
signing_certificate[R]
signing_key[R]
start_time[R]
subject[R]
subject_alternates[R]

Public Class Methods

new() click to toggle source
# File lib/flores/pki/csr.rb, line 42
def initialize
  self.serial = Flores::PKI.random_serial
  self.digest_method = default_digest_method
end

Public Instance Methods

create() click to toggle source
# File lib/flores/pki/csr.rb, line 136
def create
  validate!
  extensions = OpenSSL::X509::ExtensionFactory.new
  extensions.subject_certificate = certificate
  extensions.issuer_certificate = self_signed? ? certificate : signing_certificate

  certificate.issuer = extensions.issuer_certificate.subject
  certificate.add_extension(extensions.create_extension("subjectKeyIdentifier", "hash", false))

  # RFC 5280 4.2.1.1. Authority Key Identifier
  # This is "who signed this key"
  certificate.add_extension(extensions.create_extension("authorityKeyIdentifier", "keyid:always", false))
  #certificate.add_extension(extensions.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always", false))

  if want_signature_ability?
    # Create a CA.
    certificate.add_extension(extensions.create_extension("basicConstraints", "CA:TRUE", true))
    # Rough googling seems to indicate at least keyCertSign is required for CA and intermediate certs.
    certificate.add_extension(extensions.create_extension("keyUsage", "keyCertSign, cRLSign, digitalSignature", true))
  else
    # Create a client+server certificate
    #
    # It feels weird to create a certificate that's valid as both server and client, but a brief inspection of major
    # web properties (apple.com, google.com, yahoo.com, github.com, fastly.com, mozilla.com, amazon.com) reveals that
    # major web properties have certificates with both clientAuth and serverAuth extended key usages. Further,
    # these major server certificates all have digitalSignature and keyEncipherment for key usage.
    #
    # Here's the command I used to check this:
    #    echo mozilla.com apple.com github.com google.com yahoo.com fastly.com elastic.co amazon.com \
    #    | xargs -n1 sh -c 'openssl s_client -connect $1:443 \
    #    | sed -ne "/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p" \
    #    | openssl x509 -text -noout | sed -ne "/X509v3 extensions/,/Signature Algorithm/p" | sed -e "s/^/$1 /"' - \
    #    | grep -A2 'Key Usage'
    certificate.add_extension(extensions.create_extension("keyUsage", "digitalSignature, keyEncipherment", true))
    certificate.add_extension(extensions.create_extension("extendedKeyUsage", "clientAuth, serverAuth", false))
  end

  if @subject_alternates
    certificate.add_extension(extensions.create_extension("subjectAltName", @subject_alternates.join(",")))
  end
    
  certificate.serial = OpenSSL::BN.new(serial)
  certificate.sign(signing_key, digest_method)
  certificate
end
digest_method=(value) click to toggle source
# File lib/flores/pki/csr.rb, line 205
def digest_method=(value)
  raise InvalidData, "digest_method must be a OpenSSL::Digest (or a subclass)" unless value.is_a?(OpenSSL::Digest)
  @digest_method = value
end
expire_time=(value) click to toggle source
# File lib/flores/pki/csr.rb, line 87
def expire_time=(value)
  @expire_time = validate_time(value)
end
public_key=(value) click to toggle source
# File lib/flores/pki/csr.rb, line 70
def public_key=(value)
  @public_key = validate_public_key(value)
end
serial=(value) click to toggle source
# File lib/flores/pki/csr.rb, line 211
def serial=(value)
  begin
    Integer(value)
  rescue
    raise InvalidData, "Invalid serial value. Must be a number (or a String containing only nubers)"
  end
  @serial = value
end
signing_certificate=(certificate) click to toggle source

Set the certificate which is going to be signing this request.

# File lib/flores/pki/csr.rb, line 183
def signing_certificate=(certificate)
  raise InvalidData, "signing_certificate must be an OpenSSL::X509::Certificate" unless certificate.is_a?(OpenSSL::X509::Certificate)
  @signing_certificate = certificate
end
signing_key=(private_key) click to toggle source
# File lib/flores/pki/csr.rb, line 190
def signing_key=(private_key)
  raise InvalidData, "signing_key must be an OpenSSL::PKey::PKey (or a subclass)" unless private_key.is_a?(OpenSSL::PKey::PKey)
  @signing_key = private_key
end
start_time=(value) click to toggle source
# File lib/flores/pki/csr.rb, line 81
def start_time=(value)
  @start_time = validate_time(value)
end
subject=(value) click to toggle source
# File lib/flores/pki/csr.rb, line 58
def subject=(value)
  @subject = validate_subject(value)
end
subject_alternates=(values) click to toggle source
# File lib/flores/pki/csr.rb, line 64
def subject_alternates=(values)
  @subject_alternates = values
end
want_signature_ability=(value) click to toggle source
# File lib/flores/pki/csr.rb, line 195
def want_signature_ability=(value)
  raise InvalidData, "want_signature_ability must be a boolean" unless value == true || value == false
  @want_signature_ability = value
end
want_signature_ability?() click to toggle source
# File lib/flores/pki/csr.rb, line 200
def want_signature_ability?
  @want_signature_ability == true
end

Private Instance Methods

certificate() click to toggle source
# File lib/flores/pki/csr.rb, line 98
def certificate
  return @certificate  if @certificate
  @certificate = OpenSSL::X509::Certificate.new

  # RFC5280
  # > 4.1.2.1.  Version
  # > version MUST be 3 (value is 2).
  #
  # Version value of '2' means a v3 certificate.
  @certificate.version = 2

  @certificate.subject = subject
  @certificate.not_before = start_time
  @certificate.not_after = expire_time
  @certificate.public_key = public_key
  @certificate
end
default_digest_method() click to toggle source
# File lib/flores/pki/csr.rb, line 116
def default_digest_method
  OpenSSL::Digest::SHA256.new
end
self_signed?() click to toggle source
# File lib/flores/pki/csr.rb, line 120
def self_signed?
  @signing_certificate.nil?
end
validate!() click to toggle source
# File lib/flores/pki/csr.rb, line 124
def validate!
  if self_signed?
    if @signing_key.nil?
      raise InvalidRequest, "No signing_key given. Cannot sign key."
    end
  elsif @signing_certificate.nil? && @signing_key
    raise InvalidRequest, "signing_key given, but no signing_certificate is set"
  elsif @signing_certificate && @signing_key.nil?
    raise InvalidRequest, "signing_certificate given, but no signing_key is set"
  end
end
validate_public_key(value) click to toggle source
# File lib/flores/pki/csr.rb, line 74
def validate_public_key(value)
  raise InvalidData, "public key must be a OpenSSL::PKey::PKey" unless value.is_a? OpenSSL::PKey::PKey
  value
end
validate_subject(value) click to toggle source
# File lib/flores/pki/csr.rb, line 49
def validate_subject(value)
  OpenSSL::X509::Name.parse(value)
rescue OpenSSL::X509::NameError => e
  raise InvalidSubject, "Invalid subject '#{value}'. (#{e})"
rescue TypeError => e
  # Bug(?) in MRI 2.1.6(?)
  raise InvalidSubject, "Invalid subject '#{value}'. (#{e})"
end
validate_time(value) click to toggle source
# File lib/flores/pki/csr.rb, line 93
def validate_time(value)
  raise InvalidTime, "#{value.inspect} (class #{value.class.name})" unless value.is_a?(Time)
  value
end