#!/usr/bin/env ruby require 'openssl' require 'digest/md5' class CHashDir include Enumerable def initialize(dirpath) @dirpath = dirpath @fingerprint_cache = @cert_cache = @crl_cache = nil end def hash_dir(silent = false) # ToDo: Should lock the directory... @silent = silent @fingerprint_cache = Hash.new @cert_cache = Hash.new @crl_cache = Hash.new do_hash_dir end def get_certs(name = nil) if name @cert_cache[hash_name(name)] else @cert_cache.values.flatten end end def get_crls(name = nil) if name @crl_cache[hash_name(name)] else @crl_cache.values.flatten end end def delete_crl(crl) File.unlink(crl_filename(crl)) hash_dir(true) end def add_crl(crl) File.open(crl_filename(crl), "w") do |f| f << crl.to_pem end hash_dir(true) end def load_pem_file(filepath) str = File.read(filepath) begin OpenSSL::X509::Certificate.new(str) rescue begin OpenSSL::X509::CRL.new(str) rescue begin OpenSSL::X509::Request.new(str) rescue nil end end end end private def crl_filename(crl) path(hash_name(crl.issuer)) + '.pem' end def do_hash_dir Dir.chdir(@dirpath) do delete_symlink Dir.glob('*.pem') do |pemfile| cert = load_pem_file(pemfile) case cert when OpenSSL::X509::Certificate link_hash_cert(pemfile, cert) when OpenSSL::X509::CRL link_hash_crl(pemfile, cert) else STDERR.puts("WARNING: #{pemfile} does not contain a certificate or CRL: skipping") unless @silent end end end end def delete_symlink Dir.entries(".").each do |entry| next unless /^[\da-f]+\.r{0,1}\d+$/ =~ entry File.unlink(entry) if FileTest.symlink?(entry) end end def link_hash_cert(org_filename, cert) name_hash = hash_name(cert.subject) fingerprint = fingerprint(cert.to_der) filepath = link_hash(org_filename, name_hash, fingerprint) { |idx| "#{name_hash}.#{idx}" } unless filepath unless @silent STDERR.puts("WARNING: Skipping duplicate certificate #{org_filename}") end else (@cert_cache[name_hash] ||= []) << path(filepath) end end def link_hash_crl(org_filename, crl) name_hash = hash_name(crl.issuer) fingerprint = fingerprint(crl.to_der) filepath = link_hash(org_filename, name_hash, fingerprint) { |idx| "#{name_hash}.r#{idx}" } unless filepath unless @silent STDERR.puts("WARNING: Skipping duplicate CRL #{org_filename}") end else (@crl_cache[name_hash] ||= []) << path(filepath) end end def link_hash(org_filename, name, fingerprint) idx = 0 filepath = nil while true filepath = yield(idx) break unless FileTest.symlink?(filepath) or FileTest.exist?(filepath) if @fingerprint_cache[filepath] == fingerprint return false end idx += 1 end STDOUT.puts("#{org_filename} => #{filepath}") unless @silent symlink(org_filename, filepath) @fingerprint_cache[filepath] = fingerprint filepath end def symlink(from, to) begin File.symlink(from, to) rescue File.open(to, "w") do |f| f << File.read(from) end end end def path(filename) File.join(@dirpath, filename) end def hash_name(name) sprintf("%x", name.hash) end def fingerprint(der) Digest::MD5.hexdigest(der).upcase end end if $0 == __FILE__ dirlist = ARGV dirlist << '/usr/ssl/certs' if dirlist.empty? dirlist.each do |dir| CHashDir.new(dir).hash_dir end end