summaryrefslogtreecommitdiff
path: root/src/x509.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/x509.erl')
-rw-r--r--src/x509.erl276
1 files changed, 192 insertions, 84 deletions
diff --git a/src/x509.erl b/src/x509.erl
index 5a0e871..eae1468 100644
--- a/src/x509.erl
+++ b/src/x509.erl
@@ -3,10 +3,10 @@
-module(x509).
-export([normalise_chain/2, cert_string/1, read_pemfiles_from_dir/1,
- self_signed/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 |
@@ -14,19 +14,57 @@
encoding_invalid}.
-define(MAX_CHAIN_LENGTH, 10).
+-define(LEAF_POISON_OID, {1,3,6,1,4,1,11129,2,4,3}).
+-define(LEAF_POISON_VAL, [5,0]).
+-define(CA_POISON_OID, {1,3,6,1,4,1,11129,2,4,4}).
-spec normalise_chain([binary()], [binary()]) -> {ok, [binary()]} |
{error, reason()}.
normalise_chain(AcceptableRootCerts, CertChain) ->
- case valid_chain_p(AcceptableRootCerts, CertChain, ?MAX_CHAIN_LENGTH) of
+ case normalise_chain(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.
-%%%%%%%%%%%%%%%%%%%%
+-spec cert_string(binary()) -> string().
+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).
+
+-spec detox(binary(), [binary()]) -> {binary(), binary()}.
+%% @doc Return the detoxed cet in LeafDer and the issuer leaf hash.
+detox(LeafDer, ChainDer) ->
+ detox_precert(LeafDer, nth(1, ChainDer), nth(2, ChainDer)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Private functions.
+
+-spec normalise_chain([binary()], [binary()], integer()) ->
+ {false, reason()} | {true, list()}.
%% @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
@@ -37,12 +75,10 @@ normalise_chain(AcceptableRootCerts, CertChain) ->
%% 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 ->
+normalise_chain(_, _, MaxChainLength) when MaxChainLength =< 0 ->
%% Chain too long.
{false, chain_too_long};
-valid_chain_p(AcceptableRootCerts, [TopCert], MaxChainLength) ->
+normalise_chain(AcceptableRootCerts, [TopCert], MaxChainLength) ->
%% Check root of chain.
case lists:member(TopCert, AcceptableRootCerts) of
true ->
@@ -58,17 +94,17 @@ valid_chain_p(AcceptableRootCerts, [TopCert], MaxChainLength) ->
Root -> {true, [Root]}
end
end;
-valid_chain_p(AcceptableRootCerts, [BottomCert|Rest], MaxChainLength) ->
+normalise_chain(AcceptableRootCerts, [BottomCert|Rest], MaxChainLength) ->
case signed_by_p(BottomCert, hd(Rest)) of
- true -> valid_chain_p(AcceptableRootCerts, Rest, MaxChainLength - 1);
+ true -> normalise_chain(AcceptableRootCerts, Rest, MaxChainLength - 1);
false -> {false, signature_mismatch}
end.
+-spec signer(binary(), [binary()]) -> notfound | binary().
%% @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]) ->
@@ -82,6 +118,7 @@ signer(Cert, [H|T]) ->
signer(Cert, T)
end.
+-spec encoded_tbs_cert(binary()) -> binary().
%% Code from pubkey_cert:encoded_tbs_cert/1.
encoded_tbs_cert(DerCert) ->
{ok, PKIXCert} =
@@ -90,14 +127,14 @@ 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,
SigAlg = SigAlgRecord#'AlgorithmIdentifier'.algorithm,
- lager:debug("SigAlg: ~p", [SigAlg]),
try
{DigestType, _} = public_key:pkix_sign_types(SigAlg),
{ok, {PlainText, DigestType, Sig}}
@@ -114,7 +151,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.
@@ -125,10 +162,6 @@ verify_sig2(IssuerSPKI, {DigestOrPlainText, DigestType, Signature}) ->
algorithm = #'AlgorithmIdentifier'{algorithm = Alg, parameters = Params},
subjectPublicKey = {0, Key0}} = IssuerSPKI,
KeyType = pubkey_cert_records:supportedPublicKeyAlgorithms(Alg),
- 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' ->
@@ -141,12 +174,6 @@ verify_sig2(IssuerSPKI, {DigestOrPlainText, DigestType, Signature}) ->
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).
@@ -159,9 +186,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,58 +199,141 @@ 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.
+%%
+%% 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()}.
+%% @doc Return {DetoxedLeaf, IssuerPubKeyHash} where i) DetoxedLeaf is
+%% the tbsCertificate w/o poison and adjusted issuer and authkeyid;
+%% and ii) IssuerPubKeyHash is the hash over issuing cert's public
+%% key.
+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
+ %% potential authority key id to refer to grandparent.
+ {C, IssuerKeyHash} =
+ case is_precert_signer(Parent) of
+ true ->
+ {set_issuer_and_authkeyid(DetoxedLeafTBS, Parent),
+ 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.
+
+-spec set_issuer_and_authkeyid(#'TBSCertificate'{}, #'Certificate'{}) ->
+ #'TBSCertificate'{}.
+%% @doc Return Cert with issuer and AuthorityKeyIdentifier from Parent.
+set_issuer_and_authkeyid(TBSCert,
+ #'Certificate'{
+ tbsCertificate =
+ #'TBSCertificate'{
+ issuer = ParentIssuer,
+ extensions = ParentExtensions}}) ->
+ case pubkey_cert:select_extension(?'id-ce-authorityKeyIdentifier',
+ ParentExtensions) of
+ undefined ->
+ lager:debug("setting issuer only", []),
+ TBSCert#'TBSCertificate'{issuer = ParentIssuer};
+ ParentAuthKeyExt ->
+ NewExtensions =
+ lists:map(
+ fun(E) ->
+ case E of
+ #'Extension'{extnID =
+ ?'id-ce-authorityKeyIdentifier'} ->
+ lager:debug("swapping auth key id to ~p",
+ [ParentAuthKeyExt]),
+ ParentAuthKeyExt;
+ _ -> E
+ end
+ end,
+ TBSCert#'TBSCertificate'.extensions),
+ lager:debug("setting issuer and auth key id", []),
+ TBSCert#'TBSCertificate'{issuer = ParentIssuer,
+ extensions = NewExtensions}
+ end.
-%% 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 is_precert_signer(#'Certificate'{}) -> boolean().
+is_precert_signer(#'Certificate'{tbsCertificate = TBSCert}) ->
+ Extensions = pubkey_cert:extensions_list(TBSCert#'TBSCertificate'.extensions),
+ %% NOTE: It's OK to look at only the first extension found since
+ %% "A certificate MUST NOT include more than one instance of a
+ %% particular extension." --RFC5280 Sect 4.2
+ case pubkey_cert:select_extension(?'id-ce-extKeyUsage', Extensions) of
+ #'Extension'{extnValue = Val} ->
+ case 'OTP-PUB-KEY':decode('ExtKeyUsageSyntax', Val) of
+ %% NOTE: We require that the poisoned OID is the
+ %% _only_ extkeyusage present. RFC6962 Sect 3.1 is not
+ %% really clear.
+ {ok, [?CA_POISON_OID]} -> is_ca(TBSCert);
+ _ -> false
+ end;
+ _ -> false
+ end.
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
--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)
+-spec is_ca(#'TBSCertificate'{}) -> binary().
+is_ca(#'TBSCertificate'{extensions = Extensions}) ->
+ case pubkey_cert:select_extension(?'id-ce-basicConstraints', Extensions) of
+ #'Extension'{critical = true, extnValue = Val} ->
+ case 'OTP-PUB-KEY':decode('BasicConstraints', Val) of
+ {ok, {'BasicConstraints', true, _}} -> true;
+ _ -> false
+ end;
+ _ -> false
end.
+-spec remove_poison_ext(#'Certificate'{}) -> #'TBSCertificate'{}.
+remove_poison_ext(#'Certificate'{tbsCertificate = TBSCert}) ->
+ Extensions =
+ filter(fun(E) -> not poisoned_leaf_p(E) end,
+ pubkey_cert:extensions_list(TBSCert#'TBSCertificate'.extensions)),
+ TBSCert#'TBSCertificate'{extensions = Extensions}.
+
+-spec poisoned_leaf_p(binary()) -> boolean().
+poisoned_leaf_p(#'Extension'{extnID = ?LEAF_POISON_OID,
+ critical = true,
+ extnValue = ?LEAF_POISON_VAL}) ->
+ true;
+poisoned_leaf_p(_) ->
+ false.
+
+%%%% PEM files.
+-spec ders_from_pemfiles(string(), [string()]) -> [binary()].
ders_from_pemfiles(Dir, Filenames) ->
lists:flatten(
[ders_from_pemfile(filename:join(Dir, X)) || X <- Filenames]).
+-spec ders_from_pemfile(string()) -> [binary()].
ders_from_pemfile(Filename) ->
lager:debug("reading PEM from ~s", [Filename]),
PemBins = pems_from_file(Filename),
@@ -238,6 +345,7 @@ ders_from_pemfile(Filename) ->
end,
[der_from_pem(X) || X <- Pems].
+-spec der_from_pem(binary()) -> binary().
der_from_pem(Pem) ->
case Pem of
{_Type, Der, not_encrypted} ->
@@ -297,21 +405,21 @@ valid_cert_test_() ->
%% 'OTP-PUB-KEY':Func('OTP-X520countryname', Value0)
%% 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)),
+ ?_assertMatch({true, _}, normalise_chain(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)),
+ normalise_chain(KnownRoots,
+ lists:nth(2, Chains), 10)),
%% Leaf signed by known CA, pass.
- ?_assertMatch({true, _}, valid_chain_p(KnownRoots,
- lists:nth(3, Chains), 10)),
+ ?_assertMatch({true, _}, normalise_chain(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)),
+ ?_assertMatch({true, _}, normalise_chain(KnownRoots,
+ lists:nth(4, Chains), 3)),
%% Verify against self, pass.
%% Bug CATLFISH-??, can't handle issuer keytype ECPoint.
%% Issuer sha1: 6969562e4080f424a1e7199f14baf3ee58ab6abb
@@ -333,21 +441,21 @@ chain_test_() ->
chain_test(C0, 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)),
+ ?_assertEqual({true, [C1]}, normalise_chain([C1], [C0], 10)),
+ ?_assertEqual({true, [C1]}, normalise_chain([C1], [C0], 2)),
%% Chain too long.
- ?_assertMatch({false, chain_too_long}, valid_chain_p([C1], [C0], 1)),
+ ?_assertMatch({false, chain_too_long}, normalise_chain([C1], [C0], 1)),
%% Root in chain and in trust store.
- ?_assertEqual({true, []}, valid_chain_p([C1], [C0, C1], 2)),
+ ?_assertEqual({true, []}, normalise_chain([C1], [C0, C1], 2)),
%% Chain too long.
- ?_assertMatch({false, chain_too_long}, valid_chain_p([C1], [C0, C1], 1)),
+ ?_assertMatch({false, chain_too_long}, normalise_chain([C1], [C0, C1], 1)),
%% Root not in trust store.
- ?_assertMatch({false, root_unknown}, valid_chain_p([], [C0, C1], 10)),
+ ?_assertMatch({false, root_unknown}, normalise_chain([], [C0, C1], 10)),
%% Selfsigned. Actually OK.
- ?_assertMatch({true, []}, valid_chain_p([C0], [C0], 10)),
- ?_assertMatch({true, []}, valid_chain_p([C0], [C0], 1)),
+ ?_assertMatch({true, []}, normalise_chain([C0], [C0], 10)),
+ ?_assertMatch({true, []}, normalise_chain([C0], [C0], 1)),
%% Max chain length 0 is not OK.
- ?_assertMatch({false, chain_too_long}, valid_chain_p([C0], [C0], 0))
+ ?_assertMatch({false, chain_too_long}, normalise_chain([C0], [C0], 0))
].
%%-spec read_certs(file:filename()) -> [string:string()].