%%% 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)) ].