%%% @doc Server holding log entries in a database and hashes in a Merkle tree. %%% %%% 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'). -include("plop.hrl"). -include_lib("public_key/include/public_key.hrl"). -include_lib("eunit/include/eunit.hrl"). -export([start/2, loop/1]). -define(PLOPVERSION, 1). -record(plop, {pubkey :: public_key:rsa_public_key(), privkey :: public_key:rsa_private_key(), logid :: binary(), hashtree :: ht:head()}). -spec start(string(), string()) -> pid(). start(Keyfile, Passphrase) -> {Private_key, Public_key} = read_keyfile(Keyfile, Passphrase), LogID = crypto:hash(sha256, public_key:der_encode('RSAPublicKey', Public_key)), Plop = #plop{pubkey = Public_key, privkey = Private_key, logid = LogID, hashtree = ht:create()}, Pid = spawn_link(plop, loop, [Plop]), register(plop, Pid), Pid. log(Format, Data) -> io:format(Format, Data). loop(Plop) -> receive {From, quit} -> From ! {ok, quit}; {From, Data} -> handle_req(From, Plop, Data), loop(Plop); Unknown -> log("DEBUG: Received malformed command: ~p~n", [Unknown]), loop(Plop) end. -spec serialise(plop_entry() | plop_data()) -> iolist(). serialise(#plop_entry{type = EntryType, entry = Entry}) -> [<>, Entry]; serialise(#plop_data{version = Version, signature_type = Sigtype, timestamp = Timestamp, entry = Entry}) -> [<>, serialise(Entry)]. handle_req(From, #plop{privkey = Privkey, logid = LogID, hashtree = Tree}, Arg) -> case Arg of {add, PlopData = #plop_data{entry = Entry}} when is_record(Entry, plop_entry) -> %% fixme: add Entry to db, ht:append(Tree, serialise(Entry)), SPT = spt(LogID, Privkey, PlopData), %% io:format("adding ~p to ~p -> H: ~p, SPT: ~p~n", [Entry, Tree, H, SPT]), From ! {ok, SPT}; sth -> % Signed tree head. From ! {ok, sth(Tree)}; Unknown -> From ! {error, Unknown} end. %% @doc Signed Plop Timestamp according to RFC6962 3.2 and RFC5246 4.7. spt(LogID, PrivKey, #plop_data{version = Version, % >= 1 signature_type = Sigtype, % >= 0 timestamp = Timestamp_in, entry = Entry = #plop_entry{}}) when is_binary(LogID) -> Timestamp = case Timestamp_in of now -> {NowMegaSec, NowSec, NowMicroSec} = now(), trunc(NowMegaSec * 1.0e9 + NowSec * 1.0e3 + NowMicroSec / 1.0e3); _ -> Timestamp_in end, BinToSign = list_to_binary( serialise(#plop_data{version = Version, signature_type = Sigtype, timestamp = Timestamp, entry = Entry})), %% Was going to just 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) Signature = public_key:encrypt_private(crypto:hash(sha256, BinToSign), PrivKey, [{rsa_pad, rsa_pkcs1_padding}]), SPT = <>, %%io:format("SPT: ~p~nBinToSign: ~p~nSignature = ~p~n", [SPT, BinToSign, Signature]), SPT. %% @doc Signed Tree Head %% digitally-signed struct { %% Version version; %% SignatureType signature_type = tree_hash; %% uint64 timestamp; %% uint64 tree_size; %% opaque sha256_root_hash[32]; %% } TreeHeadSignature; sth(Tree) -> "FIXME: signed tree head for " ++ Tree. 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}. %%%%%%%%%%%%%%%%%%%% %% Tests. serialise_test_() -> Entry = #plop_entry{type = ?PLOP_ENTRY_TYPE_X509, entry = <<"foo">>}, Entry_serialised = <<0:16, "foo">>, [?_assertEqual(Entry_serialised, list_to_binary(serialise(Entry))), ?_assertEqual(<<1:8, 0:8, 0:64, Entry_serialised/binary>>, list_to_binary(serialise(#plop_data{signature_type = 0, timestamp = 0, entry = Entry})))].