summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorLinus Nordberg <linus@nordberg.se>2015-03-23 16:04:10 +0100
committerLinus Nordberg <linus@nordberg.se>2015-03-23 16:04:10 +0100
commit0fd4141586310d7735a376bdbf4541b0d8e88169 (patch)
tree73886c42f951dbffefd52dea94af3365951c2e2b /tools
parent056b69cc4891c5ef1ba8da7e0a9369cb069a03e0 (diff)
parent3f9f38468dd9f43a7a71768dbe84dd40723c30c5 (diff)
Merge branch 'precert2'
Conflicts: Makefile
Diffstat (limited to 'tools')
-rw-r--r--tools/certtools.py48
-rwxr-xr-xtools/merge.py76
-rwxr-xr-xtools/testcase1.py9
-rw-r--r--tools/testcerts/pre1.txt79
-rw-r--r--tools/testcerts/pre2.txt106
-rw-r--r--tools/testcerts/roots/root4.pem19
-rw-r--r--tools/testcerts/roots/root5.pem29
7 files changed, 319 insertions, 47 deletions
diff --git a/tools/certtools.py b/tools/certtools.py
index 2fb1492..0e639f2 100644
--- a/tools/certtools.py
+++ b/tools/certtools.py
@@ -6,6 +6,8 @@ import json
import base64
import urllib
import urllib2
+import ssl
+import urlparse
import struct
import sys
import hashlib
@@ -78,7 +80,7 @@ def get_root_cert(issuer):
return root_cert
def get_sth(baseurl):
- result = urllib2.urlopen(baseurl + "ct/v1/get-sth").read()
+ result = urllib2.urlopen(baseurl + "ct/v1/get-sth", context=ssl.SSLContext(ssl.PROTOCOL_TLSv1)).read()
return json.loads(result)
def get_proof_by_hash(baseurl, hash, tree_size):
@@ -86,7 +88,7 @@ def get_proof_by_hash(baseurl, hash, tree_size):
params = urllib.urlencode({"hash":base64.b64encode(hash),
"tree_size":tree_size})
result = \
- urllib2.urlopen(baseurl + "ct/v1/get-proof-by-hash?" + params).read()
+ urllib2.urlopen(baseurl + "ct/v1/get-proof-by-hash?" + params, context=ssl.SSLContext(ssl.PROTOCOL_TLSv1)).read()
return json.loads(result)
except urllib2.HTTPError, e:
print "ERROR:", e.read()
@@ -97,7 +99,7 @@ def get_consistency_proof(baseurl, tree_size1, tree_size2):
params = urllib.urlencode({"first":tree_size1,
"second":tree_size2})
result = \
- urllib2.urlopen(baseurl + "ct/v1/get-sth-consistency?" + params).read()
+ urllib2.urlopen(baseurl + "ct/v1/get-sth-consistency?" + params, context=ssl.SSLContext(ssl.PROTOCOL_TLSv1)).read()
return json.loads(result)["consistency"]
except urllib2.HTTPError, e:
print "ERROR:", e.read()
@@ -120,8 +122,7 @@ def unpack_tls_array(packed_data, length_len):
def add_chain(baseurl, submission):
try:
- result = urllib2.urlopen(baseurl + "ct/v1/add-chain",
- json.dumps(submission)).read()
+ result = urllib2.urlopen(baseurl + "ct/v1/add-chain", json.dumps(submission), context=ssl.SSLContext(ssl.PROTOCOL_TLSv1)).read()
return json.loads(result)
except urllib2.HTTPError, e:
print "ERROR", e.code,":", e.read()
@@ -139,7 +140,7 @@ def add_chain(baseurl, submission):
def get_entries(baseurl, start, end):
try:
params = urllib.urlencode({"start":start, "end":end})
- result = urllib2.urlopen(baseurl + "ct/v1/get-entries?" + params).read()
+ result = urllib2.urlopen(baseurl + "ct/v1/get-entries?" + params, context=ssl.SSLContext(ssl.PROTOCOL_TLSv1)).read()
return json.loads(result)
except urllib2.HTTPError, e:
print "ERROR:", e.read()
@@ -182,10 +183,35 @@ def check_signature(baseurl, signature, data):
vk.verify(unpacked_signature, data, hashfunc=hashlib.sha256,
sigdecode=ecdsa.util.sigdecode_der)
-def create_signature(privatekey, data):
+def http_request(url, data=None, key=None):
+ req = urllib2.Request(url, data)
+ (keyname, keyfile) = key
+ privatekey = get_eckey_from_file(keyfile)
sk = ecdsa.SigningKey.from_der(privatekey)
- unpacked_signature = sk.sign(data, hashfunc=hashlib.sha256,
- sigencode=ecdsa.util.sigencode_der)
+ parsed_url = urlparse.urlparse(url)
+ if data == None:
+ data = parsed_url.query
+ method = "GET"
+ else:
+ method = "POST"
+ signature = sk.sign("%s\0%s\0%s" % (method, parsed_url.path, data), hashfunc=hashlib.sha256,
+ sigencode=ecdsa.util.sigencode_der)
+ req.add_header('X-Catlfish-Auth', base64.b64encode(signature) + ";key=" + keyname)
+ result = urllib2.urlopen(req, context=ssl.SSLContext(ssl.PROTOCOL_TLSv1)).read()
+ return result
+
+def get_signature(baseurl, data, key=None):
+ try:
+ params = json.dumps({"plop_version":1, "data": base64.b64encode(data)})
+ result = http_request(baseurl + "ct/signing/sth", params, key=key)
+ parsed_result = json.loads(result)
+ return base64.b64decode(parsed_result.get(u"result"))
+ except urllib2.HTTPError, e:
+ print "ERROR: get_signature", e.read()
+ sys.exit(1)
+
+def create_signature(baseurl, data, key=None):
+ unpacked_signature = get_signature(baseurl, data, key)
return encode_signature(4, 3, unpacked_signature)
def check_sth_signature(baseurl, sth):
@@ -200,14 +226,14 @@ def check_sth_signature(baseurl, sth):
check_signature(baseurl, signature, tree_head)
-def create_sth_signature(tree_size, timestamp, root_hash, privatekey):
+def create_sth_signature(tree_size, timestamp, root_hash, baseurl, key=None):
version = struct.pack(">b", 0)
signature_type = struct.pack(">b", 1)
timestamp_packed = struct.pack(">Q", timestamp)
tree_size_packed = struct.pack(">Q", tree_size)
tree_head = version + signature_type + timestamp_packed + tree_size_packed + root_hash
- return create_signature(privatekey, tree_head)
+ return create_signature(baseurl, tree_head, key=key)
def check_sct_signature(baseurl, leafcert, sct):
publickey = base64.decodestring(publickeys[baseurl])
diff --git a/tools/merge.py b/tools/merge.py
index 6becf7e..1b94581 100755
--- a/tools/merge.py
+++ b/tools/merge.py
@@ -14,14 +14,15 @@ import time
import ecdsa
import hashlib
import urlparse
-from certtools import build_merkle_tree, create_sth_signature, check_sth_signature, get_eckey_from_file, timing_point
+import os
+from certtools import build_merkle_tree, create_sth_signature, check_sth_signature, get_eckey_from_file, timing_point, http_request
parser = argparse.ArgumentParser(description="")
parser.add_argument("--baseurl", metavar="url", help="Base URL for CT server", required=True)
parser.add_argument("--frontend", action="append", metavar="url", help="Base URL for frontend server", required=True)
parser.add_argument("--storage", action="append", metavar="url", help="Base URL for storage server", required=True)
parser.add_argument("--mergedb", metavar="dir", help="Merge database directory", required=True)
-parser.add_argument("--keyfile", metavar="keyfile", help="File containing log key", required=True)
+parser.add_argument("--signing", metavar="url", help="Base URL for signing server", required=True)
parser.add_argument("--own-keyname", metavar="keyname", help="The key name of the merge node", required=True)
parser.add_argument("--own-keyfile", metavar="keyfile", help="The file containing the private key of the merge node", required=True)
parser.add_argument("--nomerge", action='store_true', help="Don't actually do merge")
@@ -34,6 +35,10 @@ storagenodes = args.storage
chainsdir = args.mergedb + "/chains"
logorderfile = args.mergedb + "/logorder"
+own_key = (args.own_keyname, args.own_keyfile)
+
+hashed_dir = True
+
def parselogrow(row):
return base64.b16decode(row)
@@ -42,12 +47,26 @@ def get_logorder():
return [parselogrow(row.rstrip()) for row in f]
def write_chain(key, value):
- f = open(chainsdir + "/" + base64.b16encode(key), "w")
+ filename = base64.b16encode(key)
+ if hashed_dir:
+ path = chainsdir + "/" + filename[0:2] + "/" + filename[2:4] + "/" + filename[4:6]
+ try:
+ os.makedirs(path)
+ except Exception, e:
+ print e
+ else:
+ path = chainsdir
+ f = open(path + "/" + filename, "w")
f.write(value)
f.close()
def read_chain(key):
- f = open(chainsdir + "/" + base64.b16encode(key), "r")
+ filename = base64.b16encode(key)
+ path = chainsdir + "/" + filename[0:2] + "/" + filename[2:4] + "/" + filename[4:6]
+ try:
+ f = open(path + "/" + filename, "r")
+ except IOError, e:
+ f = open(chainsdir + "/" + filename, "r")
value = f.read()
f.close()
return value
@@ -57,26 +76,9 @@ def add_to_logorder(key):
f.write(base64.b16encode(key) + "\n")
f.close()
-def http_request(url, data=None):
- req = urllib2.Request(url, data)
- keyname = args.own_keyname
- privatekey = get_eckey_from_file(args.own_keyfile)
- sk = ecdsa.SigningKey.from_der(privatekey)
- parsed_url = urlparse.urlparse(url)
- if data == None:
- data = parsed_url.query
- method = "GET"
- else:
- method = "POST"
- signature = sk.sign("%s\0%s\0%s" % (method, parsed_url.path, data), hashfunc=hashlib.sha256,
- sigencode=ecdsa.util.sigencode_der)
- req.add_header('X-Catlfish-Auth', base64.b64encode(signature) + ";key=" + keyname)
- result = urllib2.urlopen(req).read()
- return result
-
def get_new_entries(baseurl):
try:
- result = http_request(baseurl + "ct/storage/fetchnewentries")
+ result = http_request(baseurl + "ct/storage/fetchnewentries", key=own_key)
parsed_result = json.loads(result)
if parsed_result.get(u"result") == u"ok":
return [base64.b64decode(entry) for entry in parsed_result[u"entries"]]
@@ -89,7 +91,7 @@ def get_new_entries(baseurl):
def get_entries(baseurl, hashes):
try:
params = urllib.urlencode({"hash":[base64.b64encode(hash) for hash in hashes]}, doseq=True)
- result = http_request(baseurl + "ct/storage/getentry?" + params)
+ result = http_request(baseurl + "ct/storage/getentry?" + params, key=own_key)
parsed_result = json.loads(result)
if parsed_result.get(u"result") == u"ok":
entries = dict([(base64.b64decode(entry["hash"]), base64.b64decode(entry["entry"])) for entry in parsed_result[u"entries"]])
@@ -104,7 +106,7 @@ def get_entries(baseurl, hashes):
def get_curpos(baseurl):
try:
- result = http_request(baseurl + "ct/frontend/currentposition")
+ result = http_request(baseurl + "ct/frontend/currentposition", key=own_key)
parsed_result = json.loads(result)
if parsed_result.get(u"result") == u"ok":
return parsed_result[u"position"]
@@ -117,7 +119,7 @@ def get_curpos(baseurl):
def sendlog(baseurl, submission):
try:
result = http_request(baseurl + "ct/frontend/sendlog",
- json.dumps(submission))
+ json.dumps(submission), key=own_key)
return json.loads(result)
except urllib2.HTTPError, e:
print "ERROR: sendlog", e.read()
@@ -133,7 +135,7 @@ def sendlog(baseurl, submission):
def sendentry(baseurl, entry, hash):
try:
result = http_request(baseurl + "ct/frontend/sendentry",
- json.dumps({"entry":base64.b64encode(entry), "treeleafhash":base64.b64encode(hash)}))
+ json.dumps({"entry":base64.b64encode(entry), "treeleafhash":base64.b64encode(hash)}), key=own_key)
return json.loads(result)
except urllib2.HTTPError, e:
print "ERROR: sendentry", e.read()
@@ -149,7 +151,7 @@ def sendentry(baseurl, entry, hash):
def sendsth(baseurl, submission):
try:
result = http_request(baseurl + "ct/frontend/sendsth",
- json.dumps(submission))
+ json.dumps(submission), key=own_key)
return json.loads(result)
except urllib2.HTTPError, e:
print "ERROR: sendsth", e.read()
@@ -164,7 +166,7 @@ def sendsth(baseurl, submission):
def get_missingentries(baseurl):
try:
- result = http_request(baseurl + "ct/frontend/missingentries")
+ result = http_request(baseurl + "ct/frontend/missingentries", key=own_key)
parsed_result = json.loads(result)
if parsed_result.get(u"result") == u"ok":
return parsed_result[u"entries"]
@@ -230,10 +232,9 @@ tree = build_merkle_tree(logorder)
tree_size = len(logorder)
root_hash = tree[-1][0]
timestamp = int(time.time() * 1000)
-privatekey = get_eckey_from_file(args.keyfile)
tree_head_signature = create_sth_signature(tree_size, timestamp,
- root_hash, privatekey)
+ root_hash, args.signing, key=own_key)
sth = {"tree_size": tree_size, "timestamp": timestamp,
"sha256_root_hash": base64.b64encode(root_hash),
@@ -255,7 +256,10 @@ for frontendnode in frontendnodes:
print "current position", curpos
entries = [base64.b64encode(entry) for entry in logorder[curpos:]]
for chunk in chunks(entries, 1000):
- sendlog(frontendnode, {"start": curpos, "hashes": chunk})
+ sendlogresult = sendlog(frontendnode, {"start": curpos, "hashes": chunk})
+ if sendlogresult["result"] != "ok":
+ print "sendlog:", sendlogresult
+ sys.exit(1)
curpos += len(chunk)
print curpos,
sys.stdout.flush()
@@ -266,8 +270,14 @@ for frontendnode in frontendnodes:
print "missing entries:", len(missingentries)
for missingentry in missingentries:
hash = base64.b64decode(missingentry)
- sendentry(frontendnode, read_chain(hash), hash)
+ sendentryresult = sendentry(frontendnode, read_chain(hash), hash)
+ if sendentryresult["result"] != "ok":
+ print "send sth:", sendentryresult
+ sys.exit(1)
timing_point(timing, "send missing")
- sendsth(frontendnode, sth)
+ sendsthresult = sendsth(frontendnode, sth)
+ if sendsthresult["result"] != "ok":
+ print "send sth:", sendsthresult
+ sys.exit(1)
timing_point(timing, "send sth")
print timing["deltatimes"]
diff --git a/tools/testcase1.py b/tools/testcase1.py
index c87e8eb..73613fb 100755
--- a/tools/testcase1.py
+++ b/tools/testcase1.py
@@ -136,13 +136,13 @@ def get_and_check_entry(timestamp, chain, leaf_index):
len(submittedcertchain))
def merge():
- return subprocess.call(["./merge.py", "--baseurl", "https://127.0.0.1:8080/", "--frontend", "https://127.0.0.1:8082/", "--storage", "https://127.0.0.1:8081/", "--mergedb", "../rel/mergedb", "--keyfile", "../rel/test/eckey.pem", "--own-keyname", "merge-1", "--own-keyfile", "../rel/privatekeys/merge-1-private.pem"])
-
-print_and_check_tree_size(0)
+ return subprocess.call(["./merge.py", "--baseurl", "https://127.0.0.1:8080/", "--frontend", "https://127.0.0.1:8082/", "--storage", "https://127.0.0.1:8081/", "--mergedb", "../rel/mergedb", "--signing", "https://127.0.0.1:8088/", "--own-keyname", "merge-1", "--own-keyfile", "../rel/privatekeys/merge-1-private.pem"])
mergeresult = merge()
assert_equal(mergeresult, 0, "merge", quiet=True)
+print_and_check_tree_size(0)
+
testgroup("cert1")
result1 = do_add_chain(cc1)
@@ -230,6 +230,9 @@ get_and_validate_proof(result4["timestamp"], cc3, 2, 3)
get_and_validate_proof(result5["timestamp"], cc4, 3, 3)
get_and_validate_proof(result6["timestamp"], cc5, 4, 1)
+mergeresult = merge()
+assert_equal(mergeresult, 0, "merge", quiet=True)
+
for first_size in range(1, 5):
for second_size in range(first_size + 1, 6):
get_and_validate_consistency_proof(size_sth[first_size], size_sth[second_size], first_size, second_size)
diff --git a/tools/testcerts/pre1.txt b/tools/testcerts/pre1.txt
new file mode 100644
index 0000000..776c38e
--- /dev/null
+++ b/tools/testcerts/pre1.txt
@@ -0,0 +1,79 @@
+Timestamp: 1383337821156
+Leafhash: A4892155FE9929177BCA785A73C15351A3EE2AF6F163DE40C15802BDE0F41302
+-----BEGIN PRECERTIFICATE-----
+MIIGqDCCBZCgAwIBAgIQCxvJV1NZEuon0JIojHqH+DANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5EaWdpQ2VydCBTSEEyIFNlY3Vy
+ZSBTZXJ2ZXIgQ0EwHhcNMTMxMTAxMDAwMDAwWhcNMTQxMTA2MTIwMDAwWjBkMQswCQYDVQQGEwJV
+UzENMAsGA1UECBMEVXRhaDENMAsGA1UEBxMETGVoaTEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4x
+HjAcBgNVBAMTFWVtYmVkLmN0LmRpZ2ljZXJ0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBANExEGl1kTCQJNWXQuTH3m4DWx7xh9Tq+EXHlhorVtgUmHLmBPn7FGC3MH51q0MXN6K7
+huQVXa9LRmCdPRNlNPSkWUqpCVTEqBZrTPuAGEs01+XgXsyhP3uwBxWZkkKJ0FJ4tu7RVHXXgmSC
++JQkSgI4MUNuMaIHvWEpEKsmov9kcQZGUTPnwEg90PyVLlbKypRoFM0dynpslh6FUH4OEAuCx4h1
+tsAN2KHk/ajYE0ND+FN0gBf5qXuY+njUEsDaGiAVKgAb16wOk//0xWy4cTWeHnyLObrsZ3F11GVl
+8cK1x0dNGxgeVfH6yTB8BJu/2wqaQSAdzf14Cie5D8YUXf0CAwEAAaOCA2swggNnMB8GA1UdIwQY
+MBaAFA+AYRyCMWHVLyjnjUY4tCzhxtniMB0GA1UdDgQWBBT8yxF+UXTw/RIW5igB3ZSRrSSkFzAg
+BgNVHREEGTAXghVlbWJlZC5jdC5kaWdpY2VydC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQW
+MBQGCCsGAQUFBwMBBggrBgEFBQcDAjBrBgNVHR8EZDBiMC+gLaArhilodHRwOi8vY3JsMy5kaWdp
+Y2VydC5jb20vc3NjYS1zaGEyLWcxLmNybDAvoC2gK4YpaHR0cDovL2NybDQuZGlnaWNlcnQuY29t
+L3NzY2Etc2hhMi1nMS5jcmwwggHEBgNVHSAEggG7MIIBtzCCAbMGCWCGSAGG/WwBATCCAaQwOgYI
+KwYBBQUHAgEWLmh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5odG0w
+ggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBzACAAQwBl
+AHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBzACAAYQBjAGMAZQBwAHQA
+YQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUAcgB0ACAAQwBQAC8AQwBQAFMAIABh
+AG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUA
+bgB0ACAAdwBoAGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAg
+AGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAgAHIA
+ZQBmAGUAcgBlAG4AYwBlAC4wfAYIKwYBBQUHAQEEcDBuMCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz
+cC5kaWdpY2VydC5jb20wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E
+aWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQwDAYDVR0TAQH/BAIwADATBgorBgEEAdZ5AgQD
+AQH/BAIFADANBgkqhkiG9w0BAQsFAAOCAQEAbHgFxzrmkXjRdQdlHj4Ey2U8rTOetMqjddrXR1DZ
+9E12vp8yWB+LkSVASutpgzxNawj/rv1w1ODdJWMTra12R1MnxqoVytSEmbE0gjgxahdWWiV8yTFB
+4tMFRHvCCwmIJqhRwjufnRs1q1+9YMxZ6reCG4kg29qgtQhh8V9vCrGfQja/4cBHa6O7w407FPra
+b2NIqtJB/47fOdACkVdFjbOVSWielDtTv7QNPi3OUfNwNE/Qqh1k5MOBDP1gif1AFzl5Z7plUos5
+3533VCBjrcOWp8WXUtNlIedlxjarUaTKSRpZVdRzY9ugvou9JLVF1SuDIAXQ3+tN44bjAjERug==
+-----END PRECERTIFICATE-----
+
+-----BEGIN CERTIFICATE-----
+MIIElDCCA3ygAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw
+HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgx
+MjAwMDBaME0xCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJzAlBgNVBAMTHkRp
+Z2lDZXJ0IFNIQTIgU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBANyuWJBNwcQwFZA1W248ghX1LFy949v/cUP6ZCWA1O4Yok3wZtAKc24RmDYXZK83nf36QYSv
+x6+M/hpzTc8zl5CilodTgyu5pnVILR1WN3vaMTIa16yrBvSqXUu3R0bdKpPDkC55gIDvEwRqFDu1
+m5K+wgdlTvza/P96rtxcflUxDOg5B6TXvi/TC2rSsd9f/ld0Uzs1gN2ujkSYs58O09rg1/RrKatE
+p0tYhG2SS4HD2nOLEpdIkARFdRrdNzGXkujNVA075ME/OV4uuPNcfhCOhkEAjUVmR7ChZc6gqikJ
+TvOX6+guqw9ypzAO+sf0/RR3w6RbKFfCs/mC/bdFWJsCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQI
+MAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0
+cDovL29jc3AuZGlnaWNlcnQuY29tMHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6Ly9jcmwzLmRpZ2lj
+ZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwN6A1oDOGMWh0dHA6Ly9jcmw0LmRpZ2lj
+ZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYI
+KwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHQYDVR0OBBYEFA+AYRyCMWHV
+LyjnjUY4tCzhxtniMB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA0GCSqGSIb3DQEB
+CwUAA4IBAQAjPt9L0jFCpbZ+QlwaRMxp0Wi0XUvgBCFsS+JtzLHgl4+mUwnNqipl5TlPHoOlblyY
+oiQm5vuh7ZPHLgLGTUq/sELfeNqzqPlt/yGFUzZgTHbO7Djc1lGA8MXW5dRNJ2Srm8c+cftIl7gz
+bckTB+6WohsYFfZcTEDts8Ls/3HB40f/1LkAtDdC2iDJ6m6K7hQGrn2iWZiIqBtvLfTyyRRfJs8s
+jX7tN8Cp1Tm5gr8ZDOo0rwAhaPitc+LJMto4JQtV05od8GiG7S5BNO98pVAdvzr508EIDObtHopY
+JeS4d60tbvVS3bR0j6tJLp07kzQoH3jOlOrHvdPJbRzeXDLz
+-----END CERTIFICATE-----
+
+-----BEGIN CERTIFICATE-----
+MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw
+HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw
+MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
+dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq
+hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn
+TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5
+BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H
+4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y
+7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB
+o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm
+8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF
+BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr
+EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt
+tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886
+UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
+CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
+-----END CERTIFICATE-----
+
diff --git a/tools/testcerts/pre2.txt b/tools/testcerts/pre2.txt
new file mode 100644
index 0000000..4c86537
--- /dev/null
+++ b/tools/testcerts/pre2.txt
@@ -0,0 +1,106 @@
+Timestamp: 1399629239033
+Leafhash: 758B8612DFED6A3321215C0586C0AC9F43137CD2BBF043C86301D66DC7D1205A
+-----BEGIN PRECERTIFICATE-----
+MIIFFzCCBAGgAwIBAgIgd+115NyVfYOnRINB2wJy2eaQRbJ6j8Zau5IdwBNpmzowCwYJKoZIhvcN
+AQELMGYxLDAqBgNVBAMMI1ByZS1jZXJ0aWZpY2F0ZSBTaWduaW5nIENlcnRpZmljYXRlMRAwDgYD
+VQQLDAdDQSBUZWFtMRcwFQYDVQQKDA5UQUlXQU4tQ0EgSU5DLjELMAkGA1UEBhMCVFcwHhcNMTQw
+NTA5MDk1MzU3WhcNMTQwNTE2MTU1OTU5WjB0MR0wGwYDVQQDDBRjdHRlc3QwNS50d2NhLmNvbS50
+dzELMAkGA1UECwwCUkQxFzAVBgNVBAoMDlRBSVdBTi1DQSBJTkMuMQ8wDQYDVQQHDAZUYWlwZWkx
+DzANBgNVBAgMBlRhaXdhbjELMAkGA1UEBhMCVFcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQDSgb3MYpsqjkNqcOJHIlEmy8ldCzXtmJfoLfvW1g8JyaGgKR6B98ceg1whThF1tPy8aqJv
+fEXGivb+2El1BmxTNvSZ+bOkOT0UsD2hiIgUppD6b/ICWQrIvwrBTNnfJtrwvGD/rygpVTZQoekX
+IVdapI95Cfn+36YXqjX7ixgItEx3t/nzOqBxJNI0p52m9l1sowi2/hGmvc/xqC0Cti4m177c8gP0
+u4oKQRJVF2690F748KfzIMcbS7KbDDDVhtWqwgKaRLvqD+gJAUZ1QYEyzDr5Xhhi1O0FXfhyeeCj
+mRUJBENmhqElt9C1HugaBsno37JP1AQdsuVg776qQQ1PAgMBAAGjggGlMIIBoTArBgNVHSMEJDAi
+gCCVnLtVYCn+QZohG69CSwl1Y2OhEQ7LbPhnh353anz2ezApBgNVHQ4EIgQgt6NL2avrK2PUt8X1
+oG0rd0Wd2ZVDVuJru2T6Z4/eJUEwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2N0dGVzdC50d2Nh
+LmNvbS50dy9zc2xzZXJ2ZXIvY3R0ZXN0LmNybDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwID
+qDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwJQYDVR0RBB4wHIIUY3R0ZXN0MDUudHdj
+YS5jb20udHeHBMCoAckwOgYIKwYBBQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRwOi8vY3R0ZXN0
+LnR3Y2EuY29tLnR3L29jc3AwUQYDVR0gBEowSDBGBgdggR4DAQgFMDswIgYIKwYBBQUHAgEWFmh0
+dHA6Ly93d3cudHdjYS5jb20udHcwFQYIKwYBBQUHAgIwCRoHMC4xLjEuMzATBgorBgEEAdZ5AgQD
+AQH/BAIFADALBgkqhkiG9w0BAQsDggEBAIkN6er89ss6KAZOH/ZpTPbXhO/J8NNq7vJBxhD4z56R
+aRTJpr7Fla9zr8K3aD7bbBUpVeMqER3YA7eeOR8ITBqzMN9SpjdpDlBLcI/6S+7iUVRw4+UvEVqL
+0xlCfdxftGLX+T77y7/qqLxyH+QVuSS4sKzTCfspqAaxteK32A5nWKAiJFUI/ise67o3T9f015hR
+7rHj+U2AomLQwnyiMg4u3D2mYzK9q7VDGJfKIW6wrFYS/lQsFKyb4sUTyGG9VuzgSDIjCXJag7fs
+MZ+/shgsVOTzHUVeHGuKsPcpps0Yvu2W3DybsVoBwtS/vePPnfNfCrDqM9vZCTurvG4KaS4=
+-----END PRECERTIFICATE-----
+
+-----BEGIN CERTIFICATE-----
+MIIEUTCCAzugAwIBAgIEATNR3TALBgkqhkiG9w0BAQswVDELMAkGA1UEBhMCVFcxFzAVBgNVBAoT
+DlRBSVdBTi1DQSBJTkMuMRAwDgYDVQQLEwdDQSBUZWFtMRowGAYDVQQDExFSRCBUV0NBIENUVEVT
+VCBDQTAeFw0xNDA1MDkwOTQzMjZaFw0xNTA1MDkxNTU5NTlaMGYxLDAqBgNVBAMMI1ByZS1jZXJ0
+aWZpY2F0ZSBTaWduaW5nIENlcnRpZmljYXRlMRAwDgYDVQQLDAdDQSBUZWFtMRcwFQYDVQQKDA5U
+QUlXQU4tQ0EgSU5DLjELMAkGA1UEBhMCVFcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQCtFIow0xs7VQ42AEck0o+D8pDDOvIclTPJG7j5+wc7lz1wOwbqP8w06Qa/18tg3sdk16dYFg9k
+pIeOU7suaWgeHifBjjj9iXTELH4U0RP3HwxlM23WArt9a5OKM5KJlA2T9obppnfsN9fm6ZGX4TTY
+JqV8x2vgXSkHhVwxl8wnZoywHlHlgThvVVi+/DzZUD8FIXz2/dPeMtSTfHQ6LqIhee9YMIVgqg/f
+tPb5lOhrJEmAl56mJWi1haVYmxZDSa4+1XCJkOxEzQDPpAvIrXVgAQzr6A5jIHZ7VucTEQ5U/9lx
+Gckzv6CFDRxYyjSpBZsxML/d4A1P9nKdWcABqO9PAgMBAAGjggEbMIIBFzArBgNVHSMEJDAigCCE
+xPSrbrwoBcYxPScQhJ7WOGJB5N3Efkav81dvue7NsjApBgNVHQ4EIgQglZy7VWAp/kGaIRuvQksJ
+dWNjoREOy2z4Z4d+d2p89nswPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2N0dGVzdC50d2NhLmNv
+bS50dy9zc2xzZXJ2ZXIvY3R0ZXN0LmNybDASBgNVHRMBAf8ECDAGAQH/AgEAMBUGA1UdJQQOMAwG
+CisGAQQB1nkCBAQwUQYDVR0gBEowSDBGBgdggR4DAQgFMDswIgYIKwYBBQUHAgEWFmh0dHA6Ly93
+d3cudHdjYS5jb20udHcwFQYIKwYBBQUHAgIwCRoHMC4xLjEuMzALBgkqhkiG9w0BAQsDggEBAN8v
+hr/zNUNSSikqAtRVZVgcJTuN3yTlaXX4hMJFAWrbBqJuN++cE6A2BBTkaLpEZajVqPKL5AxL5KWM
+dMFNkpF3i0UKbf4vnpfrQprsamDX5tKqPCAOKa8yL82CBkimOCmLx24WN+VtNitYzh/MqspApNM7
+7wCO8ncvFtS4sC1Gj5M9CjVhxKmMe15O4SZr9aZpGP7raT4CE3X95APKX5yyiAVwPcOPdPkfRRLQ
+gHko60NbxaeayH5sfWa2dNPEjbOkz0SKaXurV9pzrj/2FZNhgsnRsGIJhx2BLm7FoeUC45RarDJD
+YrscJ6DBR83YwJXsaFCyB7l5CP7L13Wr98E=
+-----END CERTIFICATE-----
+
+-----BEGIN CERTIFICATE-----
+MIIEvjCCAqagAwIBAgIQQAEzUd0AAAAAAAAAFzPdqzANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQG
+EwJUVzEbMBkGA1UEChMSVFdDQSBSRCBEZXBhcnRtZW50MRAwDgYDVQQLEwdDQSBUZWFtMSQwIgYD
+VQQDExtSRCBUV0NBIFJvb3QgQ0EgNDA5NiBTaGEyNTYwHhcNMTQwNTA5MDMyMDUyWhcNMTUwNTA5
+MTU1OTU5WjBUMQswCQYDVQQGEwJUVzEXMBUGA1UEChMOVEFJV0FOLUNBIElOQy4xEDAOBgNVBAsT
+B0NBIFRlYW0xGjAYBgNVBAMTEVJEIFRXQ0EgQ1RURVNUIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEA6xAMprFXHtOkmXIgL63aTx4S20IzdrcdDTMQvwnyYLBhJf8lWz5qeOY37SaC
+4VXILP54qVdML+YBa2BAQfgu6kS+/f73Bp3pSHx8LIHQCt5jdgkNS3OVYK8nQdbWXWeA64bCFdE/
+tlelHSTHtIKXE+v7ug+P5Q/RRBf0Dzi/M1fXTXqXeAga3LaPGPT7o6lZZJh7hp25aJxChIa/1X8x
+99sPx/BqO/WHyYKBCU9Ym05yQFel8mpCgzSbqscKTbKPkvm0ejDANX/WCEziJ3IzR5G9kPoL/zYZ
+ofIqYJMIYRsQRlD/n1ILnMxwdhN3EFlZ0e5xkyIm9TaCqeCZsdFJWQIDAQABo34wfDArBgNVHSME
+JDAigCCwvM16BvA51cl2uO30/ohdOMPVrVBVG5BZ4teNnteYnTApBgNVHQ4EIgQghMT0q268KAXG
+MT0nEISe1jhiQeTdxH5Gr/NXb7nuzbIwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8C
+AQAwDQYJKoZIhvcNAQEFBQADggIBABDkaI3GMCKBfJSfnpmxmiU1ht3cfq/9/hpJSzE6k+of5esV
+D3bYW9nnKScCcBy7poeOoc3C7p9fQtsLZbNfhYpG4/Aq0aVYGtZxw/FCWnXi9rUXpSLZh1yW1uV9
+KBj2D8yzGIx99mpHifjjeoCWG0TW/aaHeIolJm2DhkPTLWjd/urN1TP8YqhEiKMHZI3SFWeeH/BV
+WJKE5rX8vtLW1FPnlRPE+Z/FAa52qOyN4ie0A9IhNPs3gtc+bnhdcZaHnxkySqUvWxqQxkzAGaiO
+VnPlnSlnMCn5I2KOT0XVWYOyU9PP1//V/baDftv7VpT5AOtIaz8mQ6Lp4AIcoPFeU8cgJNZhXgmp
+NOv/dW8lWXH6RYxdM7NFmv98Wk3rKLCzOzR6kuXnARKOJghZf4FV+6Fvjgjf/8wLnzhSdCEbyL7A
+znkOSKc9wzUcZCxF8aTWtRT8HYIu8diZo0CzPxN8OyDl5mPsYexhguPHOXyLv/EljZ8yCdy/SsgQ
+JPzuqKu2a3RD4des15EzbnJOxn4DSeqoUfSfaU/KVfmUKpBEJ3ouD2SLAZ7L+4F6NPCte3HEE2kN
+tOmQIwe65htXmLJxDB+dwMjE4dkA2sETaN2dQ9DqpCNkpNxuNdis/uacAAXnDNddPIlR2moCtUx8
++Y7wlcqBHdmmg5rbFBuBN+esL8J8
+-----END CERTIFICATE-----
+
+-----BEGIN CERTIFICATE-----
+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
+7bTu
+-----END CERTIFICATE-----
+
diff --git a/tools/testcerts/roots/root4.pem b/tools/testcerts/roots/root4.pem
new file mode 100644
index 0000000..3fdb770
--- /dev/null
+++ b/tools/testcerts/roots/root4.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw
+HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw
+MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
+dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq
+hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn
+TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5
+BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H
+4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y
+7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB
+o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm
+8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF
+BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr
+EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt
+tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886
+UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
+CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
+-----END CERTIFICATE-----
diff --git a/tools/testcerts/roots/root5.pem b/tools/testcerts/roots/root5.pem
new file mode 100644
index 0000000..096fd18
--- /dev/null
+++ b/tools/testcerts/roots/root5.pem
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+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
+7bTu
+-----END CERTIFICATE-----