#!/usr/bin/ruby -w

require 'openssl'

evrootstmp = <<EOF
# ------------------------------------------------------------------------------
# Extended Validation CA Policy OIDs
# Last updated: 20 Oct 2008
#
# Each uncommented non-empty line contains a mapping from a CA-defined EV OID
# to the certificate file(s) in ./roots which are authoritative for that OID.
# These lines are processed by the buildEVRoots script to generate the plist.
#

# Comodo
# source: <http://www.mozilla.org/projects/security/certs/included/>
# confirmed by <http://www.comodo.com/repository/EV_CPS_120806.pdf>
#
# (1.3.6.1.4.1.6449.1.2.1.5.1) = 060C2B06010401B2310102010501
#
# root: COMODO Certification Authority
# subordinate CA of: Add Trust External CA Root
#
1.3.6.1.4.1.6449.1.2.1.5.1 \
"COMODOCertificationAuthority.crt" "AddTrust External CA Root.crt"

# Cybertrust (aka Verizon Business)
# source: <http://en.wikipedia.org/wiki/Extended_Validation_Certificate>
# confirmed by <http://cybertrust.omniroot.com/repository.cfm>
#
# (1.3.6.1.4.1.6334.1.100.1) = 060A2B06010401B13E016401
#
# root: GTE Cybertrust Global Root
# root: Baltimore Cybertrust Root
#
1.3.6.1.4.1.6334.1.100.1 "BTCTRT.cer" "GTEGB18.cer"

# DigiCert
# source: <http://www.mozilla.org/projects/security/certs/included/>
# confirmed by <https://www.digicert.com/>
# confirmed by <http://www.digicert.com/CPS_V3-0-3_3-15-2007.pdf>
#
# (2.16.840.1.114412.2.1) = 06096086480186FD6C0201  // EV CA-1
# (2.16.840.1.114412.1.3.0.2) = 060B6086480186FD6C01030002  // EV CA-2
#
# root: DigiCert High Assurance EV Root CA
# subordinate CA of: Entrust.net Secure Server Certification Authority
#
2.16.840.1.114412.2.1 \
"DigiCertHighAssuranceEVRootCA.crt" "EntrustRootCA1024.crt"
2.16.840.1.114412.1.3.0.2 \
"DigiCertHighAssuranceEVRootCA.crt" "EntrustRootCA1024.crt"

# DigiNotar
# source: <http://www.mozilla.org/projects/security/certs/included/>
# confirmed by <https://www.diginotar.com/>
#
# (2.16.528.1.1001.1.1.1.12.6.1.1.1) = 060E6084100187690101010C06010101
#
# root: DigiNotar Root CA
#
2.16.528.1.1001.1.1.1.12.6.1.1.1 "DigiNotarRootCA2007.crt"

# Entrust
# source: <http://www.mozilla.org/projects/security/certs/included/>
# confirmed by <http://www.entrust.net/CPS/pdf/webcps051404.pdf>
#
# (2.16.840.1.114028.10.1.2) = 060A6086480186FA6C0A0102
#
# root: Entrust.net Secure Server Certification Authority
# root: Entrust Root Certification Authority
#
2.16.840.1.114028.10.1.2 "EntrustRootCA1024.crt" "EntrustEVRoot.crt"

# GeoTrust
# source: <http://www.mozilla.org/projects/security/certs/included/>
# confirmed by <http://www.geotrust.com/resources/cps/pdfs/GeoTrustCPS-Version1.pdf>
#
# (1.3.6.1.4.1.14370.1.6) = 06092B06010401F0220106
#
# root: GeoTrust Primary Certification Authority
# subordinate CA of: Equifax Secure Certificate Authority
#
1.3.6.1.4.1.14370.1.6 \
"geotrust-primary-ca.crt" "Equifax_Secure_Certificate_Auth"

# GlobalSign
# source: <http://www.mozilla.org/projects/security/certs/included/>
# confirmed by <https://www.globalsign.com/>
#
# (1.3.6.1.4.1.4146.1.1) = 06092B06010401A0320101
#
# root: GlobalSign Root CA - R2
# root: GlobalSign Root CA
#
1.3.6.1.4.1.4146.1.1 "GlobalSignRootCA-R2.cer" "GlobalSign-RootCA-2028exp.cer"

