ssltool 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|