ssltool 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +56 -0
- data/bin/bootstrap-detect-intermediates +51 -0
- data/bin/bootstrap-trusted-pems-from-mozilla-certdata +93 -0
- data/bin/console +15 -0
- data/bin/ssltool-complete-chain +25 -0
- data/bin/ssltool-dbpool-from-fspool +13 -0
- data/bin/ssltool-filter-certs +18 -0
- data/bin/ssltool-print-certs-info +26 -0
- data/lib/ssltool.rb +9 -0
- data/lib/ssltool/adapters/base.rb +18 -0
- data/lib/ssltool/adapters/filesystem.rb +33 -0
- data/lib/ssltool/adapters/sequel.rb +36 -0
- data/lib/ssltool/certificate.rb +108 -0
- data/lib/ssltool/certificate_store.rb +89 -0
- data/lib/ssltool/chain_resolution.rb +56 -0
- data/lib/ssltool/key_helper.rb +22 -0
- data/lib/ssltool/pem_scanner.rb +21 -0
- data/var/mozilla-certdata.txt +24424 -0
- data/var/pools/excluded.pem +73 -0
- data/var/pools/intermediate.pem +2893 -0
- data/var/pools/trusted.pem +3989 -0
- metadata +67 -0
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
|