require 'openssl' require 'set' module R509 class 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 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? 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. 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 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. attr_reader :allowed_uses # See OpenSSL::X509::Extension#initialize def initialize(*args) super(*args) @allowed_uses = self.value.split(",").map {|use| use.strip} 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 digital_signature? allows?( AU_DIGITAL_SIGNATURE ) end def non_repudiation? allows?( AU_NON_REPUDIATION ) end def key_encipherment? allows?( AU_KEY_ENCIPHERMENT ) end def data_encipherment? allows?( AU_DATA_ENCIPHERMENT ) end def key_agreement? allows?( AU_KEY_AGREEMENT ) end def certificate_sign? allows?( AU_CERTIFICATE_SIGN ) end def crl_sign? allows?( AU_CRL_SIGN ) end def encipher_only? allows?( AU_ENCIPHER_ONLY ) end def decipher_only? allows?( AU_DECIPHER_ONLY ) 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 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" # 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} 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 ) end def web_client_authentication? allows?( AU_WEB_CLIENT_AUTH ) end def code_signing? allows?( AU_CODE_SIGNING ) end def email_protection? allows?( AU_EMAIL_PROTECTION ) 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 OID = "subjectKeyIdentifier" Extensions.register_class(self) 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 OID = "authorityKeyIdentifier" Extensions.register_class(self) 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 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 # 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] } 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 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 # 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] } end end # 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 OID = "crlDistributionPoints" Extensions.register_class(self) # An array of the CRL URIs, if any attr_reader :crl_uris # See OpenSSL::X509::Extension#initialize def initialize(*args) super(*args) @crl_uris = self.value.scan( URI_REGEX ).map { |match| match[0] } end end # # Helper class methods # # Takes OpenSSL::X509::Extension objects and wraps each in the appropriate # R509::Cert::Extensions object, and returns them in a hash. The hash is # keyed with the R509 extension class. Extensions without an R509 # implementation are ignored (see #get_unknown_extensions). def self.wrap_openssl_extensions( extensions ) r509_extensions = {} extensions.each do |openssl_extension| 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 return r509_extensions end # Given a list of OpenSSL::X509::Extension objects, returns those without # an R509 implementation. def self.get_unknown_extensions( extensions ) unknown_extensions = [] extensions.each do |openssl_extension| match_found = false R509_EXTENSION_CLASSES.each do |r509_class| if ( r509_class::OID.downcase == openssl_extension.oid.downcase ) match_found = true break end end # if we make it this far (without breaking), we didn't match unknown_extensions << openssl_extension unless match_found end return unknown_extensions end end end end