1#!/usr/bin/env ruby
2
3require 'openssl'
4require 'digest/md5'
5
6class CHashDir
7  include Enumerable
8
9  def initialize(dirpath)
10    @dirpath = dirpath
11    @fingerprint_cache = @cert_cache = @crl_cache = nil
12  end
13
14  def hash_dir(silent = false)
15    # ToDo: Should lock the directory...
16    @silent = silent
17    @fingerprint_cache = Hash.new
18    @cert_cache = Hash.new
19    @crl_cache = Hash.new
20    do_hash_dir
21  end
22
23  def get_certs(name = nil)
24    if name
25      @cert_cache[hash_name(name)]
26    else
27      @cert_cache.values.flatten
28    end
29  end
30
31  def get_crls(name = nil)
32    if name
33      @crl_cache[hash_name(name)]
34    else
35      @crl_cache.values.flatten
36    end
37  end
38
39  def delete_crl(crl)
40    File.unlink(crl_filename(crl))
41    hash_dir(true)
42  end
43
44  def add_crl(crl)
45    File.open(crl_filename(crl), "w") do |f|
46      f << crl.to_pem
47    end
48    hash_dir(true)
49  end
50
51  def load_pem_file(filepath)
52    str = File.read(filepath)
53    begin
54      OpenSSL::X509::Certificate.new(str)
55    rescue
56      begin
57	OpenSSL::X509::CRL.new(str)
58      rescue
59	begin
60	  OpenSSL::X509::Request.new(str)
61	rescue
62	  nil
63	end
64      end
65    end
66  end
67
68private
69
70  def crl_filename(crl)
71    path(hash_name(crl.issuer)) + '.pem'
72  end
73
74  def do_hash_dir
75    Dir.chdir(@dirpath) do
76      delete_symlink
77      Dir.glob('*.pem') do |pemfile|
78	cert = load_pem_file(pemfile)
79	case cert
80	when OpenSSL::X509::Certificate
81	  link_hash_cert(pemfile, cert)
82	when OpenSSL::X509::CRL
83	  link_hash_crl(pemfile, cert)
84	else
85	  STDERR.puts("WARNING: #{pemfile} does not contain a certificate or CRL: skipping") unless @silent
86	end
87      end
88    end
89  end
90
91  def delete_symlink
92    Dir.entries(".").each do |entry|
93      next unless /^[\da-f]+\.r{0,1}\d+$/ =~ entry
94      File.unlink(entry) if FileTest.symlink?(entry)
95    end
96  end
97
98  def link_hash_cert(org_filename, cert)
99    name_hash = hash_name(cert.subject)
100    fingerprint = fingerprint(cert.to_der)
101    filepath = link_hash(org_filename, name_hash, fingerprint) { |idx|
102      "#{name_hash}.#{idx}"
103    }
104    unless filepath
105      unless @silent
106	STDERR.puts("WARNING: Skipping duplicate certificate #{org_filename}")
107      end
108    else
109      (@cert_cache[name_hash] ||= []) << path(filepath)
110    end
111  end
112
113  def link_hash_crl(org_filename, crl)
114    name_hash = hash_name(crl.issuer)
115    fingerprint = fingerprint(crl.to_der)
116    filepath = link_hash(org_filename, name_hash, fingerprint) { |idx|
117      "#{name_hash}.r#{idx}"
118    }
119    unless filepath
120      unless @silent
121	STDERR.puts("WARNING: Skipping duplicate CRL #{org_filename}")
122      end
123    else
124      (@crl_cache[name_hash] ||= []) << path(filepath)
125    end
126  end
127
128  def link_hash(org_filename, name, fingerprint)
129    idx = 0
130    filepath = nil
131    while true
132      filepath = yield(idx)
133      break unless FileTest.symlink?(filepath) or FileTest.exist?(filepath)
134      if @fingerprint_cache[filepath] == fingerprint
135	return false
136      end
137      idx += 1
138    end
139    STDOUT.puts("#{org_filename} => #{filepath}") unless @silent
140    symlink(org_filename, filepath)
141    @fingerprint_cache[filepath] = fingerprint
142    filepath
143  end
144
145  def symlink(from, to)
146    begin
147      File.symlink(from, to)
148    rescue
149      File.open(to, "w") do |f|
150	f << File.read(from)
151      end
152    end
153  end
154
155  def path(filename)
156    File.join(@dirpath, filename)
157  end
158
159  def hash_name(name)
160    sprintf("%x", name.hash)
161  end
162
163  def fingerprint(der)
164    Digest::MD5.hexdigest(der).upcase
165  end
166end
167
168if $0 == __FILE__
169  dirlist = ARGV
170  dirlist << '/usr/ssl/certs' if dirlist.empty?
171  dirlist.each do |dir|
172    CHashDir.new(dir).hash_dir
173  end
174end
175