%%% Copyright (c) 2014, NORDUnet A/S. %%% See LICENSE for licensing information. %%% @doc Certificate Transparency (RFC 6962) -module(v1). %% API (URL) -export(['add-chain'/3, 'add-pre-chain'/3, 'get-sth'/3, 'get-sth-consistency'/3, 'get-proof-by-hash'/3, 'get-entries'/3, 'get-roots'/3, 'get-entry-and-proof'/3]). %% Testing -- FIXME: remove -export([hello/3]). -include("$CTROOT/plop/include/plop.hrl"). -define(PROTOCOL_VERSION, 0). %% Public functions, i.e. part of URL. 'add-chain'(SessionID, _Env, Input) -> R = case (catch jiffy:decode(Input)) of {error, E} -> html("add-chain: bad input:", E); {[{<<"chain">>, ChainBase64}]} -> case (catch [base64:decode(X) || X <- ChainBase64]) of {'EXIT', _} -> html("add-chain: invalid base64-encoded chain: ", [ChainBase64]); [LeafCert | CertChain] -> Entry = #plop_entry{type = x509, data = LeafCert}, SPT = plop:add(#timestamped_entry{entry = Entry}, list_to_binary(CertChain)), R2 = [{sct_version, ?PROTOCOL_VERSION}, {id, base64:encode(SPT#spt.logid)}, {timestamp, SPT#spt.timestamp}, {extensions, base64:encode("")}, {signature, base64:encode( plop:serialise(SPT#spt.signature))}], binary_to_list(jiffy:encode({R2})); Invalid -> html("add-chain: chain is not a list: ", [Invalid]) end; _ -> html("add-chain: missing input: chain", Input) end, deliver(SessionID, R). 'add-pre-chain'(SessionID, _Env, _Input) -> niy(SessionID). 'get-sth'(SessionID, _Env, _Input) -> #sth{ treesize = Treesize, timestamp = Timestamp, roothash = Roothash, signature = Signature} = plop:sth(), R = [{tree_size, Treesize}, {timestamp, Timestamp}, {sha256_root_hash, base64:encode(Roothash)}, {tree_head_signature, base64:encode( plop:serialise(Signature))}], deliver(SessionID, binary_to_list(jiffy:encode({R}))). 'get-sth-consistency'(SessionID, _Env, Input) -> R = case lists:sort(httpd:parse_query(Input)) of [{"first", FirstInput}, {"second", SecondInput}] -> {First, _} = string:to_integer(FirstInput), {Second, _} = string:to_integer(SecondInput), case lists:member(error, [First, Second]) of true -> html("get-sth-consistency: bad input:", [FirstInput, SecondInput]); false -> binary_to_list( jiffy:encode( {[{consistency, [base64:encode(X) || X <- plop:consistency(First, Second)]}]})) end; _ -> html("get-sth-consistency: bad input:", Input) end, deliver(SessionID, R). 'get-proof-by-hash'(SessionID, _Env, Input) -> R = case lists:sort(httpd:parse_query(Input)) 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 -> html("get-proof-by-hash: bad input:", [HashInput, TreeSizeInput]); false -> binary_to_list( jiffy:encode( case plop:inclusion(Hash, TreeSize) of {ok, Index, Path} -> {[{leaf_index, Index}, {audit_path, [base64:encode(X) || X <- Path]}]}; {notfound, Msg} -> %% FIXME: http status 400 {[{success, false}, {error_message, list_to_binary(Msg)}]} end)) end; _ -> html("get-proof-by-hash: bad input:", Input) end, deliver(SessionID, R). 'get-entries'(SessionID, _Env, Input) -> %% TODO: Limit the number of returned entreis (i.e. start-end) to %% something reasonable. R = case lists:sort(httpd:parse_query(Input)) of [{"end", EndInput}, {"start", StartInput}] -> {Start, _} = string:to_integer(StartInput), {End, _} = string:to_integer(EndInput), case lists:member(error, [Start, End]) of true -> html("get-entries: bad input:", [Start, End]); false -> encode_entries(plop:get(Start, End)) end; _ -> html("get-entries: bad input:", Input) end, deliver(SessionID, R). 'get-entry-and-proof'(SessionID, _Env, Input) -> R = case lists:sort(httpd:parse_query(Input)) 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 -> html("get-entry-and-proof: not integers: ", [IndexInput, TreeSizeInput]); false -> binary_to_list( jiffy:encode( case plop:inclusion_and_more(Index, TreeSize) of {ok, Leaf, Chain, Path} -> {[{leaf_input, base64:encode(plop:serialise(Leaf))}, {extra_data, base64:encode(Chain)}, {audit_path, [base64:encode(X) || X <- Path]}]}; {notfound, Msg} -> {[{success, false}, {error_message, list_to_binary(Msg)}]} end)) end; _ -> html("get-entry-and-proof: bad input:", Input) end, deliver(SessionID, R). 'get-roots'(SessionID, _Env, _Input) -> R = [{certificates, []}], % NIY. deliver(SessionID, binary_to_list(jiffy:encode({R}))). %% For testing. FIXME: Remove. hello(SessionID, Env, Input) -> Query = httpd:parse_query(Input), mod_esi:deliver(SessionID, io_lib:format( "Content-Type: text/html\r\n\r\n" ++ "
hello again, erlang world" ++ "SessionID: ~p~n" ++ "
Env: ~p~n" ++ "
Input, raw: ~p~n" ++ "
Input, parsed: ~p~n" ++ "", [SessionID, Env, Input, Query])). %% Private functions. -spec encode_entries([{mtl(), binary()}]) -> list(). encode_entries(Entries) -> binary_to_list(jiffy:encode({[{entries, unpack_entries(Entries)}]})). -spec unpack_entries([{mtl(), binary()}]) -> list(). unpack_entries([]) -> []; unpack_entries([H|T]) -> {MTL, Extra} = H, LeafInput = base64:encode(plop:serialise(MTL)), ExtraData = base64:encode(Extra), [{[{leaf_input, LeafInput}, {extra_data, ExtraData}]} | unpack_entries(T)]. html(Text, Input) -> io_lib:format( "Content-Type: text/html\r\n\r\n" ++ "
~n" ++ "~s~n" ++ "~p~n" ++ "~n", [Text, Input]). niy(S) -> mod_esi:deliver(S, html("NIY - Not Yet Implemented|", [])). -spec deliver(any(), string()) -> ok | {error, _Reason}. deliver(Session, Data) -> mod_esi:deliver(Session, Data).