summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile36
-rw-r--r--src/catlfish.erl234
-rw-r--r--src/v1.erl73
-rw-r--r--src/x509.erl276
-rw-r--r--test/config/frontend-1.config9
-rw-r--r--test/config/privatekeys/signing-1-private.pem5
-rw-r--r--test/config/publickeys/signing-1.pem4
-rw-r--r--test/config/signing-1.config35
-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
15 files changed, 795 insertions, 243 deletions
diff --git a/Makefile b/Makefile
index b63a11f..e1a81fb 100644
--- a/Makefile
+++ b/Makefile
@@ -32,8 +32,10 @@ tests-prepare:
mkdir -p test/nodes/frontend-1/log
mkdir -p test/nodes/storage-1/log
mkdir -p test/nodes/storage-2/log
+ mkdir -p test/nodes/signing-1/log
cp test/config/frontend-1.config rel
cp test/config/storage-1.config rel
+ cp test/config/signing-1.config rel
cp -r test/config/privatekeys rel
cp -r test/config/publickeys rel
rm -r rel/tests || true
@@ -44,34 +46,58 @@ tests-prepare:
touch rel/tests/machine/machine-1/db/index
touch rel/tests/machine/machine-1/db/newentries
+NODES=frontend-1 storage-1 signing-1
+TESTURLS=https://127.0.0.1:8080/ https://127.0.0.1:8081/ https://127.0.0.1:8082/ https://127.0.0.1:8088/
+
tests-start:
- @for node in frontend-1 storage-1; do \
+ @for node in $(NODES); do \
(cd rel ; bin/run_erl -daemon ../test/nodes/$$node/ ../test/nodes/$$node/log/ "exec bin/erl -config $$node") \
done
@for i in 1 2 3 4 5 6 7 8 9 10; do \
echo "waiting for system to start" ; \
sleep 0.5 ; \
allstarted=1 ; \
- for testurl in https://127.0.0.1:8080/ https://127.0.0.1:8081/ https://127.0.0.1:8082/; do \
+ for testurl in $(TESTURLS); do \
if curl -s -k $$testurl > /dev/null ; then : ; else allstarted=0 ; fi ; \
: ; \
done ; \
- if [ $$allstarted == 1 ]; then break ; fi ; \
+ if [ $$allstarted -eq 1 ]; then break ; fi ; \
done
tests-run:
@(cd tools ; python testcase1.py ) || echo "Tests failed"
@(cd tools ; python fetchallcerts.py https://127.0.0.1:8080/) || echo "Verification failed"
+ @(cd tools ; python submitcert.py --store testcerts/cert1.txt --check-sct --sct-file=../rel/submittedcerts https://127.0.0.1:8080/) || echo "Submission failed"
+ @(cd tools ; python submitcert.py --store testcerts/cert2.txt --check-sct --sct-file=../rel/submittedcerts https://127.0.0.1:8080/) || echo "Submission failed"
+ @(cd tools ; python submitcert.py --store testcerts/cert3.txt --check-sct --sct-file=../rel/submittedcerts https://127.0.0.1:8080/) || echo "Submission failed"
+ @(cd tools ; python submitcert.py --store testcerts/cert4.txt --check-sct --sct-file=../rel/submittedcerts https://127.0.0.1:8080/) || echo "Submission failed"
+ @(cd tools ; python submitcert.py --store testcerts/cert5.txt --check-sct --sct-file=../rel/submittedcerts https://127.0.0.1:8080/) || echo "Submission failed"
+ @(cd tools ; python submitcert.py --store testcerts/pre1.txt --check-sct --sct-file=../rel/submittedcerts https://127.0.0.1:8080/) || echo "Submission failed"
+ @(cd tools ; python submitcert.py --store testcerts/pre2.txt --check-sct --sct-file=../rel/submittedcerts https://127.0.0.1:8080/) || echo "Submission failed"
+ @(cd tools ; python 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) || echo "Merge failed"
+
+tests-run2:
+ @(cd tools ; python verifysct.py --sct-file=../rel/submittedcerts --parallel 1 https://127.0.0.1:8080/) || echo "Verification of SCT:s failed"
tests-stop:
- @for node in frontend-1 storage-1; do \
+ @for node in $(NODES); do \
./tools/halt.py ./rel/bin/to_erl test/nodes/$$node/ ; \
done
tests-wait:
sleep 5
-tests: tests-prepare tests-start tests-run tests-wait tests-stop
+tests:
+ @make tests-prepare
+ @make tests-start
+ @make tests-run
+ @make tests-wait
+ @make tests-stop
+ @make tests-wait
+ @make tests-start
+ @make tests-run2
+ @make tests-wait
+ @make tests-stop
# Unit testing.
check: all
diff --git a/src/catlfish.erl b/src/catlfish.erl
index 3956eec..2e5ffd4 100644
--- a/src/catlfish.erl
+++ b/src/catlfish.erl
@@ -2,7 +2,7 @@
%%% See LICENSE for licensing information.
-module(catlfish).
--export([add_chain/2, entries/2, entry_and_proof/2]).
+-export([add_chain/3, entries/2, entry_and_proof/2]).
-export([known_roots/0, update_known_roots/0]).
-export([init_cache_table/0]).
-include_lib("eunit/include/eunit.hrl").
@@ -21,99 +21,202 @@
-record(timestamped_entry, {timestamp :: integer(),
entry_type :: entry_type(),
- signed_entry :: binary(),
+ signed_entry :: signed_x509_entry() |
+ signed_precert_entry(),
extensions = <<>> :: binary()}).
-type timestamped_entry() :: #timestamped_entry{}.
--spec serialise(mtl() | timestamped_entry()) -> binary().
-serialise(#timestamped_entry{timestamp = Timestamp} = E) ->
- list_to_binary(
- [<<Timestamp:64>>,
- serialise_entry_type(E#timestamped_entry.entry_type),
- encode_tls_vector(E#timestamped_entry.signed_entry, 3),
- encode_tls_vector(E#timestamped_entry.extensions, 2)]);
+-record(signed_x509_entry, {asn1_cert :: binary()}).
+-type signed_x509_entry() :: #signed_x509_entry{}.
+-record(signed_precert_entry, {issuer_key_hash :: binary(),
+ tbs_certificate :: binary()}).
+-type signed_precert_entry() :: #signed_precert_entry{}.
+
+-spec serialise(mtl() | timestamped_entry() |
+ signed_x509_entry() | signed_precert_entry()) -> binary().
+%% @doc Serialise a MerkleTreeLeaf as per RFC6962 Section 3.4.
serialise(#mtl{leaf_version = LeafVersion,
leaf_type = LeafType,
entry = TimestampedEntry}) ->
list_to_binary(
[serialise_leaf_version(LeafVersion),
serialise_leaf_type(LeafType),
- serialise(TimestampedEntry)]).
+ serialise(TimestampedEntry)]);
+%% @doc Serialise a TimestampedEntry as per RFC6962 Section 3.4.
+serialise(#timestamped_entry{timestamp = Timestamp,
+ entry_type = EntryType,
+ signed_entry = SignedEntry,
+ extensions = Extensions}) ->
+ list_to_binary(
+ [<<Timestamp:64>>,
+ serialise_entry_type(EntryType),
+ serialise(SignedEntry),
+ encode_tls_vector(Extensions, 2)]);
+%% @doc Serialise an ASN1.Cert as per RFC6962 Section 3.1.
+serialise(#signed_x509_entry{asn1_cert = Cert}) ->
+ encode_tls_vector(Cert, 3);
+%% @doc Serialise a PreCert as per RFC6962 Section 3.2.
+serialise(#signed_precert_entry{
+ issuer_key_hash = IssuerKeyHash,
+ tbs_certificate = TBSCertificate}) when is_binary(IssuerKeyHash),
+ size(IssuerKeyHash) == 32 ->
+ list_to_binary(
+ [IssuerKeyHash,
+ encode_tls_vector(TBSCertificate, 3)]).
serialise_leaf_version(v1) ->
<<0:8>>;
serialise_leaf_version(v2) ->
<<1:8>>.
+deserialise_leaf_version(<<0:8>>) ->
+ v1;
+deserialise_leaf_version(<<1:8>>) ->
+ v2.
serialise_leaf_type(timestamped_entry) ->
<<0:8>>.
-%% serialise_leaf_type(_) ->
-%% <<>>.
+deserialise_leaf_type(<<0:8>>) ->
+ timestamped_entry.
serialise_entry_type(x509_entry) ->
<<0:16>>;
serialise_entry_type(precert_entry) ->
<<1:16>>.
+deserialise_entry_type(<<0:16>>) ->
+ x509_entry;
+deserialise_entry_type(<<1:16>>) ->
+ precert_entry.
+-spec serialise_signature_type(certificate_timestamp|tree_hash) -> binary().
serialise_signature_type(certificate_timestamp) ->
<<0:8>>;
serialise_signature_type(tree_hash) ->
<<1:8>>.
-build_mtl(Timestamp, LeafCert) ->
- TSE = #timestamped_entry{timestamp = Timestamp,
- entry_type = x509_entry,
- signed_entry = LeafCert},
- MTL = #mtl{leaf_version = v1,
- leaf_type = timestamped_entry,
- entry = TSE},
- serialise(MTL).
-
--spec add_chain(binary(), [binary()]) -> nonempty_string().
-add_chain(LeafCert, CertChain) ->
+calc_sct(TimestampedEntry) ->
+ plop:serialise(
+ plop:spt(list_to_binary([<<?PROTOCOL_VERSION:8>>,
+ serialise_signature_type(certificate_timestamp),
+ serialise(TimestampedEntry)]))).
+
+get_sct(Hash, TimestampedEntry) ->
+ case application:get_env(catlfish, sctcache_root_path) of
+ {ok, RootPath} ->
+ case perm:readfile(RootPath, Hash) of
+ Contents when is_binary(Contents) ->
+ Contents;
+ noentry ->
+ SCT = calc_sct(TimestampedEntry),
+ ok = perm:ensurefile_nosync(RootPath, Hash, SCT),
+ SCT
+ end;
+ _ ->
+ calc_sct(TimestampedEntry)
+ end.
+
+-spec add_chain(binary(), [binary()], normal|precert) -> nonempty_string().
+add_chain(LeafCert, CertChain, Type) ->
EntryHash = crypto:hash(sha256, [LeafCert | CertChain]),
- TimestampedEntry =
+ EntryType = case Type of
+ normal -> x509_entry;
+ precert -> precert_entry
+ end,
+ {TimestampedEntry, Hash} =
case plop:get(EntryHash) of
notfound ->
Timestamp = plop:generate_timestamp(),
- TSE = #timestamped_entry{timestamp = Timestamp,
- entry_type = x509_entry,
- signed_entry = LeafCert},
- MTL = #mtl{leaf_version = v1,
- leaf_type = timestamped_entry,
- entry = TSE},
- ok = plop:add(
- serialise_logentry(Timestamp, LeafCert, CertChain),
- ht:leaf_hash(serialise(MTL)),
- EntryHash),
- TSE;
- {_Index, _MTLHash, Entry} ->
- <<Timestamp:64, _LogEntry/binary>> = Entry,
- %% TODO: Perform a costly db consistency check against
- %% unpacked LogEntry (w/ LeafCert and CertChain)
- #timestamped_entry{timestamp = Timestamp,
- entry_type = x509_entry,
- signed_entry = LeafCert}
+ TSE = timestamped_entry(Timestamp, EntryType, LeafCert, CertChain),
+ MTLText = serialise(#mtl{leaf_version = v1,
+ leaf_type = timestamped_entry,
+ entry = TSE}),
+ MTLHash = ht:leaf_hash(MTLText),
+ ExtraData =
+ case Type of
+ normal -> CertChain;
+ precert -> [LeafCert | CertChain]
+ end,
+ LogEntry =
+ list_to_binary(
+ [encode_tls_vector(MTLText, 4),
+ encode_tls_vector(
+ encode_tls_vector(
+ list_to_binary(
+ [encode_tls_vector(C, 3) || C <- ExtraData]),
+ 3),
+ 4)]),
+ ok = plop:add(LogEntry, MTLHash, EntryHash),
+ {TSE, MTLHash};
+ {_Index, MTLHash, DBEntry} ->
+ {MTLText, _ExtraData} = unpack_entry(DBEntry),
+ MTL = deserialise_mtl(MTLText),
+ MTLText = serialise(MTL), % verify FIXME: remove
+ {MTL#mtl.entry, MTLHash}
end,
- SCT_sig =
- plop:spt(list_to_binary([<<?PROTOCOL_VERSION:8>>,
- serialise_signature_type(certificate_timestamp),
- serialise(TimestampedEntry)])),
+
+ SCT_sig = get_sct(Hash, TimestampedEntry),
{[{sct_version, ?PROTOCOL_VERSION},
{id, base64:encode(plop:get_logid())},
{timestamp, TimestampedEntry#timestamped_entry.timestamp},
{extensions, base64:encode(<<>>)},
- {signature, base64:encode(plop:serialise(SCT_sig))}]}.
+ {signature, base64:encode(SCT_sig)}]}.
--spec serialise_logentry(integer(), binary(), [binary()]) -> binary().
-serialise_logentry(Timestamp, LeafCert, CertChain) ->
- list_to_binary(
- [<<Timestamp:64>>,
- list_to_binary(
- [encode_tls_vector(LeafCert, 3),
- encode_tls_vector(
- list_to_binary(
- [encode_tls_vector(X, 3) || X <- CertChain]), 3)])]).
+-spec timestamped_entry(integer(), entry_type(), binary(), binary()) ->
+ timestamped_entry().
+timestamped_entry(Timestamp, EntryType, LeafCert, CertChain) ->
+ SignedEntry =
+ case EntryType of
+ x509_entry ->
+ #signed_x509_entry{asn1_cert = LeafCert};
+ precert_entry ->
+ {DetoxedLeafTBSCert, IssuerKeyHash} =
+ x509:detox(LeafCert, CertChain),
+ #signed_precert_entry{
+ issuer_key_hash = IssuerKeyHash,
+ tbs_certificate = DetoxedLeafTBSCert}
+ end,
+ #timestamped_entry{timestamp = Timestamp,
+ entry_type = EntryType,
+ signed_entry = SignedEntry}.
+
+-spec deserialise_mtl(binary()) -> mtl().
+deserialise_mtl(Data) ->
+ <<LeafVersionBin:1/binary,
+ LeafTypeBin:1/binary,
+ TimestampedEntryBin/binary>> = Data,
+ #mtl{leaf_version = deserialise_leaf_version(LeafVersionBin),
+ leaf_type = deserialise_leaf_type(LeafTypeBin),
+ entry = deserialise_timestampedentry(TimestampedEntryBin)}.
+
+-spec deserialise_timestampedentry(binary()) -> timestamped_entry().
+deserialise_timestampedentry(Data) ->
+ <<Timestamp:64, EntryTypeBin:2/binary, RestData/binary>> = Data,
+ EntryType = deserialise_entry_type(EntryTypeBin),
+ {SignedEntry, ExtensionsBin} =
+ case EntryType of
+ x509_entry ->
+ deserialise_signed_x509_entry(RestData);
+ precert_entry ->
+ deserialise_signed_precert_entry(RestData)
+ end,
+ {Extensions, <<>>} = decode_tls_vector(ExtensionsBin, 2),
+ #timestamped_entry{timestamp = Timestamp,
+ entry_type = EntryType,
+ signed_entry = SignedEntry,
+ extensions = Extensions}.
+
+-spec deserialise_signed_x509_entry(binary()) -> {signed_x509_entry(), binary()}.
+deserialise_signed_x509_entry(Data) ->
+ {E, D} = decode_tls_vector(Data, 3),
+ {#signed_x509_entry{asn1_cert = E}, D}.
+
+-spec deserialise_signed_precert_entry(binary()) ->
+ {signed_precert_entry(), binary()}.
+deserialise_signed_precert_entry(Data) ->
+ <<IssuerKeyHash:32/binary, RestData/binary>> = Data,
+ {TBSCertificate, RestData2} = decode_tls_vector(RestData, 3),
+ {#signed_precert_entry{issuer_key_hash = IssuerKeyHash,
+ tbs_certificate = TBSCertificate},
+ RestData2}.
-spec entries(non_neg_integer(), non_neg_integer()) -> list().
entries(Start, End) ->
@@ -123,10 +226,9 @@ entries(Start, End) ->
entry_and_proof(Index, TreeSize) ->
case plop:inclusion_and_entry(Index, TreeSize) of
{ok, Entry, Path} ->
- {Timestamp, LeafCertVector, CertChainVector} = unpack_entry(Entry),
- MTL = build_mtl(Timestamp, LeafCertVector),
+ {MTL, ExtraData} = unpack_entry(Entry),
{[{leaf_input, base64:encode(MTL)},
- {extra_data, base64:encode(CertChainVector)},
+ {extra_data, base64:encode(ExtraData)},
{audit_path, [base64:encode(X) || X <- Path]}]};
{notfound, Msg} ->
{[{success, false},
@@ -142,20 +244,20 @@ init_cache_table() ->
ets:new(?CACHE_TABLE, [set, public, named_table]).
%% Private functions.
+-spec unpack_entry(binary()) -> {binary(), binary()}.
unpack_entry(Entry) ->
- <<Timestamp:64, LogEntry/binary>> = Entry,
- {LeafCertVector, CertChainVector} = decode_tls_vector(LogEntry, 3),
- {Timestamp, LeafCertVector, CertChainVector}.
+ {MTL, Rest} = decode_tls_vector(Entry, 4),
+ {ExtraData, <<>>} = decode_tls_vector(Rest, 4),
+ {MTL, ExtraData}.
-spec x_entries([{non_neg_integer(), binary(), binary()}]) -> list().
x_entries([]) ->
[];
x_entries([H|T]) ->
{_Index, _Hash, Entry} = H,
- {Timestamp, LeafCertVector, CertChainVector} = unpack_entry(Entry),
- MTL = build_mtl(Timestamp, LeafCertVector),
- [{[{leaf_input, base64:encode(MTL)}, {extra_data, base64:encode(CertChainVector)}]} |
- x_entries(T)].
+ {MTL, ExtraData} = unpack_entry(Entry),
+ [{[{leaf_input, base64:encode(MTL)},
+ {extra_data, base64:encode(ExtraData)}]} | x_entries(T)].
-spec encode_tls_vector(binary(), non_neg_integer()) -> binary().
encode_tls_vector(Binary, LengthLen) ->
diff --git a/src/v1.erl b/src/v1.erl
index d9796fa..e672182 100644
--- a/src/v1.erl
+++ b/src/v1.erl
@@ -9,46 +9,14 @@
%% Public functions, i.e. part of URL.
request(post, "ct/v1/add-chain", Input) ->
- case (catch mochijson2:decode(Input)) of
- {error, E} ->
- html("add-chain: bad input:", E);
- {struct, [{<<"chain">>, ChainBase64}]} ->
- case (catch [base64:decode(X) || X <- ChainBase64]) of
- {'EXIT', _} ->
- html("add-chain: invalid base64-encoded chain: ",
- [ChainBase64]);
- [LeafCert | CertChain] ->
- Roots = catlfish:known_roots(),
- case x509:normalise_chain(Roots, [LeafCert|CertChain]) of
- {ok, [Leaf | Chain]} ->
- lager:info("adding ~p",
- [x509:cert_string(LeafCert)]),
- success(catlfish:add_chain(Leaf, Chain));
- {error, Reason} ->
- lager:info("rejecting ~p: ~p",
- [x509:cert_string(LeafCert), Reason]),
- html("add-chain: invalid chain", Reason)
- end;
- Invalid ->
- html("add-chain: chain is not a list: ", [Invalid])
- end;
- _ -> html("add-chain: missing input: chain", Input)
- end;
+ add_chain(Input, normal);
-request(post, "ct/v1/add-pre-chain", _Input) ->
- niy();
+request(post, "ct/v1/add-pre-chain", Input) ->
+ add_chain(Input, precert);
request(get, "ct/v1/get-sth", _Query) ->
- { Treesize,
- Timestamp,
- Roothash,
- Signature} = plop:sth(),
- R = [{tree_size, Treesize},
- {timestamp, Timestamp},
- {sha256_root_hash, base64:encode(Roothash)},
- {tree_head_signature, base64:encode(
- plop:serialise(Signature))}],
- success({R});
+ R = plop:sth(),
+ success(R);
request(get, "ct/v1/get-sth-consistency", Query) ->
case lists:sort(Query) of
@@ -138,8 +106,33 @@ html(Text, Input) ->
"~p~n" ++
"</body></html>~n", [Text, Input])}.
-niy() ->
- html("NIY - Not Implemented Yet|", []).
-
success(Data) ->
{200, [{"Content-Type", "text/json"}], mochijson2:encode(Data)}.
+
+-spec add_chain(any(), normal|precert) -> any().
+add_chain(Input, Type) ->
+ case (catch mochijson2:decode(Input)) of
+ {error, E} ->
+ html("add-chain: bad input:", E);
+ {struct, [{<<"chain">>, ChainBase64}]} ->
+ case (catch [base64:decode(X) || X <- ChainBase64]) of
+ {'EXIT', _} ->
+ html("add-chain: invalid base64-encoded chain: ",
+ [ChainBase64]);
+ [LeafCert | CertChain] ->
+ case x509:normalise_chain(catlfish:known_roots(),
+ [LeafCert|CertChain]) of
+ {ok, [Leaf | Chain]} ->
+ lager:info("adding ~p cert ~p",
+ [Type, x509:cert_string(LeafCert)]),
+ success(catlfish:add_chain(Leaf, Chain, Type));
+ {error, Reason} ->
+ lager:info("rejecting ~p: ~p",
+ [x509:cert_string(LeafCert), Reason]),
+ html("add-chain: invalid chain", Reason)
+ end;
+ Invalid ->
+ html("add-chain: chain is not a list: ", [Invalid])
+ end;
+ _ -> html("add-chain: missing input: chain", Input)
+ end.
diff --git a/src/x509.erl b/src/x509.erl
index 5a0e871..eae1468 100644
--- a/src/x509.erl
+++ b/src/x509.erl
@@ -3,10 +3,10 @@
-module(x509).
-export([normalise_chain/2, cert_string/1, read_pemfiles_from_dir/1,
- self_signed/1]).
-
+ self_signed/1, detox/2]).
-include_lib("public_key/include/public_key.hrl").
-include_lib("eunit/include/eunit.hrl").
+-import(lists, [nth/2, filter/2]).
-type reason() :: {chain_too_long |
root_unknown |
@@ -14,19 +14,57 @@
encoding_invalid}.
-define(MAX_CHAIN_LENGTH, 10).
+-define(LEAF_POISON_OID, {1,3,6,1,4,1,11129,2,4,3}).
+-define(LEAF_POISON_VAL, [5,0]).
+-define(CA_POISON_OID, {1,3,6,1,4,1,11129,2,4,4}).
-spec normalise_chain([binary()], [binary()]) -> {ok, [binary()]} |
{error, reason()}.
normalise_chain(AcceptableRootCerts, CertChain) ->
- case valid_chain_p(AcceptableRootCerts, CertChain, ?MAX_CHAIN_LENGTH) of
+ case normalise_chain(AcceptableRootCerts, CertChain, ?MAX_CHAIN_LENGTH) of
{false, Reason} ->
{error, Reason};
{true, Root} ->
- [Leaf | Chain] = CertChain,
- {ok, [detox_precert(Leaf) | Chain] ++ Root}
+ {ok, CertChain ++ Root}
end.
-%%%%%%%%%%%%%%%%%%%%
+-spec cert_string(binary()) -> string().
+cert_string(Der) ->
+ mochihex:to_hex(crypto:hash(sha, Der)).
+
+-spec read_pemfiles_from_dir(file:filename()) -> [binary()].
+%% @doc Reading certificates from files. Flattening the result -- all
+%% certs in all files are returned in a single list.
+read_pemfiles_from_dir(Dir) ->
+ case file:list_dir(Dir) of
+ {error, enoent} ->
+ lager:error("directory does not exist: ~p", [Dir]),
+ [];
+ {error, Reason} ->
+ lager:error("unable to read directory ~p: ~p", [Dir, Reason]),
+ [];
+ {ok, Filenames} ->
+ Files = lists:filter(
+ fun(F) -> string:equal(".pem", filename:extension(F)) end,
+ Filenames),
+ ders_from_pemfiles(Dir, Files)
+ end.
+
+-spec self_signed([binary()]) -> [binary()].
+%% @doc Return a list of certs in L that are self signed.
+self_signed(L) ->
+ lists:filter(fun(Cert) -> signed_by_p(Cert, Cert) end, L).
+
+-spec detox(binary(), [binary()]) -> {binary(), binary()}.
+%% @doc Return the detoxed cet in LeafDer and the issuer leaf hash.
+detox(LeafDer, ChainDer) ->
+ detox_precert(LeafDer, nth(1, ChainDer), nth(2, ChainDer)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Private functions.
+
+-spec normalise_chain([binary()], [binary()], integer()) ->
+ {false, reason()} | {true, list()}.
%% @doc Verify that the leaf cert or precert has a valid chain back to
%% an acceptable root cert. The order of certificates in the second
%% argument is: leaf cert in head, chain in tail. Order of first
@@ -37,12 +75,10 @@ normalise_chain(AcceptableRootCerts, CertChain) ->
%% amongst the acceptable root certs. Otherwise it contains exactly
%% one element, a CA cert from the acceptable root certs signing the
%% root of the chain.
--spec valid_chain_p([binary()], [binary()], integer()) ->
- {false, reason()} | {true, list()}.
-valid_chain_p(_, _, MaxChainLength) when MaxChainLength =< 0 ->
+normalise_chain(_, _, MaxChainLength) when MaxChainLength =< 0 ->
%% Chain too long.
{false, chain_too_long};
-valid_chain_p(AcceptableRootCerts, [TopCert], MaxChainLength) ->
+normalise_chain(AcceptableRootCerts, [TopCert], MaxChainLength) ->
%% Check root of chain.
case lists:member(TopCert, AcceptableRootCerts) of
true ->
@@ -58,17 +94,17 @@ valid_chain_p(AcceptableRootCerts, [TopCert], MaxChainLength) ->
Root -> {true, [Root]}
end
end;
-valid_chain_p(AcceptableRootCerts, [BottomCert|Rest], MaxChainLength) ->
+normalise_chain(AcceptableRootCerts, [BottomCert|Rest], MaxChainLength) ->
case signed_by_p(BottomCert, hd(Rest)) of
- true -> valid_chain_p(AcceptableRootCerts, Rest, MaxChainLength - 1);
+ true -> normalise_chain(AcceptableRootCerts, Rest, MaxChainLength - 1);
false -> {false, signature_mismatch}
end.
+-spec signer(binary(), [binary()]) -> notfound | binary().
%% @doc Return first cert in list signing Cert, or notfound. NOTE:
%% This is potentially expensive. It'd be more efficient to search for
%% Cert.issuer in a list of Issuer.subject's. If so, maybe make the
%% matching somewhat fuzzy unless that too is expensive.
--spec signer(binary(), [binary()]) -> notfound | binary().
signer(_Cert, []) ->
notfound;
signer(Cert, [H|T]) ->
@@ -82,6 +118,7 @@ signer(Cert, [H|T]) ->
signer(Cert, T)
end.
+-spec encoded_tbs_cert(binary()) -> binary().
%% Code from pubkey_cert:encoded_tbs_cert/1.
encoded_tbs_cert(DerCert) ->
{ok, PKIXCert} =
@@ -90,14 +127,14 @@ encoded_tbs_cert(DerCert) ->
PKIXCert,
EncodedTBSCert.
+-spec extract_verify_data(#'Certificate'{}, binary()) -> {ok, tuple()} | error.
+%% @doc Return DER encoded TBScertificate, digest type and signature.
%% Code from pubkey_cert:extract_verify_data/2.
--spec verifydata_from_cert(#'Certificate'{}, binary()) -> {ok, tuple()} | error.
-verifydata_from_cert(Cert, DerCert) ->
+extract_verify_data(Cert, DerCert) ->
PlainText = encoded_tbs_cert(DerCert),
{_, Sig} = Cert#'Certificate'.signature,
SigAlgRecord = Cert#'Certificate'.signatureAlgorithm,
SigAlg = SigAlgRecord#'AlgorithmIdentifier'.algorithm,
- lager:debug("SigAlg: ~p", [SigAlg]),
try
{DigestType, _} = public_key:pkix_sign_types(SigAlg),
{ok, {PlainText, DigestType, Sig}}
@@ -114,7 +151,7 @@ verify_sig(Cert, DerCert, % Certificate to verify.
tbsCertificate = #'TBSCertificate'{
subjectPublicKeyInfo = IssuerSPKI}}) ->
%% Dig out digest, digest type and signature from Cert/DerCert.
- case verifydata_from_cert(Cert, DerCert) of
+ case extract_verify_data(Cert, DerCert) of
error -> false;
{ok, Tuple} -> verify_sig2(IssuerSPKI, Tuple)
end.
@@ -125,10 +162,6 @@ verify_sig2(IssuerSPKI, {DigestOrPlainText, DigestType, Signature}) ->
algorithm = #'AlgorithmIdentifier'{algorithm = Alg, parameters = Params},
subjectPublicKey = {0, Key0}} = IssuerSPKI,
KeyType = pubkey_cert_records:supportedPublicKeyAlgorithms(Alg),
- lager:debug("Alg: ~p", [Alg]),
- lager:debug("Params: ~p", [Params]),
- lager:debug("KeyType: ~p", [KeyType]),
- lager:debug("Key0: ~p", [Key0]),
IssuerKey =
case KeyType of
'RSAPublicKey' ->
@@ -141,12 +174,6 @@ verify_sig2(IssuerSPKI, {DigestOrPlainText, DigestType, Signature}) ->
lager:error("NIY: Issuer key type ~p", [KeyType]),
false
end,
-
- lager:debug("DigestOrPlainText: ~p", [DigestOrPlainText]),
- lager:debug("DigestType: ~p", [DigestType]),
- lager:debug("Signature: ~p", [Signature]),
- lager:debug("IssuerKey: ~p", [IssuerKey]),
-
%% Verify the signature.
public_key:verify(DigestOrPlainText, DigestType, Signature, IssuerKey).
@@ -159,9 +186,6 @@ signed_by_p(DerCert, IssuerDerCert) when is_binary(DerCert),
DerCert,
public_key:pkix_decode_cert(IssuerDerCert, plain)).
-cert_string(Der) ->
- mochihex:to_hex(crypto:hash(sha, Der)).
-
parsable_cert_p(Der) ->
case (catch public_key:pkix_decode_cert(Der, plain)) of
#'Certificate'{} ->
@@ -175,58 +199,141 @@ parsable_cert_p(Der) ->
false
end.
--spec self_signed([binary()]) -> [binary()].
-self_signed(L) ->
- lists:filter(fun(Cert) -> signed_by_p(Cert, Cert) end, L).
-
-%%%%%%%%%%%%%%%%%%%%
-%% Precertificates according to draft-ietf-trans-rfc6962-bis-04.
-
+%% Precerts according to RFC6962.
+%%
%% Submitted precerts have a special critical poison extension -- OID
%% 1.3.6.1.4.1.11129.2.4.3, whose extnValue OCTET STRING contains
%% ASN.1 NULL data (0x05 0x00).
-
+%%
%% They are signed with either the CA cert that will sign the final
-%% cert or Precertificate Signing Certificate directly signed by the
+%% cert or a Precertificate Signing Certificate directly signed by the
%% CA cert that will sign the final cert. A Precertificate Signing
%% Certificate has CA:true and Extended Key Usage: Certificate
%% Transparency, OID 1.3.6.1.4.1.11129.2.4.4.
+%%
+%% PreCert in SignedCertificateTimestamp does _not_ contain the poison
+%% extension, nor does it have an issuer which is a Precertificate
+%% Signing Certificate. This means that we have to 1) remove the
+%% poison extension and 2) potentially change issuer and Authority Key
+%% Identifier. See RFC6962 Section 3.2.
+%%
+%% Changes in draft-ietf-trans-rfc6962-bis-??: TODO.
+
+-spec detox_precert(binary(), binary(), binary()) -> {binary(), binary()}.
+%% @doc Return {DetoxedLeaf, IssuerPubKeyHash} where i) DetoxedLeaf is
+%% the tbsCertificate w/o poison and adjusted issuer and authkeyid;
+%% and ii) IssuerPubKeyHash is the hash over issuing cert's public
+%% key.
+detox_precert(LeafDer, ParentDer, GrandParentDer) ->
+ Leaf = public_key:pkix_decode_cert(LeafDer, plain),
+ Parent = public_key:pkix_decode_cert(ParentDer, plain),
+ GrandParent = public_key:pkix_decode_cert(GrandParentDer, plain),
+ DetoxedLeafTBS = remove_poison_ext(Leaf),
+
+ %% If parent is a precert signing cert, change issuer and
+ %% potential authority key id to refer to grandparent.
+ {C, IssuerKeyHash} =
+ case is_precert_signer(Parent) of
+ true ->
+ {set_issuer_and_authkeyid(DetoxedLeafTBS, Parent),
+ extract_pub_key(GrandParent)};
+ false ->
+ {DetoxedLeafTBS, extract_pub_key(Parent)}
+ end,
+ {public_key:pkix_encode('TBSCertificate', C, plain),
+ crypto:hash(sha256, public_key:pkix_encode(
+ 'SubjectPublicKeyInfo', IssuerKeyHash, plain))}.
+
+-spec extract_pub_key(#'Certificate'{}) -> #'SubjectPublicKeyInfo'{}.
+extract_pub_key(#'Certificate'{
+ tbsCertificate = #'TBSCertificate'{
+ subjectPublicKeyInfo = SPKI}}) ->
+ SPKI.
+
+-spec set_issuer_and_authkeyid(#'TBSCertificate'{}, #'Certificate'{}) ->
+ #'TBSCertificate'{}.
+%% @doc Return Cert with issuer and AuthorityKeyIdentifier from Parent.
+set_issuer_and_authkeyid(TBSCert,
+ #'Certificate'{
+ tbsCertificate =
+ #'TBSCertificate'{
+ issuer = ParentIssuer,
+ extensions = ParentExtensions}}) ->
+ case pubkey_cert:select_extension(?'id-ce-authorityKeyIdentifier',
+ ParentExtensions) of
+ undefined ->
+ lager:debug("setting issuer only", []),
+ TBSCert#'TBSCertificate'{issuer = ParentIssuer};
+ ParentAuthKeyExt ->
+ NewExtensions =
+ lists:map(
+ fun(E) ->
+ case E of
+ #'Extension'{extnID =
+ ?'id-ce-authorityKeyIdentifier'} ->
+ lager:debug("swapping auth key id to ~p",
+ [ParentAuthKeyExt]),
+ ParentAuthKeyExt;
+ _ -> E
+ end
+ end,
+ TBSCert#'TBSCertificate'.extensions),
+ lager:debug("setting issuer and auth key id", []),
+ TBSCert#'TBSCertificate'{issuer = ParentIssuer,
+ extensions = NewExtensions}
+ end.
-%% A PreCert in a SignedCertificateTimestamp does _not_ contain the
-%% poison extension, nor a Precertificate Signing Certificate. This
-%% means that we might have to 1) remove poison extensions in leaf
-%% certs, 2) remove "poisoned signatures", 3) change issuer and
-%% Authority Key Identifier of leaf certs.
-
--spec detox_precert([#'Certificate'{}]) -> [#'Certificate'{}].
-detox_precert(CertChain) ->
- CertChain. % NYI
+-spec is_precert_signer(#'Certificate'{}) -> boolean().
+is_precert_signer(#'Certificate'{tbsCertificate = TBSCert}) ->
+ Extensions = pubkey_cert:extensions_list(TBSCert#'TBSCertificate'.extensions),
+ %% NOTE: It's OK to look at only the first extension found since
+ %% "A certificate MUST NOT include more than one instance of a
+ %% particular extension." --RFC5280 Sect 4.2
+ case pubkey_cert:select_extension(?'id-ce-extKeyUsage', Extensions) of
+ #'Extension'{extnValue = Val} ->
+ case 'OTP-PUB-KEY':decode('ExtKeyUsageSyntax', Val) of
+ %% NOTE: We require that the poisoned OID is the
+ %% _only_ extkeyusage present. RFC6962 Sect 3.1 is not
+ %% really clear.
+ {ok, [?CA_POISON_OID]} -> is_ca(TBSCert);
+ _ -> false
+ end;
+ _ -> false
+ end.
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
--spec read_pemfiles_from_dir(file:filename()) -> [binary()].
-%% @doc Reading certificates from files. Flattening the result -- all
-%% certs in all files are returned in a single list.
-read_pemfiles_from_dir(Dir) ->
- case file:list_dir(Dir) of
- {error, enoent} ->
- lager:error("directory does not exist: ~p", [Dir]),
- [];
- {error, Reason} ->
- lager:error("unable to read directory ~p: ~p", [Dir, Reason]),
- [];
- {ok, Filenames} ->
- Files = lists:filter(
- fun(F) ->
- string:equal(".pem", filename:extension(F))
- end,
- Filenames),
- ders_from_pemfiles(Dir, Files)
+-spec is_ca(#'TBSCertificate'{}) -> binary().
+is_ca(#'TBSCertificate'{extensions = Extensions}) ->
+ case pubkey_cert:select_extension(?'id-ce-basicConstraints', Extensions) of
+ #'Extension'{critical = true, extnValue = Val} ->
+ case 'OTP-PUB-KEY':decode('BasicConstraints', Val) of
+ {ok, {'BasicConstraints', true, _}} -> true;
+ _ -> false
+ end;
+ _ -> false
end.
+-spec remove_poison_ext(#'Certificate'{}) -> #'TBSCertificate'{}.
+remove_poison_ext(#'Certificate'{tbsCertificate = TBSCert}) ->
+ Extensions =
+ filter(fun(E) -> not poisoned_leaf_p(E) end,
+ pubkey_cert:extensions_list(TBSCert#'TBSCertificate'.extensions)),
+ TBSCert#'TBSCertificate'{extensions = Extensions}.
+
+-spec poisoned_leaf_p(binary()) -> boolean().
+poisoned_leaf_p(#'Extension'{extnID = ?LEAF_POISON_OID,
+ critical = true,
+ extnValue = ?LEAF_POISON_VAL}) ->
+ true;
+poisoned_leaf_p(_) ->
+ false.
+
+%%%% PEM files.
+-spec ders_from_pemfiles(string(), [string()]) -> [binary()].
ders_from_pemfiles(Dir, Filenames) ->
lists:flatten(
[ders_from_pemfile(filename:join(Dir, X)) || X <- Filenames]).
+-spec ders_from_pemfile(string()) -> [binary()].
ders_from_pemfile(Filename) ->
lager:debug("reading PEM from ~s", [Filename]),
PemBins = pems_from_file(Filename),
@@ -238,6 +345,7 @@ ders_from_pemfile(Filename) ->
end,
[der_from_pem(X) || X <- Pems].
+-spec der_from_pem(binary()) -> binary().
der_from_pem(Pem) ->
case Pem of
{_Type, Der, not_encrypted} ->
@@ -297,21 +405,21 @@ valid_cert_test_() ->
%% 'OTP-PUB-KEY':Func('OTP-X520countryname', Value0)
%% FIXME: This error doesn't make much sense -- is my
%% environment borked?
- ?_assertMatch({true, _}, valid_chain_p(lists:nth(1, Chains),
- lists:nth(1, Chains), 10)),
+ ?_assertMatch({true, _}, normalise_chain(lists:nth(1, Chains),
+ lists:nth(1, Chains), 10)),
%% Self-signed so fail.
?_assertMatch({false, root_unknown},
- valid_chain_p(KnownRoots,
- lists:nth(2, Chains), 10)),
+ normalise_chain(KnownRoots,
+ lists:nth(2, Chains), 10)),
%% Leaf signed by known CA, pass.
- ?_assertMatch({true, _}, valid_chain_p(KnownRoots,
- lists:nth(3, Chains), 10)),
+ ?_assertMatch({true, _}, normalise_chain(KnownRoots,
+ lists:nth(3, Chains), 10)),
%% Proper 3-depth chain with root in KnownRoots, pass.
%% Bug CATLFISH-19 --> [info] rejecting "3ee62cb678014c14d22ebf96f44cc899adea72f1": chain_broken
%% leaf sha1: 3ee62cb678014c14d22ebf96f44cc899adea72f1
%% leaf Subject: C=KR, O=Government of Korea, OU=Group of Server, OU=\xEA\xB5\x90\xEC\x9C\xA1\xEA\xB3\xBC\xED\x95\x99\xEA\xB8\xB0\xEC\x88\xA0\xEB\xB6\x80, CN=www.berea.ac.kr, CN=haksa.bits.ac.kr
- ?_assertMatch({true, _}, valid_chain_p(KnownRoots,
- lists:nth(4, Chains), 3)),
+ ?_assertMatch({true, _}, normalise_chain(KnownRoots,
+ lists:nth(4, Chains), 3)),
%% Verify against self, pass.
%% Bug CATLFISH-??, can't handle issuer keytype ECPoint.
%% Issuer sha1: 6969562e4080f424a1e7199f14baf3ee58ab6abb
@@ -333,21 +441,21 @@ chain_test_() ->
chain_test(C0, C1) ->
[
%% Root not in chain but in trust store.
- ?_assertEqual({true, [C1]}, valid_chain_p([C1], [C0], 10)),
- ?_assertEqual({true, [C1]}, valid_chain_p([C1], [C0], 2)),
+ ?_assertEqual({true, [C1]}, normalise_chain([C1], [C0], 10)),
+ ?_assertEqual({true, [C1]}, normalise_chain([C1], [C0], 2)),
%% Chain too long.
- ?_assertMatch({false, chain_too_long}, valid_chain_p([C1], [C0], 1)),
+ ?_assertMatch({false, chain_too_long}, normalise_chain([C1], [C0], 1)),
%% Root in chain and in trust store.
- ?_assertEqual({true, []}, valid_chain_p([C1], [C0, C1], 2)),
+ ?_assertEqual({true, []}, normalise_chain([C1], [C0, C1], 2)),
%% Chain too long.
- ?_assertMatch({false, chain_too_long}, valid_chain_p([C1], [C0, C1], 1)),
+ ?_assertMatch({false, chain_too_long}, normalise_chain([C1], [C0, C1], 1)),
%% Root not in trust store.
- ?_assertMatch({false, root_unknown}, valid_chain_p([], [C0, C1], 10)),
+ ?_assertMatch({false, root_unknown}, normalise_chain([], [C0, C1], 10)),
%% Selfsigned. Actually OK.
- ?_assertMatch({true, []}, valid_chain_p([C0], [C0], 10)),
- ?_assertMatch({true, []}, valid_chain_p([C0], [C0], 1)),
+ ?_assertMatch({true, []}, normalise_chain([C0], [C0], 10)),
+ ?_assertMatch({true, []}, normalise_chain([C0], [C0], 1)),
%% Max chain length 0 is not OK.
- ?_assertMatch({false, chain_too_long}, valid_chain_p([C0], [C0], 0))
+ ?_assertMatch({false, chain_too_long}, normalise_chain([C0], [C0], 0))
].
%%-spec read_certs(file:filename()) -> [string:string()].
diff --git a/test/config/frontend-1.config b/test/config/frontend-1.config
index 9d7e37c..8215027 100644
--- a/test/config/frontend-1.config
+++ b/test/config/frontend-1.config
@@ -8,6 +8,7 @@
{error_logger_mf_maxfiles, 10}]},
{catlfish,
[{known_roots_path, "known_roots"},
+ {sctcache_root_path, "tests/machine/machine-1/db/sctcache/"},
{https_servers,
[{external_https_api, "127.0.0.1", 8080, v1},
{frontend_https_api, "127.0.0.1", 8082, frontend}
@@ -30,10 +31,14 @@
{entryhash_root_path, "tests/machine/machine-1/db/entryhash/"},
{treesize_path, "tests/machine/machine-1/db/treesize"},
{indexforhash_root_path, "tests/machine/machine-1/db/certindex/"},
+ {sth_path, "tests/machine/machine-1/db/sth"},
{storage_nodes, ["https://127.0.0.1:8081/ct/storage/"]},
{storage_nodes_quorum, 1},
{publickey_path, "publickeys"},
+ {services, [ht]},
+ {log_public_key, "test/eckey-public.pem"},
{own_key, {"frontend-1", "privatekeys/frontend-1-private.pem"}},
+ {signing_node, "https://127.0.0.1:8088/ct/signing/"},
{allowed_clients, [{"/ct/frontend/sendentry", ["merge-1"]},
{"/ct/frontend/sendlog", ["merge-1"]},
{"/ct/frontend/sendsth", ["merge-1"]},
@@ -49,6 +54,8 @@
{"/ct/v1/get-roots", noauth}
]},
{allowed_servers, [{"/ct/storage/sendentry", ["storage-1"]},
- {"/ct/storage/entrycommitted", ["storage-1"]}
+ {"/ct/storage/entrycommitted", ["storage-1"]},
+ {"/ct/signing/sct", ["signing-1"]},
+ {"/ct/signing/sth", ["signing-1"]}
]}
]}].
diff --git a/test/config/privatekeys/signing-1-private.pem b/test/config/privatekeys/signing-1-private.pem
new file mode 100644
index 0000000..0c9f1ac
--- /dev/null
+++ b/test/config/privatekeys/signing-1-private.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEICQ+kchWtj3ZwGhzz+QkKl/CM0fsfQCDtI+1Cb3GID+moAoGCCqGSM49
+AwEHoUQDQgAEeVsqn8x1CWv4BK9+o6qQqVt+lQ7+dI6VoiwwNOT2CAvocdYHzzqW
+2/dstQZIiYSdUw1SWQMR+7fTTRDZh5bDoQ==
+-----END EC PRIVATE KEY-----
diff --git a/test/config/publickeys/signing-1.pem b/test/config/publickeys/signing-1.pem
new file mode 100644
index 0000000..cc5f472
--- /dev/null
+++ b/test/config/publickeys/signing-1.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeVsqn8x1CWv4BK9+o6qQqVt+lQ7+
+dI6VoiwwNOT2CAvocdYHzzqW2/dstQZIiYSdUw1SWQMR+7fTTRDZh5bDoQ==
+-----END PUBLIC KEY-----
diff --git a/test/config/signing-1.config b/test/config/signing-1.config
new file mode 100644
index 0000000..a11bdeb
--- /dev/null
+++ b/test/config/signing-1.config
@@ -0,0 +1,35 @@
+%% catlfish configuration file (-*- erlang -*-)
+
+[{sasl,
+ [{sasl_error_logger, false},
+ {errlog_type, error},
+ {error_logger_mf_dir, "log"},
+ {error_logger_mf_maxbytes, 10485760}, % 10 MB
+ {error_logger_mf_maxfiles, 10}]},
+ {catlfish,
+ [{known_roots_path, "known_roots"},
+ {https_servers,
+ [{signing_https_api, "127.0.0.1", 8088, signing}
+ ]},
+ {https_certfile, "catlfish/webroot/certs/webcert.pem"},
+ {https_keyfile, "catlfish/webroot/keys/webkey.pem"},
+ {https_cacertfile, "catlfish/webroot/certs/webcert.pem"}
+ ]},
+ {lager,
+ [{handlers,
+ [{lager_console_backend, info},
+ {lager_file_backend, [{file, "signing-1-error.log"}, {level, error}]},
+ {lager_file_backend, [{file, "signing-1-debug.log"}, {level, debug}]},
+ {lager_file_backend, [{file, "signing-1-console.log"}, {level, info}]}
+ ]}
+ ]},
+ {plop,
+ [{publickey_path, "publickeys"},
+ {services, [sign]},
+ {log_private_key, "test/eckey.pem"},
+ {log_public_key, "test/eckey-public.pem"},
+ {own_key, {"signing-1", "privatekeys/signing-1-private.pem"}},
+ {allowed_clients, [{"/ct/signing/sct", ["frontend-1"]},
+ {"/ct/signing/sth", ["merge-1"]}
+ ]}
+ ]}].
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-----