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