diff options
Diffstat (limited to 'src/catlfish.erl')
-rw-r--r-- | src/catlfish.erl | 415 |
1 files changed, 0 insertions, 415 deletions
diff --git a/src/catlfish.erl b/src/catlfish.erl deleted file mode 100644 index 68e96ea..0000000 --- a/src/catlfish.erl +++ /dev/null @@ -1,415 +0,0 @@ -%%% Copyright (c) 2014-2015, NORDUnet A/S. -%%% See LICENSE for licensing information. - --module(catlfish). --export([add_chain/3, entries/2, entry_and_proof/2]). --export([known_roots/0, update_known_roots/0]). --export([init_cache_table/0]). --export([entryhash_from_entry/1, verify_entry/1, verify_entry/2]). --include_lib("eunit/include/eunit.hrl"). - --define(PROTOCOL_VERSION, 0). - --type signature_type() :: certificate_timestamp | tree_hash | test. % uint8 --type entry_type() :: x509_entry | precert_entry | test. % uint16 --type leaf_type() :: timestamped_entry | test. % uint8 --type leaf_version() :: v1 | v2. % uint8 - --record(mtl, {leaf_version :: leaf_version(), - leaf_type :: leaf_type(), - entry :: timestamped_entry()}). --type mtl() :: #mtl{}. - --record(timestamped_entry, {timestamp :: integer(), - entry_type :: entry_type(), - signed_entry :: signed_x509_entry() | - signed_precert_entry(), - extensions = <<>> :: binary()}). --type timestamped_entry() :: #timestamped_entry{}. - --record(signed_x509_entry, {asn1_cert :: binary()}). --type signed_x509_entry() :: #signed_x509_entry{}. --record(signed_precert_entry, {issuer_key_hash :: binary(), - tbs_certificate :: binary()}). --type signed_precert_entry() :: #signed_precert_entry{}. - --spec serialise(mtl() | timestamped_entry() | - signed_x509_entry() | signed_precert_entry()) -> binary(). -%% @doc Serialise a MerkleTreeLeaf as per RFC6962 Section 3.4. -serialise(#mtl{leaf_version = LeafVersion, - leaf_type = LeafType, - entry = TimestampedEntry}) -> - list_to_binary( - [serialise_leaf_version(LeafVersion), - serialise_leaf_type(LeafType), - serialise(TimestampedEntry)]); -%% @doc Serialise a TimestampedEntry as per RFC6962 Section 3.4. -serialise(#timestamped_entry{timestamp = Timestamp, - entry_type = EntryType, - signed_entry = SignedEntry, - extensions = Extensions}) -> - list_to_binary( - [<<Timestamp:64>>, - serialise_entry_type(EntryType), - serialise(SignedEntry), - encode_tls_vector(Extensions, 2)]); -%% @doc Serialise an ASN1.Cert as per RFC6962 Section 3.1. -serialise(#signed_x509_entry{asn1_cert = Cert}) -> - encode_tls_vector(Cert, 3); -%% @doc Serialise a PreCert as per RFC6962 Section 3.2. -serialise(#signed_precert_entry{ - issuer_key_hash = IssuerKeyHash, - tbs_certificate = TBSCertificate}) when is_binary(IssuerKeyHash), - size(IssuerKeyHash) == 32 -> - list_to_binary( - [IssuerKeyHash, - encode_tls_vector(TBSCertificate, 3)]). - -serialise_leaf_version(v1) -> - <<0:8>>; -serialise_leaf_version(v2) -> - <<1:8>>. -deserialise_leaf_version(<<0:8>>) -> - v1; -deserialise_leaf_version(<<1:8>>) -> - v2. - -serialise_leaf_type(timestamped_entry) -> - <<0:8>>. -deserialise_leaf_type(<<0:8>>) -> - timestamped_entry. - -serialise_entry_type(x509_entry) -> - <<0:16>>; -serialise_entry_type(precert_entry) -> - <<1:16>>. -deserialise_entry_type(<<0:16>>) -> - x509_entry; -deserialise_entry_type(<<1:16>>) -> - precert_entry. - --spec serialise_signature_type(signature_type()) -> binary(). -serialise_signature_type(certificate_timestamp) -> - <<0:8>>. - -calc_sct(TimestampedEntry) -> - plop:serialise( - plop:spt(list_to_binary([<<?PROTOCOL_VERSION:8>>, - serialise_signature_type(certificate_timestamp), - serialise(TimestampedEntry)]))). - -get_sct(Hash, TimestampedEntry) -> - case application:get_env(catlfish, sctcache_root_path) of - {ok, RootPath} -> - case perm:readfile(RootPath, Hash) of - Contents when is_binary(Contents) -> - Contents; - noentry -> - SCT = calc_sct(TimestampedEntry), - ok = perm:ensurefile_nosync(RootPath, Hash, SCT), - SCT - end; - _ -> - calc_sct(TimestampedEntry) - end. - -add_to_db(Type, LeafCert, CertChain, EntryHash) -> - EntryType = case Type of - normal -> x509_entry; - precert -> precert_entry - end, - Timestamp = plop:generate_timestamp(), - TSE = timestamped_entry(Timestamp, EntryType, LeafCert, CertChain), - MTLText = serialise(#mtl{leaf_version = v1, - leaf_type = timestamped_entry, - entry = TSE}), - MTLHash = ht:leaf_hash(MTLText), - LogEntry = pack_entry(Type, MTLText, LeafCert, CertChain), - ok = plop:add(LogEntry, MTLHash, EntryHash), - {TSE, MTLHash}. - -get_ratelimit_token(Type) -> - ratelimit:get_token(Type). - --spec add_chain(binary(), [binary()], normal|precert) -> {[{_,_},...]}. -add_chain(LeafCert, CertChain, Type) -> - EntryHash = crypto:hash(sha256, [LeafCert | CertChain]), - {TimestampedEntry, Hash} = - case plop:get(EntryHash) of - notfound -> - case get_ratelimit_token(add_chain) of - ok -> - add_to_db(Type, LeafCert, CertChain, EntryHash); - _ -> - exit({internalerror, "Rate limiting"}) - end; - {_Index, MTLHash, DBEntry} -> - {_Type, MTLText, _Cert, _Chain} = unpack_entry(DBEntry), - MTL = deserialise_mtl(MTLText), - MTLText = serialise(MTL), % verify FIXME: remove - {MTL#mtl.entry, MTLHash} - end, - - SCT_sig = get_sct(Hash, TimestampedEntry), - {[{sct_version, ?PROTOCOL_VERSION}, - {id, base64:encode(plop:get_logid())}, - {timestamp, TimestampedEntry#timestamped_entry.timestamp}, - {extensions, base64:encode(<<>>)}, - {signature, base64:encode(SCT_sig)}]}. - --spec timestamped_entry(integer(), entry_type(), binary(), binary()) -> - timestamped_entry(). -timestamped_entry(Timestamp, EntryType, LeafCert, CertChain) -> - SignedEntry = - case EntryType of - x509_entry -> - #signed_x509_entry{asn1_cert = LeafCert}; - precert_entry -> - {DetoxedLeafTBSCert, IssuerKeyHash} = - x509:detox(LeafCert, CertChain), - #signed_precert_entry{ - issuer_key_hash = IssuerKeyHash, - tbs_certificate = DetoxedLeafTBSCert} - end, - #timestamped_entry{timestamp = Timestamp, - entry_type = EntryType, - signed_entry = SignedEntry}. - --spec deserialise_mtl(binary()) -> mtl(). -deserialise_mtl(Data) -> - <<LeafVersionBin:1/binary, - LeafTypeBin:1/binary, - TimestampedEntryBin/binary>> = Data, - #mtl{leaf_version = deserialise_leaf_version(LeafVersionBin), - leaf_type = deserialise_leaf_type(LeafTypeBin), - entry = deserialise_timestampedentry(TimestampedEntryBin)}. - --spec deserialise_timestampedentry(binary()) -> timestamped_entry(). -deserialise_timestampedentry(Data) -> - <<Timestamp:64, EntryTypeBin:2/binary, RestData/binary>> = Data, - EntryType = deserialise_entry_type(EntryTypeBin), - {SignedEntry, ExtensionsBin} = - case EntryType of - x509_entry -> - deserialise_signed_x509_entry(RestData); - precert_entry -> - deserialise_signed_precert_entry(RestData) - end, - {Extensions, <<>>} = decode_tls_vector(ExtensionsBin, 2), - #timestamped_entry{timestamp = Timestamp, - entry_type = EntryType, - signed_entry = SignedEntry, - extensions = Extensions}. - --spec deserialise_signed_x509_entry(binary()) -> {signed_x509_entry(), binary()}. -deserialise_signed_x509_entry(Data) -> - {E, D} = decode_tls_vector(Data, 3), - {#signed_x509_entry{asn1_cert = E}, D}. - --spec deserialise_signed_precert_entry(binary()) -> - {signed_precert_entry(), binary()}. -deserialise_signed_precert_entry(Data) -> - <<IssuerKeyHash:32/binary, RestData/binary>> = Data, - {TBSCertificate, RestData2} = decode_tls_vector(RestData, 3), - {#signed_precert_entry{issuer_key_hash = IssuerKeyHash, - tbs_certificate = TBSCertificate}, - RestData2}. - -serialise_extra_data(Type, Cert, Chain) -> - EncodedChain = encode_tls_vector( - list_to_binary( - [encode_tls_vector(C, 3) || C <- Chain]), 3), - case Type of - normal -> - EncodedChain; - precert -> - list_to_binary( - [encode_tls_vector(Cert, 3), EncodedChain]) - end. - --spec entries(non_neg_integer(), non_neg_integer()) -> {[{entries, list()},...]}. -entries(Start, End) -> - {[{entries, x_entries(plop:get(Start, End))}]}. - --spec entry_and_proof(non_neg_integer(), non_neg_integer()) -> {[{_,_},...]}. -entry_and_proof(Index, TreeSize) -> - case plop:inclusion_and_entry(Index, TreeSize) of - {ok, Entry, Path} -> - {Type, MTLText, Cert, Chain} = unpack_entry(Entry), - ExtraData = serialise_extra_data(Type, Cert, Chain), - {[{leaf_input, base64:encode(MTLText)}, - {extra_data, base64:encode(ExtraData)}, - {audit_path, [base64:encode(X) || X <- Path]}]}; - {notfound, Msg} -> - {[{success, false}, - {error_message, list_to_binary(Msg)}]} - end. - --define(CACHE_TABLE, catlfish_cache). -init_cache_table() -> - case ets:info(?CACHE_TABLE) of - undefined -> ok; - _ -> ets:delete(?CACHE_TABLE) - end, - ets:new(?CACHE_TABLE, [set, public, named_table]). - -verify_mtl(MTL, LeafCert, CertChain) -> - Timestamp = MTL#mtl.entry#timestamped_entry.timestamp, - EntryType = MTL#mtl.entry#timestamped_entry.entry_type, - TSE = timestamped_entry(Timestamp, EntryType, LeafCert, CertChain), - case MTL of - #mtl{leaf_version = v1, - leaf_type = timestamped_entry, - entry = TSE} -> - ok; - _ -> - error - end. - -verify_entry(Entry) -> - RootCerts = known_roots(), - verify_entry(Entry, RootCerts). - -%% Used from plop. -verify_entry(PackedEntry, RootCerts) -> - {_Type, MTLText, Cert, Chain} = unpack_entry(PackedEntry), - case x509:normalise_chain(RootCerts, [Cert | Chain]) of - {ok, [Cert | FullChain]} -> - case verify_mtl(deserialise_mtl(MTLText), Cert, FullChain) of - ok -> - {ok, ht:leaf_hash(MTLText)}; - error -> - {error, "MTL verification failed"} - end; - {error, Reason} -> - {error, Reason} - end. - -%% Used from plop. -entryhash_from_entry(PackedEntry) -> - {_Type, _MTLText, Cert, Chain} = unpack_entry(PackedEntry), - crypto:hash(sha256, [Cert | Chain]). - -%% Private functions. --spec pack_entry(normal|precert, binary(), binary(), [binary()]) -> binary(). -pack_entry(Type, MTLText, EndEntityCert, CertChain) -> - [{<<"MTL1">>, MTLText}, - {case Type of - normal -> <<"EEC1">>; - precert -> <<"PRC1">> - end, EndEntityCert}, - {<<"CHN1">>, - list_to_binary( - [tlv:encode(<<"X509">>, E) || E <- CertChain])}]. - --spec unpack_entry(binary()) -> {normal|precert, binary(), binary(), [binary()]}. -unpack_entry(Entry) -> - [{<<"MTL1">>, MTLText}|Rest1] = Entry, - [{EECType, EndEntityCert}|Rest2] = Rest1, - Type = case EECType of - <<"EEC1">> -> - normal; - <<"PRC1">> -> - precert - end, - [{<<"CHN1">>, PackedChain}|_Rest3] = Rest2, - Chain = unpack_certchain(PackedChain), - {Type, MTLText, EndEntityCert, Chain}. - -unpack_certchain(<<>>) -> - []; -unpack_certchain(Data) -> - {<<"X509">>, Unpacked, Rest} = tlv:decode(Data), - [Unpacked | unpack_certchain(Rest)]. - --spec x_entries([{non_neg_integer(), binary(), binary()}]) -> list(). -x_entries([]) -> - []; -x_entries([H|T]) -> - {_Index, _Hash, Entry} = H, - {Type, MTL, Cert, Chain} = unpack_entry(Entry), - ExtraData = serialise_extra_data(Type, Cert, Chain), - [{[{leaf_input, base64:encode(MTL)}, - {extra_data, base64:encode(ExtraData)}]} | x_entries(T)]. - --spec encode_tls_vector(binary(), non_neg_integer()) -> binary(). -encode_tls_vector(Binary, LengthLen) -> - Length = byte_size(Binary), - <<Length:LengthLen/integer-unit:8, Binary/binary>>. - --spec decode_tls_vector(binary(), non_neg_integer()) -> {binary(), binary()}. -decode_tls_vector(Binary, LengthLen) -> - <<Length:LengthLen/integer-unit:8, Rest/binary>> = Binary, - <<ExtractedBinary:Length/binary-unit:8, Rest2/binary>> = Rest, - {ExtractedBinary, Rest2}. - --define(ROOTS_CACHE_KEY, roots). - -update_known_roots() -> - case application:get_env(catlfish, known_roots_path) of - {ok, Dir} -> update_known_roots(Dir); - undefined -> [] - end. - -update_known_roots(Directory) -> - known_roots(Directory, update_tab). - -known_roots() -> - case application:get_env(catlfish, known_roots_path) of - {ok, Dir} -> known_roots(Dir, use_cache); - undefined -> [] - end. - --spec known_roots(file:filename(), use_cache|update_tab) -> [binary()]. -known_roots(Directory, CacheUsage) -> - case CacheUsage of - use_cache -> - case ets:lookup(?CACHE_TABLE, ?ROOTS_CACHE_KEY) of - [] -> - read_files_and_update_table(Directory); - [{roots, DerList}] -> - DerList - end; - update_tab -> - read_files_and_update_table(Directory) - end. - -read_files_and_update_table(Directory) -> - Certs = x509:read_pemfiles_from_dir(Directory), - Proper = x509:self_signed(Certs), - case length(Certs) - length(Proper) of - 0 -> ok; - N -> lager:warning( - "Ignoring ~p root certificates not signing themselves properly", - [N]) - end, - true = ets:insert(?CACHE_TABLE, {?ROOTS_CACHE_KEY, Proper}), - lager:info("Known roots imported: ~p", [length(Proper)]), - Proper. - -%%%%%%%%%%%%%%%%%%%% -%% Testing internal functions. --define(PEMFILES_DIR_OK, "test/testdata/known_roots"). --define(PEMFILES_DIR_NONEXISTENT, "test/testdata/nonexistent-dir"). - -read_pemfiles_test_() -> - {setup, - fun() -> - init_cache_table(), - {known_roots(?PEMFILES_DIR_OK, update_tab), - known_roots(?PEMFILES_DIR_OK, use_cache)} - end, - fun(_) -> ets:delete(?CACHE_TABLE, ?ROOTS_CACHE_KEY) end, - fun({L, LCached}) -> - [?_assertMatch(4, length(L)), - ?_assertEqual(L, LCached)] - end}. - -read_pemfiles_fail_test_() -> - {setup, - fun() -> - init_cache_table(), - known_roots(?PEMFILES_DIR_NONEXISTENT, update_tab) - end, - fun(_) -> ets:delete(?CACHE_TABLE, ?ROOTS_CACHE_KEY) end, - fun(Empty) -> [?_assertMatch([], Empty)] end}. |