%%% Copyright (c) 2014-2015, NORDUnet A/S.
%%% See LICENSE for licensing information.
%%% @doc Certificate Transparency (RFC 6962)
-module(v1).
%% API (URL)
-export([request/3]).
%% Public functions, i.e. part of URL.
request(post, "ct/v1/add-chain", Input) ->
add_chain(Input, normal);
request(post, "ct/v1/add-pre-chain", Input) ->
add_chain(Input, precert);
request(get, "ct/v1/get-sth", _Query) ->
case plop:sth() of
noentry ->
lager:error("No valid STH found"),
internalerror("No valid STH found");
R ->
success(R)
end;
request(get, "ct/v1/get-sth-consistency", Query) ->
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 ->
html("get-sth-consistency: bad input:",
[FirstInput, SecondInput]);
false ->
success(
{[{consistency,
[base64:encode(X) ||
X <- plop:consistency(First, Second)]}]})
end;
_ -> html("get-sth-consistency: bad input:", Query)
end;
request(get, "ct/v1/get-proof-by-hash", Query) ->
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 ->
html("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} ->
html("get-proof-by-hash: hash not found", Msg)
end
end;
_ -> html("get-proof-by-hash: bad input:", Query)
end;
request(get, "ct/v1/get-entries", Query) ->
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 -> html("get-entries: bad input:", [Start, End]);
false -> success(
catlfish:entries(Start, min(End, Start + 999)))
end;
_ -> html("get-entries: bad input:", Query)
end;
request(get, "ct/v1/get-entry-and-proof", Query) ->
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 ->
html("get-entry-and-proof: not integers: ",
[IndexInput, TreeSizeInput]);
false -> success(catlfish:entry_and_proof(Index, TreeSize))
end;
_ -> html("get-entry-and-proof: bad input:", Query)
end;
request(get, "ct/v1/get-roots", _Query) ->
R = [{certificates,
[base64:encode(Der) ||
Der <- catlfish:update_known_roots()]}],
success({R});
request(_Method, _Path, _) ->
none.
%% Private functions.
html(Text, Input) ->
{400, [{"Content-Type", "text/html"}],
io_lib:format(
"<html><body><p>~n" ++
"~s~n" ++
"~p~n" ++
"</body></html>~n", [Text, Input])}.
success(Data) ->
{200, [{"Content-Type", "text/json"}], mochijson2:encode(Data)}.
internalerror(Text) ->
{500, [{"Content-Type", "text/html"}],
io_lib:format(
"<html><body><p>~n" ++
"~s~n" ++
"</body></html>~n", [Text])}.
-spec add_chain(any(), normal|precert) -> any().
add_chain(Input, Type) ->
case (catch mochijson2:decode(Input)) of
{error, E} ->
html("add-chain: bad input:", E);
{struct, [{<<"chain">>, ChainBase64}]} ->
case (catch [base64:decode(X) || X <- ChainBase64]) of
{'EXIT', _} ->
html("add-chain: invalid base64-encoded chain: ",
[ChainBase64]);
[LeafCert | CertChain] ->
case x509:normalise_chain(catlfish:known_roots(),
[LeafCert|CertChain]) of
{ok, [Leaf | Chain]} ->
lager:info("adding ~p cert ~p",
[Type, x509:cert_string(LeafCert)]),
success(catlfish:add_chain(Leaf, Chain, Type));
{error, Reason} ->
lager:info("rejecting ~p: ~p",
[x509:cert_string(LeafCert), Reason]),
html("add-chain: invalid chain", Reason)
end;
Invalid ->
html("add-chain: chain is not a list: ", [Invalid])
end;
_ -> html("add-chain: missing input: chain", Input)
end.