lib/r509/cert/extensions.rb in r509-0.8.1 vs lib/r509/cert/extensions.rb in r509-0.9

- old
+ new

@@ -1,266 +1,633 @@ require 'openssl' +require 'r509/asn1' require 'set' module R509 class Cert + # module to contain extension classes for R509::Cert module Extensions private - # Regexes for OpenSSL's parsed values - DNS_REGEX = /DNS:([^,\n]+)/ - IP_ADDRESS_REGEX = /IP:([^,\n]+)/ - URI_REGEX = /URI:([^,\n]+)/ - R509_EXTENSION_CLASSES = Set.new # Registers a class as being an R509 certificate extension class. Registered # classes are used by #wrap_openssl_extensions to wrap OpenSSL extensions # in R509 extensions, based on the OID. def self.register_class( r509_ext_class ) raise ArgumentError.new("R509 certificate extensions must have an OID") if r509_ext_class::OID.nil? R509_EXTENSION_CLASSES << r509_ext_class end + public # Implements the BasicConstraints certificate extension, with methods to # provide access to the components and meaning of the extension's contents. class BasicConstraints < OpenSSL::X509::Extension + # friendly name for BasicConstraints OID OID = "basicConstraints" Extensions.register_class(self) attr_reader :path_length # See OpenSSL::X509::Extension#initialize def initialize(*args) super(*args) - @is_ca = ! ( self.value =~ /CA:TRUE/ ).nil? - pathlen_match = self.value.match( /pathlen:(\d+)/ ) - @path_length = pathlen_match[1].to_i unless pathlen_match.nil? + data = R509::ASN1.get_extension_payload(self) + @is_ca = false + # BasicConstraints ::= SEQUENCE { + # cA BOOLEAN DEFAULT FALSE, + # pathLenConstraint INTEGER (0..MAX) OPTIONAL } + data.entries.each do |entry| + if entry.kind_of?(OpenSSL::ASN1::Boolean) + # since the boolean is optional it may not be present + @is_ca = entry.value + else + # There are only two kinds of entries permitted so anything + # else is an integer pathlength + @path_length = entry.value + end + end end def is_ca?() return @is_ca == true end # Returns true if the path length allows this certificate to be used to - # sign CA certificates. + # create subordinate signing certificates beneath it. Does not check if + # there is a pathlen restriction in the cert chain above the current cert def allows_sub_ca?() return false if @path_length.nil? return @path_length > 0 end end # Implements the KeyUsage certificate extension, with methods to # provide access to the components and meaning of the extension's contents. class KeyUsage < OpenSSL::X509::Extension + # friendly name for KeyUsage OID OID = "keyUsage" Extensions.register_class(self) - # The OpenSSL friendly name for the "digitalSignature" key use. - AU_DIGITAL_SIGNATURE = "Digital Signature" - # The OpenSSL friendly name for the "nonRepudiation" key use. - AU_NON_REPUDIATION = "Non Repudiation" - # The OpenSSL friendly name for the "keyEncipherment" key use. - AU_KEY_ENCIPHERMENT = "Key Encipherment" - # The OpenSSL friendly name for the "dataEncipherment" key use. - AU_DATA_ENCIPHERMENT = "Data Encipherment" - # The OpenSSL friendly name for the "keyAgreement" key use. - AU_KEY_AGREEMENT = "Key Agreement" - # The OpenSSL friendly name for the "keyCertSign" key use. - AU_CERTIFICATE_SIGN = "Certificate Sign" - # The OpenSSL friendly name for the "cRLSign" key use. - AU_CRL_SIGN = "CRL Sign" - # The OpenSSL friendly name for the "encipherOnly" key use. - AU_ENCIPHER_ONLY = "Encipher Only" - # The OpenSSL friendly name for the "decipherOnly" key use. - AU_DECIPHER_ONLY = "Decipher Only" - - # An array of the key uses allowed. See the AU_* constants in this class. + # An array of the key uses allowed. attr_reader :allowed_uses + # OpenSSL short name for Digital Signature + AU_DIGITAL_SIGNATURE = "digitalSignature" + # OpenSSL short name for Non Repudiation (also known as content commitment) + AU_NON_REPUDIATION = "nonRepudiation" + # OpenSSL short name for Key Encipherment + AU_KEY_ENCIPHERMENT = "keyEncipherment" + # OpenSSL short name for Data Encipherment + AU_DATA_ENCIPHERMENT = "dataEncipherment" + # OpenSSL short name for Key Agreement + AU_KEY_AGREEMENT = "keyAgreement" + # OpenSSL short name for Certificate Sign + AU_KEY_CERT_SIGN = "keyCertSign" + # OpenSSL short name for CRL Sign + AU_CRL_SIGN = "cRLSign" + # OpenSSL short name for Encipher Only + AU_ENCIPHER_ONLY = "encipherOnly" + # OpenSSL short name for Decipher Only + AU_DECIPHER_ONLY = "decipherOnly" + # See OpenSSL::X509::Extension#initialize def initialize(*args) super(*args) - @allowed_uses = self.value.split(",").map {|use| use.strip} + data = R509::ASN1.get_extension_payload(self) + + # There are 9 possible bits, which means we need 2 bytes + # to represent them all. When the last bit is not set + # the second byte is not encoded. let's add it back so we can + # have the full bitmask for comparison + if data.size == 1 + data = data + "\0" + end + bit_mask = data.unpack('n')[0] # treat it as a 16-bit unsigned big endian + # KeyUsage ::= BIT STRING { + # digitalSignature (0), + # nonRepudiation (1), -- recent editions of X.509 have + # -- renamed this bit to contentCommitment + # keyEncipherment (2), + # dataEncipherment (3), + # keyAgreement (4), + # keyCertSign (5), + # cRLSign (6), + # encipherOnly (7), + # decipherOnly (8) } + @allowed_uses = [] + if bit_mask & 0b1000000000000000 > 0 + @digital_signature = true + @allowed_uses << AU_DIGITAL_SIGNATURE + end + if bit_mask & 0b0100000000000000 > 0 + @non_repudiation = true + @allowed_uses << AU_NON_REPUDIATION + end + if bit_mask & 0b0010000000000000 > 0 + @key_encipherment = true + @allowed_uses << AU_KEY_ENCIPHERMENT + end + if bit_mask & 0b0001000000000000 > 0 + @data_encipherment = true + @allowed_uses << AU_DATA_ENCIPHERMENT + end + if bit_mask & 0b0000100000000000 > 0 + @key_agreement = true + @allowed_uses << AU_KEY_AGREEMENT + end + if bit_mask & 0b0000010000000000 > 0 + @key_cert_sign = true + @allowed_uses << AU_KEY_CERT_SIGN + end + if bit_mask & 0b0000001000000000 > 0 + @crl_sign = true + @allowed_uses << AU_CRL_SIGN + end + if bit_mask & 0b0000000100000000 > 0 + @encipher_only = true + @allowed_uses << AU_ENCIPHER_ONLY + end + if bit_mask & 0b0000000010000000 > 0 + @decipher_only = true + @allowed_uses << AU_DECIPHER_ONLY + end end # Returns true if the given use is allowed by this extension. - # @param [string] friendly_use_name One of the AU_* constants in this class. + # @param [String] friendly_use_name key usage short name (e.g. digitalSignature, cRLSign, etc) + # or one of the AU_* constants in this class + # @return [Boolean] def allows?( friendly_use_name ) @allowed_uses.include?( friendly_use_name ) end def digital_signature? - allows?( AU_DIGITAL_SIGNATURE ) + (@digital_signature == true) end def non_repudiation? - allows?( AU_NON_REPUDIATION ) + (@non_repudiation == true) end def key_encipherment? - allows?( AU_KEY_ENCIPHERMENT ) + (@key_encipherment == true) end def data_encipherment? - allows?( AU_DATA_ENCIPHERMENT ) + (@data_encipherment == true) end def key_agreement? - allows?( AU_KEY_AGREEMENT ) + (@key_agreement == true) end - def certificate_sign? - allows?( AU_CERTIFICATE_SIGN ) + def key_cert_sign? + (@key_cert_sign == true) end def crl_sign? - allows?( AU_CRL_SIGN ) + (@crl_sign == true) end def encipher_only? - allows?( AU_ENCIPHER_ONLY ) + (@encipher_only == true) end def decipher_only? - allows?( AU_DECIPHER_ONLY ) + (@decipher_only == true) end end # Implements the ExtendedKeyUsage certificate extension, with methods to # provide access to the components and meaning of the extension's contents. class ExtendedKeyUsage < OpenSSL::X509::Extension + # friendly name for EKU OID OID = "extendedKeyUsage" Extensions.register_class(self) - # The OpenSSL friendly name for the "serverAuth" extended key use. - AU_WEB_SERVER_AUTH = "TLS Web Server Authentication" - # The OpenSSL friendly name for the "clientAuth" extended key use. - AU_WEB_CLIENT_AUTH = "TLS Web Client Authentication" - # The OpenSSL friendly name for the "codeSigning" extended key use. - AU_CODE_SIGNING = "Code Signing" - # The OpenSSL friendly name for the "emailProtection" extended key use. - AU_EMAIL_PROTECTION = "E-mail Protection" + # The OpenSSL short name for TLS Web Server Authentication + AU_WEB_SERVER_AUTH = "serverAuth" + # The OpenSSL short name for TLS Web Client Authentication + AU_WEB_CLIENT_AUTH = "clientAuth" + # The OpenSSL short name for Code Signing + AU_CODE_SIGNING = "codeSigning" + # The OpenSSL short name for E-mail Protection + AU_EMAIL_PROTECTION = "emailProtection" + # The OpenSSL short name for OCSP Signing + AU_OCSP_SIGNING = "OCSPSigning" + # The OpenSSL short name for Time Stamping + AU_TIME_STAMPING = "timeStamping" + # The OpenSSL short name for Any Extended Key Usage + AU_ANY_EXTENDED_KEY_USAGE = "anyExtendedKeyUsage" - # An array of the key uses allowed. See the AU_* constants in this class. attr_reader :allowed_uses # See OpenSSL::X509::Extension#initialize def initialize(*args) super(*args) - @allowed_uses = self.value.split(",").map {|use| use.strip} + @allowed_uses = [] + data = R509::ASN1.get_extension_payload(self) + + data.entries.each do |eku| + # The following key usage purposes are defined: + # + # anyExtendedKeyUsage OBJECT IDENTIFIER ::= { id-ce-extKeyUsage 0 } + # + # id-kp OBJECT IDENTIFIER ::= { id-pkix 3 } + # id-kp-serverAuth OBJECT IDENTIFIER ::= { id-kp 1 } + # -- TLS WWW server authentication + # -- Key usage bits that may be consistent: digitalSignature, + # -- keyEncipherment or keyAgreement + # + # id-kp-clientAuth OBJECT IDENTIFIER ::= { id-kp 2 } + # -- TLS WWW client authentication + # -- Key usage bits that may be consistent: digitalSignature + # -- and/or keyAgreement + # + # id-kp-codeSigning OBJECT IDENTIFIER ::= { id-kp 3 } + # -- Signing of downloadable executable code + # -- Key usage bits that may be consistent: digitalSignature + # + # id-kp-emailProtection OBJECT IDENTIFIER ::= { id-kp 4 } + # -- Email protection + # -- Key usage bits that may be consistent: digitalSignature, + # -- nonRepudiation, and/or (keyEncipherment or keyAgreement) + # + # id-kp-timeStamping OBJECT IDENTIFIER ::= { id-kp 8 } + # -- Binding the hash of an object to a time + # -- Key usage bits that may be consistent: digitalSignature + # -- and/or nonRepudiation + # + # id-kp-OCSPSigning OBJECT IDENTIFIER ::= { id-kp 9 } + # -- Signing OCSP responses + # -- Key usage bits that may be consistent: digitalSignature + # -- and/or nonRepudiation + + case eku.value + when AU_WEB_SERVER_AUTH + @web_server_authentication = true + when AU_WEB_CLIENT_AUTH + @web_client_authentication = true + when AU_CODE_SIGNING + @code_signing = true + when AU_EMAIL_PROTECTION + @email_protection = true + when AU_OCSP_SIGNING + @ocsp_signing = true + when AU_TIME_STAMPING + @time_stamping = true + when AU_ANY_EXTENDED_KEY_USAGE + @any_extended_key_usage = true + end + @allowed_uses << eku.value + end end # Returns true if the given use is allowed by this extension. # @param [string] friendly_use_name One of the AU_* constants in this class. def allows?( friendly_use_name ) @allowed_uses.include?( friendly_use_name ) end def web_server_authentication? - allows?( AU_WEB_SERVER_AUTH ) + (@web_server_authentication == true) end def web_client_authentication? - allows?( AU_WEB_CLIENT_AUTH ) + (@web_client_authentication == true) end def code_signing? - allows?( AU_CODE_SIGNING ) + (@code_signing == true) end def email_protection? - allows?( AU_EMAIL_PROTECTION ) + (@email_protection == true) end - # ... + def ocsp_signing? + (@ocsp_signing == true) + end + + def time_stamping? + (@time_stamping == true) + end + + def any_extended_key_usage? + (@any_extended_key_usage == true) + end end # Implements the SubjectKeyIdentifier certificate extension, with methods to # provide access to the components and meaning of the extension's contents. class SubjectKeyIdentifier < OpenSSL::X509::Extension + # friendly name for Subject Key Identifier OID OID = "subjectKeyIdentifier" Extensions.register_class(self) + # @return value of key def key() return self.value end end # Implements the AuthorityKeyIdentifier certificate extension, with methods to # provide access to the components and meaning of the extension's contents. class AuthorityKeyIdentifier < OpenSSL::X509::Extension + # friendly name for Authority Key Identifier OID OID = "authorityKeyIdentifier" Extensions.register_class(self) + # key_identifier, if present, will be a hex string delimited by colons + # authority_cert_issuer, if present, will be a GeneralName object + # authority_cert_serial_number, if present, will be a hex string delimited by colons + attr_reader :key_identifier, :authority_cert_issuer, :authority_cert_serial_number + + def initialize(*args) + super(*args) + + data = R509::ASN1.get_extension_payload(self) + # AuthorityKeyIdentifier ::= SEQUENCE { + # keyIdentifier [0] KeyIdentifier OPTIONAL, + # authorityCertIssuer [1] GeneralNames OPTIONAL, + # authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL } + data.entries.each do |el| + case el.tag + when 0 + @key_identifier = el.value.unpack("H*")[0].upcase.scan(/../).join(":") + when 1 + @authority_cert_issuer = R509::ASN1::GeneralName.new(el.value.first) + when 2 + arr = el.value.unpack("H*")[0].upcase.scan(/../) + # OpenSSL's convention is to drop leading 00s, so let's strip that off if + # present + if arr[0] == "00" + arr.delete_at(0) + end + @authority_cert_serial_number = arr.join(":") + end + end + + end + end # Implements the SubjectAlternativeName certificate extension, with methods to # provide access to the components and meaning of the extension's contents. class SubjectAlternativeName < OpenSSL::X509::Extension + # friendly name for SAN OID OID = "subjectAltName" Extensions.register_class(self) - # An array of the DNS alternative names, if any - attr_reader :dns_names - # An array of the IP-address alternative names, if any - attr_reader :ip_addresses - # An array of the URI alternative names, if any - attr_reader :uris + attr_reader :general_names # See OpenSSL::X509::Extension#initialize def initialize(*args) super(*args) - @dns_names = self.value.scan( DNS_REGEX ).map { |match| match[0] } - @ip_addresses = self.value.scan( IP_ADDRESS_REGEX ).map { |match| match[0] } - @uris = self.value.scan( URI_REGEX ).map { |match| match[0] } + data = R509::ASN1.get_extension_payload(self) + @general_names = R509::ASN1::GeneralNames.new + data.entries.each do |gn| + @general_names.add_item(gn) + end end + + # @return [Array<String>] DNS names + def dns_names + @general_names.dns_names + end + + # @return [Array<String>] IP addresses formatted as dotted quad + def ip_addresses + @general_names.ip_addresses + end + + # @return [Array<String>] email addresses + def rfc_822_names + @general_names.rfc_822_names + end + + # @return [Array<String>] URIs (not typically found in SAN extensions) + def uris + @general_names.uris + end + + # @return [Array<R509::Subject>] directory names + def directory_names + @general_names.directory_names + end + + # @return [Array] array of GeneralName objects preserving order found in the extension + def names + @general_names.names + end end # Implements the AuthorityInfoAccess certificate extension, with methods to # provide access to the components and meaning of the extension's contents. class AuthorityInfoAccess < OpenSSL::X509::Extension + # friendly name for AIA OID OID = "authorityInfoAccess" Extensions.register_class(self) - # An array of the OCSP URIs, if any - attr_reader :ocsp_uris - # An array of the CA issuers URIs, if any - attr_reader :ca_issuers_uris + # An array of the OCSP data, if any + attr_reader :ocsp + # An array of the CA issuers data, if any + attr_reader :ca_issuers # See OpenSSL::X509::Extension#initialize def initialize(*args) super(*args) - @ocsp_uris = self.value.scan( /OCSP - #{URI_REGEX}/ ).map { |match| match[0] } - @ca_issuers_uris = self.value.scan( /CA Issuers - #{URI_REGEX}/ ).map { |match| match[0] } + data = R509::ASN1.get_extension_payload(self) + @ocsp= R509::ASN1::GeneralNames.new + @ca_issuers= R509::ASN1::GeneralNames.new + data.entries.each do |access_description| + # AccessDescription ::= SEQUENCE { + # accessMethod OBJECT IDENTIFIER, + # accessLocation GeneralName } + case access_description.entries[0].value + when "OCSP" + @ocsp.add_item(access_description.entries[1]) + when "caIssuers" + @ca_issuers.add_item(access_description.entries[1]) + end + end end end - # Implements the CrlDistributionPoints certificate extension, with methods to + # Implements the CRLDistributionPoints certificate extension, with methods to # provide access to the components and meaning of the extension's contents. - class CrlDistributionPoints < OpenSSL::X509::Extension + class CRLDistributionPoints < OpenSSL::X509::Extension + # friendly name for CDP OID OID = "crlDistributionPoints" Extensions.register_class(self) # An array of the CRL URIs, if any - attr_reader :crl_uris + attr_reader :crl # See OpenSSL::X509::Extension#initialize def initialize(*args) super(*args) - @crl_uris = self.value.scan( URI_REGEX ).map { |match| match[0] } + @crl= R509::ASN1::GeneralNames.new + data = R509::ASN1.get_extension_payload(self) + data.entries.each do |distribution_point| + # DistributionPoint ::= SEQUENCE { + # distributionPoint [0] DistributionPointName OPTIONAL, + # reasons [1] ReasonFlags OPTIONAL, + # cRLIssuer [2] GeneralNames OPTIONAL } + # DistributionPointName ::= CHOICE { + # fullName [0] GeneralNames, + # nameRelativeToCRLIssuer [1] RelativeDistinguishedName } + # We're only going to handle DistributionPointName [0] for now + # so grab entries[0] and then get the fullName with value[0] + # and the value of that ASN1Data with value[0] again + @crl.add_item(distribution_point.entries[0].value[0].value[0]) + end end end + # Implements the OCSP noCheck certificate extension + class OCSPNoCheck < OpenSSL::X509::Extension + # friendly name for OCSP No Check + OID = "noCheck" + Extensions.register_class(self) + # See OpenSSL::X509::Extension#initialize + def initialize(*args) + super(*args) + end + end + + + # Implements the CertificatePolicies certificate extension, with methods to + # provide access to the components and meaning of the extension's contents. + class CertificatePolicies < OpenSSL::X509::Extension + # friendly name for CP OID + OID = "certificatePolicies" + Extensions.register_class(self) + attr_reader :policies + + def initialize(*args) + @policies = [] + super(*args) + + data = R509::ASN1.get_extension_payload(self) + + # each element of this sequence should be part of a policy + qualifiers + # certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation + data.each do |cp| + @policies << R509::ASN1::PolicyInformation.new(cp) + end if data.respond_to?(:each) + end + end + + # Implements the InhibitAnyPolicy certificate extension, with methods to + # provide access to the component and meaning of the extension's contents. + class InhibitAnyPolicy < OpenSSL::X509::Extension + # friendly name for CP OID + OID = "inhibitAnyPolicy" + Extensions.register_class(self) + + attr_reader :skip_certs + + def initialize(*args) + super(*args) + + # id-ce-inhibitAnyPolicy OBJECT IDENTIFIER ::= { id-ce 54 } + # InhibitAnyPolicy ::= SkipCerts + # SkipCerts ::= INTEGER (0..MAX) + @skip_certs = R509::ASN1.get_extension_payload(self) # returns a non-negative integer + end + end + + # Implements the PolicyConstraints certificate extension, with methods to + # provide access to the components and meaning of the extension's contents. + class PolicyConstraints < OpenSSL::X509::Extension + # friendly name for CP OID + OID = "policyConstraints" + Extensions.register_class(self) + + attr_reader :require_explicit_policy + attr_reader :inhibit_policy_mapping + + def initialize(*args) + super(*args) + + # id-ce-policyConstraints OBJECT IDENTIFIER ::= { id-ce 36 } + # PolicyConstraints ::= SEQUENCE { + # requireExplicitPolicy [0] SkipCerts OPTIONAL, + # inhibitPolicyMapping [1] SkipCerts OPTIONAL } + # + # SkipCerts ::= INTEGER (0..MAX) + data = R509::ASN1.get_extension_payload(self) + data.each do |pc| + if pc.tag == 0 + @require_explicit_policy = pc.value.bytes.to_a[0] + elsif pc.tag == 1 + @inhibit_policy_mapping = pc.value.bytes.to_a[0] + end + end + end + end + + # Implements the NameConstraints certificate extension, with methods to + # provide access to the components and meaning of the extension's contents. + class NameConstraints < OpenSSL::X509::Extension + # friendly name for CP OID + OID = "nameConstraints" + Extensions.register_class(self) + + attr_reader :permitted_names, :excluded_names + + # id-ce-nameConstraints OBJECT IDENTIFIER ::= { id-ce 30 } + # NameConstraints ::= SEQUENCE { + # permittedSubtrees [0] GeneralSubtrees OPTIONAL, + # excludedSubtrees [1] GeneralSubtrees OPTIONAL } + # + # GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree + # + # per RFC 5280 + # Within this profile, the minimum and maximum fields are not used with + # any name forms, thus, the minimum MUST be zero, and maximum MUST be + # absent + # GeneralSubtree ::= SEQUENCE { + # base GeneralName, + # minimum [0] BaseDistance DEFAULT 0, + # maximum [1] BaseDistance OPTIONAL } + # + # BaseDistance ::= INTEGER (0..MAX) + def initialize(*args) + super(*args) + + @permitted_names = [] + @excluded_names = [] + + data = R509::ASN1.get_extension_payload(self) + data.each do |gs| + gs.value.each do |asn_data| + asn_data.value.each do |obj| + gn = R509::ASN1::GeneralName.new(obj) + if gs.tag == 0 # permittedSubtrees + @permitted_names << gn + elsif gs.tag == 1 #excludedSubtrees + @excluded_names << gn + end + end + end + end + end + end + + + # # Helper class methods # # Takes OpenSSL::X509::Extension objects and wraps each in the appropriate @@ -273,10 +640,10 @@ R509_EXTENSION_CLASSES.each do |r509_class| if ( r509_class::OID.downcase == openssl_extension.oid.downcase ) if r509_extensions.has_key?(r509_class) raise ArgumentError.new("Only one extension object allowed per OID") end - + r509_extensions[r509_class] = r509_class.new( openssl_extension ) break end end end