]> gitweb.fluxo.info Git - leap/leap_cli.git/commitdiff
fixed `leap cert csr` to add correct "Requested Extensions" attribute on the CSR.
authorelijah <elijah@riseup.net>
Wed, 22 Oct 2014 00:32:28 +0000 (17:32 -0700)
committerelijah <elijah@riseup.net>
Wed, 22 Oct 2014 00:32:28 +0000 (17:32 -0700)
leap_cli.gemspec
lib/leap_cli/commands/ca.rb
vendor/certificate_authority/certificate_authority.gemspec
vendor/certificate_authority/lib/certificate_authority/certificate.rb
vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb
vendor/certificate_authority/lib/certificate_authority/extensions.rb
vendor/certificate_authority/lib/certificate_authority/key_material.rb
vendor/certificate_authority/lib/certificate_authority/serial_number.rb
vendor/certificate_authority/lib/certificate_authority/signing_request.rb

index d0b9a998c4b3052f65fe98e324e23eeab60d9a74..cbf0674df44ef7bb47403ae74d4169d18bab1d15 100644 (file)
@@ -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
index ef8cd71a6b66c6d09e709ebda4dc2acf1b5b9079..ea4c8a821a54c978003fa825c22a973de250d100 100644 (file)
@@ -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)
index be8cd91d12eafa778b5a52ec523d99eb936dddf4..b7e86767b1173b522306a00fa9381561ab5f58ac 100644 (file)
@@ -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<activemodel>, [">= 3.0.6"])
+      s.add_runtime_dependency(%q<activesupport>, [">= 3.0.6"])
       s.add_development_dependency(%q<rspec>, [">= 0"])
       s.add_development_dependency(%q<jeweler>, [">= 1.5.2"])
     else
       s.add_dependency(%q<activemodel>, [">= 3.0.6"])
+      s.add_dependency(%q<activesupport>, [">= 3.0.6"])
       s.add_dependency(%q<rspec>, [">= 0"])
       s.add_dependency(%q<jeweler>, [">= 1.5.2"])
     end
   else
     s.add_dependency(%q<activemodel>, [">= 3.0.6"])
+    s.add_dependency(%q<activesupport>, [">= 3.0.6"])
     s.add_dependency(%q<rspec>, [">= 0"])
     s.add_dependency(%q<jeweler>, [">= 1.5.2"])
   end
index ca8bc7cdde22c842a07ec9156a11f210134d2301..f096c5afed4745046f08bc5370f8c1343ef5d762 100644 (file)
@@ -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
 
index 165fe29b3b5c2b6c0d0b7c3264df8516565eeb28..32d9c1eaee77c4f1007807e4b922c4aa8540c497 100644 (file)
@@ -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?
index e5a8e8510237fc3c5d0e7a58747efb7f9ba07511..89de0321749ae4cbdb5617b4dd0603e722e289fb 100644 (file)
@@ -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
 
index 75ec62e226c6d65e2d7d94a9755aef274626ca7c..1fd4dd920de8626b05bd2325219aec259e1fbb6b 100644 (file)
@@ -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
index ec0b836ea63b2d3663f66b1897d902e2c86e7067..143c144c502ff37be49ecdfc1b287c6315ddced5 100644 (file)
@@ -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
index 590d5be17c3987ddc29a9520eef782f42741ffe5..72d9e2b124b56903b577f20ffcef32d5f94681d1 100644 (file)
@@ -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