ssltool 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # ssltool-complete-chain
2
+
3
+ Re-orders and completes a chain for a given certificate.
4
+
5
+ More precisely, it takes any string as input, scans for PEMs, detects one that is a certificate for
6
+ a domain name, then goes on to complete its chain, in correct order.
7
+
8
+ The pool of possible chain completions are whatever other certificates that are passed as input, plus
9
+ all certificates in the intermediate and trusted pools (see `var/pools/`).
10
+
11
+ The output is the correct and complete chain. Everything else from the input is discarded.
12
+
13
+ If the chain is incomplete, untrusted, or the certificate is self-signed, warnings will be printed to stderr.
14
+
15
+
16
+ ### Usage
17
+
18
+ `ssltool-complete-chain` can work with either stdin or file arguments, so all of the below are valid:
19
+
20
+ ### pipe a file in:
21
+ $ ssltool-complete-chain < example.com.pem
22
+
23
+ ### pass multiple file names as arguments:
24
+ $ ssltool-complete-chain example.com.pem issuer-intermediates.pem
25
+
26
+ ### pass a file descriptor from a command's stdout:
27
+ $ ssltool-complete-chain <(pbpaste)
28
+
29
+ ### or just pipe that command in:
30
+ $ pbpaste | ssltool-complete-chain
31
+
32
+
33
+
34
+ # Bootstrapping
35
+
36
+ This is how we get the list of trusted roots and the intermediates file.
37
+
38
+ This process has already been done for you, you don't need to repeat it unless you want updated data.
39
+
40
+ 1. Downloaded an updated list of trusted roots:
41
+
42
+ $ SRC="http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1"
43
+ $ curl -s "$SRC" > var/mozilla-certdata.txt
44
+ $ bin/bootstrap-trusted-pems-from-mozilla-certdata < var/mozilla-certdata.txt > var/pools/trusted.pem
45
+
46
+
47
+ 2. Generate the intermediates pool:
48
+
49
+ $ bin/bootstrap-detect-intermediates var/all-the-certs.pem
50
+
51
+ The `var/all-the-certs.pem` is a pool of assorted certificates to extract intermediates from. You'll
52
+ have to compile this file yourself.
53
+
54
+ If circular chains are detected, all members of them will be rejected and printed to stderr. You can
55
+ resolve the cycle manually, and decide which certificate(s) to exclude to break the cycle. Add those
56
+ to `var/pools/excluded.pem` and generate the intermediates pool again.
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ require_relative "../lib/ssltool/certificate"
5
+ require_relative "../lib/ssltool/certificate_store"
6
+
7
+ (puts DATA.read.gsub("$0", File.basename($0)); exit 1) if ARGV.empty? && STDIN.tty?
8
+
9
+ def notify_circular_chains_detected(circular_chains)
10
+ $stderr.puts "The following circular chains were detected:"
11
+ $stderr.puts
12
+ $stderr.puts "Resolve this manually and exclude the necessary certs to break the cycle by putting them your exclude pool; rerun this script."
13
+ $stderr.puts
14
+ $stderr.puts "All certificates currently participating in a cyclic chain were removed from the final intermediate pool."
15
+ $stderr.puts
16
+ cert_numbers = circular_chains.flatten.uniq.each_with_index.inject({}) { |h, (c, i)| h[c] = i; h }
17
+ circular_chains.each { |chain|
18
+ $stderr.puts "---"
19
+ chain.each { |cert|
20
+ $stderr.puts "- cert %d (signed by %s) (signs %s)" % [
21
+ cert_numbers[cert],
22
+ chain.select { |other_cert| cert.signed_by?(other_cert) }.map { |cert| cert_numbers[cert] }.join(", "),
23
+ chain.select { |other_cert| cert.signs?(other_cert) }.map { |cert| cert_numbers[cert] }.join(", "),]}}
24
+
25
+ circular_chains.flatten.uniq.each do |cert|
26
+ $stderr.puts "--- #{cert_numbers[cert]} ---"
27
+ $stderr.puts "subject.....: #{cert.subject}"
28
+ $stderr.puts "issuer......: #{cert.issuer}"
29
+ $stderr.puts "self_signed.: #{cert.self_signed?}"
30
+ $stderr.puts cert.to_s
31
+ end
32
+ end
33
+ public :notify_circular_chains_detected
34
+
35
+ store = SSLTool::CertificateStore.new("file://var/pools")
36
+ store.register_for_circular_chain_detection_notification(self)
37
+ store.detect_and_merge_intermediates!(SSLTool::Certificate.scan(ARGF.read), false)
38
+
39
+ __END__
40
+ Usage:
41
+ $0 <src-pems>
42
+
43
+ <src-pems> is the pool of certificates to extract chain-forming
44
+ intermediates from.
45
+
46
+ If circular chains are detected they'll be printed to stderr,
47
+ and all its members will be excluded from the final intermediate
48
+ pool.
49
+
50
+ You should review the circular chain manually and put the
51
+ offending certs in the exclude pool, then rerun this script.
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ require 'digest/sha1'
4
+
5
+ (puts DATA.read.gsub("$0", File.basename($0)); exit 1) if $stdin.tty? && ARGV.empty?
6
+
7
+ def get_line
8
+ return unless line = ARGF.gets
9
+ line.chomp
10
+ end
11
+
12
+ ### parse the data into the certificates and trust arrays
13
+ license = ""
14
+ certificates = []
15
+ trust = []
16
+ while line = get_line
17
+ case line
18
+ when /^#.*BEGIN LICENSE BLOCK/
19
+ begin license << line << "\n"; end until line =~ /^#.*END LICENSE BLOCK/ || !(line = get_line)
20
+ when /^$/; next
21
+ when /^#/; next
22
+ when /^CVS_ID\b/; next
23
+ when "BEGINDATA"; next
24
+ when /^CKA_CLASS CK_OBJECT_CLASS CKO_NSS_BUILTIN_ROOT_LIST$/; object = {}
25
+ when /^CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE$/ ; (certificates << object = {})
26
+ when /^CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST$/ ; (trust << object = {})
27
+ when /^(CKA_\S+) (\S+) (.+)$/ ; object[$1] = [$2, $3]
28
+ when /^(CKA_\S+) (MULTILINE_OCTAL)$/ ; object[$1] = [$2, "".tap { |data| data << line while line = get_line and line != "END" }]
29
+ else raise "Could not parse line: #{line}"
30
+ end
31
+ end
32
+
33
+ ### transform values according to data type; make keys more ruby-friendly
34
+ (certificates + trust).each do |object|
35
+ object.entries.each do |k, (t, v)|
36
+ object.delete k
37
+ object[k.sub(/^CKA_/, '').downcase.to_sym] = case t
38
+ when "CK_CERTIFICATE_TYPE"; v == "CKC_X_509" or raise "CK_CERTIFICATE_TYPE should always be CKC_X_509; Found: #{v.inspect}"; v
39
+ when "CK_TRUST" ; case v
40
+ when "CKT_NSS_MUST_VERIFY_TRUST"; :must_verify_trust
41
+ when "CKT_NSS_NOT_TRUSTED" ; :not_trusted
42
+ when "CKT_NSS_TRUSTED_DELEGATOR"; :trusted_delegator
43
+ when "CKT_NSS_TRUST_UNKNOWN" ; :trust_unknown
44
+ else raise "Unexpected CK_TRUST value: #{v.inspect}"
45
+ end
46
+ when "CK_BBOOL" ; case v
47
+ when "CK_FALSE"; false
48
+ when "CK_TRUE" ; true
49
+ else raise "Unexpected CK_BBOOL value: #{v.inspect}"
50
+ end
51
+ when "UTF8" ; v =~ /\A"(.*)"\z/ or raise "Unexpected UTF8 value: #{v}.inspect"
52
+ $1.force_encoding("UTF-8")
53
+ when "MULTILINE_OCTAL" ; v =~ /\A(\\[0-3][0-7][0-7])*\z/ or raise "Unexpected MULTILINE_OCTAL value: #{v}.inspect"
54
+ v.gsub(/\\([0-3][0-7][0-7])/) { $1.oct.chr }
55
+ else raise "Unexpected type: #{t.inspect}"
56
+ end
57
+ end
58
+ end
59
+
60
+ ### match trust data to certs, merge data
61
+ trust = trust.inject({}) { |h, t| t.key?(:cert_sha1_hash) and h[t[:cert_sha1_hash]] = t; h }
62
+ certificates.each do |cert|
63
+ trust[Digest::SHA1.digest(cert[:value])].each do |k, v|
64
+ raise "Cert / Trust value mismatch for key #{k.inspect}" if cert.key?(k) && cert[k] != v
65
+ cert[k] = v
66
+ end
67
+ end
68
+
69
+ ### select trusted certificates
70
+ trusted_certificates = certificates.select { |c| c[:trust_server_auth] == :trusted_delegator }
71
+
72
+ ### print it all
73
+ puts license
74
+ trusted_certificates.each do |cert|
75
+ puts
76
+ puts cert[:label]
77
+ puts cert[:label].gsub(/./, '=')
78
+ puts "-----BEGIN CERTIFICATE-----"
79
+ puts [cert[:value]].pack("m50")
80
+ puts "-----END CERTIFICATE-----"
81
+ end
82
+
83
+ # Hey, at least it's not MORK!
84
+
85
+
86
+ __END__
87
+ Print trusted certificates from Mozilla's certdata.txt, converting from its crazy-ass data format to saner PEM strings.
88
+
89
+ Usage:
90
+ $0 < certdata.txt
91
+
92
+ Example:
93
+ curl -s 'http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1' | $0
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ require "irb"
5
+ require "yaml"
6
+
7
+ require_relative "../lib/ssltool/certificate_store"
8
+
9
+ $all = SSLTool::Certificate.scan(IO.read("var/all-the-certs.pem")) rescue []
10
+ $store = SSLTool::CertificateStore.new("file://var/pools")
11
+ $trusted = $store.trusted_pool
12
+ $pool = $store.intermediate_pool
13
+ $excluded = $store.excluded_pool
14
+ $domains = $all.select(&:for_domain_name?)
15
+ IRB.start
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ # input..: certificate chain PEM string + garbage
5
+ # output.: oredered / complete certificate chain PEM string
6
+
7
+ require_relative "../lib/ssltool/chain_resolution"
8
+
9
+ def die(msg, code=1)
10
+ $stderr.puts msg unless msg.empty?
11
+ exit code
12
+ end
13
+
14
+ begin
15
+ store = SSLTool::CertificateStore.new("file://var/pools")
16
+ chain = store.resolve_chain_from_pem_string(ARGF.read)
17
+ rescue SSLTool::ChainResolution::ZeroCertsChainResolutionError ; die("No certificate given.", 1)
18
+ rescue SSLTool::ChainResolution::ZeroHeadsChainResolutionError ; die("No certificate given covers a domain name.", 2)
19
+ rescue SSLTool::ChainResolution::TooManyHeadsChainResolutionError; die("More than one domain name certificate given.", 3)
20
+ end
21
+
22
+ puts chain
23
+ $stderr.puts("!! Failed to complete chain.") unless chain.complete?
24
+ $stderr.puts("!! Certificate is self-signed.") if chain.self_signed_untrusted?
25
+ $stderr.puts("!! Certificate is not trusted.") unless chain.trusted? || chain.self_signed?
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ require_relative "../lib/ssltool/certificate_store"
5
+ require_relative "../lib/ssltool/adapters/filesystem"
6
+ require_relative "../lib/ssltool/adapters/sequel"
7
+
8
+ fs_store = SSLTool::CertificateStore::FilesystemAdapter.new "var/pools"
9
+ db_store = SSLTool::CertificateStore::SequelAdapter.new "sqlite://var/pools/store.sqlite"
10
+
11
+ %w[ excluded intermediate trusted ].each do |pool|
12
+ db_store.store_pool(pool, fs_store.load_pool(pool))
13
+ end
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ # input..: any strings containing valid PEM strings
5
+ # output.: valid certificate PEM strings
6
+
7
+ require_relative "../lib/ssltool/certificate"
8
+
9
+ certs = SSLTool::PEMScanner.new(ARGF.read).certs.map(&:strip).uniq.map do |s|
10
+ begin
11
+ SSLTool::Certificate.new(s)
12
+ rescue => e
13
+ $stderr.puts "--- Failed to instantiate certificate from:", s, e
14
+ exit 1
15
+ end
16
+ end.uniq
17
+
18
+ puts certs
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ # input..: multiple certificate PEM string + garbage
5
+ # output.: cert information
6
+
7
+ require_relative "../lib/ssltool/certificate_store"
8
+ require_relative "../lib/ssltool/adapters/filesystem"
9
+
10
+ store = SSLTool::CertificateStore.new("file://var/pools")
11
+ certs = SSLTool::Certificate.scan(ARGF.read)
12
+
13
+ certs.each do |cert|
14
+ trusted = store.trust?(cert)
15
+ puts "---"
16
+ puts cert.to_s.split(/\n/)[1, 2]
17
+ puts "Domains.....: #{cert.domain_names.join(', ')}" if cert.for_domain_name?
18
+ puts "Subject.....: #{cert.subject}"
19
+ puts "Issuer......: #{cert.issuer}"
20
+ puts "Self-signed.: #{cert.self_signed?}"
21
+ puts "Trusted.....: #{trusted}"
22
+ puts "Issued......: #{cert.not_before}"
23
+ puts "Expires.....: #{cert.not_after}"
24
+ puts "Other.......: version #{cert.version} / certificate_sign:#{cert.certificate_sign?} / certificate_authority:#{cert.certificate_authority?}"
25
+ puts
26
+ end
data/lib/ssltool.rb ADDED
@@ -0,0 +1,9 @@
1
+ module SSLTool; end
2
+
3
+ require_relative "ssltool/pem_scanner"
4
+ require_relative "ssltool/certificate"
5
+ require_relative "ssltool/certificate_store"
6
+ require_relative "ssltool/chain_resolution"
7
+ require_relative "ssltool/key_helper"
8
+ require_relative "ssltool/adapters/filesystem"
9
+ require_relative "ssltool/adapters/sequel"
@@ -0,0 +1,18 @@
1
+ # encoding: UTF-8
2
+ require 'set'
3
+
4
+ module SSLTool
5
+ class CertificateStore
6
+ class Adapter
7
+
8
+ def load_pool(pool_name)
9
+ raise NotImplementedError
10
+ end
11
+
12
+ def store_pool(pool_name, certs)
13
+ raise NotImplementedError
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: UTF-8
2
+ require_relative 'base'
3
+
4
+ module SSLTool
5
+ class CertificateStore
6
+ class FilesystemAdapter < Adapter
7
+ def initialize(base_path)
8
+ @base_path = base_path
9
+ end
10
+
11
+ def load_pool(pool_name)
12
+ Certificate.scan(read_pool(pool_name)).to_set
13
+ end
14
+
15
+ def store_pool(pool_name, certs)
16
+ return if read_pool(pool_name) == certs.to_set
17
+ open(pool_path(pool_name), 'w') { |io| io.puts certs.map(&:to_pem).sort }
18
+ end
19
+
20
+ private
21
+
22
+ def pool_path(pool_name)
23
+ File.join(@base_path, "#{pool_name}.pem")
24
+ end
25
+
26
+ def read_pool(pool_name)
27
+ path = pool_path(pool_name)
28
+ return "" unless File.exists?(path)
29
+ File.read(path).strip
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ # encoding: UTF-8
2
+ require 'sequel'
3
+ require_relative 'base'
4
+
5
+ module SSLTool
6
+ class CertificateStore
7
+ class SequelAdapter < Adapter
8
+ def initialize(database_url)
9
+ @database = Sequel.connect(database_url)
10
+ @database.create_table? :certificates do
11
+ column :pool, :varchar, null:false
12
+ column :pem, :text, null:false
13
+ column :fingerprint, :char, null:false, size:40
14
+ index :fingerprint
15
+ index [:pool, :fingerprint], unique:true
16
+ end
17
+ @certificates = @database[:certificates]
18
+ end
19
+
20
+ def load_pool(pool_name)
21
+ @certificates.filter(pool:pool_name.to_s).map(:pem).map { |pem| Certificate.new pem }.to_set
22
+ end
23
+
24
+ def store_pool(pool_name, certs)
25
+ @database.transaction do
26
+ current_set = load_pool(pool_name)
27
+ replacement_set = certs.to_set
28
+ delete_set = (current_set - replacement_set).map { |cert| { fingerprint:cert.fingerprint, pool:pool_name.to_s } }
29
+ insert_set = (replacement_set - current_set).map { |cert| { fingerprint:cert.fingerprint, pool:pool_name.to_s, pem:cert.to_pem } }
30
+ delete_set.each { |params| @certificates.where(params).delete }
31
+ @certificates.multi_insert(insert_set)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,108 @@
1
+ # encoding: UTF-8
2
+ require 'openssl'
3
+ require 'weakref'
4
+ require 'digest/sha1'
5
+
6
+ require_relative 'pem_scanner'
7
+
8
+ module SSLTool
9
+ class Certificate < OpenSSL::X509::Certificate
10
+ RX_DOMAIN_NAME = /^(\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+$/
11
+
12
+ # The certificate_cache stuff ensures we always get the same object every time we instantiate the same certificate.
13
+ # This is regardless of differences in the string used to instantiate the object.
14
+ # The weakrefs ensure we don't hold on to certs that are not being used outside of the cache.
15
+ @@certificate_cache = {}
16
+ def self.new(s)
17
+ cert = super(s)
18
+ k = cert.fingerprint
19
+ @@certificate_cache.delete(k) if v = @@certificate_cache[k] and v.respond_to?(:weakref_alive?) && !v.weakref_alive?
20
+ (@@certificate_cache[k] ||= WeakRef.new(cert)).__getobj__
21
+ end
22
+
23
+ # returns an array of Certificate objects created from cert strings found in s
24
+ def self.scan(s)
25
+ PEMScanner.new(s).certs.map { |s| new(s) }.uniq
26
+ end
27
+
28
+ ### signing
29
+
30
+ def signed_by?(other_cert)
31
+ verify(other_cert.public_key)
32
+ rescue OpenSSL::X509::CertificateError => e
33
+ # catching common error cases and returning nil
34
+ return nil if e.message == "wrong public key type" && other_cert.signature_algorithm =~ /^ecdsa/ # this error was seen with ecdsa-with-SHA384 signers, not sure why
35
+ return nil if e.message == "unknown message digest algorithm" && signature_algorithm =~ /^md2/ # md2 is not present in later versions of openssl
36
+ raise e
37
+ end
38
+
39
+ def signs?(other_cert)
40
+ other_cert.signed_by?(self)
41
+ end
42
+
43
+ def self_signed?
44
+ signs?(self)
45
+ end
46
+
47
+ def self_issued?
48
+ subject.eql?(issuer)
49
+ end
50
+
51
+ ### properties
52
+
53
+ def fingerprint
54
+ @fingerprint ||= Digest::SHA1.hexdigest(to_der)
55
+ end
56
+
57
+ def common_name
58
+ k, v, t = subject.to_a.find { |k, v, t| k == "CN" }; v
59
+ end
60
+
61
+ def for_domain_name?
62
+ common_name =~ RX_DOMAIN_NAME
63
+ end
64
+
65
+ def domain_names
66
+ [ (common_name if for_domain_name?),
67
+ map_extension_value('subjectAltName') { |s| s.scan(/\bDNS:([^\s,]+)/) },
68
+ ].flatten.compact.sort.uniq
69
+ end
70
+
71
+ def certificate_authority?
72
+ map_extension_value('basicConstraints') { |s| s.split(", ").include?('CA:TRUE') }
73
+ end
74
+
75
+ def certificate_sign?
76
+ map_extension_value('keyUsage') { |s| s.split(", ").include?('Certificate Sign') }
77
+ end
78
+
79
+ ### extensions
80
+
81
+ module Extensions
82
+ def [](k)
83
+ return super if k.is_a?(Integer) || !k.respond_to?(:to_str)
84
+ find { |e| e.oid == k.to_str }
85
+ end
86
+ end
87
+
88
+ def extensions
89
+ super.tap { |a| class << a; include Extensions; end }
90
+ end
91
+
92
+ def map_extension_value(extension_name, default = nil)
93
+ e = extensions[extension_name]
94
+ return default if e.nil?
95
+ yield(e.value)
96
+ end
97
+
98
+ ### chain
99
+
100
+ def chain_from(certs)
101
+ chain = [self]
102
+ parent = (certs - chain).find { |cert| cert.signs?(self) }
103
+ chain.concat(parent.chain_from(certs - chain)) if parent
104
+ chain
105
+ end
106
+
107
+ end
108
+ end