summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emakefile3
-rw-r--r--Makefile2
-rw-r--r--README.md3
-rw-r--r--catlfish.config2
-rw-r--r--ebin/catlfish.app2
-rw-r--r--reltool.config2
-rw-r--r--src/catlfish.erl107
-rw-r--r--src/v1.erl27
-rw-r--r--src/x509.erl140
-rw-r--r--src/x509_test.hrl111
-rw-r--r--test/testdata/known_roots/IL.StartCom Certification Authority+Go Daddy Secure Certification Authority.pem73
-rw-r--r--test/testdata/known_roots/SE.AddTrust External CA Root.pem26
-rw-r--r--test/testdata/known_roots/US.DigiCert High Assurance EV Root CA.pem39
-rw-r--r--test/testdata/known_roots/US.DigiCert SHA2 High Assurance Server CA.pem28
-rw-r--r--test/testdata/known_roots/US.RapidSSL CA.pem23
-rw-r--r--test/testdata/known_roots/US.thawte Primary Root CA.pem25
-rw-r--r--test/testdata/known_roots/broken.invalid-b64.pem3
-rw-r--r--test/testdata/known_roots/broken.invalid-der.pem3
18 files changed, 605 insertions, 14 deletions
diff --git a/Emakefile b/Emakefile
index a42a6ee..f6cea09 100644
--- a/Emakefile
+++ b/Emakefile
@@ -2,4 +2,5 @@
{["src/*", "test/*"],
[debug_info,
{i, "include/"},
- {outdir, "ebin/"}]}.
+ {outdir, "ebin/"},
+ {parse_transform, lager_transform}]}.
diff --git a/Makefile b/Makefile
index b446536..78f8a6b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
build all:
- erl -make
+ erl -pa ../lager/ebin -make
clean:
-rm ebin/*.beam
release:
diff --git a/README.md b/README.md
index 21d86e5..514e87f 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ catlfish is a Certificate Transparency log server (RFC 6962).
# Compile
- $ CTROOT=.. make
+ $ make
$ make release
# Requirements
@@ -12,6 +12,7 @@ catlfish is a Certificate Transparency log server (RFC 6962).
A compiled plop application in ../plop
A compiled https://github.com/davisp/jiffy (for JSON encoding and decoding) in ../jiffy
+A compiled https://github.com/basho/lager (for logging) in ../lager
# Start
diff --git a/catlfish.config b/catlfish.config
index 9b80c6e..c8fcb73 100644
--- a/catlfish.config
+++ b/catlfish.config
@@ -10,6 +10,8 @@
{inets,
[{services,
[{httpd, [{proplist_file, "httpd_props.conf"}]}]}]},
+ {catlfish,
+ [{known_roots_path, "known_roots"}]},
{plop,
[{entry_root_path, "db/certentries/"},
{index_path, "db/index"},
diff --git a/ebin/catlfish.app b/ebin/catlfish.app
index 8cbe5f7..44c9e0f 100644
--- a/ebin/catlfish.app
+++ b/ebin/catlfish.app
@@ -8,5 +8,5 @@
[{description, "catlfish -- Certificate Transparency Log Server"},
{vsn, "0.2.0-dev"},
{modules, [v1, catlfish_app]},
- {applications, [kernel, stdlib, plop, inets, jiffy]},
+ {applications, [kernel, stdlib, plop, inets, jiffy, lager]},
{mod, {catlfish_app, []}}]}.
diff --git a/reltool.config b/reltool.config
index 80b374d..6f9d52a 100644
--- a/reltool.config
+++ b/reltool.config
@@ -13,5 +13,7 @@
{excl_archive_filters, ["^include$","^priv$","^\\.git$"]},
{app, catlfish, [{app_file, all}, {lib_dir, "."}]},
{app, plop, [{app_file, all}, {lib_dir, "../plop"}]},
+ {app, lager, [{app_file, all}, {lib_dir, "../lager"}]},
+ {app, goldrush, [{app_file, all}, {lib_dir, "../lager/deps/goldrush"}]},
{app, jiffy, [{app_file, all}, {lib_dir, "../jiffy"}]}
]}.
diff --git a/src/catlfish.erl b/src/catlfish.erl
index e261824..73066bb 100644
--- a/src/catlfish.erl
+++ b/src/catlfish.erl
@@ -3,7 +3,8 @@
-module(catlfish).
-export([add_chain/2, entries/2, entry_and_proof/2]).
--include("$CTROOT/plop/include/plop.hrl").
+-export([known_roots/0, update_known_roots/0]).
+-include_lib("eunit/include/eunit.hrl").
-define(PROTOCOL_VERSION, 0).
@@ -162,3 +163,107 @@ decode_tls_vector(Binary, LengthLen) ->
<<Length:LengthLen/integer-unit:8, Rest/binary>> = Binary,
<<ExtractedBinary:Length/binary-unit:8, Rest2/binary>> = Rest,
{ExtractedBinary, Rest2}.
+
+-define(ROOTS_TABLE, catlfish_roots).
+
+update_known_roots() ->
+ case application:get_env(catlfish, known_roots_path) of
+ {ok, Dir} -> update_known_roots(Dir);
+ undefined -> []
+ end.
+
+update_known_roots(Directory) ->
+ known_roots(Directory, update_tab).
+
+known_roots() ->
+ case application:get_env(catlfish, known_roots_path) of
+ {ok, Dir} -> known_roots(Dir, use_cache);
+ undefined -> []
+ end.
+
+-spec known_roots(file:filename(), use_cache|update_tab) -> list().
+known_roots(Directory, CacheUsage) ->
+ case ets:info(?ROOTS_TABLE) of
+ undefined ->
+ read_pemfiles_from_dir(
+ ets:new(?ROOTS_TABLE, [set, protected, named_table]),
+ Directory);
+ _ ->
+ case CacheUsage of
+ use_cache ->
+ ets:lookup_element(?ROOTS_TABLE, list, 2);
+ update_tab ->
+ read_pemfiles_from_dir(?ROOTS_TABLE, Directory)
+ end
+ end.
+
+-spec read_pemfiles_from_dir(ets:tab(), file:filename()) -> list().
+read_pemfiles_from_dir(Tab, Dir) ->
+ DerList =
+ case file:list_dir(Dir) of
+ {error, enoent} ->
+ []; % FIXME: log enoent
+ {error, _Reason} ->
+ []; % FIXME: log Reason
+ {ok, Filenames} ->
+ Files = lists:filter(
+ fun(F) ->
+ string:equal(".pem", filename:extension(F))
+ end,
+ Filenames),
+ ders_from_pemfiles(Dir, Files)
+ end,
+ true = ets:insert(Tab, {list, DerList}),
+ DerList.
+
+ders_from_pemfiles(Dir, Filenames) ->
+ L = [ders_from_pemfile(filename:join(Dir, X)) || X <- Filenames],
+ lists:flatten(L).
+
+ders_from_pemfile(Filename) ->
+ Pems = case (catch public_key:pem_decode(pems_from_file(Filename))) of
+ {'EXIT', _} -> [];
+ P -> P
+ end,
+ [der_from_pem(X) || X <- Pems].
+
+-include_lib("public_key/include/public_key.hrl").
+der_from_pem(Pem) ->
+ case Pem of
+ {_Type, Der, not_encrypted} ->
+ case (catch public_key:pkix_decode_cert(Der, otp)) of
+ {'EXIT', _} ->
+ [];
+ #'OTPCertificate'{} ->
+ Der;
+ _Unknown ->
+ []
+ end;
+ _ -> []
+ end.
+
+pems_from_file(Filename) ->
+ {ok, Pems} = file:read_file(Filename),
+ Pems.
+
+%%%%%%%%%%%%%%%%%%%%
+%% Testing internal functions.
+-define(PEMFILES_DIR_OK, "../test/testdata/known-roots").
+-define(PEMFILES_DIR_NONEXISTENT, "../test/testdata/nonexistent-dir").
+
+read_pemfiles_test_() ->
+ {setup,
+ fun() -> {known_roots(?PEMFILES_DIR_OK, use_cache),
+ known_roots(?PEMFILES_DIR_OK, use_cache)}
+ end,
+ fun(_) -> ets:delete(?ROOTS_TABLE) end,
+ fun({L, LCached}) ->
+ [?_assertMatch(7, length(L)),
+ ?_assertEqual(L, LCached)]
+ end}.
+
+read_pemfiles_fail_test_() ->
+ {setup,
+ fun() -> known_roots(?PEMFILES_DIR_NONEXISTENT, use_cache) end,
+ fun(_) -> ets:delete(?ROOTS_TABLE) end,
+ fun(Empty) -> [?_assertMatch([], Empty)] end}.
diff --git a/src/v1.erl b/src/v1.erl
index ba5c456..d5e65ea 100644
--- a/src/v1.erl
+++ b/src/v1.erl
@@ -9,8 +9,6 @@
'get-sth-consistency'/3, 'get-proof-by-hash'/3, 'get-entries'/3,
'get-roots'/3, 'get-entry-and-proof'/3]).
--include("$CTROOT/plop/include/plop.hrl").
-
%% Public functions, i.e. part of URL.
'add-chain'(SessionID, _Env, Input) ->
R = case (catch jiffy:decode(Input)) of
@@ -22,7 +20,17 @@
html("add-chain: invalid base64-encoded chain: ",
[ChainBase64]);
[LeafCert | CertChain] ->
- catlfish:add_chain(LeafCert, CertChain);
+ Roots = catlfish:known_roots(),
+ case x509:normalise_chain(Roots, [LeafCert|CertChain]) of
+ {ok, [Leaf | Chain]} ->
+ io:format("[info] adding ~p~n",
+ [x509:cert_string(LeafCert)]),
+ catlfish:add_chain(Leaf, Chain);
+ {Err, Msg} ->
+ io:format("[info] rejecting ~p: ~p~n",
+ [x509:cert_string(LeafCert), Err]),
+ html("add-chain: ", [Msg, Err])
+ end;
Invalid ->
html("add-chain: chain is not a list: ", [Invalid])
end;
@@ -34,11 +42,10 @@
niy(SessionID).
'get-sth'(SessionID, _Env, _Input) ->
- #sth{
- treesize = Treesize,
- timestamp = Timestamp,
- roothash = Roothash,
- signature = Signature} = plop:sth(),
+ { Treesize,
+ Timestamp,
+ Roothash,
+ Signature} = plop:sth(),
R = [{tree_size, Treesize},
{timestamp, Timestamp},
{sha256_root_hash, base64:encode(Roothash)},
@@ -125,7 +132,9 @@
deliver(SessionID, R).
'get-roots'(SessionID, _Env, _Input) ->
- R = [{certificates, []}], % NIY.
+ R = [{certificates,
+ [base64:encode(Der) ||
+ Der <- catlfish:update_known_roots()]}],
deliver(SessionID, binary_to_list(jiffy:encode({R}))).
%% Private functions.
diff --git a/src/x509.erl b/src/x509.erl
new file mode 100644
index 0000000..9b6b386
--- /dev/null
+++ b/src/x509.erl
@@ -0,0 +1,140 @@
+%%% Copyright (c) 2014, NORDUnet A/S.
+%%% See LICENSE for licensing information.
+
+-module(x509).
+-export([normalise_chain/2, cert_string/1]).
+
+-include_lib("public_key/include/public_key.hrl").
+
+-type reason() :: {chain_too_long | root_unknown | chain_broken}.
+
+-define(MAX_CHAIN_LENGTH, 10).
+
+-spec normalise_chain([binary()], [binary()]) -> [binary()].
+normalise_chain(AcceptableRootCerts, CertChain) ->
+ case valid_chain_p(AcceptableRootCerts, CertChain, ?MAX_CHAIN_LENGTH) of
+ {false, Reason} ->
+ {Reason, "invalid chain"};
+ {true, Root} ->
+ [Leaf | Chain] = CertChain,
+ {ok, [detox_precert(Leaf) | Chain] ++ Root}
+ end.
+
+%%%%%%%%%%%%%%%%%%%%
+%% @doc Verify that the leaf cert or precert has a valid chain back to
+%% an acceptable root cert. Order of certificates in second argument
+%% is: leaf cert in head, chain in tail. Order of first argument is
+%% irrelevant.
+
+-spec valid_chain_p([binary()], [binary()], integer()) ->
+ {false, reason()} | {true, list()}.
+valid_chain_p(_, _, MaxChainLength) when MaxChainLength =< 0 ->
+ %% Chain too long.
+ {false, chain_too_long};
+valid_chain_p(AcceptableRootCerts, [TopCert], MaxChainLength) ->
+ %% Check root of chain.
+ case lists:member(TopCert, AcceptableRootCerts) of
+ true ->
+ %% Top cert is part of chain.
+ {true, []};
+ false when MaxChainLength =< 1 ->
+ %% Chain too long.
+ {false, chain_too_long};
+ false ->
+ %% Top cert _might_ be signed by a cert in truststore.
+ case signer(TopCert, AcceptableRootCerts) of
+ notfound -> {false, root_unknown};
+ Root -> {true, [Root]}
+ end
+ end;
+valid_chain_p(AcceptableRootCerts, [BottomCert|Rest], MaxChainLength) ->
+ case signed_by_p(BottomCert, hd(Rest)) of
+ false -> {false, chain_broken};
+ true -> valid_chain_p(AcceptableRootCerts, Rest, MaxChainLength - 1)
+ end.
+
+%% @doc Return list with first
+-spec signer(binary(), [binary()]) -> list().
+signer(_Cert, []) ->
+ notfound;
+signer(Cert, [H|T]) ->
+ case signed_by_p(Cert, H) of
+ true -> H;
+ false -> signer(Cert, T)
+ end.
+
+-spec signed_by_p(binary(), binary()) -> boolean().
+signed_by_p(Cert, IssuerCert) ->
+ %% FIXME: Validate presence and contents (against constraints) of
+ %% names (subject, subjectAltName, emailAddress) too?
+ case public_key:pkix_is_issuer(Cert, IssuerCert) of
+ true -> % Cert.issuer does match IssuerCert.subject.
+ public_key:pkix_verify(Cert, public_key(IssuerCert));
+ false ->
+ false
+ end.
+
+-spec public_key(binary() | #'OTPCertificate'{}) -> public_key:public_key().
+public_key(CertDer) when is_binary(CertDer) ->
+ public_key(public_key:pkix_decode_cert(CertDer, otp));
+public_key(#'OTPCertificate'{
+ tbsCertificate =
+ #'OTPTBSCertificate'{subjectPublicKeyInfo =
+ #'OTPSubjectPublicKeyInfo'{
+ subjectPublicKey = Key}}}) ->
+ Key.
+
+cert_string(Der) ->
+ lists:flatten([io_lib:format("~2.16.0B", [X]) ||
+ X <- binary_to_list(crypto:hash(sha, Der))]).
+
+%%%%%%%%%%%%%%%%%%%%
+%% Precertificates according to draft-ietf-trans-rfc6962-bis-04.
+
+%% 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
+%% 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.
+
+%% 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
+
+%%%%%%%%%%%%%%%%%%%%
+%% Testing private functions.
+-include_lib("eunit/include/eunit.hrl").
+-include("x509_test.hrl").
+valid_cert_test_() ->
+ C0 = ?C0,
+ C1 = ?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)),
+ %% Chain too long.
+ ?_assertMatch({false, chain_too_long}, valid_chain_p([C1], [C0], 1)),
+ %% Root in chain and in trust store.
+ ?_assertEqual({true, []}, valid_chain_p([C1], [C0, C1], 2)),
+ %% Chain too long.
+ ?_assertMatch({false, chain_too_long}, valid_chain_p([C1], [C0, C1], 1)),
+ %% Root not in trust store.
+ ?_assertMatch({false, root_unknown}, valid_chain_p([], [C0, C1], 10)),
+ %% Invalid signer.
+ ?_assertMatch({false, chain_broken}, valid_chain_p([C0], [C1, C0], 10)),
+ %% Selfsigned. Actually OK.
+ ?_assertMatch({true, []}, valid_chain_p([C0], [C0], 10)),
+ ?_assertMatch({true, []}, valid_chain_p([C0], [C0], 1)),
+ %% Max chain length 0 is not OK.
+ ?_assertMatch({false, chain_too_long}, valid_chain_p([C0], [C0], 0))
+ ].
diff --git a/src/x509_test.hrl b/src/x509_test.hrl
new file mode 100644
index 0000000..93803a2
--- /dev/null
+++ b/src/x509_test.hrl
@@ -0,0 +1,111 @@
+%% C1 signs C0
+
+-define(C0,
+ <<48,130,5,5,48,130,3,237,160,3,2,1,2,2,17,0,223,204,174,11,223,131,19,38,89,
+ 67,221,26,255,213,96,72,48,13,6,9,42,134,72,134,247,13,1,1,5,5,0,48,54,49,11,
+ 48,9,6,3,85,4,6,19,2,78,76,49,15,48,13,6,3,85,4,10,19,6,84,69,82,69,78,65,49,
+ 22,48,20,6,3,85,4,3,19,13,84,69,82,69,78,65,32,83,83,76,32,67,65,48,30,23,13,
+ 49,52,48,55,48,51,48,48,48,48,48,48,90,23,13,49,55,48,55,48,50,50,51,53,57,
+ 53,57,90,48,129,188,49,11,48,9,6,3,85,4,6,19,2,83,69,49,15,48,13,6,3,85,4,17,
+ 19,6,49,49,52,32,50,55,49,24,48,22,6,3,85,4,8,12,15,83,116,111,99,107,104,
+ 111,108,109,115,32,108,195,164,110,49,18,48,16,6,3,85,4,7,19,9,83,116,111,99,
+ 107,104,111,108,109,49,26,48,24,6,3,85,4,9,12,17,86,97,108,104,97,108,108,97,
+ 118,195,164,103,101,110,32,55,57,49,37,48,35,6,3,85,4,10,12,28,75,117,110,
+ 103,108,105,103,97,32,84,101,107,110,105,115,107,97,32,72,195,182,103,115,
+ 107,111,108,97,110,49,12,48,10,6,3,85,4,11,19,3,73,84,65,49,29,48,27,6,3,85,
+ 4,3,19,20,110,115,45,118,105,112,45,48,49,46,115,121,115,46,107,116,104,46,
+ 115,101,48,130,1,34,48,13,6,9,42,134,72,134,247,13,1,1,1,5,0,3,130,1,15,0,48,
+ 130,1,10,2,130,1,1,0,166,33,67,34,241,62,140,127,233,46,254,30,221,209,182,
+ 44,139,207,232,43,36,138,78,153,18,175,55,128,110,229,13,134,105,211,249,84,
+ 204,189,189,176,231,127,10,112,242,167,58,17,70,21,17,119,47,55,218,214,62,
+ 244,188,171,139,89,183,203,169,1,206,93,173,24,171,76,219,154,245,77,20,127,
+ 84,182,27,105,154,254,238,143,154,60,184,174,228,77,35,69,216,92,218,90,121,
+ 183,130,14,144,114,76,88,0,4,181,120,111,112,141,27,223,73,36,176,205,188,52,
+ 123,252,183,182,248,77,250,252,94,175,54,87,242,136,152,225,209,86,154,217,
+ 143,69,255,113,19,95,11,145,11,172,227,80,9,60,177,254,225,32,151,110,155,
+ 137,159,133,104,222,68,228,173,154,118,49,154,56,109,158,143,7,252,182,231,
+ 177,104,192,211,168,146,68,6,253,8,58,192,229,254,245,101,191,204,112,163,
+ 241,26,168,125,161,33,154,152,45,87,35,164,150,2,100,13,50,205,241,159,60,
+ 235,214,104,248,219,118,77,224,242,61,100,107,248,140,69,73,86,66,92,9,196,
+ 47,74,130,49,128,235,28,12,217,40,153,3,2,3,1,0,1,163,130,1,133,48,130,1,129,
+ 48,31,6,3,85,29,35,4,24,48,22,128,20,12,189,147,104,12,243,222,171,163,73,
+ 107,43,55,87,71,234,144,227,185,237,48,29,6,3,85,29,14,4,22,4,20,220,21,64,
+ 117,11,185,137,252,236,135,28,40,51,178,241,101,243,183,42,113,48,14,6,3,85,
+ 29,15,1,1,255,4,4,3,2,5,160,48,12,6,3,85,29,19,1,1,255,4,2,48,0,48,29,6,3,85,
+ 29,37,4,22,48,20,6,8,43,6,1,5,5,7,3,1,6,8,43,6,1,5,5,7,3,2,48,34,6,3,85,29,
+ 32,4,27,48,25,48,13,6,11,43,6,1,4,1,178,49,1,2,2,29,48,8,6,6,103,129,12,1,2,
+ 2,48,58,6,3,85,29,31,4,51,48,49,48,47,160,45,160,43,134,41,104,116,116,112,
+ 58,47,47,99,114,108,46,116,99,115,46,116,101,114,101,110,97,46,111,114,103,
+ 47,84,69,82,69,78,65,83,83,76,67,65,46,99,114,108,48,109,6,8,43,6,1,5,5,7,1,
+ 1,4,97,48,95,48,53,6,8,43,6,1,5,5,7,48,2,134,41,104,116,116,112,58,47,47,99,
+ 114,116,46,116,99,115,46,116,101,114,101,110,97,46,111,114,103,47,84,69,82,
+ 69,78,65,83,83,76,67,65,46,99,114,116,48,38,6,8,43,6,1,5,5,7,48,1,134,26,104,
+ 116,116,112,58,47,47,111,99,115,112,46,116,99,115,46,116,101,114,101,110,97,
+ 46,111,114,103,48,51,6,3,85,29,17,4,44,48,42,130,20,110,115,45,118,105,112,
+ 45,48,49,46,115,121,115,46,107,116,104,46,115,101,130,6,107,116,104,46,115,
+ 101,130,10,119,119,119,46,107,116,104,46,115,101,48,13,6,9,42,134,72,134,247,
+ 13,1,1,5,5,0,3,130,1,1,0,174,206,231,39,9,125,240,24,18,81,52,193,118,3,201,
+ 18,26,144,85,135,110,139,142,210,95,67,250,122,79,51,63,41,43,50,30,44,70,
+ 120,235,251,2,43,209,62,53,221,192,144,34,41,47,72,89,126,217,82,42,192,224,
+ 222,106,105,2,3,33,48,185,246,253,59,177,29,233,76,148,74,157,192,222,255,
+ 237,248,141,60,108,236,5,139,184,124,26,169,25,215,91,35,175,81,89,255,245,
+ 39,52,118,93,234,59,234,68,58,80,44,32,220,72,171,97,252,59,206,151,149,243,
+ 87,45,245,252,88,112,81,50,105,184,134,215,201,123,134,102,4,155,253,210,120,
+ 167,145,13,198,22,106,233,62,132,31,228,131,185,115,8,157,170,100,82,36,254,
+ 196,12,16,73,136,236,155,232,54,224,31,173,102,48,126,191,97,27,127,66,107,3,
+ 80,209,183,134,158,80,0,43,18,94,83,98,2,213,38,229,245,199,1,187,82,69,69,
+ 203,12,69,58,254,99,194,31,27,232,13,150,30,90,254,240,201,247,195,39,72,150,
+ 105,241,6,104,213,53,195,138,113,36,251,107,82,29,84,121,138,41,245,160,225,
+ 245,15,173>>).
+-define(C1,
+ <<48,130,4,152,48,130,3,128,160,3,2,1,2,2,16,75,200,20,3,47,7,250,106,164,240,
+ 218,41,223,97,121,186,48,13,6,9,42,134,72,134,247,13,1,1,5,5,0,48,129,151,49,
+ 11,48,9,6,3,85,4,6,19,2,85,83,49,11,48,9,6,3,85,4,8,19,2,85,84,49,23,48,21,6,
+ 3,85,4,7,19,14,83,97,108,116,32,76,97,107,101,32,67,105,116,121,49,30,48,28,
+ 6,3,85,4,10,19,21,84,104,101,32,85,83,69,82,84,82,85,83,84,32,78,101,116,119,
+ 111,114,107,49,33,48,31,6,3,85,4,11,19,24,104,116,116,112,58,47,47,119,119,
+ 119,46,117,115,101,114,116,114,117,115,116,46,99,111,109,49,31,48,29,6,3,85,
+ 4,3,19,22,85,84,78,45,85,83,69,82,70,105,114,115,116,45,72,97,114,100,119,97,
+ 114,101,48,30,23,13,48,57,48,53,49,56,48,48,48,48,48,48,90,23,13,50,48,48,53,
+ 51,48,49,48,52,56,51,56,90,48,54,49,11,48,9,6,3,85,4,6,19,2,78,76,49,15,48,
+ 13,6,3,85,4,10,19,6,84,69,82,69,78,65,49,22,48,20,6,3,85,4,3,19,13,84,69,82,
+ 69,78,65,32,83,83,76,32,67,65,48,130,1,34,48,13,6,9,42,134,72,134,247,13,1,1,
+ 1,5,0,3,130,1,15,0,48,130,1,10,2,130,1,1,0,195,227,72,196,47,92,193,203,169,
+ 153,253,27,162,131,93,138,61,173,58,208,226,164,67,31,77,14,254,53,37,48,165,
+ 105,27,196,232,229,193,143,84,126,225,106,162,154,92,92,222,61,252,2,206,150,
+ 184,95,143,131,91,204,96,64,144,248,228,182,58,37,156,95,20,81,236,177,231,
+ 175,158,80,161,49,85,199,2,189,172,82,138,127,53,142,130,250,132,173,21,254,
+ 162,127,131,16,58,85,83,148,44,1,22,116,148,84,99,40,163,242,91,41,61,148,
+ 136,128,32,226,20,89,33,25,180,164,152,225,96,230,242,235,162,128,131,67,224,
+ 173,104,243,121,25,139,104,67,81,63,138,155,65,133,12,53,140,93,181,241,182,
+ 229,167,195,131,181,107,35,111,212,165,235,80,229,148,241,74,95,238,39,75,20,
+ 18,21,36,76,13,207,98,141,183,0,33,173,58,50,15,88,11,95,30,155,209,223,157,
+ 142,169,25,53,80,47,65,169,173,59,198,224,69,178,83,57,127,33,191,34,26,135,
+ 92,52,174,82,111,7,125,162,11,78,159,43,121,166,125,19,221,245,127,131,124,
+ 47,90,93,119,120,120,145,160,20,191,125,2,3,1,0,1,163,130,1,62,48,130,1,58,
+ 48,31,6,3,85,29,35,4,24,48,22,128,20,161,114,95,38,27,40,152,67,149,93,7,55,
+ 213,133,150,157,75,210,195,69,48,29,6,3,85,29,14,4,22,4,20,12,189,147,104,12,
+ 243,222,171,163,73,107,43,55,87,71,234,144,227,185,237,48,14,6,3,85,29,15,1,
+ 1,255,4,4,3,2,1,6,48,18,6,3,85,29,19,1,1,255,4,8,48,6,1,1,255,2,1,0,48,24,6,
+ 3,85,29,32,4,17,48,15,48,13,6,11,43,6,1,4,1,178,49,1,2,2,29,48,68,6,3,85,29,
+ 31,4,61,48,59,48,57,160,55,160,53,134,51,104,116,116,112,58,47,47,99,114,108,
+ 46,117,115,101,114,116,114,117,115,116,46,99,111,109,47,85,84,78,45,85,83,69,
+ 82,70,105,114,115,116,45,72,97,114,100,119,97,114,101,46,99,114,108,48,116,6,
+ 8,43,6,1,5,5,7,1,1,4,104,48,102,48,61,6,8,43,6,1,5,5,7,48,2,134,49,104,116,
+ 116,112,58,47,47,99,114,116,46,117,115,101,114,116,114,117,115,116,46,99,111,
+ 109,47,85,84,78,65,100,100,84,114,117,115,116,83,101,114,118,101,114,95,67,
+ 65,46,99,114,116,48,37,6,8,43,6,1,5,5,7,48,1,134,25,104,116,116,112,58,47,47,
+ 111,99,115,112,46,117,115,101,114,116,114,117,115,116,46,99,111,109,48,13,6,
+ 9,42,134,72,134,247,13,1,1,5,5,0,3,130,1,1,0,78,35,238,72,156,246,133,139,
+ 113,196,10,110,115,147,114,192,58,142,128,138,217,179,202,178,212,1,156,40,
+ 207,242,92,14,33,68,147,11,182,26,33,227,152,1,148,14,103,73,129,30,190,61,
+ 13,78,96,218,239,160,49,78,149,239,243,221,122,90,130,32,67,182,161,99,67,
+ 179,80,105,67,98,75,86,98,176,52,138,185,19,67,89,147,236,20,121,136,243,72,
+ 147,232,157,201,250,135,114,12,107,86,160,195,21,141,104,165,135,31,113,45,
+ 230,90,109,60,105,113,64,4,85,220,160,67,148,32,69,56,120,215,189,138,216,57,
+ 198,223,9,183,90,154,169,3,184,40,16,120,205,191,1,27,90,17,62,56,244,216,27,
+ 52,121,207,51,210,1,253,172,152,205,109,71,17,144,76,187,185,91,216,112,231,
+ 213,175,182,204,196,134,230,117,192,158,41,182,43,15,42,165,105,2,13,227,233,
+ 162,180,93,192,243,206,44,106,133,56,118,97,198,73,130,171,81,179,130,166,
+ 185,65,152,40,152,251,107,254,138,22,255,49,126,84,71,168,60,220,67,38,169,
+ 155,5,183,158,192,52,67,145,48,212,50,195,17,90,225>>).
diff --git a/test/testdata/known_roots/IL.StartCom Certification Authority+Go Daddy Secure Certification Authority.pem b/test/testdata/known_roots/IL.StartCom Certification Authority+Go Daddy Secure Certification Authority.pem
new file mode 100644
index 0000000..eeae491
--- /dev/null
+++ b/test/testdata/known_roots/IL.StartCom Certification Authority+Go Daddy Secure Certification Authority.pem
@@ -0,0 +1,73 @@
+-----BEGIN CERTIFICATE-----
+MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW
+MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg
+Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9
+MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi
+U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh
+cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA
+A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk
+pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf
+OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C
+Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT
+Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi
+HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM
+Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w
++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+
+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3
+Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B
+26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID
+AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE
+FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j
+ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js
+LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM
+BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0
+Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy
+dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh
+cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh
+YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg
+dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp
+bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ
+YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT
+TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ
+9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8
+jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW
+FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz
+ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1
+ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L
+EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu
+L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq
+yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC
+O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V
+um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh
+NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIE3jCCA8agAwIBAgICAwEwDQYJKoZIhvcNAQEFBQAwYzELMAkGA1UEBhMCVVMx
+ITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g
+RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMTYw
+MTU0MzdaFw0yNjExMTYwMTU0MzdaMIHKMQswCQYDVQQGEwJVUzEQMA4GA1UECBMH
+QXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTEaMBgGA1UEChMRR29EYWRkeS5j
+b20sIEluYy4xMzAxBgNVBAsTKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5j
+b20vcmVwb3NpdG9yeTEwMC4GA1UEAxMnR28gRGFkZHkgU2VjdXJlIENlcnRpZmlj
+YXRpb24gQXV0aG9yaXR5MREwDwYDVQQFEwgwNzk2OTI4NzCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAMQt1RWMnCZM7DI161+4WQFapmGBWTtwY6vj3D3H
+KrjJM9N55DrtPDAjhI6zMBS2sofDPZVUBJ7fmd0LJR4h3mUpfjWoqVTr9vcyOdQm
+VZWt7/v+WIbXnvQAjYwqDL1CBM6nPwT27oDyqu9SoWlm2r4arV3aLGbqGmu75RpR
+SgAvSMeYddi5Kcju+GZtCpyz8/x4fKL4o/K1w/O5epHBp+YlLpyo7RJlbmr2EkRT
+cDCVw5wrWCs9CHRK8r5RsL+H0EwnWGu1NcWdrxcx+AuP7q2BNgWJCJjPOq8lh8BJ
+6qf9Z/dFjpfMFDniNoW1fho3/Rb2cRGadDAW/hOUoz+EDU8CAwEAAaOCATIwggEu
+MB0GA1UdDgQWBBT9rGEyk2xF1uLuhV+auud2mWjM5zAfBgNVHSMEGDAWgBTSxLDS
+kdRMEXGzYcs9of7dqGrU4zASBgNVHRMBAf8ECDAGAQH/AgEAMDMGCCsGAQUFBwEB
+BCcwJTAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZ29kYWRkeS5jb20wRgYDVR0f
+BD8wPTA7oDmgN4Y1aHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNvbS9yZXBv
+c2l0b3J5L2dkcm9vdC5jcmwwSwYDVR0gBEQwQjBABgRVHSAAMDgwNgYIKwYBBQUH
+AgEWKmh0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVwb3NpdG9yeTAO
+BgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBANKGwOy9+aG2Z+5mC6IG
+OgRQjhVyrEp0lVPLN8tESe8HkGsz2ZbwlFalEzAFPIUyIXvJxwqoJKSQ3kbTJSMU
+A2fCENZvD117esyfxVgqwcSeIaha86ykRvOe5GPLL5CkKSkB2XIsKd83ASe8T+5o
+0yGPwLPk9Qnt0hCqU7S+8MxZC9Y7lhyVJEnfzuz9p0iRFEUOOjZv2kWzRaJBydTX
+RE4+uXR21aITVSzGh6O1mawGhId/dQb8vxRMDsxuxN89txJx9OjxUUAiKEngHUuH
+qDTMBqLdElrRhjZkAzVvb3du6/KFUJheqwNTrZEjYx8WnM25sgVjOuH0aBsXBTWV
+U+4=
+-----END CERTIFICATE-----
diff --git a/test/testdata/known_roots/SE.AddTrust External CA Root.pem b/test/testdata/known_roots/SE.AddTrust External CA Root.pem
new file mode 100644
index 0000000..02c3944
--- /dev/null
+++ b/test/testdata/known_roots/SE.AddTrust External CA Root.pem
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE-----
+MIIEZDCCA0ygAwIBAgIRALmfsKN7LvrBTlo9bsrluT0wDQYJKoZIhvcNAQEFBQAw
+NjELMAkGA1UEBhMCTkwxDzANBgNVBAoTBlRFUkVOQTEWMBQGA1UEAxMNVEVSRU5B
+IFNTTCBDQTAeFw0xMzAzMjEwMDAwMDBaFw0xNjA0MDIyMzU5NTlaMDkxITAfBgNV
+BAsTGERvbWFpbiBDb250cm9sIFZhbGlkYXRlZDEUMBIGA1UEAxQLKi5ub3JkdS5u
+ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQClir/sHXJpaMQ8SpK1
+giyizJhK9GSuZkoTaIKiK2hXkHUbxJ09w6pspWXPbUwLK8ZFn32vHMabshKxe4fL
+d0kR/AEr9okwfnABK7+u4CBEs10D2oVrRFS2GFAUtri8v+5+n/mWDoqGc2XybQNs
+CoYyVdSYs6YO/+b8dEGfOrRD2XFoTtP32T35YIlejwpg72f9lUnvOi6Jh+s6jV8P
+hIJV6w3exVQojDiEPSQ3fV/KF6FAaQK4XyEspHL4TH0mtaJhEjnAvHDmN1Bw4WhV
+0Bm86alryZxYNTmpPXDD5AFNBIuL+5FfQgZm+s7QzZriguRGDv8L+YKePFvhiaPV
+AagTAgMBAAGjggFoMIIBZDAfBgNVHSMEGDAWgBQMvZNoDPPeq6NJays3V0fqkOO5
+7TAdBgNVHQ4EFgQU6YkL0qj0tSK5bsZfjDUNLwXUlFgwDgYDVR0PAQH/BAQDAgWg
+MAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMCIG
+A1UdIAQbMBkwDQYLKwYBBAGyMQECAh0wCAYGZ4EMAQIBMDoGA1UdHwQzMDEwL6At
+oCuGKWh0dHA6Ly9jcmwudGNzLnRlcmVuYS5vcmcvVEVSRU5BU1NMQ0EuY3JsMG0G
+CCsGAQUFBwEBBGEwXzA1BggrBgEFBQcwAoYpaHR0cDovL2NydC50Y3MudGVyZW5h
+Lm9yZy9URVJFTkFTU0xDQS5jcnQwJgYIKwYBBQUHMAGGGmh0dHA6Ly9vY3NwLnRj
+cy50ZXJlbmEub3JnMBYGA1UdEQQPMA2CCyoubm9yZHUubmV0MA0GCSqGSIb3DQEB
+BQUAA4IBAQAdj2R0qT47oLIMnYw69qU58VZB/rnejwhNVdzLtLZ+vQ1YwcXoabOi
+9LmSOZ019ESWxZ415/FjvoLXYKpkq8w96bDw/jqPhUWwK2U6EpD/MlYUKWyAH9XP
+ZLBaYewZEBjkwxYIlroUboPWXUYJIDwotvNgSE9N8Xy1XZ4oi0UVfxxyo3XRpS49
+Ch1az16jKS5rF5R1Q/t6UxYrnfx4XMZHFx56ks6kpucxch37JJ/2i1O84/T9lX17
+7qwk+SO93EmtgxE40wtvL1i2cTZaNHcybyClV6N3Bm8Hu2L4e35SF761CMc4rzlu
+SbDmRK4Rxa5UmgfZnezD0snHVUCrzKzP
+-----END CERTIFICATE-----
diff --git a/test/testdata/known_roots/US.DigiCert High Assurance EV Root CA.pem b/test/testdata/known_roots/US.DigiCert High Assurance EV Root CA.pem
new file mode 100644
index 0000000..c9eb314
--- /dev/null
+++ b/test/testdata/known_roots/US.DigiCert High Assurance EV Root CA.pem
@@ -0,0 +1,39 @@
+-----BEGIN CERTIFICATE-----
+MIIG2jCCBcKgAwIBAgIQAbtvABIrF382yrSc6otrJjANBgkqhkiG9w0BAQsFADB1
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk
+IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE0MDkwNTAwMDAwMFoXDTE2MDkwOTEy
+MDAwMFowgfkxHTAbBgNVBA8TFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB
+BAGCNzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMRAwDgYDVQQF
+EwczMzU5MzAwMRQwEgYDVQQJEwsxNiBBbGxlbiBSZDETMBEGA1UEERMKMDM4OTQt
+NDgwMTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAk5IMRMwEQYDVQQHEwpXb2xmZWJv
+cm8sMSMwIQYDVQQKExpQeXRob24gU29mdHdhcmUgRm91bmRhdGlvbjEXMBUGA1UE
+AxMOd3d3LnB5dGhvbi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQCtUnfHpOteoIqZxGsaR/2tIenj0+pBtNBiWT6PlYLLXC6MNRjFwtnhRzEVanAm
+GEEOEQwUokYZHw8kCL2SIZ1DFI5IIFyhTFql1dqiKtoQse0LAZlUHscVxn9OZyWM
+DA4JZ6A4c3/j5SA9hGO3+KyTc95GfiEXqkSkmjH3aBtY2flr+H1fvatQA8AIAD5k
+weQLFbbqi33Uvf4sJ3OhY63Kf1ZWteXSeCT+FRMlFTaYbauo86AmU9X2/b85wold
+naUO3VjcGjTSoSuaxtWuHFRxpOTBG7bqPbtWk+X5l+rjsIoGJ6ZrRFbAtHqG+S3v
+luEG9FtgGAo+3hKm99U8UKKVAgMBAAGjggLfMIIC2zAfBgNVHSMEGDAWgBQ901Cl
+1qCt7vNKYApl0yHU+PjWDzAdBgNVHQ4EFgQUTWfmKThuIBhkZX4B3yNf+DpBqokw
+ggEUBgNVHREEggELMIIBB4IOd3d3LnB5dGhvbi5vcmeCCnB5dGhvbi5vcmeCD3B5
+cGkucHl0aG9uLm9yZ4IPZG9jcy5weXRob24ub3JnghN0ZXN0cHlwaS5weXRob24u
+b3Jngg9idWdzLnB5dGhvbi5vcmeCD3dpa2kucHl0aG9uLm9yZ4INaGcucHl0aG9u
+Lm9yZ4IPbWFpbC5weXRob24ub3JnghRwYWNrYWdpbmcucHl0aG9uLm9yZ4IQcHl0
+aG9uaG9zdGVkLm9yZ4IUd3d3LnB5dGhvbmhvc3RlZC5vcmeCFXRlc3QucHl0aG9u
+aG9zdGVkLm9yZ4IMdXMucHljb24ub3Jngg1pZC5weXRob24ub3JnMA4GA1UdDwEB
+/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdQYDVR0fBG4w
+bDA0oDKgMIYuaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItZXYtc2VydmVy
+LWcxLmNybDA0oDKgMIYuaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItZXYt
+c2VydmVyLWcxLmNybDBCBgNVHSAEOzA5MDcGCWCGSAGG/WwCATAqMCgGCCsGAQUF
+BwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIGIBggrBgEFBQcBAQR8
+MHowJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBSBggrBgEF
+BQcwAoZGaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkV4
+dGVuZGVkVmFsaWRhdGlvblNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqG
+SIb3DQEBCwUAA4IBAQBsTgMOFUP8wHVpgCzm/fQTrKp4nxcb9m9gkTW1aRKuhlAY
+g/CUQ8DC0Ii1XqOolTmGi6NIyX2Xf+RWqh7UzK+Q30Y2RGGb/47uZaif9WaIlKGn
+40D1mzzyGjrfTMSSFlrtwyg/3yM8KN800Cz5HgXnHD2qIuYcYqXRRS6E7PEHB1Dm
+h72iCAHYwUTgfcfqUWVEZ26EQhP4Lk4+hs2UJsAUnMWj7/bnk8LR/KZumLuuv3RK
+lmR1Qg+9AChafiCCFra1UxfgznvF5ocJzr6nNmYc6k1ImaipRq7c/OuwUTTqNqR2
+FceHmpqlkA2AvjdvSvwnODux3QPbMucIaJXrUUwf
+-----END CERTIFICATE-----
diff --git a/test/testdata/known_roots/US.DigiCert SHA2 High Assurance Server CA.pem b/test/testdata/known_roots/US.DigiCert SHA2 High Assurance Server CA.pem
new file mode 100644
index 0000000..8c4c741
--- /dev/null
+++ b/test/testdata/known_roots/US.DigiCert SHA2 High Assurance Server CA.pem
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIIEsTCCA5mgAwIBAgIQBOHnpNxc8vNtwCtCuF0VnzANBgkqhkiG9w0BAQsFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcDEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvbTEvMC0GA1UEAxMmRGlnaUNlcnQgU0hBMiBIaWdoIEFzc3Vy
+YW5jZSBTZXJ2ZXIgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2
+4C/CJAbIbQRf1+8KZAayfSImZRauQkCbztyfn3YHPsMwVYcZuU+UDlqUH1VWtMIC
+Kq/QmO4LQNfE0DtyyBSe75CxEamu0si4QzrZCwvV1ZX1QK/IHe1NnF9Xt4ZQaJn1
+itrSxwUfqJfJ3KSxgoQtxq2lnMcZgqaFD15EWCo3j/018QsIJzJa9buLnqS9UdAn
+4t07QjOjBSjEuyjMmqwrIw14xnvmXnG3Sj4I+4G3FhahnSMSTeXXkgisdaScus0X
+sh5ENWV/UyU50RwKmmMbGZJ0aAo3wsJSSMs5WqK24V3B3aAguCGikyZvFEohQcft
+bZvySC/zA/WiaJJTL17jAgMBAAGjggFJMIIBRTASBgNVHRMBAf8ECDAGAQH/AgEA
+MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw
+NAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy
+dC5jb20wSwYDVR0fBEQwQjBAoD6gPIY6aHR0cDovL2NybDQuZGlnaWNlcnQuY29t
+L0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUVWUm9vdENBLmNybDA9BgNVHSAENjA0MDIG
+BFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQ
+UzAdBgNVHQ4EFgQUUWj/kK8CB3U8zNllZGKiErhZcjswHwYDVR0jBBgwFoAUsT7D
+aQP4v0cB1JgmGggC72NkK8MwDQYJKoZIhvcNAQELBQADggEBABiKlYkD5m3fXPwd
+aOpKj4PWUS+Na0QWnqxj9dJubISZi6qBcYRb7TROsLd5kinMLYBq8I4g4Xmk/gNH
+E+r1hspZcX30BJZr01lYPf7TMSVcGDiEo+afgv2MW5gxTs14nhr9hctJqvIni5ly
+/D6q1UEL2tU2ob8cbkdJf17ZSHwD2f2LSaCYJkJA69aSEaRkCldUxPUd1gJea6zu
+xICaEnL6VpPX/78whQYwvwt/Tv9XBZ0k7YXDK/umdaisLRbvfXknsuvCnQsH6qqF
+0wGjIChBWUMo0oHjqvbsezt3tkBigAVBRQHvFwY+3sAzm2fTYS5yh+Rp/BIAV0Ae
+cPUeybQ=
+-----END CERTIFICATE-----
diff --git a/test/testdata/known_roots/US.RapidSSL CA.pem b/test/testdata/known_roots/US.RapidSSL CA.pem
new file mode 100644
index 0000000..71af595
--- /dev/null
+++ b/test/testdata/known_roots/US.RapidSSL CA.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID1TCCAr2gAwIBAgIDAjbRMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
+MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
+YWwgQ0EwHhcNMTAwMjE5MjI0NTA1WhcNMjAwMjE4MjI0NTA1WjA8MQswCQYDVQQG
+EwJVUzEXMBUGA1UEChMOR2VvVHJ1c3QsIEluYy4xFDASBgNVBAMTC1JhcGlkU1NM
+IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3H4Vsce2cy1rfa0
+l6P7oeYLUF9QqjraD/w9KSRDxhApwfxVQHLuverfn7ZB9EhLyG7+T1cSi1v6kt1e
+6K3z8Buxe037z/3R5fjj3Of1c3/fAUnPjFbBvTfjW761T4uL8NpPx+PdVUdp3/Jb
+ewdPPeWsIcHIHXro5/YPoar1b96oZU8QiZwD84l6pV4BcjPtqelaHnnzh8jfyMX8
+N8iamte4dsywPuf95lTq319SQXhZV63xEtZ/vNWfcNMFbPqjfWdY3SZiHTGSDHl5
+HI7PynvBZq+odEj7joLCniyZXHstXZu8W1eefDp6E63yoxhbK1kPzVw662gzxigd
+gtFQiwIDAQABo4HZMIHWMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUa2k9ahhC
+St2PAmU5/TUkhniRFjAwHwYDVR0jBBgwFoAUwHqYaI2J+6sFZAwRfap9ZbjKzE4w
+EgYDVR0TAQH/BAgwBgEB/wIBADA6BgNVHR8EMzAxMC+gLaArhilodHRwOi8vY3Js
+Lmdlb3RydXN0LmNvbS9jcmxzL2d0Z2xvYmFsLmNybDA0BggrBgEFBQcBAQQoMCYw
+JAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmdlb3RydXN0LmNvbTANBgkqhkiG9w0B
+AQUFAAOCAQEAq7y8Cl0YlOPBscOoTFXWvrSY8e48HM3P8yQkXJYDJ1j8Nq6iL4/x
+/torAsMzvcjdSCIrYA+lAxD9d/jQ7ZZnT/3qRyBwVNypDFV+4ZYlitm12ldKvo2O
+SUNjpWxOJ4cl61tt/qJ/OCjgNqutOaWlYsS3XFgsql0BYKZiZ6PAx2Ij9OdsRu61
+04BqIhPSLT90T+qvjF+0OJzbrs6vhB6m9jRRWXnT43XcvNfzc9+S7NIgWW+c+5X4
+knYYCnwPLKbK3opie9jzzl9ovY8+wXS7FXI6FoOpC+ZNmZzYV+yoAVHHb1c0XqtK
+LEL2TxyJeN4mTvVvk0wVaydWTQBUbHq3tw==
+-----END CERTIFICATE-----
diff --git a/test/testdata/known_roots/US.thawte Primary Root CA.pem b/test/testdata/known_roots/US.thawte Primary Root CA.pem
new file mode 100644
index 0000000..6f25824
--- /dev/null
+++ b/test/testdata/known_roots/US.thawte Primary Root CA.pem
@@ -0,0 +1,25 @@
+-----BEGIN CERTIFICATE-----
+MIIERTCCA66gAwIBAgIQM2VQCHmtc+IwueAdDX+skTANBgkqhkiG9w0BAQUFADCB
+zjELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJ
+Q2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UE
+CxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhh
+d3RlIFByZW1pdW0gU2VydmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNl
+cnZlckB0aGF3dGUuY29tMB4XDTA2MTExNzAwMDAwMFoXDTIwMTIzMDIzNTk1OVow
+gakxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwx0aGF3dGUsIEluYy4xKDAmBgNVBAsT
+H0NlcnRpZmljYXRpb24gU2VydmljZXMgRGl2aXNpb24xODA2BgNVBAsTLyhjKSAy
+MDA2IHRoYXd0ZSwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYD
+VQQDExZ0aGF3dGUgUHJpbWFyeSBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEArKDw+4BZ1JzHpM+doVlzCRBFDA0sbmjxbFtIaElZN/wLMxnC
+d3/MEC2VNBzm600JpxzSuMmXNgK3idQkXwbAzESUlI0CYm/rWt0RjSiaXISQEHoN
+vXRmL2o4oOLVVETrHQefB7pv7un9Tgsp9T6EoAHxnKv4HH6JpOih2HFlDaNRe+68
+0iJgDblbnd+6/FFbC6+Ysuku6QToYofeK8jXTsFMZB7dz4dYukpPymgHHRydSsbV
+L5HMfHFyHMXAZ+sy/cmSXJTahcCbv1N9Kwn0jJ2RH5dqUsveCTakd9h7h1BE1T5u
+KWn7OUkmHgmlgHtALevoJ4XJ/mH9fuZ8lx3VnQIDAQABo4HCMIG/MA8GA1UdEwEB
+/wQFMAMBAf8wOwYDVR0gBDQwMjAwBgRVHSAAMCgwJgYIKwYBBQUHAgEWGmh0dHBz
+Oi8vd3d3LnRoYXd0ZS5jb20vY3BzMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU
+e1tFz6/Oy3r9MZIaarbzRutXSFAwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cDovL2Ny
+bC50aGF3dGUuY29tL1RoYXd0ZVByZW1pdW1TZXJ2ZXJDQS5jcmwwDQYJKoZIhvcN
+AQEFBQADgYEAhKhMyT4qvJrizI8LsiV3xGGJiWNa1KMVQNT7Xj+0Q+pjFytrmXSe
+Cajd1FYVLnp5MV9jllMbNNkV6k9tcMq+9oKp7dqFd8x2HGqBCiHYQZl/Xi6Cweiq
+95OBBaqStB+3msAHF/XLxrRMDtdW3HEgdDjWdMbWj2uvi42gbCkLYeA=
+-----END CERTIFICATE-----
diff --git a/test/testdata/known_roots/broken.invalid-b64.pem b/test/testdata/known_roots/broken.invalid-b64.pem
new file mode 100644
index 0000000..0156ad0
--- /dev/null
+++ b/test/testdata/known_roots/broken.invalid-b64.pem
@@ -0,0 +1,3 @@
+-----BEGIN CERTIFICATE-----
+this is not valid base64
+-----END CERTIFICATE-----
diff --git a/test/testdata/known_roots/broken.invalid-der.pem b/test/testdata/known_roots/broken.invalid-der.pem
new file mode 100644
index 0000000..1d4f375
--- /dev/null
+++ b/test/testdata/known_roots/broken.invalid-der.pem
@@ -0,0 +1,3 @@
+-----BEGIN CERTIFICATE-----
+dGhpcyBpcyBub3QgYSB2YWxpZCBERVItZW5jb2RlZCB4NTA5IGNlcnQK
+-----END CERTIFICATE-----