From 0aeb7d1de8e50dd0fa92e763ce4c8dd3c172dac8 Mon Sep 17 00:00:00 2001 From: Linus Nordberg Date: Wed, 15 Oct 2014 16:03:25 +0200 Subject: Implement cert chain validation. NOTE: Presence of and constraints on names are not being validated. --- src/catlfish.erl | 106 ++++++++++++++++++++++++++++++++++++++++++ src/v1.erl | 12 ++++- src/x509.erl | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/x509_test.hrl | 111 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 src/x509.erl create mode 100644 src/x509_test.hrl (limited to 'src') diff --git a/src/catlfish.erl b/src/catlfish.erl index e261824..cd871f5 100644 --- a/src/catlfish.erl +++ b/src/catlfish.erl @@ -3,7 +3,9 @@ -module(catlfish). -export([add_chain/2, entries/2, entry_and_proof/2]). +-export([known_roots/0, update_known_roots/0]). -include("$CTROOT/plop/include/plop.hrl"). +-include_lib("eunit/include/eunit.hrl"). -define(PROTOCOL_VERSION, 0). @@ -162,3 +164,107 @@ decode_tls_vector(Binary, LengthLen) -> <> = 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..46b5235 100644 --- a/src/v1.erl +++ b/src/v1.erl @@ -22,7 +22,13 @@ 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]} -> + catlfish:add_chain(Leaf, Chain); + {Err, Msg} -> + html("add-chain: ", [Msg, Err]) + end; Invalid -> html("add-chain: chain is not a list: ", [Invalid]) end; @@ -125,7 +131,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..aef9ae7 --- /dev/null +++ b/src/x509.erl @@ -0,0 +1,137 @@ +%%% Copyright (c) 2014, NORDUnet A/S. +%%% See LICENSE for licensing information. + +-module(x509). +-export([normalise_chain/2]). + +-include_lib("public_key/include/public_key.hrl"). + +-type der_encoded() :: public_key:der_encoded(). +-type reason() :: {chain_too_long | root_unknown | chain_broken}. + +-define(MAX_CHAIN_LENGTH, 10). + +-spec normalise_chain([der_encoded()], [der_encoded()]) -> [der_encoded()]. +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([der_encoded()], [der_encoded()], 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(der_encoded(), [der_encoded()]) -> 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(der_encoded(), der_encoded()) -> 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(der_encoded() | #'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. + +%%%%%%%%%%%%%%%%%%%% +%% 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>>). -- cgit v1.1