%%% @doc Server holding log entries in a database and hashes in a %%% Merkle tree. A backend for things like Certificate Transparency %%% (RFC 6962). %%% %%% When you submit data for insertion in the log, the data and a hash %%% of it is stored in a way that [mumble FIXME and FIXME]. In return %%% you will get a proof of your entry being included in the log. This %%% proof can later, together with the public key of the log, be used %%% to prove that your entry is indeed present in the log. -module('plop'). -behaviour(gen_server). %% API. -export([start_link/0, start_link/2, stop/0]). -export([add/1, sth/0]). %% API for tests. -export([sth/1]). %% gen_server callbacks. -export([init/1, handle_call/3, terminate/2, handle_cast/2, handle_info/2, code_change/3]). -include("plop.hrl"). -include_lib("public_key/include/public_key.hrl"). -include_lib("eunit/include/eunit.hrl"). -define(PLOPVERSION, 1). -record(state, {pubkey :: public_key:rsa_public_key(), privkey :: public_key:rsa_private_key(), logid :: binary(), hashtree :: ht:head()}). start_link() -> start_link("src/test/rsakey.pem", "sikrit"). % FIXME: Remove. start_link(Keyfile, Passphrase) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [Keyfile, Passphrase], []). stop() -> gen_server:call(?MODULE, stop). %%%%%%%%%%%%%%%%%%%% init([Keyfile, Passphrase]) -> {Private_key, Public_key} = read_keyfile(Keyfile, Passphrase), LogID = crypto:hash(sha256, public_key:der_encode('RSAPublicKey', Public_key)), {ok, #state{pubkey = Public_key, privkey = Private_key, logid = LogID, hashtree = ht:create()}}. handle_cast(_Request, State) -> {noreply, State}. handle_info(_Info, State) -> {noreply, State}. code_change(_OldVsn, State, _Extra) -> {ok, State}. terminate(_Reason, _State) -> io:format("~p terminating~n", [?MODULE]), ok. %%%%%%%%%%%%%%%%%%%% add(Data) when is_record(Data, timestamped_entry) -> gen_server:call(?MODULE, {add, Data}). sth() -> gen_server:call(?MODULE, {sth, []}). sth(Data) -> gen_server:call(?MODULE, {sth, Data}). %%%%%%%%%%%%%%%%%%%% handle_call(stop, _From, State) -> {stop, normal, stopped, State}; %% FIXME: What's the right interface for add()? Need to be able to set %% version and signature type, at least. That's missing from %% #timestamped_entry, so add it somehow. handle_call({add, #timestamped_entry{} = TimestampedEntry}, _From, State = #state{privkey = Privkey, logid = LogID, hashtree = Tree}) -> {NewTree, SPT} = do_add(TimestampedEntry, Privkey, LogID, Tree), io:format("Index: ~p~nSPT: ~p~n", [ht:size(NewTree), SPT]), {reply, SPT, State#state{hashtree = NewTree}}; handle_call({sth, Data}, _From, Plop = #state{privkey = PrivKey, hashtree = Tree}) -> {reply, sth(PrivKey, Tree, Data), Plop}. %%%%%%%%%%%%%%%%%%%% do_add(TimestampedEntry, Privkey, LogID, Tree) -> H = crypto:hash(sha256, serialise(TimestampedEntry)), Record = db:find(H), TmpFixm = case Record of #plop{index = I, spt = S} -> io:format("Found entry w/ index=~p, SPT: ~p~n", [I, S]), %% DB consistency check: %%Record = #plop{hash = H, spt = serialise(Data)}, % FIXME: Remove. {Tree, % State not changed. S}; % Cached SPT. _ -> NewSPT = spt(LogID, Privkey, TimestampedEntry), DbData = #plop{index = ht:size(Tree) + 1, hash = H, spt = NewSPT}, db:add(DbData), LeafData = #mtl{version = 1, leaf_type = timestamped_entry, entry = TimestampedEntry}, {ht:append(Tree, serialise(LeafData)), % New tree. NewSPT} % New SPT. end, TmpFixm. %% @doc Signed Plop Timestamp, conformant to an SCT in RFC6962 3.2 and %% RFC5246 4.7. -spec spt(binary(), public_key:rsa_private_key(), timestamped_entry()) -> binary(). spt(LogID, PrivKey, #timestamped_entry{ timestamp = Timestamp_in, entry_type = EntryType, entry = Entry }) -> Timestamp = timestamp(Timestamp_in), BinToSign = list_to_binary(serialise(#spt_signed{ version = 1, signature_type = certificate_timestamp, timestamp = Timestamp, entry_type = EntryType, signed_entry = Entry})), Signature = signhash(BinToSign, PrivKey), SPT = serialise(#spt_on_wire{ version = ?PLOPVERSION, logid = LogID, timestamp = Timestamp, signature = Signature}), io:format("SPT: ~p~nBinToSign: ~p~nSignature = ~p~n", [SPT, BinToSign, Signature]), list_to_binary(SPT). %% @doc Signed Tree Head as described in RFC6962 section 3.2. sth(PrivKey, Tree, []) -> sth(PrivKey, Tree, #sth{timestamp = now}); sth(PrivKey, Tree, #sth{version = Version, timestamp = Timestamp_in}) -> Timestamp = timestamp(Timestamp_in), Treesize = ht:size(Tree), Roothash = ht:tree_hash(Tree), BinToSign = list_to_binary(serialise(#sth{version = Version, signature_type = tree_hash, timestamp = Timestamp, tree_size = Treesize, root_hash = Roothash})), Signature = signhash(BinToSign, PrivKey), STH = <>, io:format("STH: ~p~nBinToSign: ~p~nSignature: ~p~nTimestamp: ~p~n", [STH, BinToSign, Signature, Timestamp]), STH. read_keyfile(Filename, Passphrase) -> {ok, PemBin} = file:read_file(Filename), [Entry] = public_key:pem_decode(PemBin), Privatekey = public_key:pem_entry_decode(Entry, Passphrase), {Privatekey, public_key(Privatekey)}. public_key(#'RSAPrivateKey'{modulus = Mod, publicExponent = Exp}) -> #'RSAPublicKey'{modulus = Mod, publicExponent = Exp}. -spec signhash(iolist() | binary(), public_key:rsa_private_key()) -> binary(). signhash(Data, PrivKey) -> %% Was going to just crypto:sign/3 the hash but looking at %% digitally_signed() in lib/ssl/src/ssl_handshake.erl it seems %% like we should rather use (undocumented) encrypt_private/3. %public_key:sign(hash(sha256, BinToSign), sha256, PrivKey) public_key:encrypt_private(crypto:hash(sha256, Data), PrivKey, [{rsa_pad, rsa_pkcs1_padding}]). %%%%%%%%%%%%%%%%%%%% %% Serialisation of data. %% FIXME: Make the type conversion functions return binaries instead %% of integers. Moving knowledge about width of these to one place. -spec signature_type(signature_type()) -> integer(). signature_type(certificate_timestamp) -> 0; signature_type(tree_hash) -> 1; signature_type(test) -> 2. -spec entry_type(entry_type()) -> integer(). entry_type(x509) -> 0; entry_type(precert) -> 1; entry_type(test) -> 2. -spec leaf_type(leaf_type()) -> integer(). leaf_type(timestamped_entry) -> 0; leaf_type(test) -> 1. -spec timestamp(now | integer()) -> integer(). timestamp(Timestamp) -> case Timestamp of now -> {NowMegaSec, NowSec, NowMicroSec} = now(), trunc(NowMegaSec * 1.0e9 + NowSec * 1.0e3 + NowMicroSec / 1.0e3); _ -> Timestamp end. -spec serialise(timestamped_entry() | spt_on_wire() | spt_signed() | mtl() | sth()) -> iolist(). serialise(#timestamped_entry{ timestamp = Timestamp, entry_type = TypeAtom, entry = Entry }) -> EntryType = entry_type(TypeAtom), [<>]; serialise(#spt_on_wire{ version = Version, logid = LogID, timestamp = Timestamp, signature = Signature }) -> [<>]; serialise(#spt_signed{ version = Version, signature_type = SigtypeAtom, timestamp = Timestamp, entry_type = EntrytypeAtom, signed_entry = Entry }) -> Sigtype = signature_type(SigtypeAtom), Entrytype = entry_type(EntrytypeAtom), [<>]; serialise(#mtl{ % Merkle Tree Leaf. version = Version, leaf_type = TypeAtom, entry = TimestampedEntry }) -> LeafType = leaf_type(TypeAtom), [<>, serialise(TimestampedEntry)]; serialise(#sth{ % Signed Tree Head. version = Version, signature_type = SigtypeAtom, timestamp = Timestamp, tree_size = Treesize, root_hash = Roothash }) -> Sigtype = signature_type(SigtypeAtom), [<>]. %%%%%%%%%%%%%%%%%%%% %% Tests. serialise_test_() -> [?_assertEqual( <<1:8, 0:8, 0:64, 0:16, "foo">>, list_to_binary(serialise(#spt_signed{ version = 1, signature_type = certificate_timestamp, timestamp = 0, entry_type = x509, signed_entry = <<"foo">>})))]. add_test_() -> {ok, S} = init(["test/rsakey.pem", "sikrit"]), [?_assertEqual( <<"fixme">>, do_add(#timestamped_entry{ timestamp = 4711, entry_type = test, entry = <<"some data">>}, S#state.privkey, S#state.logid, S#state.hashtree))].