diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/catlfish.erl | 56 | ||||
-rw-r--r-- | src/x509.erl | 212 |
2 files changed, 201 insertions, 67 deletions
diff --git a/src/catlfish.erl b/src/catlfish.erl index 98ec4dd..83ca3db 100644 --- a/src/catlfish.erl +++ b/src/catlfish.erl @@ -177,66 +177,24 @@ known_roots() -> undefined -> [] end. --spec known_roots(file:filename(), use_cache|update_tab) -> list(). +-spec known_roots(file:filename(), use_cache|update_tab) -> [binary()]. known_roots(Directory, CacheUsage) -> case CacheUsage of use_cache -> case ets:lookup(?CACHE_TABLE, ?ROOTS_CACHE_KEY) of [] -> - read_pemfiles_from_dir(Directory); + read_files_and_udpate_table(Directory); [{roots, DerList}] -> DerList end; update_tab -> - read_pemfiles_from_dir(Directory) + read_files_and_udpate_table(Directory) end. --spec read_pemfiles_from_dir(file:filename()) -> list(). -read_pemfiles_from_dir(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(?CACHE_TABLE, {?ROOTS_CACHE_KEY, 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', Reason} -> - lager:info("badly encoded cert in ~p: ~p", [Filename, Reason]), - []; - 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 x509:valid_cert_p(Der) of - true -> Der; - false -> [] - end; - _ -> [] - end. - -pems_from_file(Filename) -> - {ok, Pems} = file:read_file(Filename), - Pems. +read_files_and_udpate_table(Directory) -> + L = x509:read_pemfiles_from_dir(Directory), + true = ets:insert(?CACHE_TABLE, {?ROOTS_CACHE_KEY, L}), + L. %%%%%%%%%%%%%%%%%%%% %% Testing internal functions. diff --git a/src/x509.erl b/src/x509.erl index 5a96a29..b0363cd 100644 --- a/src/x509.erl +++ b/src/x509.erl @@ -2,9 +2,10 @@ %%% See LICENSE for licensing information. -module(x509). --export([normalise_chain/2, cert_string/1, valid_cert_p/1]). +-export([normalise_chain/2, cert_string/1, read_pemfiles_from_dir/1]). -include_lib("public_key/include/public_key.hrl"). +-include_lib("eunit/include/eunit.hrl"). -type reason() :: {chain_too_long | root_unknown | @@ -68,14 +69,72 @@ signer(Cert, [H|T]) -> {false, _} -> signer(Cert, T) end. +%% encoded_tbs_cert: verbatim from pubkey_cert.erl +encoded_tbs_cert(Cert) -> + {ok, PKIXCert} = + 'OTP-PUB-KEY':decode_TBSCert_exclusive(Cert), + {'Certificate', + {'Certificate_tbsCertificate', EncodedTBSCert}, _, _} = PKIXCert, + EncodedTBSCert. + +%% extract_verify_data: close to pubkey_cert:extract_verify_data/2 +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), + #'SubjectPublicKeyInfo'{ + algorithm = #'AlgorithmIdentifier'{algorithm = Alg, parameters = Params}, + subjectPublicKey = {0, Key0}} = IssuerSPKI, + KeyType = pubkey_cert_records:supportedPublicKeyAlgorithms(Alg), + + %% public_key:pem_entry_decode() + 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) + end, + public_key:verify(DigestOrPlainText, DigestType, Signature, IssuerKey). + -spec signed_by_p(binary(), binary()) -> true | {false, reason()}. -signed_by_p(Cert, IssuerCert) -> +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", - [mochihex:to_hex(crypto:hash(sha, Cert)), Reason]), + [cert_string(Cert), Reason]), {false, encoding_invalid}; true -> %% Cert.issuer does match IssuerCert.subject. Now verify @@ -99,23 +158,18 @@ public_key(#'OTPCertificate'{ Key. cert_string(Der) -> - lists:flatten([io_lib:format("~2.16.0B", [X]) || - X <- binary_to_list(crypto:hash(sha, Der))]). - -valid_cert_p(Der) -> - %% Use the customized ASN.1 specification "OTP-PKIX.asn1" since - %% that's what's required for public_key functions we're using - %% (pkix_verify, public_key:pkix_is_issuer). - case (catch public_key:pkix_decode_cert(Der, otp)) of - #'OTPCertificate'{} -> + mochihex:to_hex(crypto:hash(sha, Der)). + +parsable_cert_p(Der) -> + case (catch public_key:pkix_decode_cert(Der, plain)) of + #'Certificate'{} -> true; {'EXIT', Reason} -> - lager:info("invalid certificate: ~p: ~p", - [mochihex:to_hex(crypto:hash(sha, Der)), Reason]), + lager:info("invalid certificate: ~p: ~p", [cert_string(Der), Reason]), false; Unknown -> lager:info("unknown error decoding cert: ~p: ~p", - [mochihex:to_hex(crypto:hash(sha, Der)), Unknown]), + [cert_string(Der), Unknown]), false end. @@ -142,13 +196,118 @@ valid_cert_p(Der) -> detox_precert(CertChain) -> CertChain. % NYI +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-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. + +ders_from_pemfiles(Dir, Filenames) -> + lists:flatten( + [ders_from_pemfile(filename:join(Dir, X)) || X <- Filenames]). + +ders_from_pemfile(Filename) -> + PemBins = pems_from_file(Filename), + Pems = case (catch public_key:pem_decode(PemBins)) of + {'EXIT', Reason} -> + lager:info("~p: invalid PEM-encoding: ~p", [Filename, Reason]), + []; + P -> P + end, + [der_from_pem(X) || X <- Pems]. + +der_from_pem(Pem) -> + case Pem of + {_Type, Der, not_encrypted} -> + case parsable_cert_p(Der) of + true -> + Der; + false -> + dump_unparsable_cert(Der), + [] + end; + Fail -> + lager:info("ignoring PEM-encoded data: ~p~n", [Fail]), + [] + end. + +-spec pems_from_file(file:filename()) -> binary(). +pems_from_file(Filename) -> + {ok, Pems} = file:read_file(Filename), + Pems. + +-spec dump_unparsable_cert(binary()) -> ok | {error, atom()} | not_logged. +dump_unparsable_cert(CertDer) -> + case application:get_env(catlfish, rejected_certs_path) of + {ok, Directory} -> + {NowMegaSec, NowSec, NowMicroSec} = now(), + Filename = + filename:join(Directory, + io_lib:format("~p:~p.~p", + [cert_string(CertDer), + NowMegaSec * 1000 * 1000 + NowSec, + NowMicroSec])), + lager:debug("dumping cert to ~p~n", [Filename]), + file:write_file(Filename, CertDer); + _ -> + not_logged + end. + %%%%%%%%%%%%%%%%%%%% %% Testing private functions. --include_lib("eunit/include/eunit.hrl"). -include("x509_test.hrl"). +sign_test_() -> + {setup, + fun() -> ok end, + fun(_) -> ok end, + fun(_) -> [?_assertMatch(true, signed_by_p(?C0, ?C1))] end}. + valid_cert_test_() -> - C0 = ?C0, - C1 = ?C1, + {setup, + 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: + %% {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 + ?_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)) + ] end}. + +chain_test_() -> + {setup, + fun() -> {?C0, ?C1} end, + fun(_) -> ok end, + fun({C0, C1}) -> chain_test(C0, C1) end}. + +chain_test(C0, C1) -> [ %% Root not in chain but in trust store. ?_assertEqual({true, [C1]}, valid_chain_p([C1], [C0], 10)), @@ -169,3 +328,20 @@ valid_cert_test_() -> %% Max chain length 0 is not OK. ?_assertMatch({false, chain_too_long}, valid_chain_p([C0], [C0], 0)) ]. + +%%-spec read_certs(file:filename()) -> [string:string()]. +-spec read_certs(file:filename()) -> [[binary()]]. +read_certs(Dir) -> + {ok, Fnames} = file:list_dir(Dir), + PemBins = + [Pems || {ok, Pems} <- + [file:read_file(filename:join(Dir, F)) || + F <- lists:sort( + lists:filter( + fun(FN) -> string:equal( + ".pem", filename:extension(FN)) + end, + Fnames))]], + PemEntries = [public_key:pem_decode(P) || P <- PemBins], + lists:map(fun(L) -> [Der || {'Certificate', Der, not_encrypted} <- L] end, + PemEntries). |