# Go Daddy (aka Starfield Technologies)
# source: <http://www.mozilla.org/projects/security/certs/included/>
# confirmed by <https://certs.starfieldtech.com/repository/StarfieldCP-CPS.pdf>
#
# (2.16.840.1.114413.1.7.23.3) = 060B6086480186FD6D01071703
# (2.16.840.1.114414.1.7.23.3) = 060B6086480186FD6E01071703
#
# root: Go Daddy Class 2 Certification Authority (for 114413)
# root: Starfield Class 2 Certificate Authority (for 114414)
# subordinate CA of: Valicert Class 2 Policy Validation Authority (both)
#
2.16.840.1.114413.1.7.23.3 "GD-Class2-root.crt" "ValiCertClass2PVA.cer"
2.16.840.1.114414.1.7.23.3 "SF-Class2-root.crt" "ValiCertClass2PVA.cer"

# Network Solutions
# source: <http://www.mozilla.org/projects/security/certs/included/>
# confirmed by <https://www.networksolutions.com/legal/SSL-legal-repository-ev-cps.jsp>
#
# (1.3.6.1.4.1.782.1.2.1.8.1) = 060C2B06010401860E0102010801
#
# root: Network Solutions Certificate Authority
# subordinate CA of: AddTrust External CA Root
#
1.3.6.1.4.1.782.1.2.1.8.1 \
"NetworkSolutionsEVRoot.crt" "AddTrust External CA Root.crt"

# QuoVadis
# source: <http://www.mozilla.org/projects/security/certs/included/>
# confirmed by <http://www.quovadisglobal.bm/Repository.aspx>
#
# (1.3.6.1.4.1.8024.0.2.100.1.2) = 060C2B06010401BE580002640102
#
# root: QuoVadis Root Certification Authority
# root: QuoVadis Root CA 2
#
1.3.6.1.4.1.8024.0.2.100.1.2 "qvrca.crt" "qvrca2.crt"

# Secom (aka SECOM Trust Systems Co., Ltd.)
# source: <https://repository.secomtrust.net/SC-Root1/>
#
# (1.2.392.200091.100.721.1) = ...
#
# root: Security Communication RootCA1
#
1.2.392.200091.100.721.1 \
"SCRoot1ca.cer"

# Trustwave (aka SecureTrust, formerly XRamp)
# source: <http://www.mozilla.org/projects/security/certs/included/>
#
# (2.16.840.1.114404.1.1.2.4.1) = 060C6086480186FD640101020401
#
# root: SecureTrust CA
# root: Secure Global CA
# root: XRamp Global CA
# subordinate CA of: Entrust.net Secure Server Certification Authority
#
2.16.840.1.114404.1.1.2.4.1 \
"Trustwave-STCA.der" "Trustwave-SGCA.der" "XGCA.crt" "EntrustRootCA1024.crt"

# Thawte
# source: <http://www.mozilla.org/projects/security/certs/included/>
#
# (2.16.840.1.113733.1.7.48.1) = 060B6086480186F84501073001
#
# root: thawte Primary Root CA
# subordinate CA of: Thawte Premium Server CA
#
2.16.840.1.113733.1.7.48.1 "thawte-primary-root-ca.crt" "serverpremium.crt"

# VeriSign
# source: <http://www.mozilla.org/projects/security/certs/included/>
#
# (2.16.840.1.113733.1.7.23.6) = 060B6086480186F84501071706
#
# root: VeriSign Class 3 Public Primary Certification Authority - G5
# subordinate CA of: Class 3 Public Primary Certification Authority
#
2.16.840.1.113733.1.7.23.6 \
"VeriSignC3PublicPrimaryCA-G5.cer" "PCA3ss_v4.509"

# ------------------------------------------------------------------------------
EOF

