From a6b4ff1c21915475655a4a28c163904687d1035e Mon Sep 17 00:00:00 2001 From: elijah Date: Tue, 21 Oct 2014 17:32:28 -0700 Subject: fixed `leap cert csr` to add correct "Requested Extensions" attribute on the CSR. --- leap_cli.gemspec | 1 + lib/leap_cli/commands/ca.rb | 43 +- .../certificate_authority.gemspec | 5 +- .../lib/certificate_authority/certificate.rb | 118 ++++-- .../certificate_authority/distinguished_name.rb | 5 + .../lib/certificate_authority/extensions.rb | 447 +++++++++++++++++++-- .../lib/certificate_authority/key_material.rb | 34 -- .../lib/certificate_authority/serial_number.rb | 4 + .../lib/certificate_authority/signing_request.rb | 37 +- 9 files changed, 583 insertions(+), 111 deletions(-) diff --git a/leap_cli.gemspec b/leap_cli.gemspec index d0b9a99..cbf0674 100644 --- a/leap_cli.gemspec +++ b/leap_cli.gemspec @@ -78,4 +78,5 @@ spec = Gem::Specification.new do |s| # certificate_authority s.add_runtime_dependency("activemodel", ">= 3.0.6") + s.add_runtime_dependency("activesupport", ">= 3.0.6") end diff --git a/lib/leap_cli/commands/ca.rb b/lib/leap_cli/commands/ca.rb index ef8cd71..ea4c8a8 100644 --- a/lib/leap_cli/commands/ca.rb +++ b/lib/leap_cli/commands/ca.rb @@ -116,7 +116,6 @@ module LeapCli; module Commands # CSR dn = CertificateAuthority::DistinguishedName.new - csr = CertificateAuthority::SigningRequest.new dn.common_name = domain dn.organization = options[:organization] || provider.name[provider.default_language] dn.ou = options[:organizational_unit] # optional @@ -127,9 +126,7 @@ module LeapCli; module Commands digest = options[:digest] || server_certificates.digest log :generating, "CSR with #{digest} digest and #{print_dn(dn)}" do - csr.distinguished_name = dn - csr.key_material = keypair - csr.digest = digest + csr = create_csr(dn, keypair, digest) request = csr.to_x509_csr write_file! [:commercial_csr, domain], csr.to_pem end @@ -289,6 +286,44 @@ module LeapCli; module Commands yield cert.key_material.private_key.to_pem, cert.to_pem end + # + # creates a CSR and returns it. + # with the correct extReq attribute so that the CA + # doens't generate certs with extensions we don't want. + # + def create_csr(dn, keypair, digest) + csr = CertificateAuthority::SigningRequest.new + csr.distinguished_name = dn + csr.key_material = keypair + csr.digest = digest + + # define extensions manually (library doesn't support setting these on CSRs) + extensions = [] + extensions << CertificateAuthority::Extensions::BasicConstraints.new.tap {|basic| + basic.ca = false + } + extensions << CertificateAuthority::Extensions::KeyUsage.new.tap {|keyusage| + keyusage.usage = ["digitalSignature", "nonRepudiation"] + } + extensions << CertificateAuthority::Extensions::ExtendedKeyUsage.new.tap {|extkeyusage| + extkeyusage.usage = [ "serverAuth"] + } + + # convert extensions to attribute 'extReq' + # aka "Requested Extensions" + factory = OpenSSL::X509::ExtensionFactory.new + attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence( + extensions.map{|e| factory.create_ext(e.openssl_identifier, e.to_s, e.critical)} + )]) + attrs = [ + OpenSSL::X509::Attribute.new("extReq", attrval), + OpenSSL::X509::Attribute.new("msExtReq", attrval) + ] + csr.attributes = attrs + + return csr + end + def ca_root @ca_root ||= begin load_certificate_file(:ca_cert, :ca_key) diff --git a/vendor/certificate_authority/certificate_authority.gemspec b/vendor/certificate_authority/certificate_authority.gemspec index be8cd91..b7e8676 100644 --- a/vendor/certificate_authority/certificate_authority.gemspec +++ b/vendor/certificate_authority/certificate_authority.gemspec @@ -61,7 +61,7 @@ Gem::Specification.new do |s| "spec/units/units_helper.rb", "spec/units/working_with_openssl_spec.rb" ] - s.homepage = "http://github.com/cchandler/certificate_authority" + s.homepage = "https://github.com/cchandler/certificate_authority" s.licenses = ["MIT"] s.require_paths = ["lib"] s.rubygems_version = "1.8.15" @@ -72,15 +72,18 @@ Gem::Specification.new do |s| if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_runtime_dependency(%q, [">= 3.0.6"]) + s.add_runtime_dependency(%q, [">= 3.0.6"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 1.5.2"]) else s.add_dependency(%q, [">= 3.0.6"]) + s.add_dependency(%q, [">= 3.0.6"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 1.5.2"]) end else s.add_dependency(%q, [">= 3.0.6"]) + s.add_dependency(%q, [">= 3.0.6"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 1.5.2"]) end diff --git a/vendor/certificate_authority/lib/certificate_authority/certificate.rb b/vendor/certificate_authority/lib/certificate_authority/certificate.rb index ca8bc7c..f096c5a 100644 --- a/vendor/certificate_authority/lib/certificate_authority/certificate.rb +++ b/vendor/certificate_authority/lib/certificate_authority/certificate.rb @@ -1,3 +1,5 @@ +require 'active_support/all' + module CertificateAuthority class Certificate include ActiveModel::Validations @@ -32,8 +34,8 @@ module CertificateAuthority self.distinguished_name = DistinguishedName.new self.serial_number = SerialNumber.new self.key_material = MemoryKeyMaterial.new - self.not_before = Time.now - self.not_after = Time.now + 60 * 60 * 24 * 365 #One year + self.not_before = Time.now.change(:min => 0).utc + self.not_after = Time.now.change(:min => 0).utc + 1.year self.parent = self self.extensions = load_extensions() @@ -41,12 +43,31 @@ module CertificateAuthority end +=begin + def self.from_openssl openssl_cert + unless openssl_cert.is_a? OpenSSL::X509::Certificate + raise "Can only construct from an OpenSSL::X509::Certificate" + end + + certificate = Certificate.new + # Only subject, key_material, and body are used for signing + certificate.distinguished_name = DistinguishedName.from_openssl openssl_cert.subject + certificate.key_material.public_key = openssl_cert.public_key + certificate.openssl_body = openssl_cert + certificate.serial_number.number = openssl_cert.serial.to_i + certificate.not_before = openssl_cert.not_before + certificate.not_after = openssl_cert.not_after + # TODO extensions + certificate + end +=end + def sign!(signing_profile={}) raise "Invalid certificate #{self.errors.full_messages}" unless valid? merge_profile_with_extensions(signing_profile) openssl_cert = OpenSSL::X509::Certificate.new - openssl_cert.version = 2 + openssl_cert.version = 2 openssl_cert.not_before = self.not_before openssl_cert.not_after = self.not_after openssl_cert.public_key = self.key_material.public_key @@ -58,7 +79,6 @@ module CertificateAuthority require 'tempfile' t = Tempfile.new("bullshit_conf") - # t = File.new("/tmp/openssl.cnf") ## The config requires a file even though we won't use it openssl_config = OpenSSL::Config.new(t.path) @@ -85,7 +105,7 @@ module CertificateAuthority self.extensions.keys.sort{|a,b| b<=>a}.each do |k| e = extensions[k] next if e.to_s.nil? or e.to_s == "" ## If the extension returns an empty string we won't include it - ext = factory.create_ext(e.openssl_identifier, e.to_s) + ext = factory.create_ext(e.openssl_identifier, e.to_s, e.critical) openssl_cert.add_extension(ext) end @@ -94,9 +114,10 @@ module CertificateAuthority else digest = OpenSSL::Digest::Digest.new(signing_profile["digest"]) end - self.openssl_body = openssl_cert.sign(parent.key_material.private_key,digest) - t.close! if t.is_a?(Tempfile)# We can get rid of the ridiculous temp file - self.openssl_body + + self.openssl_body = openssl_cert.sign(parent.key_material.private_key, digest) + ensure + t.close! if t # We can get rid of the ridiculous temp file end def is_signing_entity? @@ -116,6 +137,34 @@ module CertificateAuthority self.openssl_body.to_pem end + def to_csr + csr = SigningRequest.new + csr.distinguished_name = self.distinguished_name + csr.key_material = self.key_material + factory = OpenSSL::X509::ExtensionFactory.new + exts = [] + self.extensions.keys.each do |k| + ## Don't copy over key identifiers for CSRs + next if k == "subjectKeyIdentifier" || k == "authorityKeyIdentifier" + e = extensions[k] + ## If the extension returns an empty string we won't include it + next if e.to_s.nil? or e.to_s == "" + exts << factory.create_ext(e.openssl_identifier, e.to_s, e.critical) + end + attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(exts)]) + attrs = [ + OpenSSL::X509::Attribute.new("extReq", attrval), + OpenSSL::X509::Attribute.new("msExtReq", attrval) + ] + csr.attributes = attrs + csr + end + + def self.from_x509_cert(raw_cert) + openssl_cert = OpenSSL::X509::Certificate.new(raw_cert) + Certificate.from_openssl(openssl_cert) + end + def is_root_entity? self.parent == self && is_signing_entity? end @@ -134,6 +183,16 @@ module CertificateAuthority items = signing_config[k] items.keys.each do |profile_item_key| if extension.respond_to?("#{profile_item_key}=".to_sym) + if k == 'subjectAltName' && profile_item_key == 'emails' + items[profile_item_key].map do |email| + if email == 'email:copy' + fail "no email address provided for subject: #{subject.to_x509_name}" unless subject.email_address + "email:#{subject.email_address}" + else + email + end + end + end extension.send("#{profile_item_key}=".to_sym, items[profile_item_key] ) else p "Tried applying '#{profile_item_key}' to #{extension.class} but it doesn't respond!" @@ -142,30 +201,25 @@ module CertificateAuthority end end + # Enumeration of the extensions. Not the worst option since + # the likelihood of these needing to be updated is low at best. + EXTENSIONS = [ + CertificateAuthority::Extensions::BasicConstraints, + CertificateAuthority::Extensions::CrlDistributionPoints, + CertificateAuthority::Extensions::SubjectKeyIdentifier, + CertificateAuthority::Extensions::AuthorityKeyIdentifier, + CertificateAuthority::Extensions::AuthorityInfoAccess, + CertificateAuthority::Extensions::KeyUsage, + CertificateAuthority::Extensions::ExtendedKeyUsage, + CertificateAuthority::Extensions::SubjectAlternativeName, + CertificateAuthority::Extensions::CertificatePolicies + ] + def load_extensions extension_hash = {} - temp_extensions = [] - basic_constraints = CertificateAuthority::Extensions::BasicConstraints.new - temp_extensions << basic_constraints - crl_distribution_points = CertificateAuthority::Extensions::CrlDistributionPoints.new - temp_extensions << crl_distribution_points - subject_key_identifier = CertificateAuthority::Extensions::SubjectKeyIdentifier.new - temp_extensions << subject_key_identifier - authority_key_identifier = CertificateAuthority::Extensions::AuthorityKeyIdentifier.new - temp_extensions << authority_key_identifier - authority_info_access = CertificateAuthority::Extensions::AuthorityInfoAccess.new - temp_extensions << authority_info_access - key_usage = CertificateAuthority::Extensions::KeyUsage.new - temp_extensions << key_usage - extended_key_usage = CertificateAuthority::Extensions::ExtendedKeyUsage.new - temp_extensions << extended_key_usage - subject_alternative_name = CertificateAuthority::Extensions::SubjectAlternativeName.new - temp_extensions << subject_alternative_name - certificate_policies = CertificateAuthority::Extensions::CertificatePolicies.new - temp_extensions << certificate_policies - - temp_extensions.each do |extension| + EXTENSIONS.each do |klass| + extension = klass.new extension_hash[extension.openssl_identifier] = extension end @@ -192,7 +246,11 @@ module CertificateAuthority certificate.serial_number.number = openssl_cert.serial.to_i certificate.not_before = openssl_cert.not_before certificate.not_after = openssl_cert.not_after - # TODO extensions + EXTENSIONS.each do |klass| + _,v,c = (openssl_cert.extensions.detect { |e| e.to_a.first == klass::OPENSSL_IDENTIFIER } || []).to_a + certificate.extensions[klass::OPENSSL_IDENTIFIER] = klass.parse(v, c) if v + end + certificate end diff --git a/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb b/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb index 165fe29..32d9c1e 100644 --- a/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb +++ b/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb @@ -32,11 +32,16 @@ module CertificateAuthority alias :emailAddress :email_address alias :emailAddress= :email_address= + attr_accessor :serial_number + alias :serialNumber :serial_number + alias :serialNumber= :serial_number= + def to_x509_name raise "Invalid Distinguished Name" unless valid? # NB: the capitalization in the strings counts name = OpenSSL::X509::Name.new + name.add_entry("serialNumber", serial_number) unless serial_number.blank? name.add_entry("C", country) unless country.blank? name.add_entry("ST", state) unless state.blank? name.add_entry("L", locality) unless locality.blank? diff --git a/vendor/certificate_authority/lib/certificate_authority/extensions.rb b/vendor/certificate_authority/lib/certificate_authority/extensions.rb index e5a8e85..89de032 100644 --- a/vendor/certificate_authority/lib/certificate_authority/extensions.rb +++ b/vendor/certificate_authority/lib/certificate_authority/extensions.rb @@ -5,6 +5,10 @@ module CertificateAuthority raise "Implementation required" end + def self.parse(value, critical) + raise "Implementation required" + end + def config_extensions {} end @@ -12,21 +16,40 @@ module CertificateAuthority def openssl_identifier raise "Implementation required" end + + def ==(value) + raise "Implementation required" + end end + # Specifies whether an X.509v3 certificate can act as a CA, signing other + # certificates to be verified. If set, a path length constraint can also be + # specified. + # Reference: Section 4.2.1.10 of RFC3280 + # http://tools.ietf.org/html/rfc3280#section-4.2.1.10 class BasicConstraints + OPENSSL_IDENTIFIER = "basicConstraints" + include ExtensionAPI include ActiveModel::Validations + + attr_accessor :critical attr_accessor :ca attr_accessor :path_len + validates :critical, :inclusion => [true,false] validates :ca, :inclusion => [true,false] def initialize - self.ca = false + @critical = false + @ca = false + end + + def openssl_identifier + OPENSSL_IDENTIFIER end def is_ca? - self.ca + @ca end def path_len=(value) @@ -34,29 +57,54 @@ module CertificateAuthority @path_len = value end - def openssl_identifier - "basicConstraints" + def to_s + res = [] + res << "CA:#{@ca}" + res << "pathlen:#{@path_len}" unless @path_len.nil? + res.join(',') end - def to_s - result = "" - result += "CA:#{self.ca}" - result += ",pathlen:#{self.path_len}" unless self.path_len.nil? - result + def ==(o) + o.class == self.class && o.state == state + end + + def self.parse(value, critical) + obj = self.new + return obj if value.nil? + obj.critical = critical + value.split(/,\s*/).each do |v| + c = v.split(':', 2) + obj.ca = (c.last.upcase == "TRUE") if c.first == "CA" + obj.path_len = c.last.to_i if c.first == "pathlen" + end + obj + end + + protected + def state + [@critical,@ca,@path_len] end end + # Specifies where CRL information be be retrieved. This extension isn't + # critical, but is recommended for proper CAs. + # Reference: Section 4.2.1.14 of RFC3280 + # http://tools.ietf.org/html/rfc3280#section-4.2.1.14 class CrlDistributionPoints + OPENSSL_IDENTIFIER = "crlDistributionPoints" + include ExtensionAPI - attr_accessor :uri + attr_accessor :critical + attr_accessor :uris def initialize - # self.uri = "http://moo.crlendPoint.example.com/something.crl" + @critical = false + @uris = [] end def openssl_identifier - "crlDistributionPoints" + OPENSSL_IDENTIFIER end ## NB: At this time it seems OpenSSL's extension handlers don't support @@ -69,99 +117,302 @@ module CertificateAuthority } end + # This is for legacy support. Technically it can (and probably should) + # be an array. But if someone is calling the old accessor we shouldn't + # necessarily break it. + def uri=(value) + @uris << value + end + def to_s - return "" if self.uri.nil? - "URI:#{self.uri}" + res = [] + @uris.each do |uri| + res << "URI:#{uri}" + end + res.join(',') + end + + def ==(o) + o.class == self.class && o.state == state + end + + def self.parse(value, critical) + obj = self.new + return obj if value.nil? + obj.critical = critical + value.split(/,\s*/).each do |v| + c = v.split(':', 2) + obj.uris << c.last if c.first == "URI" + end + obj + end + + protected + def state + [@critical,@uri] end end + # Identifies the public key associated with a given certificate. + # Should be required for "CA" certificates. + # Reference: Section 4.2.1.2 of RFC3280 + # http://tools.ietf.org/html/rfc3280#section-4.2.1.2 class SubjectKeyIdentifier + OPENSSL_IDENTIFIER = "subjectKeyIdentifier" + include ExtensionAPI + + attr_accessor :critical + attr_accessor :identifier + + def initialize + @critical = false + @identifier = "hash" + end + def openssl_identifier - "subjectKeyIdentifier" + OPENSSL_IDENTIFIER end def to_s - "hash" + res = [] + res << @identifier + res.join(',') + end + + def ==(o) + o.class == self.class && o.state == state + end + + def self.parse(value, critical) + obj = self.new + return obj if value.nil? + obj.critical = critical + obj.identifier = value + obj + end + + protected + def state + [@critical,@identifier] end end + # Identifies the public key associated with a given private key. + # Reference: Section 4.2.1.1 of RFC3280 + # http://tools.ietf.org/html/rfc3280#section-4.2.1.1 class AuthorityKeyIdentifier + OPENSSL_IDENTIFIER = "authorityKeyIdentifier" + include ExtensionAPI + attr_accessor :critical + attr_accessor :identifier + + def initialize + @critical = false + @identifier = ["keyid", "issuer"] + end + def openssl_identifier - "authorityKeyIdentifier" + OPENSSL_IDENTIFIER end def to_s - "keyid,issuer" + res = [] + res += @identifier + res.join(',') + end + + def ==(o) + o.class == self.class && o.state == state + end + + def self.parse(value, critical) + obj = self.new + return obj if value.nil? + obj.critical = critical + obj.identifier = value.split(/,\s*/).last.chomp + obj + end + + protected + def state + [@critical,@identifier] end end + # Specifies how to access CA information and services for the CA that + # issued this certificate. + # Generally used to specify OCSP servers. + # Reference: Section 4.2.2.1 of RFC3280 + # http://tools.ietf.org/html/rfc3280#section-4.2.2.1 class AuthorityInfoAccess + OPENSSL_IDENTIFIER = "authorityInfoAccess" + include ExtensionAPI + attr_accessor :critical attr_accessor :ocsp + attr_accessor :ca_issuers def initialize - self.ocsp = [] + @critical = false + @ocsp = [] + @ca_issuers = [] end def openssl_identifier - "authorityInfoAccess" + OPENSSL_IDENTIFIER end def to_s - return "" if self.ocsp.empty? - "OCSP;URI:#{self.ocsp}" + res = [] + res += @ocsp.map {|o| "OCSP;URI:#{o}" } + res += @ca_issuers.map {|c| "caIssuers;URI:#{c}" } + res.join(',') + end + + def ==(o) + o.class == self.class && o.state == state + end + + def self.parse(value, critical) + obj = self.new + return obj if value.nil? + obj.critical = critical + value.split("\n").each do |v| + if v.starts_with?("OCSP") + obj.ocsp << v.split.last + end + + if v.starts_with?("CA Issuers") + obj.ca_issuers << v.split.last + end + end + obj + end + + protected + def state + [@critical,@ocsp,@ca_issuers] end end + # Specifies the allowed usage purposes of the keypair specified in this certificate. + # Reference: Section 4.2.1.3 of RFC3280 + # http://tools.ietf.org/html/rfc3280#section-4.2.1.3 + # + # Note: OpenSSL when parsing an extension will return results in the form + # 'Digital Signature', but on signing you have to set it to 'digitalSignature'. + # So copying an extension from an imported cert isn't going to work yet. class KeyUsage + OPENSSL_IDENTIFIER = "keyUsage" + include ExtensionAPI + attr_accessor :critical attr_accessor :usage def initialize - self.usage = ["digitalSignature", "nonRepudiation"] + @critical = false + @usage = ["digitalSignature", "nonRepudiation"] end def openssl_identifier - "keyUsage" + OPENSSL_IDENTIFIER end def to_s - "#{self.usage.join(',')}" + res = [] + res += @usage + res.join(',') + end + + def ==(o) + o.class == self.class && o.state == state + end + + def self.parse(value, critical) + obj = self.new + return obj if value.nil? + obj.critical = critical + obj.usage = value.split(/,\s*/) + obj + end + + protected + def state + [@critical,@usage] end end + # Specifies even more allowed usages in addition to what is specified in + # the Key Usage extension. + # Reference: Section 4.2.1.13 of RFC3280 + # http://tools.ietf.org/html/rfc3280#section-4.2.1.13 class ExtendedKeyUsage + OPENSSL_IDENTIFIER = "extendedKeyUsage" + include ExtensionAPI + attr_accessor :critical attr_accessor :usage def initialize - self.usage = ["serverAuth","clientAuth"] + @critical = false + @usage = ["serverAuth"] end def openssl_identifier - "extendedKeyUsage" + OPENSSL_IDENTIFIER end def to_s - "#{self.usage.join(',')}" + res = [] + res += @usage + res.join(',') + end + + def ==(o) + o.class == self.class && o.state == state + end + + def self.parse(value, critical) + obj = self.new + return obj if value.nil? + obj.critical = critical + obj.usage = value.split(/,\s*/) + obj + end + + protected + def state + [@critical,@usage] end end + # Specifies additional "names" for which this certificate is valid. + # Reference: Section 4.2.1.7 of RFC3280 + # http://tools.ietf.org/html/rfc3280#section-4.2.1.7 class SubjectAlternativeName + OPENSSL_IDENTIFIER = "subjectAltName" + include ExtensionAPI - attr_accessor :uris, :dns_names, :ips + attr_accessor :critical + attr_accessor :uris, :dns_names, :ips, :emails def initialize - self.uris = [] - self.dns_names = [] - self.ips = [] + @critical = false + @uris = [] + @dns_names = [] + @ips = [] + @emails = [] + end + + def openssl_identifier + OPENSSL_IDENTIFIER end def uris=(value) @@ -179,22 +430,50 @@ module CertificateAuthority @ips = value end - def openssl_identifier - "subjectAltName" + def emails=(value) + raise "Emails must be an array" unless value.is_a?(Array) + @emails = value end def to_s - res = self.uris.map {|u| "URI:#{u}" } - res += self.dns_names.map {|d| "DNS:#{d}" } - res += self.ips.map {|i| "IP:#{i}" } + res = [] + res += @uris.map {|u| "URI:#{u}" } + res += @dns_names.map {|d| "DNS:#{d}" } + res += @ips.map {|i| "IP:#{i}" } + res += @emails.map {|i| "email:#{i}" } + res.join(',') + end + + def ==(o) + o.class == self.class && o.state == state + end + + def self.parse(value, critical) + obj = self.new + return obj if value.nil? + obj.critical = critical + value.split(/,\s*/).each do |v| + c = v.split(':', 2) + obj.uris << c.last if c.first == "URI" + obj.dns_names << c.last if c.first == "DNS" + obj.ips << c.last if c.first == "IP" + obj.emails << c.last if c.first == "EMAIL" + end + obj + end - return res.join(',') + protected + def state + [@critical,@uris,@dns_names,@ips,@emails] end end class CertificatePolicies + OPENSSL_IDENTIFIER = "certificatePolicies" + include ExtensionAPI + attr_accessor :critical attr_accessor :policy_identifier attr_accessor :cps_uris ##User notice @@ -203,12 +482,12 @@ module CertificateAuthority attr_accessor :notice_numbers def initialize + self.critical = false @contains_data = false end - def openssl_identifier - "certificatePolicies" + OPENSSL_IDENTIFIER end def user_notice=(value={}) @@ -258,7 +537,93 @@ module CertificateAuthority def to_s return "" unless @contains_data - "ia5org,@custom_policies" + res = [] + res << "ia5org" + res += @config_extensions["custom_policies"] unless @config_extensions.nil? + res.join(',') + end + + def self.parse(value, critical) + obj = self.new + return obj if value.nil? + obj.critical = critical + value.split(/,\s*/).each do |v| + c = v.split(':', 2) + obj.policy_identifier = c.last if c.first == "policyIdentifier" + obj.cps_uris << c.last if c.first =~ %r{CPS.\d+} + # TODO: explicit_text, organization, notice_numbers + end + obj + end + end + + # DEPRECATED + # Specifics the purposes for which a certificate can be used. + # The basicConstraints, keyUsage, and extendedKeyUsage extensions are now used instead. + # https://www.openssl.org/docs/apps/x509v3_config.html#Netscape_Certificate_Type + class NetscapeCertificateType + OPENSSL_IDENTIFIER = "nsCertType" + + include ExtensionAPI + + attr_accessor :critical + attr_accessor :flags + + def initialize + self.critical = false + self.flags = [] + end + + def openssl_identifier + OPENSSL_IDENTIFIER + end + + def to_s + res = [] + res += self.flags + res.join(',') + end + + def self.parse(value, critical) + obj = self.new + return obj if value.nil? + obj.critical = critical + obj.flags = value.split(/,\s*/) + obj + end + end + + # DEPRECATED + # Contains a comment which will be displayed when the certificate is viewed in some browsers. + # https://www.openssl.org/docs/apps/x509v3_config.html#Netscape_String_extensions_ + class NetscapeComment + OPENSSL_IDENTIFIER = "nsComment" + + include ExtensionAPI + + attr_accessor :critical + attr_accessor :comment + + def initialize + self.critical = false + end + + def openssl_identifier + OPENSSL_IDENTIFIER + end + + def to_s + res = [] + res << self.comment if self.comment + res.join(',') + end + + def self.parse(value, critical) + obj = self.new + return obj if value.nil? + obj.critical = critical + obj.comment = value + obj end end diff --git a/vendor/certificate_authority/lib/certificate_authority/key_material.rb b/vendor/certificate_authority/lib/certificate_authority/key_material.rb index 75ec62e..1fd4dd9 100644 --- a/vendor/certificate_authority/lib/certificate_authority/key_material.rb +++ b/vendor/certificate_authority/lib/certificate_authority/key_material.rb @@ -111,38 +111,4 @@ module CertificateAuthority @public_key end end - - class SigningRequestKeyMaterial - include KeyMaterial - include ActiveModel::Validations - - validates_each :public_key do |record, attr, value| - record.errors.add :public_key, "cannot be blank" if record.public_key.nil? - end - - attr_accessor :public_key - - def initialize(request=nil) - if request.is_a? OpenSSL::X509::Request - raise "Invalid certificate signing request" unless request.verify request.public_key - self.public_key = request.public_key - end - end - - def is_in_hardware? - false - end - - def is_in_memory? - true - end - - def private_key - nil - end - - def public_key - @public_key - end - end end diff --git a/vendor/certificate_authority/lib/certificate_authority/serial_number.rb b/vendor/certificate_authority/lib/certificate_authority/serial_number.rb index ec0b836..143c144 100644 --- a/vendor/certificate_authority/lib/certificate_authority/serial_number.rb +++ b/vendor/certificate_authority/lib/certificate_authority/serial_number.rb @@ -6,5 +6,9 @@ module CertificateAuthority attr_accessor :number validates :number, :presence => true, :numericality => {:greater_than => 0} + + def initialize + self.number = SecureRandom.random_number(2**128-1) + end end end diff --git a/vendor/certificate_authority/lib/certificate_authority/signing_request.rb b/vendor/certificate_authority/lib/certificate_authority/signing_request.rb index 590d5be..72d9e2b 100644 --- a/vendor/certificate_authority/lib/certificate_authority/signing_request.rb +++ b/vendor/certificate_authority/lib/certificate_authority/signing_request.rb @@ -5,6 +5,29 @@ module CertificateAuthority attr_accessor :raw_body attr_accessor :openssl_csr attr_accessor :digest + attr_accessor :attributes + + def initialize() + @attributes = [] + end + + # Fake attribute for convenience because adding + # alternative names on a CSR is remarkably non-trivial. + def subject_alternative_names=(alt_names) + raise "alt_names must be an Array" unless alt_names.is_a?(Array) + + factory = OpenSSL::X509::ExtensionFactory.new + name_list = alt_names.map{|m| "DNS:#{m}"}.join(",") + ext = factory.create_ext("subjectAltName",name_list,false) + ext_set = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence([ext])]) + attr = OpenSSL::X509::Attribute.new("extReq", ext_set) + @attributes << attr + end + + def read_attributes_by_oid(*oids) + attributes.detect { |a| oids.include?(a.oid) } + end + protected :read_attributes_by_oid def to_cert cert = Certificate.new @@ -12,6 +35,15 @@ module CertificateAuthority cert.distinguished_name = @distinguished_name end cert.key_material = @key_material + if attribute = read_attributes_by_oid('extReq', 'msExtReq') + set = OpenSSL::ASN1.decode(attribute.value) + seq = set.value.first + seq.value.collect { |asn1ext| OpenSSL::X509::Extension.new(asn1ext).to_a }.each do |o, v, c| + Certificate::EXTENSIONS.each do |klass| + cert.extensions[klass::OPENSSL_IDENTIFIER] = klass.parse(v, c) if v && klass::OPENSSL_IDENTIFIER == o + end + end + end cert end @@ -24,10 +56,12 @@ module CertificateAuthority raise "Invalid DN in request" unless @distinguished_name.valid? raise "CSR must have key material" if @key_material.nil? raise "CSR must include a public key on key material" if @key_material.public_key.nil? + raise "Need a private key on key material for CSR generation" if @key_material.private_key.nil? opensslcsr = OpenSSL::X509::Request.new opensslcsr.subject = @distinguished_name.to_x509_name opensslcsr.public_key = @key_material.public_key + opensslcsr.attributes = @attributes unless @attributes.nil? opensslcsr.sign @key_material.private_key, OpenSSL::Digest::Digest.new(@digest || "SHA512") opensslcsr end @@ -38,6 +72,7 @@ module CertificateAuthority csr.distinguished_name = DistinguishedName.from_openssl openssl_csr.subject csr.raw_body = raw_csr csr.openssl_csr = openssl_csr + csr.attributes = openssl_csr.attributes key_material = SigningRequestKeyMaterial.new key_material.public_key = openssl_csr.public_key csr.key_material = key_material @@ -53,4 +88,4 @@ module CertificateAuthority csr end end -end \ No newline at end of file +end -- cgit v1.2.3