diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/catlfish.erl | 47 | ||||
-rw-r--r-- | src/catlfish.hrl | 4 | ||||
-rw-r--r-- | src/catlfish_app.erl | 10 | ||||
-rw-r--r-- | src/catlfish_web.erl | 32 | ||||
-rw-r--r-- | src/v1.erl | 12 | ||||
-rw-r--r-- | src/x509.erl | 199 |
6 files changed, 175 insertions, 129 deletions
diff --git a/src/catlfish.erl b/src/catlfish.erl index 83ca3db..3956eec 100644 --- a/src/catlfish.erl +++ b/src/catlfish.erl @@ -4,8 +4,8 @@ -module(catlfish). -export([add_chain/2, 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"). --include("catlfish.hrl"). -define(PROTOCOL_VERSION, 0). @@ -133,6 +133,14 @@ entry_and_proof(Index, TreeSize) -> {error_message, list_to_binary(Msg)}]} end. +-define(CACHE_TABLE, catlfish_cache). +init_cache_table() -> + case ets:info(?CACHE_TABLE) of + undefined -> ok; + _ -> ets:delete(?CACHE_TABLE) + end, + ets:new(?CACHE_TABLE, [set, public, named_table]). + %% Private functions. unpack_entry(Entry) -> <<Timestamp:64, LogEntry/binary>> = Entry, @@ -183,37 +191,50 @@ known_roots(Directory, CacheUsage) -> use_cache -> case ets:lookup(?CACHE_TABLE, ?ROOTS_CACHE_KEY) of [] -> - read_files_and_udpate_table(Directory); + read_files_and_update_table(Directory); [{roots, DerList}] -> DerList end; update_tab -> - read_files_and_udpate_table(Directory) + read_files_and_update_table(Directory) end. -read_files_and_udpate_table(Directory) -> - L = x509:read_pemfiles_from_dir(Directory), - true = ets:insert(?CACHE_TABLE, {?ROOTS_CACHE_KEY, L}), - L. +read_files_and_update_table(Directory) -> + Certs = x509:read_pemfiles_from_dir(Directory), + Proper = x509:self_signed(Certs), + case length(Certs) - length(Proper) of + 0 -> ok; + N -> lager:warning( + "Ignoring ~p root certificates not signing themselves properly", + [N]) + end, + true = ets:insert(?CACHE_TABLE, {?ROOTS_CACHE_KEY, Proper}), + lager:info("Known roots imported: ~p", [length(Proper)]), + Proper. %%%%%%%%%%%%%%%%%%%% %% Testing internal functions. --define(PEMFILES_DIR_OK, "../test/testdata/known-roots"). --define(PEMFILES_DIR_NONEXISTENT, "../test/testdata/nonexistent-dir"). +-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)} + fun() -> + init_cache_table(), + {known_roots(?PEMFILES_DIR_OK, update_tab), + known_roots(?PEMFILES_DIR_OK, use_cache)} end, fun(_) -> ets:delete(?CACHE_TABLE, ?ROOTS_CACHE_KEY) end, fun({L, LCached}) -> - [?_assertMatch(7, length(L)), + [?_assertMatch(4, length(L)), ?_assertEqual(L, LCached)] end}. read_pemfiles_fail_test_() -> {setup, - fun() -> known_roots(?PEMFILES_DIR_NONEXISTENT, use_cache) end, + fun() -> + init_cache_table(), + known_roots(?PEMFILES_DIR_NONEXISTENT, update_tab) + end, fun(_) -> ets:delete(?CACHE_TABLE, ?ROOTS_CACHE_KEY) end, fun(Empty) -> [?_assertMatch([], Empty)] end}. diff --git a/src/catlfish.hrl b/src/catlfish.hrl deleted file mode 100644 index 46e882b..0000000 --- a/src/catlfish.hrl +++ /dev/null @@ -1,4 +0,0 @@ -%%% Copyright (c) 2014, NORDUnet A/S. -%%% See LICENSE for licensing information. - --define(CACHE_TABLE, catlfish_cache). diff --git a/src/catlfish_app.erl b/src/catlfish_app.erl index e24a1bb..56f6cc2 100644 --- a/src/catlfish_app.erl +++ b/src/catlfish_app.erl @@ -8,20 +8,12 @@ %% Application callbacks -export([start/2, stop/1]). --include("catlfish.hrl"). - %% =================================================================== %% Application callbacks %% =================================================================== start(normal, Args) -> - case ets:info(?CACHE_TABLE) of - undefined -> - ok; - _ -> - ets:delete(?CACHE_TABLE) - end, - ets:new(?CACHE_TABLE, [set, public, named_table]), + catlfish:init_cache_table(), catlfish_sup:start_link(Args). stop(_State) -> diff --git a/src/catlfish_web.erl b/src/catlfish_web.erl index 9869b21..5ee5743 100644 --- a/src/catlfish_web.erl +++ b/src/catlfish_web.erl @@ -11,15 +11,31 @@ start(Options, Module) -> end, mochiweb_http:start([{name, Module}, {loop, Loop} | Options]). + +add_auth(Path, {Code, Headers, Data}) -> + AuthHeader = http_auth:create_auth("REPLY", Path, Data), + lager:debug("sent auth header: ~p", [AuthHeader]), + {Code, [{"X-Catlfish-Auth", AuthHeader} | Headers], Data}. + loop(Req, Module) -> "/" ++ Path = Req:get(path), try Starttime = os:timestamp(), + AuthHeader = Req:get_header_value("X-Catlfish-Auth"), case Req:get(method) of 'GET' -> Query = Req:parse_qs(), - lager:debug("GET ~p ~p", [Path, Query]), - Result = Module:request(get, Path, Query), + {_, RawQuery, _} = mochiweb_util:urlsplit_path(Req:get(raw_path)), + Result = case http_auth:verify_auth(AuthHeader, "GET", "/" ++ Path, RawQuery) of + failure -> + {403, [{"Content-Type", "text/plain"}], "Invalid credentials"}; + success -> + lager:debug("GET ~p ~p", [Path, Query]), + add_auth("/" ++ Path, Module:request(get, Path, Query)); + noauth -> + lager:debug("GET ~p ~p", [Path, Query]), + Module:request(get, Path, Query) + end, lager:debug("GET finished: ~p us", [timer:now_diff(os:timestamp(), Starttime)]), case Result of none -> @@ -29,8 +45,16 @@ loop(Req, Module) -> end; 'POST' -> Body = Req:recv_body(), - lager:debug("POST ~p ~p", [Path, Body]), - Result = Module:request(post, Path, Body), + Result = case http_auth:verify_auth(AuthHeader, "POST", "/" ++ Path, Body) of + failure -> + {403, [{"Content-Type", "text/plain"}], "Invalid credentials"}; + success -> + lager:debug("POST ~p ~p", [Path, Body]), + add_auth("/" ++ Path, Module:request(post, Path, Body)); + noauth -> + lager:debug("POST ~p ~p", [Path, Body]), + Module:request(post, Path, Body) + end, lager:debug("POST finished: ~p us", [timer:now_diff(os:timestamp(), Starttime)]), case Result of none -> @@ -39,16 +39,8 @@ request(post, "ct/v1/add-pre-chain", _Input) -> niy(); 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 diff --git a/src/x509.erl b/src/x509.erl index b0363cd..5a0e871 100644 --- a/src/x509.erl +++ b/src/x509.erl @@ -2,14 +2,14 @@ %%% See LICENSE for licensing information. -module(x509). --export([normalise_chain/2, cert_string/1, read_pemfiles_from_dir/1]). +-export([normalise_chain/2, cert_string/1, read_pemfiles_from_dir/1, + self_signed/1]). -include_lib("public_key/include/public_key.hrl"). -include_lib("eunit/include/eunit.hrl"). -type reason() :: {chain_too_long | root_unknown | - chain_broken | signature_mismatch | encoding_invalid}. @@ -28,10 +28,15 @@ normalise_chain(AcceptableRootCerts, CertChain) -> %%%%%%%%%%%%%%%%%%%% %% @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. - +%% an acceptable root cert. The order of certificates in the second +%% argument is: leaf cert in head, chain in tail. Order of first +%% argument is irrelevant. +%% +%% Return {false, Reason} or {true, ListWithRoot}. Note that +%% ListWithRoot is the empty list when the root of the chain is found +%% 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 -> @@ -56,106 +61,103 @@ valid_chain_p(AcceptableRootCerts, [TopCert], MaxChainLength) -> valid_chain_p(AcceptableRootCerts, [BottomCert|Rest], MaxChainLength) -> case signed_by_p(BottomCert, hd(Rest)) of true -> valid_chain_p(AcceptableRootCerts, Rest, MaxChainLength - 1); - Err -> Err + false -> {false, signature_mismatch} end. -%% @doc Return first cert in list signing Cert, or notfound. +%% @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]) -> + lager:debug("Is ~p signed by ~p?", [cert_string(Cert), cert_string(H)]), case signed_by_p(Cert, H) of - true -> H; - {false, _} -> signer(Cert, T) + true -> + lager:debug("~p is signed by ~p", + [cert_string(Cert), cert_string(H)]), + H; + false -> + signer(Cert, T) end. -%% encoded_tbs_cert: verbatim from pubkey_cert.erl -encoded_tbs_cert(Cert) -> +%% Code from pubkey_cert:encoded_tbs_cert/1. +encoded_tbs_cert(DerCert) -> {ok, PKIXCert} = - 'OTP-PUB-KEY':decode_TBSCert_exclusive(Cert), - {'Certificate', - {'Certificate_tbsCertificate', EncodedTBSCert}, _, _} = PKIXCert, + 'OTP-PUB-KEY':decode_TBSCert_exclusive(DerCert), + {'Certificate', {'Certificate_tbsCertificate', EncodedTBSCert}, _, _} = + PKIXCert, EncodedTBSCert. -%% extract_verify_data: close to pubkey_cert:extract_verify_data/2 +%% Code from pubkey_cert:extract_verify_data/2. +-spec verifydata_from_cert(#'Certificate'{}, binary()) -> {ok, tuple()} | error. verifydata_from_cert(Cert, DerCert) -> PlainText = encoded_tbs_cert(DerCert), {_, Sig} = Cert#'Certificate'.signature, SigAlgRecord = Cert#'Certificate'.signatureAlgorithm, SigAlg = SigAlgRecord#'AlgorithmIdentifier'.algorithm, - {DigestType,_} = public_key:pkix_sign_types(SigAlg), - {PlainText, DigestType, Sig}. - -verify(Cert, DerCert, - #'Certificate'{ - tbsCertificate = #'TBSCertificate'{ - subjectPublicKeyInfo = IssuerSPKI}}) -> - {DigestOrPlainText, DigestType, Signature} = - verifydata_from_cert(Cert, DerCert), + lager:debug("SigAlg: ~p", [SigAlg]), + try + {DigestType, _} = public_key:pkix_sign_types(SigAlg), + {ok, {PlainText, DigestType, Sig}} + catch + error:function_clause -> + lager:debug("signature algorithm not supported: ~p", [SigAlg]), + error + end. + +%% @doc Verify that Cert/DerCert is signed by Issuer. +-spec verify_sig(#'Certificate'{}, binary(), #'Certificate'{}) -> boolean(). +verify_sig(Cert, DerCert, % Certificate to verify. + #'Certificate'{ % Issuer. + tbsCertificate = #'TBSCertificate'{ + subjectPublicKeyInfo = IssuerSPKI}}) -> + %% Dig out digest, digest type and signature from Cert/DerCert. + case verifydata_from_cert(Cert, DerCert) of + error -> false; + {ok, Tuple} -> verify_sig2(IssuerSPKI, Tuple) + end. + +verify_sig2(IssuerSPKI, {DigestOrPlainText, DigestType, Signature}) -> + %% Dig out issuer key from issuer cert. #'SubjectPublicKeyInfo'{ algorithm = #'AlgorithmIdentifier'{algorithm = Alg, parameters = Params}, subjectPublicKey = {0, Key0}} = IssuerSPKI, KeyType = pubkey_cert_records:supportedPublicKeyAlgorithms(Alg), - - %% public_key:pem_entry_decode() + 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' -> public_key:der_decode(KeyType, Key0); - 'DSAPublicKey' -> - {params, DssParams} = public_key:der_decode('DSAParams', Params), - {public_key:der_decode(KeyType, Key0), DssParams}; 'ECPoint' -> - public_key:der_decode(KeyType, Key0) + Point = #'ECPoint'{point = Key0}, + ECParams = public_key:der_decode('EcpkParameters', Params), + {Point, ECParams}; + _ -> % FIXME: 'DSAPublicKey' + 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). --spec signed_by_p(binary(), binary()) -> true | {false, reason()}. +%% @doc Is Cert signed by Issuer? Only verify that the signature +%% matches and don't check things like Cert.issuer == Issuer.subject. +-spec signed_by_p(binary(), binary()) -> boolean(). signed_by_p(DerCert, IssuerDerCert) when is_binary(DerCert), is_binary(IssuerDerCert) -> - Cert = public_key:pkix_decode_cert(DerCert, plain), - TBSCert = Cert#'Certificate'.tbsCertificate, - IssuerCert = public_key:pkix_decode_cert(IssuerDerCert, plain), - IssuerTBSCert = IssuerCert#'Certificate'.tbsCertificate, - case pubkey_cert:is_issuer(TBSCert#'TBSCertificate'.issuer, - IssuerTBSCert#'TBSCertificate'.subject) of - false -> - {false, chain_broken}; - true -> % Verify signature. - case verify(Cert, DerCert, IssuerCert) of - false -> {false, signature_mismatch}; - true -> true - end - end; -signed_by_p(#'OTPCertificate'{} = Cert, - #'OTPCertificate'{} = IssuerCert) -> - %% FIXME: Validate presence and contents (against constraints) of - %% names (subject, subjectAltName, emailAddress) too? - case (catch public_key:pkix_is_issuer(Cert, IssuerCert)) of - {'EXIT', Reason} -> - lager:info("invalid certificate: ~p: ~p", - [cert_string(Cert), Reason]), - {false, encoding_invalid}; - true -> - %% Cert.issuer does match IssuerCert.subject. Now verify - %% the signature. - case public_key:pkix_verify(Cert, public_key(IssuerCert)) of - true -> true; - false -> {false, signature_mismatch} - end; - false -> - {false, chain_broken} - 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. + verify_sig(public_key:pkix_decode_cert(DerCert, plain), + DerCert, + public_key:pkix_decode_cert(IssuerDerCert, plain)). cert_string(Der) -> mochihex:to_hex(crypto:hash(sha, Der)). @@ -173,6 +175,10 @@ 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. @@ -222,6 +228,7 @@ ders_from_pemfiles(Dir, Filenames) -> [ders_from_pemfile(filename:join(Dir, X)) || X <- Filenames]). ders_from_pemfile(Filename) -> + lager:debug("reading PEM from ~s", [Filename]), PemBins = pems_from_file(Filename), Pems = case (catch public_key:pem_decode(PemBins)) of {'EXIT', Reason} -> @@ -279,26 +286,42 @@ sign_test_() -> valid_cert_test_() -> {setup, - fun() -> {read_pemfiles_from_dir("../test/testdata/known_roots"), - read_certs("../test/testdata/chains")} end, + fun() -> {read_pemfiles_from_dir("test/testdata/known_roots"), + read_certs("test/testdata/chains")} end, fun(_) -> ok end, fun({KnownRoots, Chains}) -> [ - %% self-signed, not a valid OTPCertificate: + %% Self-signed but verified against itself so pass. + %% Not a valid OTPCertificate: %% {error,{asn1,{invalid_choice_tag,{22,<<"US">>}}}} %% 'OTP-PUB-KEY':Func('OTP-X520countryname', Value0) - %% FIXME: this doesn't make much sense -- is my environment borked? - ?_assertMatch({true, _}, - valid_chain_p(lists:nth(1, Chains), - lists:nth(1, Chains), 10)), - %% self-signed + %% 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)), + %% Self-signed so fail. ?_assertMatch({false, root_unknown}, valid_chain_p(KnownRoots, lists:nth(2, Chains), 10)), - %% leaf signed by known CA - ?_assertMatch({true, _}, - valid_chain_p(KnownRoots, - lists:nth(3, Chains), 10)) + %% Leaf signed by known CA, pass. + ?_assertMatch({true, _}, valid_chain_p(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)), + %% Verify against self, pass. + %% Bug CATLFISH-??, can't handle issuer keytype ECPoint. + %% Issuer sha1: 6969562e4080f424a1e7199f14baf3ee58ab6abb + ?_assertMatch(true, signed_by_p(hd(lists:nth(5, Chains)), + hd(lists:nth(5, Chains)))), + %% Unsupported signature algorithm MD2-RSA, fail. + %% Signature Algorithm: md2WithRSAEncryption + %% CA cert with sha1 96974cd6b663a7184526b1d648ad815cf51e801a + ?_assertMatch(false, signed_by_p(hd(lists:nth(6, Chains)), + hd(lists:nth(6, Chains)))) ] end}. chain_test_() -> @@ -320,8 +343,6 @@ chain_test(C0, C1) -> ?_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)), |