class Anchor
  @@RootDir="/Users/michael/Projects/tla/security_certificates/roots"
  @@hash_to_anchor = {}
  @@anchors = []
  @@issuers = {}

  def self.next_ix(anchor)
    @@anchors << anchor
    return @@anchors.length - 1;
  end

  def self.for_filename(filename)
    path = @@RootDir + '/' + filename
    return nil if !File.file?(path)
    a = Anchor.new(path)
    digest = a.digest
    old_a = @@hash_to_anchor[digest]
    if old_a
      if a.path != old_a.path
          warn "Anchor at #{a.path} is same as #{old_a.path}: skipped"
      end
      return old_a
    end

    a.ix = next_ix(a)
    @@hash_to_anchor[digest] = a
    issuer = a.issuer
    old_a = @@issuers[issuer]
    if old_a
      warn "Anchor at #{a.path} has same issuer as #{old_a.path}"
    else
      @@issuers[issuer] = a
    end
    return a
  end

  def self.loadAll
    Dir.foreach(@@RootDir) { |c| for_filename(c) unless c =~ /^[.]/ }
  end

  def self.dump
    #print gperf(digest_to_ix)
    print gperf(issuer_to_ix)
    #print ix_to_oids
    print anchorslist
    #print ix_to_anchors
  end

  def self.digest_to_ix(buf = "")

    buf << <<EOF
%compare-lengths
%language=ANSI-C
%readonly-tables
%switch=1
%define slot-name digest
%define initializer-suffix _digest
%define length-table-name digest_lengthtable
%define lookup-function-name digest_to_anchor_ix
%define hash-function-name digest_hash
%define word-array-name digest_wordlist
%struct-type
struct digest_to_ix_t { char *digest; int anchor_ix; }
%%
EOF
    @@anchors.each_index { |ix|
      a = @@anchors[ix]
      buf << "#{to_hex_escaped(a.digest)}, #{ix}\n"
    }
    gperf_postamble(buf)
    return buf
  end

  def self.issuer_to_ix(buf = "")
    buf << <<EOF
%compare-lengths
%language=ANSI-C
%readonly-tables
%switch=1
%define slot-name issuer
%define initializer-suffix _issuer
%define length-table-name issuer_lengthtable
%define lookup-function-name issuer_to_anchor_ix
%define hash-function-name issuer_hash
%define word-array-name issuer_wordlist
%struct-type
struct issuer_to_ix_t { char *issuer; int anchor_ix; }
%%
EOF
    @@anchors.each_index { |ix|
      a = @@anchors[ix]
      der = a.issuer.to_der
      # @@@ We need to normalize the issuer still
      #l = der[1]
      #if (l < 0x80)
      #  toskip = 2
      #else
      #  toskip = 2 + l & 0x7f
      #end
      #warn "l: #{l} toskip: #{toskip} #{der.class}"
      #der.slice!(0, toskip)
      buf << "#{to_hex_escaped(der, true)}, #{ix}\n"
    }
    gperf_postamble(buf)
    return buf
  end

  def self.ix_to_oids(buf = "")
    max_ix_with_oids = 0
    max_oids = 0

    @@anchors.each_index { |ix|
      a = @@anchors[ix]
      num_oids = a.oids.length
      if (num_oids > 0)
        max_ix_with_oids = ix #if ix > max_ix_with_oids
        max_oids = num_oids if num_oids > max_oids
      end
    }

    buf << "#define MAX_OIDS_PER_ANCHOR  (#{max_oids})\n"
    buf << "#define MAX_ANCHOR_IX  (#{max_ix_with_oids})\n\n"
    buf << "struct ev_anchor_to_oid {\n"
    max_oids.times { |ix| buf << "    char *oid_#{ix};\n" }
    buf << "};\n\n"

    buf << "struct ev_anchor_to_oid oids_by_anchor_ix = {\n"
    0.upto(max_ix_with_oids) { |ix|
      a = @@anchors[ix]
      buf << "   { "
      a.oids.each { |oid|
        buf << " #{to_hex_escaped(oid_to_binary(oid))},"
      }
      buf << " },\n"
    }
    buf << "};\n\n"
    return buf
  end

  def self.anchorslist(buf = "")
    buf << "struct anchorslist_t { int length; char *data; };\n"
    total_len = 0
    @@anchors.each_index { |ix|
      a = @@anchors[ix]
      len = a.to_der.length
      total_len += len
    }
    buf << "/* Anchors #{@@anchors.length} using #{total_len} bytes. */\n"
    buf << "const struct anchorslist_t anchorslist[] = {\n"
    @@anchors.each_index { |ix|
      a = @@anchors[ix]
      buf << "    /* Anchor: #{ix} serial: #{a.serial.to_s}\n"
      buf << "       #{a.issuer.to_s}\n     */\n"
      buf << "    { #{a.to_der.length}, #{to_hex_escaped(a.to_der, true)} },\n"
    }
    buf << "};\n"
    return buf
  end

  def self.ix_to_anchors(buf = "")
    buf << "/* Anchors struct #{@@anchors.length} anchors. */\n"
    buf << "struct anchorpool_t {\n"
    total_len = 0
    @@anchors.each_index { |ix|
      a = @@anchors[ix]
      len = a.to_der.length
      total_len += len
      buf << "    const char anchor_#{ix}[#{len}];\n"
    }
    buf << "};\n"
    buf << "/* Anchors #{@@anchors.length} using #{total_len} bytes. */\n"
    buf << "const struct anchorpool_t anchorpool_contents = {\n"
    @@anchors.each_index { |ix|
      a = @@anchors[ix]
      buf << "    /* anchor_#{ix} serial: #{a.serial.to_s}\n"
      buf << "       #{a.issuer.to_s}\n     */\n"
      buf << "    #{to_hex_escaped(a.to_der, true)},\n"
    }
    buf << "};\n"
    buf << "#define anchorpool ((const char *) &anchorpool_contents)\n"

    buf << "static const int anchorlist[] = {\n"

    @@anchors.each_index { |ix|
      a = @@anchors[ix]
      buf << "    offsetof(struct anchorpool_t, anchor_#{ix}),\n"
    }
    buf << "};\n"
    return buf
  end

  attr_reader :digest, :path, :oids
  attr_accessor :ix

  def initialize(path)
    @oids = []
    @path = path
    f = File.new(@path, "r")
    #puts "Reading #{path}"
    @cert = OpenSSL::X509::Certificate.new(f)
    f.close()
    @digest = Digest::SHA1.digest(to_der)
    if (issuer.to_s != @cert.subject.to_s)
        raise "#{path}: issuer: #{issuer} != subject: #{@cert.subject}"
    end

    if (!@cert.verify(@cert.public_key))
        raise "#{path}: doesn't self verify"
    end

  end

  def to_der
    return @cert.to_der
  end

  def issuer
    # We need to normalize the issuer
    return @cert.issuer
  end

  def serial
    return @cert.serial
  end

  def add_oid(oid)
    @oids << oid
  end

