%%% Copyright (c) 2014-2016, NORDUnet A/S. %%% See LICENSE for licensing information. %%% @doc Certificate Transparency (RFC 6962) -module(v1). %% API (URL) -export([request/4]). -define(APPURL_CT_V1, "dt/v1"). check_valid_sth() -> case plop:sth() of noentry -> lager:error("No valid STH found"), exit({internalerror, "No valid STH found"}); {struct, PropList} -> Now = plop:generate_timestamp(), Timestamp = proplists:get_value(<<"timestamp">>, PropList), MMD = application:get_env(catlfish, mmd, 86400) * 1000, if Now - Timestamp > MMD -> lager:error("Old STH found, " ++ "now: ~p, STH timestamp: ~p, diff: ~p", [Now, Timestamp, Now - Timestamp]), exit({internalerror, "No valid STH found"}); true -> ok end end. %% Public functions, i.e. part of URL. request(post, ?APPURL_CT_V1, "add-ds-rr", Input) -> check_valid_sth(), add_ds(Input); request(get, ?APPURL_CT_V1, "get-sth", _Query) -> check_valid_sth(), case plop:sth() of noentry -> lager:error("No valid STH found"), internalerror("No valid STH found"); R -> success(R) end; request(get, ?APPURL_CT_V1, "get-sth-consistency", Query) -> check_valid_sth(), case lists:sort(Query) of [{"first", FirstInput}, {"second", SecondInput}] -> {First, _} = string:to_integer(FirstInput), {Second, _} = string:to_integer(SecondInput), case lists:member(error, [First, Second]) of true -> err400("get-sth-consistency: bad input:", [FirstInput, SecondInput]); false -> success( {[{consistency, [base64:encode(X) || X <- plop:consistency(First, Second)]}]}) end; _ -> err400("get-sth-consistency: bad input:", Query) end; request(get, ?APPURL_CT_V1, "get-proof-by-hash", Query) -> check_valid_sth(), case lists:sort(Query) of [{"hash", HashInput}, {"tree_size", TreeSizeInput}] -> Hash = case (catch base64:decode(HashInput)) of {'EXIT', _} -> error; H -> H end, {TreeSize, _} = string:to_integer(TreeSizeInput), case lists:member(error, [Hash, TreeSize]) of true -> err400("get-proof-by-hash: bad input:", [HashInput, TreeSizeInput]); false -> case plop:inclusion(Hash, TreeSize) of {ok, Index, Path} -> success({[{leaf_index, Index}, {audit_path, [base64:encode(X) || X <- Path]}]}); {notfound, Msg} -> err400("get-proof-by-hash: hash not found", Msg) end end; _ -> err400("get-proof-by-hash: bad input:", Query) end; request(get, ?APPURL_CT_V1, "get-entries", Query) -> check_valid_sth(), case lists:sort(Query) of [{"end", EndInput}, {"start", StartInput}] -> {Start, _} = string:to_integer(StartInput), {End, _} = string:to_integer(EndInput), case lists:member(error, [Start, End]) of true -> err400("get-entries: bad input:", [Start, End]); false -> success( catlfish:entries(Start, min(End, Start + 999))) end; _ -> err400("get-entries: bad input:", Query) end; request(get, ?APPURL_CT_V1, "get-entry-and-proof", Query) -> check_valid_sth(), case lists:sort(Query) of [{"leaf_index", IndexInput}, {"tree_size", TreeSizeInput}] -> {Index, _} = string:to_integer(IndexInput), {TreeSize, _} = string:to_integer(TreeSizeInput), case lists:member(error, [Index, TreeSize]) of true -> err400("get-entry-and-proof: not integers: ", [IndexInput, TreeSizeInput]); false -> success(catlfish:entry_and_proof(Index, TreeSize)) end; _ -> err400("get-entry-and-proof: bad input:", Query) end; request(get, ?APPURL_CT_V1, "get-roots", _Query) -> check_valid_sth(), R = [{certificates, [base64:encode(Der) || Der <- catlfish:update_known_roots()]}], success({R}); request(_Method, _App, _Fun, _) -> none. %% Private functions. err400(Text, Input) -> {400, [{"Content-Type", "text/html"}], io_lib:format( "

~n" ++ "~s~n" ++ "~p~n" ++ "~n", [Text, Input])}. success(Data) -> {200, [{"Content-Type", "text/json"}], mochijson2:encode(Data)}. internalerror(Text) -> {500, [{"Content-Type", "text/html"}], io_lib:format( "

~n" ++ "~s~n" ++ "~n", [Text])}. -spec add_ds(any()) -> any(). add_ds(Input) -> case (catch mochijson2:decode(Input)) of {error, E} -> err400("add-ds-rr: bad input:", E); {struct, [{<<"chain">>, List}]} -> case decode_chain(List) of {invalid, ErrText} -> err400(io:format("add-ds-rr: ~p", [ErrText]), List); Data when is_list(Data) -> add_ds_helper(Data); _ -> err400("add-ds-rr: missing one or more entries", List) end; _ -> err400("add-ds-rr: missing input: chain", Input) end. decode_chain(List) -> case (catch [base64:decode(X) || X <- List]) of {'EXIT', _} -> {invalid, "invalid base64-encoding"}; L -> L end. add_ds_helper(Data) -> case dnssecport:dnssec_validate(Data) of {ok, [DS | Chain]} -> success(catlfish:add_chain(DS, Chain, normal)); {error, ErrorCode} -> err400(io:format("add-ds-rr: invalid DS record: ~p", [ErrorCode]), Data) end.