diff options
author | elijah <elijah@riseup.net> | 2012-11-16 14:30:20 -0800 |
---|---|---|
committer | elijah <elijah@riseup.net> | 2012-11-16 14:30:20 -0800 |
commit | 76a3a736cfb50cb1c6d926d1e3afb0f504818157 (patch) | |
tree | 95df178ce78ba5220eea267bdb21a04f2f975c75 | |
parent | beb6496309b3640d957428b52b4906a1279457ce (diff) | |
download | leap_cli-76a3a736cfb50cb1c6d926d1e3afb0f504818157.tar.gz leap_cli-76a3a736cfb50cb1c6d926d1e3afb0f504818157.tar.bz2 |
added CSR ability (and vendored certificate_authority gem, so we can get the unreleased fixes we need).
22 files changed, 1365 insertions, 36 deletions
@@ -11,7 +11,8 @@ gemspec # :vendor_path - where this gem is vendored. this path is used if it exists and we are running in 'production mode' # development_gems = { - 'supply_drop' => {:dev_path => '../gems/supply_drop', :vendor_path => 'vendor/supply_drop'} + 'supply_drop' => {:dev_path => '../gems/supply_drop', :vendor_path => 'vendor/supply_drop'}, + 'certificate_authority' => {:dev_path => '../gems/certificate_authority', :vendor_path => 'vendor/certificate_authority'} } # @@ -157,19 +157,31 @@ Ubuntu Install from git -------------------------------------- -Download the source and install the required gems: +Download the source: git clone git://leap.se/leap_cli # clone leap_cli code cd leap_cli - bundle # install required gems + +Running as a gem +-------------------------------------- + +To install ``leap`` as a gem, do this: + + cd leap_cli + rake build + rake install + +And then make sure your PATH is set to include where leap is installed. +It should warn you if this is not the case. Running from the source directory -------------------------------------- -To run the ``leap`` command directly from the source tree, symlink bin/leap -into your path: +To run the ``leap`` command directly from the source tree, you need to install +the required gems using ``bundle`` and symlink ``bin/leap`` into your path: cd leap_cli + bundle # install required gems ln -s `pwd`/bin/leap ~/bin # link executable somewhere in your bin path which leap # make sure you will run leap_cli/bin/leap leap help @@ -183,15 +195,3 @@ other places, it is easier to create the symlink. If you run ``leap`` directly, the command launcher that rubygems installs, leap will run in a mode that simulates ``bundle exec leap`` (i.e. only gems included in Gemfile are allow to be loaded). -Running as a gem --------------------------------------- - -To install ``leap`` as a gem, do this: - - cd leap_cli - rake build - rake install - -And then make sure your PATH is set to include where leap is installed. -It should warn you if this is not the case. - diff --git a/lib/leap_cli/commands/ca.rb b/lib/leap_cli/commands/ca.rb index 5b556a3..1763ba3 100644 --- a/lib/leap_cli/commands/ca.rb +++ b/lib/leap_cli/commands/ca.rb @@ -50,6 +50,7 @@ module LeapCli; module Commands c.action do |global_options,options,args| assert_files_exist! :ca_cert, :ca_key, :msg => 'Run init-ca to create them' assert_config! 'provider.ca.server_certificates.bit_size' + assert_config! 'provider.ca.server_certificates.digest' assert_config! 'provider.ca.server_certificates.life_span' assert_config! 'common.x509.use' @@ -82,6 +83,72 @@ module LeapCli; module Commands end end + # + # hints: + # + # inspect CSR: + # openssl req -noout -text -in files/cert/x.csr + # + # generate CSR with openssl to see how it compares: + # openssl req -sha256 -nodes -newkey rsa:2048 -keyout example.key -out example.csr + # + # validate a CSR: + # http://certlogik.com/decoder/ + # + # nice details about CSRs: + # http://www.redkestrel.co.uk/Articles/CSR.html + # + desc 'Creates a Certificate Signing Request for use in purchasing a commercial x509 certificate' + command :'init-csr' do |c| + c.switch 'sign', :desc => 'additionally creates a cert that is signed by your own CA (recommended only for testing)', :negatable => false + c.action do |global_options,options,args| + assert_config! 'provider.domain' + assert_config! 'provider.name' + assert_config! 'provider.default_language' + assert_config! 'provider.ca.server_certificates.bit_size' + assert_config! 'provider.ca.server_certificates.digest' + assert_files_missing! [:commercial_key, manager.provider.domain], [:commercial_csr, manager.provider.domain], :msg => 'If you really want to create a new key and CSR, remove these files first.' + if options[:sign] + assert_files_exist! :ca_cert, :ca_key, :msg => 'Run init-ca to create them' + end + + # RSA key + keypair = CertificateAuthority::MemoryKeyMaterial.new + log :generating, "%s bit RSA key" % manager.provider.ca.server_certificates.bit_size do + keypair.generate_key(manager.provider.ca.server_certificates.bit_size) + write_file! [:commercial_key, manager.provider.domain], keypair.private_key.to_pem + end + + # CSR + dn = CertificateAuthority::DistinguishedName.new + csr = CertificateAuthority::SigningRequest.new + dn.common_name = manager.provider.domain + dn.organization = manager.provider.name[manager.provider.default_language] + log :generating, "CSR with commonName => '%s', organization => '%s'" % [dn.common_name, dn.organization] do + csr.distinguished_name = dn + csr.key_material = keypair + csr.digest = manager.provider.ca.server_certificates.digest + request = csr.to_x509_csr + write_file! [:commercial_csr, manager.provider.domain], csr.to_pem + end + + # Sign using our own CA, for use in testing but hopefully not production. + # It is not that commerical CAs are so secure, it is just that signing your own certs is + # a total drag for the user because they must click through dire warnings. + if options[:sign] + log :generating, "x509 server certificate for testing purposes" do + cert = csr.to_cert + cert.serial_number.number = cert_serial_number(manager.provider.domain) + cert.not_before = today + cert.not_after = years_from_today(1) + cert.parent = ca_root + cert.sign! test_cert_signing_profile + write_file! [:commercial_cert, manager.provider.domain], cert.to_pem + end + end + end + end + private def cert_needs_updating?(node) @@ -182,11 +249,11 @@ module LeapCli; module Commands # for keyusage, openvpn server certs can have keyEncipherment or keyAgreement. I am not sure which is preferable. # going with keyAgreement for now. # - # digest options: SHA512, SHA1 + # digest options: SHA512, SHA256, SHA1 # def server_signing_profile(node) { - "digest" => "SHA256", + "digest" => manager.provider.ca.server_certificates.digest, "extensions" => { "keyUsage" => { "usage" => ["digitalSignature", "keyAgreement"] @@ -202,6 +269,25 @@ module LeapCli; module Commands } end + # + # This is used when signing the main cert for the provider's domain + # with our own CA (for testing purposes). Typically, this cert would + # be purchased from a commercial CA, and not signed this way. + # + def test_cert_signing_profile + { + "digest" => "SHA256", + "extensions" => { + "keyUsage" => { + "usage" => ["digitalSignature", "keyAgreement"] + }, + "extendedKeyUsage" => { + "usage" => ["serverAuth"] + } + } + } + end + def dns_names_for_node(node) names = [node.domain.internal] if node['dns'] && node.dns['aliases'] && node.dns.aliases.any? diff --git a/lib/leap_cli/log.rb b/lib/leap_cli/log.rb index 1cc1c6a..0821177 100644 --- a/lib/leap_cli/log.rb +++ b/lib/leap_cli/log.rb @@ -8,6 +8,12 @@ module LeapCli def log_level=(value) @log_level = value end + def indent_level + @indent_level ||= 0 + end + def indent_level=(value) + @indent_level = value + end end ## @@ -34,7 +40,8 @@ def log(*args) level = args.grep(Integer).first || 1 title = args.grep(Symbol).first message = args.grep(String).first - options = args.grep(Hash).first || {:indent => 0} + options = args.grep(Hash).first || {} + options[:indent] ||= LeapCli.indent_level if message && LeapCli.log_level >= level print " " * (options[:indent]+1) if options[:indent] > 0 @@ -66,5 +73,10 @@ def log(*args) end end puts "#{message}" + if block_given? + LeapCli.indent_level += 1 + yield + LeapCli.indent_level -= 1 + end end end diff --git a/lib/leap_cli/path.rb b/lib/leap_cli/path.rb index bf4c89f..de01fdb 100644 --- a/lib/leap_cli/path.rb +++ b/lib/leap_cli/path.rb @@ -34,6 +34,9 @@ module LeapCli; module Path :ca_key => 'files/ca/ca.key', :ca_cert => 'files/ca/ca.crt', :dh_params => 'files/ca/dh.pem', + :commercial_key => 'files/cert/#{arg}.key', + :commercial_csr => 'files/cert/#{arg}.csr', + :commercial_cert => 'files/cert/#{arg}.crt', :node_x509_key => 'files/nodes/#{arg}/#{arg}.key', :node_x509_cert => 'files/nodes/#{arg}/#{arg}.crt', :vagrantfile => 'test/Vagrantfile' diff --git a/lib/leap_cli/util.rb b/lib/leap_cli/util.rb index 9b04894..0b0fb9e 100644 --- a/lib/leap_cli/util.rb +++ b/lib/leap_cli/util.rb @@ -55,7 +55,7 @@ module LeapCli # def assert_bin!(cmd_name) assert! `which #{cmd_name}`.strip.any? do - log 0, :missing, "command '%s'" % cmd_name + log :missing, "command '%s'" % cmd_name end end @@ -68,9 +68,9 @@ module LeapCli output = `#{cmd}` unless $?.success? bail! do - log 0, :run, cmd - log 0, :failed, "(exit #{$?.exitstatus}) #{output}", :indent => 1 - log 0, message, :indent => 1 if message + log :run, cmd + log :failed, "(exit #{$?.exitstatus}) #{output}", :indent => 1 + log message, :indent => 1 if message end else log 2, :ran, cmd @@ -86,13 +86,13 @@ module LeapCli }.compact if file_list.length > 1 bail! do - log 0, :error, "Sorry, we can't continue because these files already exist: #{file_list.join(', ')}." - log 0, options[:msg] if options[:msg] + log :error, "Sorry, we can't continue because these files already exist: #{file_list.join(', ')}." + log options[:msg] if options[:msg] end elsif file_list.length == 1 bail! do - log 0, :error, "Sorry, we can't continue because this file already exists: #{file_list.first}." - log 0, options[:msg] if options[:msg] + log :error, "Sorry, we can't continue because this file already exists: #{file_list.first}." + log options[:msg] if options[:msg] end end end @@ -104,8 +104,8 @@ module LeapCli rescue NoMethodError rescue NameError end - assert! !value.nil? do - log 0, :missing, "configuration value for #{conf_path}" + assert! !value.nil? && value != "REQUIRED" do + log :missing, "required configuration value for #{conf_path}" end end @@ -117,13 +117,13 @@ module LeapCli }.compact if file_list.length > 1 bail! do - log 0, :missing, "these files: #{file_list.join(', ')}" - log 0, options[:msg] if options[:msg] + log :missing, "these files: #{file_list.join(', ')}" + log options[:msg] if options[:msg] end elsif file_list.length == 1 bail! do - log 0, :missing, "file #{file_list.first}" - log 0, options[:msg] if options[:msg] + log :missing, "file #{file_list.first}" + log options[:msg] if options[:msg] end end end diff --git a/lib/leap_cli/version.rb b/lib/leap_cli/version.rb index 83c2159..0dbd215 100644 --- a/lib/leap_cli/version.rb +++ b/lib/leap_cli/version.rb @@ -3,6 +3,6 @@ module LeapCli VERSION = '0.1.3' SUMMARY = 'Command line interface to the LEAP platform' DESCRIPTION = 'The command "leap" can be used to manage a bevy of servers running the LEAP platform from the comfort of your own home.' - REQUIRE_PATHS = ['lib', 'vendor/supply_drop/lib'] + REQUIRE_PATHS = ['lib', 'vendor/supply_drop/lib', 'vendor/certificate_authority/lib'] end end diff --git a/test/leap_platform/provider_base/provider.json b/test/leap_platform/provider_base/provider.json index a144d04..de5ad44 100644 --- a/test/leap_platform/provider_base/provider.json +++ b/test/leap_platform/provider_base/provider.json @@ -15,13 +15,15 @@ "organization": "= global.provider.name[global.provider.default_language]", "organizational_unit": "= 'https://' + global.common.domain.full_suffix", "bit_size": 4096, + "digest": "SHA256", "life_span": "10y", "server_certificates": { "bit_size": 3248, + "digest": "SHA256", "life_span": "1y" } }, "vagrant":{ "network":"10.5.5.0/24" } -}
\ No newline at end of file +} diff --git a/vendor/certificate_authority/certificate_authority.gemspec b/vendor/certificate_authority/certificate_authority.gemspec new file mode 100644 index 0000000..be8cd91 --- /dev/null +++ b/vendor/certificate_authority/certificate_authority.gemspec @@ -0,0 +1,88 @@ +# 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 = "http://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_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<rspec>, [">= 0"]) + s.add_dependency(%q<jeweler>, [">= 1.5.2"]) + end + else + s.add_dependency(%q<activemodel>, [">= 3.0.6"]) + s.add_dependency(%q<rspec>, [">= 0"]) + s.add_dependency(%q<jeweler>, [">= 1.5.2"]) + end +end + diff --git a/vendor/certificate_authority/lib/certificate_authority.rb b/vendor/certificate_authority/lib/certificate_authority.rb new file mode 100644 index 0000000..a697c1b --- /dev/null +++ b/vendor/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/certificate_authority/lib/certificate_authority/certificate.rb b/vendor/certificate_authority/lib/certificate_authority/certificate.rb new file mode 100644 index 0000000..ca8bc7c --- /dev/null +++ b/vendor/certificate_authority/lib/certificate_authority/certificate.rb @@ -0,0 +1,200 @@ +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 + self.not_after = Time.now + 60 * 60 * 24 * 365 #One year + self.parent = self + self.extensions = load_extensions() + + self.signing_entity = false + + 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") + # t = File.new("/tmp/openssl.cnf") + ## The config requires a file even though we won't use it + openssl_config = OpenSSL::Config.new(t.path) + + 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) + openssl_cert.add_extension(ext) + end + + if signing_profile["digest"].nil? + digest = OpenSSL::Digest::Digest.new("SHA512") + else + digest = OpenSSL::Digest::Digest.new(signing_profile["digest"]) + end + self.openssl_body = openssl_cert.sign(parent.key_material.private_key,digest) + t.close! if t.is_a?(Tempfile)# We can get rid of the ridiculous temp file + self.openssl_body + 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 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) + 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 + + def load_extensions + extension_hash = {} + + temp_extensions = [] + basic_constraints = CertificateAuthority::Extensions::BasicConstraints.new + temp_extensions << basic_constraints + crl_distribution_points = CertificateAuthority::Extensions::CrlDistributionPoints.new + temp_extensions << crl_distribution_points + subject_key_identifier = CertificateAuthority::Extensions::SubjectKeyIdentifier.new + temp_extensions << subject_key_identifier + authority_key_identifier = CertificateAuthority::Extensions::AuthorityKeyIdentifier.new + temp_extensions << authority_key_identifier + authority_info_access = CertificateAuthority::Extensions::AuthorityInfoAccess.new + temp_extensions << authority_info_access + key_usage = CertificateAuthority::Extensions::KeyUsage.new + temp_extensions << key_usage + extended_key_usage = CertificateAuthority::Extensions::ExtendedKeyUsage.new + temp_extensions << extended_key_usage + subject_alternative_name = CertificateAuthority::Extensions::SubjectAlternativeName.new + temp_extensions << subject_alternative_name + certificate_policies = CertificateAuthority::Extensions::CertificatePolicies.new + temp_extensions << certificate_policies + + temp_extensions.each do |extension| + 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 + # TODO extensions + certificate + end + + end +end diff --git a/vendor/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb b/vendor/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb new file mode 100644 index 0000000..e222e26 --- /dev/null +++ b/vendor/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb @@ -0,0 +1,77 @@ +module CertificateAuthority + class CertificateRevocationList + include ActiveModel::Validations + + attr_accessor :certificates + attr_accessor :parent + attr_accessor :crl_body + attr_accessor :next_update + + 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 + 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 + 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::Digest.new("SHA512") + else + digest = OpenSSL::Digest::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/certificate_authority/lib/certificate_authority/distinguished_name.rb b/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb new file mode 100644 index 0000000..165fe29 --- /dev/null +++ b/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb @@ -0,0 +1,97 @@ +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= + + 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("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/certificate_authority/lib/certificate_authority/extensions.rb b/vendor/certificate_authority/lib/certificate_authority/extensions.rb new file mode 100644 index 0000000..e5a8e85 --- /dev/null +++ b/vendor/certificate_authority/lib/certificate_authority/extensions.rb @@ -0,0 +1,266 @@ +module CertificateAuthority + module Extensions + module ExtensionAPI + def to_s + raise "Implementation required" + end + + def config_extensions + {} + end + + def openssl_identifier + raise "Implementation required" + end + end + + class BasicConstraints + include ExtensionAPI + include ActiveModel::Validations + attr_accessor :ca + attr_accessor :path_len + validates :ca, :inclusion => [true,false] + + def initialize + self.ca = false + end + + def is_ca? + self.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 openssl_identifier + "basicConstraints" + end + + def to_s + result = "" + result += "CA:#{self.ca}" + result += ",pathlen:#{self.path_len}" unless self.path_len.nil? + result + end + end + + class CrlDistributionPoints + include ExtensionAPI + + attr_accessor :uri + + def initialize + # self.uri = "http://moo.crlendPoint.example.com/something.crl" + end + + def openssl_identifier + "crlDistributionPoints" + 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 + + def to_s + return "" if self.uri.nil? + "URI:#{self.uri}" + end + end + + class SubjectKeyIdentifier + include ExtensionAPI + def openssl_identifier + "subjectKeyIdentifier" + end + + def to_s + "hash" + end + end + + class AuthorityKeyIdentifier + include ExtensionAPI + + def openssl_identifier + "authorityKeyIdentifier" + end + + def to_s + "keyid,issuer" + end + end + + class AuthorityInfoAccess + include ExtensionAPI + + attr_accessor :ocsp + + def initialize + self.ocsp = [] + end + + def openssl_identifier + "authorityInfoAccess" + end + + def to_s + return "" if self.ocsp.empty? + "OCSP;URI:#{self.ocsp}" + end + end + + class KeyUsage + include ExtensionAPI + + attr_accessor :usage + + def initialize + self.usage = ["digitalSignature", "nonRepudiation"] + end + + def openssl_identifier + "keyUsage" + end + + def to_s + "#{self.usage.join(',')}" + end + end + + class ExtendedKeyUsage + include ExtensionAPI + + attr_accessor :usage + + def initialize + self.usage = ["serverAuth","clientAuth"] + end + + def openssl_identifier + "extendedKeyUsage" + end + + def to_s + "#{self.usage.join(',')}" + end + end + + class SubjectAlternativeName + include ExtensionAPI + + attr_accessor :uris, :dns_names, :ips + + def initialize + self.uris = [] + self.dns_names = [] + self.ips = [] + 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 openssl_identifier + "subjectAltName" + end + + def to_s + res = self.uris.map {|u| "URI:#{u}" } + res += self.dns_names.map {|d| "DNS:#{d}" } + res += self.ips.map {|i| "IP:#{i}" } + + return res.join(',') + end + end + + class CertificatePolicies + include ExtensionAPI + + attr_accessor :policy_identifier + attr_accessor :cps_uris + ##User notice + attr_accessor :explicit_text + attr_accessor :organization + attr_accessor :notice_numbers + + def initialize + @contains_data = false + end + + + def openssl_identifier + "certificatePolicies" + 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 + "ia5org,@custom_policies" + end + end + + end +end diff --git a/vendor/certificate_authority/lib/certificate_authority/key_material.rb b/vendor/certificate_authority/lib/certificate_authority/key_material.rb new file mode 100644 index 0000000..75ec62e --- /dev/null +++ b/vendor/certificate_authority/lib/certificate_authority/key_material.rb @@ -0,0 +1,148 @@ +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 + + class SigningRequestKeyMaterial + include KeyMaterial + include ActiveModel::Validations + + validates_each :public_key do |record, attr, value| + record.errors.add :public_key, "cannot be blank" if record.public_key.nil? + end + + attr_accessor :public_key + + def initialize(request=nil) + if request.is_a? OpenSSL::X509::Request + raise "Invalid certificate signing request" unless request.verify request.public_key + self.public_key = request.public_key + end + end + + def is_in_hardware? + false + end + + def is_in_memory? + true + end + + def private_key + nil + end + + def public_key + @public_key + end + end +end diff --git a/vendor/certificate_authority/lib/certificate_authority/ocsp_handler.rb b/vendor/certificate_authority/lib/certificate_authority/ocsp_handler.rb new file mode 100644 index 0000000..e101f98 --- /dev/null +++ b/vendor/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/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb b/vendor/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb new file mode 100644 index 0000000..d4ebc47 --- /dev/null +++ b/vendor/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/certificate_authority/lib/certificate_authority/revocable.rb b/vendor/certificate_authority/lib/certificate_authority/revocable.rb new file mode 100644 index 0000000..eba5d98 --- /dev/null +++ b/vendor/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/certificate_authority/lib/certificate_authority/serial_number.rb b/vendor/certificate_authority/lib/certificate_authority/serial_number.rb new file mode 100644 index 0000000..ec0b836 --- /dev/null +++ b/vendor/certificate_authority/lib/certificate_authority/serial_number.rb @@ -0,0 +1,10 @@ +module CertificateAuthority + class SerialNumber + include ActiveModel::Validations + include Revocable + + attr_accessor :number + + validates :number, :presence => true, :numericality => {:greater_than => 0} + end +end diff --git a/vendor/certificate_authority/lib/certificate_authority/signing_entity.rb b/vendor/certificate_authority/lib/certificate_authority/signing_entity.rb new file mode 100644 index 0000000..748350b --- /dev/null +++ b/vendor/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/certificate_authority/lib/certificate_authority/signing_request.rb b/vendor/certificate_authority/lib/certificate_authority/signing_request.rb new file mode 100644 index 0000000..590d5be --- /dev/null +++ b/vendor/certificate_authority/lib/certificate_authority/signing_request.rb @@ -0,0 +1,56 @@ +module CertificateAuthority + class SigningRequest + attr_accessor :distinguished_name + attr_accessor :key_material + attr_accessor :raw_body + attr_accessor :openssl_csr + attr_accessor :digest + + def to_cert + cert = Certificate.new + if !@distinguished_name.nil? + cert.distinguished_name = @distinguished_name + end + cert.key_material = @key_material + 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? + + opensslcsr = OpenSSL::X509::Request.new + opensslcsr.subject = @distinguished_name.to_x509_name + opensslcsr.public_key = @key_material.public_key + opensslcsr.sign @key_material.private_key, OpenSSL::Digest::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 + 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
\ No newline at end of file diff --git a/vendor/certificate_authority/lib/tasks/certificate_authority.rake b/vendor/certificate_authority/lib/tasks/certificate_authority.rake new file mode 100644 index 0000000..e7d5bf9 --- /dev/null +++ b/vendor/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 |