summaryrefslogtreecommitdiffstats
path: root/ext/cert_inspector
blob: 1effcaa04cf0421a1aa8bb5b468e48df742891c2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/usr/bin/env ruby
require 'openssl'

class X509Collector
  include Enumerable

  def initialize
    @collected_data = {}
  end

  def interpret_contents(contents)
    cls = case contents.split("\n")[0]
          when /BEGIN X509 CRL/ then OpenSSL::X509::CRL
          when /BEGIN CERTIFICATE REQUEST/ then OpenSSL::X509::Request
          when /BEGIN CERTIFICATE/ then OpenSSL::X509::Certificate
          when /BEGIN RSA (PRIVATE|PUBLIC) KEY/ then OpenSSL::PKey::RSA
          else return nil
          end
    cls.new(contents)
  rescue
    nil
  end

  def expected_non_x509_files
    ['inventory.txt', 'ca.pass', 'serial']
  end

  def investigate_path(path)
    if File.directory?(path)
      Dir.foreach(path) do |x|
        next if ['.', '..'].include? x
        investigate_path File.join(path, x)
      end
    else
      contents = File.read path
      meaning = interpret_contents contents
      unless meaning || expected_non_x509_files.include?(File.basename(path))
        puts "WARNING: file #{path.inspect} could not be interpreted"
      end
      @collected_data[path] = meaning if meaning
    end
  end

  def each(&block)
    @collected_data.each(&block)
  end

  def extract_public_key_info(path, meaning)
    case meaning
    when OpenSSL::PKey::RSA
      if meaning.private?
        [meaning.public_key, 2, path]
      else
        [meaning, 3, path]
      end
    when OpenSSL::X509::Certificate
      [meaning.public_key, 0, meaning.subject.to_s]
    when OpenSSL::X509::Request
      [meaning.public_key, 1, meaning.subject.to_s]
    end
  end

  def who_signed(meaning, key_names, keys)
    signing_key = keys.find { |key| meaning.verify(key) }
    if signing_key then "#{key_names[signing_key.to_s]}" else "???" end
  end

  def explain(meaning, key_names, keys)
    case meaning
    when OpenSSL::PKey::RSA
      if meaning.private?
        "Private key for #{key_names[meaning.public_key.to_s]}"
      else
        "Public key for #{key_names[meaning.public_key.to_s]}"
      end
    when OpenSSL::X509::Certificate
      signature_desc = who_signed(meaning, key_names, keys)
      "Certificate assigning name #{meaning.subject.to_s} to #{key_names[meaning.public_key.to_s]}\n    serial number #{meaning.serial}\n    issued by #{meaning.issuer.to_s}\n    signed by #{signature_desc}"
    when OpenSSL::X509::Request
      signature_desc = who_signed(meaning, key_names, keys)
      "Certificate request for #{meaning.subject.to_s} having key #{key_names[meaning.public_key.to_s]}\n    signed by #{signature_desc}"
    when OpenSSL::X509::CRL
      signature_desc = who_signed(meaning, key_names, keys)
      revoked_serial_numbers = meaning.revoked.map { |r| r.serial }
      revoked_desc = if revoked_serial_numbers.count > 0 then "serial numbers #{revoked_serial_numbers.inspect}" else "nothing" end
      "Certificate revocation list revoking #{revoked_desc}\n    issued by #{meaning.issuer.to_s}\n    signed by #{signature_desc}"
    else
      "Unknown"
    end
  end

  # Yield unique public keys, with a canonical name for each.
  def collect_public_keys
    key_data = {} # pem => (priority, name, public_key)
    @collected_data.collect do |path, meaning|
      begin
        next unless public_key_info = extract_public_key_info(path, meaning)
        public_key, priority, name = public_key_info
        pem = public_key.to_s
        existing_priority, existing_name, existing_public_key = key_data[pem]
        next if existing_priority and existing_priority < priority
        key_data[pem] = priority, name, public_key
      rescue
        puts "exception!"
      end
    end
    name_to_key_hash = {}
    key_data.each do |pem, data|
      priority, name, public_key = data
      if name_to_key_hash[name]
        suffix_num = 2
        while name_to_key_hash[name + " (#{suffix_num})"]
          suffix_num += 1
        end
        name = name + " (#{suffix_num})"
      end
      name_to_key_hash[name] = public_key
    end
    key_names = {}
    keys = []
    name_to_key_hash.each do |name, public_key|
      key_names[public_key.to_s] = "key<#{name}>"
      keys << public_key
    end
    [key_names, keys]
  end
end

collector = X509Collector.new
ARGV.each do |path|
  collector.investigate_path(path)
end
key_names, keys = collector.collect_public_keys
collector.map do |path, meaning|
  [collector.explain(meaning, key_names, keys), path]
end.sort.each do |description, path|
  puts "#{path}:"
  puts "  #{description}"
  puts
end