%%% 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 |
                   signature_mismatch |
                   encoding_invalid}.

-define(MAX_CHAIN_LENGTH, 10).

-spec normalise_chain([binary()], [binary()]) -> {ok, [binary()]} |
                                                 {error, reason()}.
normalise_chain(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}
    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
        true -> valid_chain_p(AcceptableRootCerts, Rest, MaxChainLength - 1);
        Err -> Err
    end.

%% @doc Return first cert in list signing Cert, or notfound.
-spec signer(binary(), [binary()]) -> notfound | binary().
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()) -> true | {false, reason()}.
signed_by_p(Cert, 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} ->
            %% Invalid ASN.1.
            {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.

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