diff options
18 files changed, 2012 insertions, 11 deletions
@@ -14,6 +14,11 @@ end gem "ruby-srp", "~> 0.2.1" gem "rails_warden" +## CRYPTO +# we need certificate_authority v2.0, but was never released to rubygems, +# and travis does not work well with github sources, so vendored here: +gem 'certificate_authority', :path => 'vendor/gems/certificate_authority' + ## LOCALIZATION gem 'http_accept_language' gem 'rails-i18n' # locale files for built-in validation messages and times @@ -111,7 +116,4 @@ end ## DEPENDENCIES FOR OPTIONAL ENGINES ## -gem 'certificate_authority', # unreleased so far ... but leap_web_certs need it - :git => 'https://github.com/cchandler/certificate_authority.git' - gem 'valid_email' # used by leap_web_help diff --git a/Gemfile.lock b/Gemfile.lock index 413e8ea..f70add6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,3 @@ -GIT - remote: https://github.com/cchandler/certificate_authority.git - revision: 050f2829ed12b6c257e24f4c8077c6e1b38bd979 - specs: - certificate_authority (0.2.0) - activemodel (>= 3.0.6) - activesupport (>= 3.0.6) - PATH remote: engines/billing specs: @@ -18,6 +10,13 @@ PATH leap_web_help (0.6.0) PATH + remote: vendor/gems/certificate_authority + specs: + certificate_authority (0.2.0) + activemodel (>= 3.0.6) + activesupport (>= 3.0.6) + +PATH remote: vendor/gems/common_languages specs: common_languages (0.0.1) diff --git a/vendor/gems/certificate_authority/README.rdoc b/vendor/gems/certificate_authority/README.rdoc new file mode 100644 index 0000000..7a44122 --- /dev/null +++ b/vendor/gems/certificate_authority/README.rdoc @@ -0,0 +1,332 @@ += CertificateAuthority - Because it shouldn't be this damned complicated + +{<img src="https://travis-ci.org/cchandler/certificate_authority.png?branch=master" alt="Build Status" />}[https://travis-ci.org/cchandler/certificate_authority] +{<img src="https://codeclimate.com/github/cchandler/certificate_authority.png" alt="Code Climate" />}[https://codeclimate.com/github/cchandler/certificate_authority] +{<img src="https://coveralls.io/repos/cchandler/certificate_authority/badge.png?branch=master" alt="Code Coverage" />}[https://coveralls.io/r/cchandler/certificate_authority] + +This is meant to provide a (more) programmer-friendly implementation of all the basic functionality contained in RFC-3280 to implement your own certificate authority. + +You can generate root certificates, intermediate certificates, and terminal certificates. You can also generate/manage Certificate Revocation Lists (CRLs) and Online Certificate Status Protocol (OCSP) messages. + +Because this library is built using the native Ruby bindings for OpenSSL it also supports PKCS#11 cryptographic hardware for secure maintenance of private key materials. + += So you want to maintain a certificate authority root + +Let's suppose hypothetically you want to be able to issue and manage your own certificates. This section is meant to outline the basic functions you'll need(optionally want) to support. Not everyone is out to be in total compliance with WebTrust[link:http://www.webtrust.org/] or {Mozilla's rules for CA inclusion}[link:https://wiki.mozilla.org/CA:How_to_apply]. + +The three primary elements to be aware of are: + +[Certificate Authority] These are the functions primarily related to the signing, issuance, and revocation of certificates. + +[Registration Authority] These are the functions primarily related to registering and requesting certificates and vetting of the entities requesting certification. + +[Validation Authority] These are the functions related to verifying the status of certificates out in the wild. Mostly CRLs and OCSP related functions. + += Establishing a new root in software + +Let's look at a complete example for generating a new root certificate. Assuming that you don't have a PKCS#11 hardware token available (lists coming...) we'll have to store this safe. + +Generating a self-signed root certificate is fairly easy: + + require 'certificate_authority' + root = CertificateAuthority::Certificate.new + root.subject.common_name= "http://mydomain.com" + root.serial_number.number=1 + root.key_material.generate_key + root.signing_entity = true + signing_profile = {"extensions" => {"keyUsage" => {"usage" => ["critical", "keyCertSign"] }} } + root.sign!(signing_profile) + +The required elements for the gem at this time are a common name for the subject and a serial number for the certificate. Since this is our self-signed root we're going to give it the first serial available of 1. Because certificate_authority is not designed to manage the issuance lifecycle you'll be expected to store serial numbers yourself. + +Next, after taking care of required fields, we will require key material for the new certificate. There's a convenience method made available on the key_material object for generating new keys. The private key will be available in: + + root.key_material.private_key + +and the public key: + + root.key_material.public_key + +Make sure to save the private key somewhere safe! + +Lastly, we declare that the certificate we're about to sign is itself a signing entity so we can continue on and sign other certificates. + +== Creating a new intermediate + +Maybe you don't want to actually sign certificates with your super-secret root certificate. This is actually how a good number of most public certificate authorities do it. Rather than sign with the primary root, they generate an intermediate root that is then responsible for signing the final certificates. If you wanted to create an intermediate root certificate you would do something like the following: + + intermediate = CertificateAuthority::Certificate.new + intermediate.subject.common_name= "My snazzy intermediate!" + intermediate.serial_number.number=2 + intermediate.key_material.generate_key + intermediate.signing_entity = true + intermediate.parent = root + signing_profile = {"extensions" => {"keyUsage" => {"usage" => ["critical", "keyCertSign"] }} } + intermediate.sign!(signing_profile) + +All we have to do is create another certificate like we did with the root. In this example we gave it the next available serial number, which for us, was 2. We then generate (and save!) key material for this new entity. Even the +signing_entity+ is set to true so this certificate can sign other certificates. The difference here is that the +parent+ field is set to the root. Going forward, whatever entity you want to sign a certificate, you set that entity to be the parent. In this case, our root will be responsible for signing this intermediate when we call +sign!+. + += Creating new certificates (in general) + +Now that we have a root certificate (and possibly an intermediate) we can sign end-user certificates. It is, perhaps unsurprisingly, similar to all the others: + + plain_cert = CertificateAuthority::Certificate.new + plain_cert.subject.common_name= "http://mydomain.com" + plain_cert.serial_number.number=4 + plain_cert.key_material.generate_key + plain_cert.parent = root # or intermediate + plain_cert.sign! + +That's all there is to it! In this example we generate the key material ourselves, but it's possible for the end-user to generate certificate signing request (CSR) that we can then parse and consume automatically (coming soon). To get the PEM formatted certificate for the user you would need to call: + + plain_cert.to_pem + +to get the certificate body. + += Signing Profiles + +Creating basic certificates is all well and good, but maybe you want _more_ signing control. +certificate_authority+ supports the idea of signing profiles. These are hashes containing values that +sign!+ will use to merge in additional control options for setting extensions on the certificate. + +Here's an example of a full signing profile for most of the common V3 extensions: + + signing_profile = { + "extensions" => { + "basicConstraints" => {"ca" => false}, + "crlDistributionPoints" => {"uri" => "http://notme.com/other.crl" }, + "subjectKeyIdentifier" => {}, + "authorityKeyIdentifier" => {}, + "authorityInfoAccess" => {"ocsp" => ["http://youFillThisOut/ocsp/"] }, + "keyUsage" => {"usage" => ["digitalSignature","nonRepudiation"] }, + "extendedKeyUsage" => {"usage" => [ "serverAuth","clientAuth"]}, + "subjectAltName" => {"uris" => ["http://subdomains.youFillThisOut/"]}, + "certificatePolicies" => { + "policy_identifier" => "1.3.5.8", "cps_uris" => ["http://my.host.name/", "http://my.your.name/"], + "user_notice" => { + "explicit_text" => "Explicit Text Here", + "organization" => "Organization name", + "notice_numbers" => "1,2,3,4" + } + } + } + } + +Using a signing profile is done this way: + + certificate.sign!(signing_profile) + +At that point all the configuration options will be merged into the extensions. + +== Basic Constraints + +The basic constraints extension allows you to control whether or not a certificate can sign other certificates. + +[CA] If this value is true then this certificate has the authority to sign additional certificates. + +[pathlen] This is the maximum length of the chain-of-trust. For instance, if an intermediate certificate has a pathlen of 1 then it can sign additional certificates, but it cannot create another signing entity because the total chain-of-trust would have a length greater than 1. + +== CRL Distribution Points + +This extension controls where a conformant client can go to obtain a list of certificate revocation information. At this point +certificate_authority+ only supports a list of URIs. The formal RFC however provides for the ability to provide a URI and an issuer identifier that allows a different signing entity to generate/sign the CRL. + +[uri] The URI in subject alternative name format of the URI endpoint. Example: "http://ca.chrischandler.name/some_identifier.crl" + +== Subject Key Identifier + +This extension is required to be present, but doesn't offer any configurable parameters. Directly from the RFC: + + The subject key identifier extension provides a means of identifying + certificates that contain a particular public key. + + To facilitate certification path construction, this extension MUST + appear in all conforming CA certificates, that is, all certificates + including the basic constraints extension (section 4.2.1.10) where + the value of cA is TRUE. The value of the subject key identifier + MUST be the value placed in the key identifier field of the Authority + Key Identifier extension (section 4.2.1.1) of certificates issued by + the subject of this certificate. + +== Authority Key Identifier + +Just like the subject key identifier, this is required under most circumstances and doesn't contain any meaningful configuration options. From the RFC: + + The keyIdentifier field of the authorityKeyIdentifier extension MUST + be included in all certificates generated by conforming CAs to + facilitate certification path construction. There is one exception; + where a CA distributes its public key in the form of a "self-signed" + certificate, the authority key identifier MAY be omitted. The + signature on a self-signed certificate is generated with the private + key associated with the certificate's subject public key. (This + proves that the issuer possesses both the public and private keys.) + In this case, the subject and authority key identifiers would be + identical, but only the subject key identifier is needed for + certification path building. + +== Authority Info Access + +The authority info access extension allows a CA to sign a certificate with information a client can use to get up-to-the-minute status information on a signed certificate. This takes the form of an OCSP[link:http://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol] (Online Certificate Status Protocol) endpoints. + +[ocsp] This is an array of URIs specifying possible endpoints that will be able to provide a signed response. +certificate_authority+ has an OCSP message handler for parsing OCSP requests and generating OCSP signed responses. + +== Key Usage + +This extension contains a list of the functions this certificate is allowed to participate in. + +[usage] An array of OIDs in string format. The acceptable values are specified by OpenSSL and are: +digitalSignature+, +nonRepudiation+, +keyEncipherment+, +dataEncipherment+, +keyAgreement+, +keyCertSign+, +cRLSign+, +encipherOnly+ and +decipherOnly+. + +== Extended Key Usage + +This one is like key usage, but allows for certain application specific purposes. It's generally only present in end-user certificates. + +[usage] An array of OIDs in string format. The only ones with practical significance at this point are: +serverAuth+, +clientAuth+, and +codeSigning+. + +== Subject Alternative Name + +If the certificate needs to work for multiple domains then you can specify the others for which it is valid in the subject alternative name field. + +[uris] An array of full URIs for other common names this certificate should be valid for. For instance, if you want http://ca.chrischandler.name and http://www.ca.chrischandler.name to share the same cert you would place both in the +uris+ attribute of the subject alternative name. + +== Certificate Policies + +This is one of the most esoteric of the extensions. This allows a conformant certificate authority to embed signing policy information into the certificate body. Public certificate authorities are required to maintain a Certificate Practice Statement in accordance with {RFC 2527}[link:http://www.ietf.org/rfc/rfc2527.txt]. + +These CPSs define what vetting criteria and maintenance practices are required to issue, maintain, and revoke a certificate. While it might be overkill for private certificates, if you wanted to make an actual public CA you would need to put together a practice statement and embed it in certificates you issue. + +[policy_identifier] This is an arbitrary OID (that you make up!) that uniquely represents the policy you are enforcing for whatever kind of certificate this is meant to be. + +[cps_uris] This is an array of URIs where a client or human can go to get information related to your certification practice. + +[user_notice] This is a nested field containing explicit human readable text if you want to embed a notice in the certificate body related to certification practices. It contains nested attributes of +explicit_text+ for the notice, +organization+ and +notice_numbers+. Refer to the RFC for specific implications of how these are set, but whether or not browsers implement the correct specified behavior for their presence is another issue. + += Certificate Signing Requests (CSRs) + +If you want certificate requestors to be able to request certificates without moving the private key you'll need to generate a CSR and submit it to the certificate authority. + +Here's an example of using +certificate_authority+ to generate a CSR. + + csr = CertificateAuthority::SigningRequest.new + dn = CertificateAuthority::DistinguishedName.new + dn.common_name = "localhost" + csr.distinguished_name = dn + k = CertificateAuthority::MemoryKeyMaterial.new + k.generate_key(2048) + csr.key_material = k + csr.digest = "SHA256" + csr.to_x509_csr.to_pem + +Similarly, reading a CSR in is as simple as providing the PEM formatted version to +SigningRequest.from_x509_csr+. + + csr = CertificateAuthority::SigningRequest.from_x509_csr(@pem_csr) + +Once you have the CSR in the form of a +SigningRequest+ you can transform it to a +Certificate+ with +to_cert+. At this point it works just like any other certificate. You'll have to provide a serial number to actually sign it. + += Certificate Revocation Lists (CRLs) + +Revocation lists let clients know when a certificate in the wild should no longer be trusted. + +Like end-user certificates, CRLs have to be signed by a signing authority to be valid. Additionally, you will need to furnish a +nextUpdate+ value that indicates to the client when it should look for updates to the CRL and how long it should consider a cached value valid. + +Ideally you would place the result CRL somewhere generally accessible on the Internet and reference the URI in the +crlDistributionPoints+ extension on issued certificates. + + crl = CertificateAuthority::CertificateRevocationList.new + crl << certificate # Some CertificateAuthority::Certificate + crl << serial_number # Also works with plain CertificateAuthority::SerialNumber + crl.parent = root_certificate # A valid root + crl.next_update = (60 * 60 * 10) # 10 Hours + crl.sign! + crl.to_pem + += OCSP Support + +OCSP is the Online Certificate Status Protocol. It provides a mechanism to query an authority to see if a certificate is still valid without downloading an entire CRL. To use this mechanism you provide a URI in the Authority Information Access extension. +If a client wishes to check the validity of a certificate they can query this endpoint. +This request will only contain serial numbers, so you'll need to uniquely identify your authority in the AIA path. + +If a client sends you a DER encoded OCSP request you can read it out via +OCSPRequestReader+ + + ocsp_request_reader = CertificateAuthority::OCSPRequestReader.from_der(@ocsp_request.to_der) + ocsp_request_reader.serial_numbers + +Then, you can construct a response like this + + response_builder = CertificateAuthority::OCSPResponseBuilder.from_request_reader(ocsp_request_reader) + response_builder.parent = root + response = response_builder.build_response # Returns OpenSSL::OCSP::Response + response.to_der + +The response builder will copy a (possible) nonce from the request. By default, the +OCSPResponseBuilder+ will say that every certificate is GOOD. +You should definitely override this if you plan on revoking certificates. +If you want to override this you'll need to supply a proc/lambda that takes a serial number and returns an array of status and reason. + + response_builder = CertificateAuthority::OCSPResponseBuilder.from_request_reader(ocsp_request_reader) + response_builder.verification_mechanism = lambda {|certid| + [CertificateAuthority::OCSPResponseBuilder::REVOKED,CertificateAuthority::OCSPResponseBuilder::UNSPECIFIED] + } + response_builder.parent = root + response = response_builder.build_response # Response will say everything is revoked for unspecified reasons + +Lastly, you can configure a nextUpdate time in the response. This is the length of time for which a client may consider this response valid. +The default is 15 minutes. + + response_builder.next_update = 30 * 60 # 30 minutes + += PKCS#11 Support + +If you happen to have a PKCS#11 compliant hardware token you can use +certificate_authority+ to maintain private key materials in hardware security modules. At this point the scope of operating that hardware is out of scope of this README but it's there and it is supported. + +To configure a certificate to utilize PKCS#11 instead of in memory keys all you need to do is: + + root = CertificateAuthority::Certificate.new + root.subject.common_name= "http://mydomain.com" + root.serial_number.number=1 + root.signing_entity = true + + key_material_in_hardware = CertificateAuthority::Pkcs11KeyMaterial.new + key_material_in_hardware.token_id = "46" + key_material_in_hardware.pkcs11_lib = "/usr/lib/libeTPkcs11.so" + key_material_in_hardware.openssl_pkcs11_engine_lib = "/usr/lib/engines/engine_pkcs11.so" + key_material_in_hardware.pin = "11111111" + + root.key_material = key_material_in_hardware + root.sign! + +Your current version of OpenSSL _must_ include dynamic engine support and you will need to have OpenSSL PKCS#11 engine support. You will also require the actual PKCS#11 driver from the hardware manufacturer. As of today the only tokens I've gotten to work are: + +[eTokenPro] Released by Aladdin (now SafeNet Inc.). I have only had success with the version 4 and 5 (32 bit only) copy of the driver. The newer authentication client released by SafeNet appears to be completely broken for interacting with the tokens outside of SafeNet's own tools. If anyone has a different experience I'd like to hear from you. + +[ACS CryptoMate] Also a 32-bit only driver. You'll have to jump through some hoops to get the Linux PKCS#11 driver but it works surprisingly well. It also appears to support symmetric key operations in hardware. + +[Your company] Do you make a PKCS#11 device? I'd love to get it working but I probably can't afford your device. Get in touch with me and if you're willing to loan me one for a week I can get it listed. + +Also of note, I have gotten these to work with 32-bit copies of Ubuntu 10.10 and pre-Snow Leopard versions of OS X. If you are running Snow Leopard you're out of luck since none of the companies I've contacted make a 64 bit driver. + += Hopefully in the future + +* More PKCS#11 hardware (I need driver support from the manufacturers) + += Todone + +* Support for working with CSRs to request & issue certificates +* OCSP support + += Misc notes + +* Firefox will complain about root/intermediate certificates unless both digitalSignature and keyEncipherment are specified as keyUsage attributes. Thanks diogomonica + += Special thanks and Contributions + +* Diogo Monica @diogo +* Justin Cummins @sul3n3t +* @databus23 +* Colin Jones @trptcolin +* Eric Monti @emonti +* TJ Vanderpoel @bougyman + +== Meta + +Written by Chris Chandler(http://chrischandler.name) + +Released under the MIT License: http://www.opensource.org/licenses/mit-license.php + +Main page: http://github.com/cchandler/certificate_authority + +Issue tracking: https://github.com/cchandler/certificate_authority/issues diff --git a/vendor/gems/certificate_authority/VERSION.yml b/vendor/gems/certificate_authority/VERSION.yml new file mode 100644 index 0000000..437bf68 --- /dev/null +++ b/vendor/gems/certificate_authority/VERSION.yml @@ -0,0 +1,5 @@ +--- +:major: 0 +:minor: 2 +:patch: 0 +:build: diff --git a/vendor/gems/certificate_authority/certificate_authority.gemspec b/vendor/gems/certificate_authority/certificate_authority.gemspec new file mode 100644 index 0000000..b7e8676 --- /dev/null +++ b/vendor/gems/certificate_authority/certificate_authority.gemspec @@ -0,0 +1,91 @@ +# Generated by jeweler +# DO NOT EDIT THIS FILE DIRECTLY +# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' +# -*- encoding: utf-8 -*- + +Gem::Specification.new do |s| + s.name = "certificate_authority" + s.version = "0.2.0" + + s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= + s.authors = ["Chris Chandler"] + s.date = "2012-09-16" + s.email = "chris@flatterline.com" + s.extra_rdoc_files = [ + "README.rdoc" + ] + s.files = [ + "Gemfile", + "Gemfile.lock", + "README.rdoc", + "Rakefile", + "VERSION.yml", + "certificate_authority.gemspec", + "lib/certificate_authority.rb", + "lib/certificate_authority/certificate.rb", + "lib/certificate_authority/certificate_revocation_list.rb", + "lib/certificate_authority/distinguished_name.rb", + "lib/certificate_authority/extensions.rb", + "lib/certificate_authority/key_material.rb", + "lib/certificate_authority/ocsp_handler.rb", + "lib/certificate_authority/pkcs11_key_material.rb", + "lib/certificate_authority/revocable.rb", + "lib/certificate_authority/serial_number.rb", + "lib/certificate_authority/signing_entity.rb", + "lib/certificate_authority/signing_request.rb", + "lib/tasks/certificate_authority.rake", + "spec/samples/certs/DigiCertHighAssuranceEVCA-1.pem", + "spec/samples/certs/apple_wwdr_issued_cert.pem", + "spec/samples/certs/apple_wwdr_issuer.pem", + "spec/samples/certs/ca.crt", + "spec/samples/certs/ca.key", + "spec/samples/certs/client.crt", + "spec/samples/certs/client.csr", + "spec/samples/certs/client.key", + "spec/samples/certs/github.com.pem", + "spec/samples/certs/server.crt", + "spec/samples/certs/server.csr", + "spec/samples/certs/server.key", + "spec/spec_helper.rb", + "spec/units/certificate_authority_spec.rb", + "spec/units/certificate_revocation_list_spec.rb", + "spec/units/certificate_spec.rb", + "spec/units/distinguished_name_spec.rb", + "spec/units/extensions_spec.rb", + "spec/units/key_material_spec.rb", + "spec/units/ocsp_handler_spec.rb", + "spec/units/pkcs11_key_material_spec.rb", + "spec/units/serial_number_spec.rb", + "spec/units/signing_entity_spec.rb", + "spec/units/signing_request_spec.rb", + "spec/units/units_helper.rb", + "spec/units/working_with_openssl_spec.rb" + ] + s.homepage = "https://github.com/cchandler/certificate_authority" + s.licenses = ["MIT"] + s.require_paths = ["lib"] + s.rubygems_version = "1.8.15" + s.summary = "Ruby gem for managing the core functions outlined in RFC-3280 for PKI" + + if s.respond_to? :specification_version then + s.specification_version = 3 + + 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 +end + diff --git a/vendor/gems/certificate_authority/lib/certificate_authority.rb b/vendor/gems/certificate_authority/lib/certificate_authority.rb new file mode 100644 index 0000000..a697c1b --- /dev/null +++ b/vendor/gems/certificate_authority/lib/certificate_authority.rb @@ -0,0 +1,21 @@ +$:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) + +#Exterior requirements +require 'openssl' +require 'active_model' + +#Internal modules +require 'certificate_authority/signing_entity' +require 'certificate_authority/revocable' +require 'certificate_authority/distinguished_name' +require 'certificate_authority/serial_number' +require 'certificate_authority/key_material' +require 'certificate_authority/pkcs11_key_material' +require 'certificate_authority/extensions' +require 'certificate_authority/certificate' +require 'certificate_authority/certificate_revocation_list' +require 'certificate_authority/ocsp_handler' +require 'certificate_authority/signing_request' + +module CertificateAuthority +end diff --git a/vendor/gems/certificate_authority/lib/certificate_authority/certificate.rb b/vendor/gems/certificate_authority/lib/certificate_authority/certificate.rb new file mode 100644 index 0000000..0bddb86 --- /dev/null +++ b/vendor/gems/certificate_authority/lib/certificate_authority/certificate.rb @@ -0,0 +1,258 @@ +require 'active_support/all' + +module CertificateAuthority + class Certificate + include ActiveModel::Validations + include Revocable + + attr_accessor :distinguished_name + attr_accessor :serial_number + attr_accessor :key_material + attr_accessor :not_before + attr_accessor :not_after + attr_accessor :extensions + attr_accessor :openssl_body + + alias :subject :distinguished_name #Same thing as the DN + + attr_accessor :parent + + validate do |certificate| + errors.add :base, "Distinguished name must be valid" unless distinguished_name.valid? + errors.add :base, "Key material must be valid" unless key_material.valid? + errors.add :base, "Serial number must be valid" unless serial_number.valid? + errors.add :base, "Extensions must be valid" unless extensions.each do |item| + unless item.respond_to?(:valid?) + true + else + item.valid? + end + end + end + + def initialize + self.distinguished_name = DistinguishedName.new + self.serial_number = SerialNumber.new + self.key_material = MemoryKeyMaterial.new + 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() + + self.signing_entity = false + + 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.not_before = self.not_before + openssl_cert.not_after = self.not_after + openssl_cert.public_key = self.key_material.public_key + + openssl_cert.serial = self.serial_number.number + + openssl_cert.subject = self.distinguished_name.to_x509_name + openssl_cert.issuer = parent.distinguished_name.to_x509_name + + require 'tempfile' + t = Tempfile.new("bullshit_conf") + ## The config requires a file even though we won't use it + openssl_config = OpenSSL::Config.new(t.path) + + factory = OpenSSL::X509::ExtensionFactory.new + factory.subject_certificate = openssl_cert + + #NB: If the parent doesn't have an SSL body we're making this a self-signed cert + if parent.openssl_body.nil? + factory.issuer_certificate = openssl_cert + else + factory.issuer_certificate = parent.openssl_body + end + + self.extensions.keys.each do |k| + config_extensions = extensions[k].config_extensions + openssl_config = merge_options(openssl_config,config_extensions) + end + + # p openssl_config.sections + + factory.config = openssl_config + + # Order matters: e.g. for self-signed, subjectKeyIdentifier must come before authorityKeyIdentifier + 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, e.critical) + openssl_cert.add_extension(ext) + end + + if signing_profile["digest"].nil? + digest = OpenSSL::Digest.new("SHA512") + else + digest = OpenSSL::Digest.new(signing_profile["digest"]) + end + + 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? + self.extensions["basicConstraints"].ca + end + + def signing_entity=(signing) + self.extensions["basicConstraints"].ca = signing + end + + def revoked? + !self.revoked_at.nil? + end + + def to_pem + raise "Certificate has no signed body" if self.openssl_body.nil? + 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 + + def is_intermediate_entity? + (self.parent != self) && is_signing_entity? + end + + private + + def merge_profile_with_extensions(signing_profile={}) + return self.extensions if signing_profile["extensions"].nil? + signing_config = signing_profile["extensions"] + signing_config.keys.each do |k| + extension = self.extensions[k] + 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!" + end + end + 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 = {} + + EXTENSIONS.each do |klass| + extension = klass.new + extension_hash[extension.openssl_identifier] = extension + end + + extension_hash + end + + def merge_options(config,hash) + hash.keys.each do |k| + config[k] = hash[k] + end + config + end + + 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 + 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 + + end +end diff --git a/vendor/gems/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb b/vendor/gems/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb new file mode 100644 index 0000000..1c68d09 --- /dev/null +++ b/vendor/gems/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb @@ -0,0 +1,79 @@ +module CertificateAuthority + class CertificateRevocationList + include ActiveModel::Validations + + attr_accessor :certificates + attr_accessor :parent + attr_accessor :crl_body + attr_accessor :next_update + attr_accessor :last_update_skew_seconds + + validate do |crl| + errors.add :next_update, "Next update must be a positive value" if crl.next_update < 0 + errors.add :parent, "A parent entity must be set" if crl.parent.nil? + end + + def initialize + self.certificates = [] + self.next_update = 60 * 60 * 4 # 4 hour default + self.last_update_skew_seconds = 0 + end + + def <<(revocable) + case revocable + when Revocable + raise "Only revoked entities can be added to a CRL" unless revocable.revoked? + self.certificates << revocable + when OpenSSL::X509::Certificate + raise "Not implemented yet" + else + raise "#{revocable.class} cannot be included in a CRL" + end + end + + def sign!(signing_profile={}) + raise "No parent entity has been set!" if self.parent.nil? + raise "Invalid CRL" unless self.valid? + + revocations = self.certificates.collect do |revocable| + revocation = OpenSSL::X509::Revoked.new + + ## We really just need a serial number, now we have to dig it out + case revocable + when Certificate + x509_cert = OpenSSL::X509::Certificate.new(revocable.to_pem) + revocation.serial = x509_cert.serial + when SerialNumber + revocation.serial = revocable.number + end + revocation.time = revocable.revoked_at + revocation + end + + crl = OpenSSL::X509::CRL.new + revocations.each do |revocation| + crl.add_revoked(revocation) + end + + crl.version = 1 + crl.last_update = Time.now - self.last_update_skew_seconds + crl.next_update = Time.now + self.next_update + + signing_cert = OpenSSL::X509::Certificate.new(self.parent.to_pem) + if signing_profile["digest"].nil? + digest = OpenSSL::Digest.new("SHA512") + else + digest = OpenSSL::Digest.new(signing_profile["digest"]) + end + crl.issuer = signing_cert.subject + self.crl_body = crl.sign(self.parent.key_material.private_key, digest) + + self.crl_body + end + + def to_pem + raise "No signed CRL body" if self.crl_body.nil? + self.crl_body.to_pem + end + end#CertificateRevocationList +end diff --git a/vendor/gems/certificate_authority/lib/certificate_authority/distinguished_name.rb b/vendor/gems/certificate_authority/lib/certificate_authority/distinguished_name.rb new file mode 100644 index 0000000..32d9c1e --- /dev/null +++ b/vendor/gems/certificate_authority/lib/certificate_authority/distinguished_name.rb @@ -0,0 +1,102 @@ +module CertificateAuthority + class DistinguishedName + include ActiveModel::Validations + + validates_presence_of :common_name + + attr_accessor :common_name + alias :cn :common_name + alias :cn= :common_name= + + attr_accessor :locality + alias :l :locality + alias :l= :locality= + + attr_accessor :state + alias :s :state + alias :st= :state= + + attr_accessor :country + alias :c :country + alias :c= :country= + + attr_accessor :organization + alias :o :organization + alias :o= :organization= + + attr_accessor :organizational_unit + alias :ou :organizational_unit + alias :ou= :organizational_unit= + + attr_accessor :email_address + 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? + name.add_entry("O", organization) unless organization.blank? + name.add_entry("OU", organizational_unit) unless organizational_unit.blank? + name.add_entry("CN", common_name) + name.add_entry("emailAddress", email_address) unless email_address.blank? + name + end + + def ==(other) + # Use the established OpenSSL comparison + self.to_x509_name() == other.to_x509_name() + end + + def self.from_openssl openssl_name + unless openssl_name.is_a? OpenSSL::X509::Name + raise "Argument must be a OpenSSL::X509::Name" + end + + WrappedDistinguishedName.new(openssl_name) + end + end + + ## This is a significantly more complicated case. It's possible that + ## generically handled certificates will include custom OIDs in the + ## subject. + class WrappedDistinguishedName < DistinguishedName + attr_accessor :x509_name + + def initialize(x509_name) + @x509_name = x509_name + + subject = @x509_name.to_a + subject.each do |element| + field = element[0].downcase + value = element[1] + #type = element[2] ## -not used + method_sym = "#{field}=".to_sym + if self.respond_to?(method_sym) + self.send("#{field}=",value) + else + ## Custom OID + @custom_oids = true + end + end + + end + + def to_x509_name + @x509_name + end + + def custom_oids? + @custom_oids + end + end +end diff --git a/vendor/gems/certificate_authority/lib/certificate_authority/extensions.rb b/vendor/gems/certificate_authority/lib/certificate_authority/extensions.rb new file mode 100644 index 0000000..89de032 --- /dev/null +++ b/vendor/gems/certificate_authority/lib/certificate_authority/extensions.rb @@ -0,0 +1,631 @@ +module CertificateAuthority + module Extensions + module ExtensionAPI + def to_s + raise "Implementation required" + end + + def self.parse(value, critical) + raise "Implementation required" + end + + def config_extensions + {} + end + + 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 + @critical = false + @ca = false + end + + def openssl_identifier + OPENSSL_IDENTIFIER + end + + def is_ca? + @ca + end + + def path_len=(value) + raise "path_len must be a non-negative integer" if value < 0 or !value.is_a?(Fixnum) + @path_len = value + end + + def to_s + res = [] + res << "CA:#{@ca}" + res << "pathlen:#{@path_len}" unless @path_len.nil? + 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.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 :critical + attr_accessor :uris + + def initialize + @critical = false + @uris = [] + end + + def openssl_identifier + OPENSSL_IDENTIFIER + end + + ## NB: At this time it seems OpenSSL's extension handlers don't support + ## any of the config options the docs claim to support... everything comes back + ## "missing value" on GENERAL NAME. Even if copied verbatim + def config_extensions + { + # "custom_crl_fields" => {"fullname" => "URI:#{fullname}"}, + # "issuer_sect" => {"CN" => "crlissuer.com", "C" => "US", "O" => "shudder"} + } + 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 + 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 + OPENSSL_IDENTIFIER + end + + def to_s + 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 + OPENSSL_IDENTIFIER + end + + def to_s + 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 + @critical = false + @ocsp = [] + @ca_issuers = [] + end + + def openssl_identifier + OPENSSL_IDENTIFIER + end + + def to_s + 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 + @critical = false + @usage = ["digitalSignature", "nonRepudiation"] + end + + def openssl_identifier + OPENSSL_IDENTIFIER + end + + def to_s + 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 + @critical = false + @usage = ["serverAuth"] + end + + def openssl_identifier + OPENSSL_IDENTIFIER + end + + def to_s + 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 :critical + attr_accessor :uris, :dns_names, :ips, :emails + + def initialize + @critical = false + @uris = [] + @dns_names = [] + @ips = [] + @emails = [] + end + + def openssl_identifier + OPENSSL_IDENTIFIER + end + + def uris=(value) + raise "URIs must be an array" unless value.is_a?(Array) + @uris = value + end + + def dns_names=(value) + raise "DNS names must be an array" unless value.is_a?(Array) + @dns_names = value + end + + def ips=(value) + raise "IPs must be an array" unless value.is_a?(Array) + @ips = value + end + + def emails=(value) + raise "Emails must be an array" unless value.is_a?(Array) + @emails = value + end + + def to_s + 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 + + 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 + attr_accessor :explicit_text + attr_accessor :organization + attr_accessor :notice_numbers + + def initialize + self.critical = false + @contains_data = false + end + + def openssl_identifier + OPENSSL_IDENTIFIER + end + + def user_notice=(value={}) + value.keys.each do |key| + self.send("#{key}=".to_sym, value[key]) + end + end + + def config_extensions + config_extension = {} + custom_policies = {} + notice = {} + unless self.policy_identifier.nil? + custom_policies["policyIdentifier"] = self.policy_identifier + end + + if !self.cps_uris.nil? and self.cps_uris.is_a?(Array) + self.cps_uris.each_with_index do |cps_uri,i| + custom_policies["CPS.#{i}"] = cps_uri + end + end + + unless self.explicit_text.nil? + notice["explicitText"] = self.explicit_text + end + + unless self.organization.nil? + notice["organization"] = self.organization + end + + unless self.notice_numbers.nil? + notice["noticeNumbers"] = self.notice_numbers + end + + if notice.keys.size > 0 + custom_policies["userNotice.1"] = "@notice" + config_extension["notice"] = notice + end + + if custom_policies.keys.size > 0 + config_extension["custom_policies"] = custom_policies + @contains_data = true + end + + config_extension + end + + def to_s + return "" unless @contains_data + 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 + + end +end diff --git a/vendor/gems/certificate_authority/lib/certificate_authority/key_material.rb b/vendor/gems/certificate_authority/lib/certificate_authority/key_material.rb new file mode 100644 index 0000000..1fd4dd9 --- /dev/null +++ b/vendor/gems/certificate_authority/lib/certificate_authority/key_material.rb @@ -0,0 +1,114 @@ +module CertificateAuthority + module KeyMaterial + def public_key + raise "Required implementation" + end + + def private_key + raise "Required implementation" + end + + def is_in_hardware? + raise "Required implementation" + end + + def is_in_memory? + raise "Required implementation" + end + + def self.from_x509_key_pair(pair,password=nil) + if password.nil? + key = OpenSSL::PKey::RSA.new(pair) + else + key = OpenSSL::PKey::RSA.new(pair,password) + end + mem_key = MemoryKeyMaterial.new + mem_key.public_key = key.public_key + mem_key.private_key = key + mem_key + end + + def self.from_x509_public_key(public_key_pem) + key = OpenSSL::PKey::RSA.new(public_key_pem) + signing_request_key = SigningRequestKeyMaterial.new + signing_request_key.public_key = key.public_key + signing_request_key + end + end + + class MemoryKeyMaterial + include KeyMaterial + include ActiveModel::Validations + + attr_accessor :keypair + attr_accessor :private_key + attr_accessor :public_key + + def initialize + end + + validates_each :private_key do |record, attr, value| + record.errors.add :private_key, "cannot be blank" if record.private_key.nil? + end + validates_each :public_key do |record, attr, value| + record.errors.add :public_key, "cannot be blank" if record.public_key.nil? + end + + def is_in_hardware? + false + end + + def is_in_memory? + true + end + + def generate_key(modulus_bits=2048) + self.keypair = OpenSSL::PKey::RSA.new(modulus_bits) + self.private_key = keypair + self.public_key = keypair.public_key + self.keypair + end + + def private_key + @private_key + end + + def public_key + @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/gems/certificate_authority/lib/certificate_authority/ocsp_handler.rb b/vendor/gems/certificate_authority/lib/certificate_authority/ocsp_handler.rb new file mode 100644 index 0000000..e101f98 --- /dev/null +++ b/vendor/gems/certificate_authority/lib/certificate_authority/ocsp_handler.rb @@ -0,0 +1,144 @@ +module CertificateAuthority + class OCSPResponseBuilder + attr_accessor :ocsp_response + attr_accessor :verification_mechanism + attr_accessor :ocsp_request_reader + attr_accessor :parent + attr_accessor :next_update + + GOOD = OpenSSL::OCSP::V_CERTSTATUS_GOOD + REVOKED = OpenSSL::OCSP::V_CERTSTATUS_REVOKED + + NO_REASON=0 + KEY_COMPROMISED=OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE + UNSPECIFIED=OpenSSL::OCSP::REVOKED_STATUS_UNSPECIFIED + + def build_response() + raise "Requires a parent for signing" if @parent.nil? + if @verification_mechanism.nil? + ## If no verification callback is provided we're marking it GOOD + @verification_mechanism = lambda {|cert_id| [GOOD,NO_REASON] } + end + + @ocsp_request_reader.ocsp_request.certid.each do |cert_id| + result,reason = verification_mechanism.call(cert_id.serial) + + ## cert_id, status, reason, rev_time, this update, next update, ext + ## - unit of time is seconds + ## - rev_time is currently set to "now" + @ocsp_response.add_status(cert_id, + result, reason, + 0, 0, @next_update, nil) + end + + @ocsp_response.sign(OpenSSL::X509::Certificate.new(@parent.to_pem), @parent.key_material.private_key, nil, nil) + OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, @ocsp_response) + end + + def self.from_request_reader(request_reader,verification_mechanism=nil) + response_builder = OCSPResponseBuilder.new + response_builder.ocsp_request_reader = request_reader + + ocsp_response = OpenSSL::OCSP::BasicResponse.new + ocsp_response.copy_nonce(request_reader.ocsp_request) + response_builder.ocsp_response = ocsp_response + response_builder.next_update = 60*15 #Default of 15 minutes + response_builder + end + end + + class OCSPRequestReader + attr_accessor :raw_ocsp_request + attr_accessor :ocsp_request + + def serial_numbers + @ocsp_request.certid.collect do |cert_id| + cert_id.serial + end + end + + def self.from_der(request_body) + reader = OCSPRequestReader.new + reader.raw_ocsp_request = request_body + reader.ocsp_request = OpenSSL::OCSP::Request.new(request_body) + + reader + end + end + + ## DEPRECATED + class OCSPHandler + include ActiveModel::Validations + + attr_accessor :ocsp_request + attr_accessor :certificate_ids + + attr_accessor :certificates + attr_accessor :parent + + attr_accessor :ocsp_response_body + + validate do |crl| + errors.add :parent, "A parent entity must be set" if parent.nil? + end + validate :all_certificates_available + + def initialize + self.certificates = {} + end + + def <<(cert) + self.certificates[cert.serial_number.number.to_s] = cert + end + + def extract_certificate_serials + openssl_request = OpenSSL::OCSP::Request.new(@ocsp_request) + + self.certificate_ids = openssl_request.certid.collect do |cert_id| + cert_id.serial + end + + self.certificate_ids + end + + + def response + raise "Invalid response" unless valid? + + openssl_ocsp_response = OpenSSL::OCSP::BasicResponse.new + openssl_ocsp_request = OpenSSL::OCSP::Request.new(self.ocsp_request) + openssl_ocsp_response.copy_nonce(openssl_ocsp_request) + + openssl_ocsp_request.certid.each do |cert_id| + certificate = self.certificates[cert_id.serial.to_s] + + openssl_ocsp_response.add_status(cert_id, + OpenSSL::OCSP::V_CERTSTATUS_GOOD, 0, + 0, 0, 30, nil) + end + + + openssl_ocsp_response.sign(OpenSSL::X509::Certificate.new(self.parent.to_pem), self.parent.key_material.private_key, nil, nil) + final_response = OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, openssl_ocsp_response) + self.ocsp_response_body = final_response + self.ocsp_response_body + end + + def to_der + raise "No signed OCSP response body available" if self.ocsp_response_body.nil? + self.ocsp_response_body.to_der + end + + private + + def all_certificates_available + openssl_ocsp_request = OpenSSL::OCSP::Request.new(self.ocsp_request) + + openssl_ocsp_request.certid.each do |cert_id| + certificate = self.certificates[cert_id.serial.to_s] + errors.add(:base, "Certificate #{cert_id.serial} has not been added yet") if certificate.nil? + end + end + + end +end diff --git a/vendor/gems/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb b/vendor/gems/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb new file mode 100644 index 0000000..d4ebc47 --- /dev/null +++ b/vendor/gems/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb @@ -0,0 +1,65 @@ +module CertificateAuthority + class Pkcs11KeyMaterial + include KeyMaterial + include ActiveModel::Validations + include ActiveModel::Serialization + + attr_accessor :engine + attr_accessor :token_id + attr_accessor :pkcs11_lib + attr_accessor :openssl_pkcs11_engine_lib + attr_accessor :pin + + def initialize(attributes = {}) + @attributes = attributes + initialize_engine + end + + def is_in_hardware? + true + end + + def is_in_memory? + false + end + + def generate_key(modulus_bits=1024) + puts "Key generation is not currently supported in hardware" + nil + end + + def private_key + initialize_engine + self.engine.load_private_key(self.token_id) + end + + def public_key + initialize_engine + self.engine.load_public_key(self.token_id) + end + + private + + def initialize_engine + ## We're going to return early and try again later if params weren't passed in + ## at initialization. Any attempt at getting a public/private key will try + ## again. + return false if self.openssl_pkcs11_engine_lib.nil? or self.pkcs11_lib.nil? + return self.engine unless self.engine.nil? + OpenSSL::Engine.load + + pkcs11 = OpenSSL::Engine.by_id("dynamic") do |e| + e.ctrl_cmd("SO_PATH",self.openssl_pkcs11_engine_lib) + e.ctrl_cmd("ID","pkcs11") + e.ctrl_cmd("LIST_ADD","1") + e.ctrl_cmd("LOAD") + e.ctrl_cmd("PIN",self.pin) unless self.pin.nil? or self.pin == "" + e.ctrl_cmd("MODULE_PATH",self.pkcs11_lib) + end + + self.engine = pkcs11 + pkcs11 + end + + end +end diff --git a/vendor/gems/certificate_authority/lib/certificate_authority/revocable.rb b/vendor/gems/certificate_authority/lib/certificate_authority/revocable.rb new file mode 100644 index 0000000..eba5d98 --- /dev/null +++ b/vendor/gems/certificate_authority/lib/certificate_authority/revocable.rb @@ -0,0 +1,14 @@ +module CertificateAuthority + module Revocable + attr_accessor :revoked_at + + def revoke!(time=Time.now) + @revoked_at = time + end + + def revoked? + # If we have a time, then we're revoked + !@revoked_at.nil? + end + end +end diff --git a/vendor/gems/certificate_authority/lib/certificate_authority/serial_number.rb b/vendor/gems/certificate_authority/lib/certificate_authority/serial_number.rb new file mode 100644 index 0000000..143c144 --- /dev/null +++ b/vendor/gems/certificate_authority/lib/certificate_authority/serial_number.rb @@ -0,0 +1,14 @@ +module CertificateAuthority + class SerialNumber + include ActiveModel::Validations + include Revocable + + 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/gems/certificate_authority/lib/certificate_authority/signing_entity.rb b/vendor/gems/certificate_authority/lib/certificate_authority/signing_entity.rb new file mode 100644 index 0000000..748350b --- /dev/null +++ b/vendor/gems/certificate_authority/lib/certificate_authority/signing_entity.rb @@ -0,0 +1,16 @@ +module CertificateAuthority + module SigningEntity + + def self.included(mod) + mod.class_eval do + attr_accessor :signing_entity + end + end + + def signing_entity=(val) + raise "invalid param" unless [true,false].include?(val) + @signing_entity = val + end + + end +end diff --git a/vendor/gems/certificate_authority/lib/certificate_authority/signing_request.rb b/vendor/gems/certificate_authority/lib/certificate_authority/signing_request.rb new file mode 100644 index 0000000..3584dac --- /dev/null +++ b/vendor/gems/certificate_authority/lib/certificate_authority/signing_request.rb @@ -0,0 +1,91 @@ +module CertificateAuthority + class SigningRequest + attr_accessor :distinguished_name + attr_accessor :key_material + 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 + if !@distinguished_name.nil? + 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 + + def to_pem + to_x509_csr.to_pem + end + + def to_x509_csr + raise "Must specify a DN/subject on csr" if @distinguished_name.nil? + 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.new(@digest || "SHA512") + opensslcsr + end + + def self.from_x509_csr(raw_csr) + csr = SigningRequest.new + openssl_csr = OpenSSL::X509::Request.new(raw_csr) + 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 + csr + end + + def self.from_netscape_spkac(raw_spkac) + openssl_spkac = OpenSSL::Netscape::SPKI.new raw_spkac + csr = SigningRequest.new + csr.raw_body = raw_spkac + key_material = SigningRequestKeyMaterial.new + key_material.public_key = openssl_spkac.public_key + csr + end + end +end diff --git a/vendor/gems/certificate_authority/lib/tasks/certificate_authority.rake b/vendor/gems/certificate_authority/lib/tasks/certificate_authority.rake new file mode 100644 index 0000000..e7d5bf9 --- /dev/null +++ b/vendor/gems/certificate_authority/lib/tasks/certificate_authority.rake @@ -0,0 +1,23 @@ +require 'certificate_authority' + +namespace :certificate_authority do + desc "Generate a quick self-signed cert" + task :self_signed do + + cn = "http://localhost" + cn = ENV['DOMAIN'] unless ENV['DOMAIN'].nil? + + root = CertificateAuthority::Certificate.new + root.subject.common_name= cn + root.key_material.generate_key + root.signing_entity = true + root.valid? + root.sign! + + print "Your cert for #{cn}\n" + print root.to_pem + + print "Your private key\n" + print root.key_material.private_key.to_pem + end +end |