end

def gperf(program)
  gperf = open("|gperf", "w+")
  gperf.write(program)
  gperf.close_write
  result = gperf.read()
  gperf.close_read
  return result
end

def gperf_postamble(stream)
    stream << <<EOF
%%
#undef TOTAL_KEYWORDS
#undef MIN_WORD_LENGTH
#undef MAX_WORD_LENGTH
#undef MIN_HASH_VALUE
#undef MAX_HASH_VALUE

EOF
end

def oid_to_binary(string_oid)
  der_oid = OpenSSL::ASN1::ObjectId(string_oid).to_der
  return der_oid.slice(2,der_oid.length - 2)
end

def to_hex_escaped(string, hex_only = true)
  res ='"'
  string.each_byte { |b|
    if (hex_only || b < 32 || b > ?~ || b == ?" || b == ?, || b == ?\\ )
      res << '\\' + 'x' + (b/ 16).to_s(16) + (b & 15).to_s(16)
    else
      res << b
    end
  }
  res << '"'
  return res
end

def split_quoted_strings(myString)
  myString.split(/"/).collect{|t| t.strip==t ? t : t.split(' ')}.flatten
end

gperf_input = <<EOF
%compare-lengths
%language=ANSI-C
%readonly-tables
%define slot-name oid
%define initializer-suffix _ev
%define length-table-name ev_lengthtable
%define lookup-function-name ev_oid
%define hash-function-name ev_hash
%define word-array-name ev_wordlist
%switch=1
%struct-type
struct ev_oids { char *oid; }
%%
EOF

evrootstmp.each { |l|
    next if l =~ /^#/
    certs = split_quoted_strings(l)
    oid = certs.delete_at(0)
    next if !oid
    anchors = []
    certs.map { |c|
      a = Anchor.for_filename(c)
      if a
        a.add_oid(oid)
        anchors << a.ix
      else
        warn "#{c}: Not an anchor file"
      end
    }
    gperf_input << to_hex_escaped(oid_to_binary(oid)) << ', ' << "\n"
}
gperf_postamble(gperf_input)

Anchor.loadAll

Anchor.dump

print gperf(gperf_input)
