1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
|
require "flores/namespace"
module Flores::PKI
# 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)
class CertificateSigningRequest
# raised when an invalid signing configuration is given
class InvalidRequest < StandardError; end
# raised when invalid data is present in a certificate request
class InvalidData < StandardError; end
# raised when an invalid subject (format, or whatever) is given in a certificate request
class InvalidSubject < InvalidData; end
# raised when an invalid time value is given for a certificate request
class InvalidTime < InvalidData; end
def initialize
self.serial = Flores::PKI.random_serial
self.digest_method = default_digest_method
end
private
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
def subject=(value)
@subject = validate_subject(value)
end
attr_reader :subject
def subject_alternates=(values)
@subject_alternates = values
end
attr_reader :subject_alternates
def public_key=(value)
@public_key = validate_public_key(value)
end
def validate_public_key(value)
raise InvalidData, "public key must be a OpenSSL::PKey::PKey" unless value.is_a? OpenSSL::PKey::PKey
value
end
attr_reader :public_key
def start_time=(value)
@start_time = validate_time(value)
end
attr_reader :start_time
def expire_time=(value)
@expire_time = validate_time(value)
end
attr_reader :expire_time
def validate_time(value)
raise InvalidTime, "#{value.inspect} (class #{value.class.name})" unless value.is_a?(Time)
value
end
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
def default_digest_method
OpenSSL::Digest::SHA256.new
end
def self_signed?
@signing_certificate.nil?
end
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
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
# Set the certificate which is going to be signing this request.
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
attr_reader :signing_certificate
attr_reader :signing_key
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
def want_signature_ability=(value)
raise InvalidData, "want_signature_ability must be a boolean" unless value == true || value == false
@want_signature_ability = value
end
def want_signature_ability?
@want_signature_ability == true
end
attr_reader :digest_method
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
attr_reader :serial
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
public(:serial, :serial=)
public(:subject, :subject=)
public(:subject_alternates, :subject_alternates=)
public(:public_key, :public_key=)
public(:start_time, :start_time=)
public(:expire_time, :expire_time=)
public(:digest_method, :digest_method=)
public(:want_signature_ability?, :want_signature_ability=)
public(:signing_key, :signing_key=)
public(:signing_certificate, :signing_certificate=)
public(:create)
end # class CertificateSigningRequest
end
|