diff options
Diffstat (limited to 'src/x509.erl')
-rw-r--r-- | src/x509.erl | 199 |
1 files changed, 110 insertions, 89 deletions
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)), |