diff options
Diffstat (limited to 'src/x509.erl')
-rw-r--r-- | src/x509.erl | 175 |
1 files changed, 122 insertions, 53 deletions
diff --git a/src/x509.erl b/src/x509.erl index 5a0e871..278686b 100644 --- a/src/x509.erl +++ b/src/x509.erl @@ -2,11 +2,11 @@ %%% See LICENSE for licensing information. -module(x509). --export([normalise_chain/2, cert_string/1, read_pemfiles_from_dir/1, - self_signed/1]). - +-export([valid_chain_p/2, cert_string/1, read_pemfiles_from_dir/1, + self_signed/1, detox/2]). -include_lib("public_key/include/public_key.hrl"). -include_lib("eunit/include/eunit.hrl"). +-import(lists, [nth/2, filter/2]). -type reason() :: {chain_too_long | root_unknown | @@ -15,18 +15,52 @@ -define(MAX_CHAIN_LENGTH, 10). --spec normalise_chain([binary()], [binary()]) -> {ok, [binary()]} | - {error, reason()}. -normalise_chain(AcceptableRootCerts, CertChain) -> +-spec valid_chain_p([binary()], [binary()]) -> {ok, [binary()]} | + {error, reason()}. +valid_chain_p(AcceptableRootCerts, CertChain) -> case valid_chain_p(AcceptableRootCerts, CertChain, ?MAX_CHAIN_LENGTH) of {false, Reason} -> {error, Reason}; {true, Root} -> - [Leaf | Chain] = CertChain, - {ok, [detox_precert(Leaf) | Chain] ++ Root} + {ok, CertChain ++ Root} end. -%%%%%%%%%%%%%%%%%%%% +cert_string(Der) -> + mochihex:to_hex(crypto:hash(sha, Der)). + +-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. + +-spec self_signed([binary()]) -> [binary()]. +%% @doc Return a list of certs in L that are self signed. +self_signed(L) -> + lists:filter(fun(Cert) -> signed_by_p(Cert, Cert) end, L). + +%% @doc Return the detoxed cet in LeafDer and the issuer leaf hash. +-spec detox(binary(), [binary()]) -> {binary(), binary()}. +detox(LeafDer, ChainDer) -> + detox_precert(LeafDer, nth(1, ChainDer), nth(2, ChainDer)). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Private functions. + %% @doc Verify that the leaf cert or precert has a valid chain back to %% an acceptable root cert. The order of certificates in the second %% argument is: leaf cert in head, chain in tail. Order of first @@ -90,9 +124,10 @@ encoded_tbs_cert(DerCert) -> PKIXCert, EncodedTBSCert. +-spec extract_verify_data(#'Certificate'{}, binary()) -> {ok, tuple()} | error. +%% @doc Return DER encoded TBScertificate, digest type and signature. %% Code from pubkey_cert:extract_verify_data/2. --spec verifydata_from_cert(#'Certificate'{}, binary()) -> {ok, tuple()} | error. -verifydata_from_cert(Cert, DerCert) -> +extract_verify_data(Cert, DerCert) -> PlainText = encoded_tbs_cert(DerCert), {_, Sig} = Cert#'Certificate'.signature, SigAlgRecord = Cert#'Certificate'.signatureAlgorithm, @@ -114,7 +149,7 @@ verify_sig(Cert, DerCert, % Certificate to verify. tbsCertificate = #'TBSCertificate'{ subjectPublicKeyInfo = IssuerSPKI}}) -> %% Dig out digest, digest type and signature from Cert/DerCert. - case verifydata_from_cert(Cert, DerCert) of + case extract_verify_data(Cert, DerCert) of error -> false; {ok, Tuple} -> verify_sig2(IssuerSPKI, Tuple) end. @@ -159,9 +194,6 @@ signed_by_p(DerCert, IssuerDerCert) when is_binary(DerCert), DerCert, public_key:pkix_decode_cert(IssuerDerCert, plain)). -cert_string(Der) -> - mochihex:to_hex(crypto:hash(sha, Der)). - parsable_cert_p(Der) -> case (catch public_key:pkix_decode_cert(Der, plain)) of #'Certificate'{} -> @@ -175,54 +207,91 @@ 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. +%% Precerts according to RFC6962. %% 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 +%% cert or a 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 - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --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. - +%% PreCert in SignedCertificateTimestamp does _not_ contain the poison +%% extension, nor does it have an issuer which is a Precertificate +%% Signing Certificate. This means that we have to 1) remove the +%% poison extension and 2) potentially change issuer and Authority Key +%% Identifier. See RFC6962 Section 3.2. + +%% Changes in draft-ietf-trans-rfc6962-bis-??: TODO. + +-spec detox_precert(binary(), binary(), binary()) -> {binary(), binary()}. +detox_precert(LeafDer, ParentDer, GrandParentDer) -> + Leaf = public_key:pkix_decode_cert(LeafDer, plain), + Parent = public_key:pkix_decode_cert(ParentDer, plain), + GrandParent = public_key:pkix_decode_cert(GrandParentDer, plain), + + DetoxedLeafTBS = remove_poison_ext(Leaf), + + %% If parent is a precert signing cert, change issuer and + %% authority key id to refer to grandparent. + {C, IssuerKeyHash} = + case is_precert_signer(Parent) of + true -> + GrandParent = public_key:pkix_decode_cert(GrandParentDer, plain), + {change_issuer(DetoxedLeafTBS, GrandParent), + extract_pub_key(GrandParent)}; + false -> + {DetoxedLeafTBS, extract_pub_key(Parent)} + end, + {public_key:pkix_encode('TBSCertificate', C, plain), + crypto:hash(sha256, public_key:pkix_encode( + 'SubjectPublicKeyInfo', IssuerKeyHash, plain))}. + +-spec extract_pub_key(#'Certificate'{}) -> #'SubjectPublicKeyInfo'{}. +extract_pub_key(#'Certificate'{ + tbsCertificate = #'TBSCertificate'{ + subjectPublicKeyInfo = SPKI}}) -> + SPKI. + +change_issuer(Cert, _) -> + %% FIXME: NIY. + Cert. + +-spec is_precert_signer(#'Certificate'{}) -> boolean(). +is_precert_signer(Cert) -> + %%ca_cert_p(Cert) and has_critext(Cert, {1,3,6,1,4,1,11129,2,4,4}. + ca_cert_p(Cert), false. % FIXME: NIY + +-spec ca_cert_p(#'Certificate'{}) -> boolean(). +ca_cert_p(Cert) -> + %% TBS = Cert#'Certificate'.tbsCertificate, + %% {ExtnID, Critical, ExtnValue} = TBS#'TBSCertificate'.extensions, + Cert. % FIXME: NIY + +-define(LEAF_POISON_OID, {1,3,6,1,4,1,11129,2,4,3}). +-define(LEAF_POISON_VAL, asn1_NOVALUE). + +-spec remove_poison_ext(#'Certificate'{}) -> #'TBSCertificate'{}. +remove_poison_ext(Cert) -> + TBSCert = Cert#'Certificate'.tbsCertificate, + Extensions = pubkey_cert:extensions_list(TBSCert#'TBSCertificate'.extensions), + SanitisedExtensions = + filter(fun(E) -> not poisoned_leaf_p(E) end, Extensions), + NewTBSCert = TBSCert#'TBSCertificate'{extensions = SanitisedExtensions}, + %%Cert#'Certificate'{tbsCertificate = NewTBSCert}. + NewTBSCert. + +poisoned_leaf_p(#'Extension'{extnID = ?LEAF_POISON_OID, + critical = true, + extnValue = ?LEAF_POISON_VAL}) -> + true; +poisoned_leaf_p(_) -> + false. + +%%%% PEM files. ders_from_pemfiles(Dir, Filenames) -> lists:flatten( [ders_from_pemfile(filename:join(Dir, X)) || X <- Filenames]). |