From df6fca69a7d9bb11d7c6116a9cc4062a6e5e040d Mon Sep 17 00:00:00 2001 From: Linus Nordberg Date: Fri, 2 May 2014 18:21:47 +0200 Subject: Sign using ECDSA and fix a couple bugs. Revive the plop_entry and hash over that instead of the full MTL, for the db hash. We don't want the timestamp in that hash! Use ECDSA instead of RSA for signing stuff. That's what Google does and we want to use their test suites. An annoyance with DSA is that the signature isn't deterministic. Testing just became less easy. Fix db:find() now that the hash is no longer the primary key. --- src/plop.erl | 176 ++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 127 insertions(+), 49 deletions(-) (limited to 'src/plop.erl') diff --git a/src/plop.erl b/src/plop.erl index 63545f0..5ca595f 100644 --- a/src/plop.erl +++ b/src/plop.erl @@ -16,6 +16,8 @@ -export([add/1, sth/0]). %% API for tests. -export([sth/1]). +-export([read_keyfile_rsa/2, read_keyfiles_ec/2]). +-export([testing_get_pubkey/0]). %% gen_server callbacks. -export([init/1, handle_call/3, terminate/2, handle_cast/2, handle_info/2, code_change/3]). @@ -26,8 +28,8 @@ -include_lib("eunit/include/eunit.hrl"). -define(PLOPVERSION, 1). --define(TESTKEYFILE, "src/test/rsakey.pem"). --define(TESTKEYPASSPHRASE, "sikrit"). +-define(TESTPRIVKEYFILE, "test/eckey.pem"). +-define(TESTPUBKEYFILE, "test/eckey-public.pem"). -record(state, {pubkey :: public_key:rsa_public_key(), privkey :: public_key:rsa_private_key(), @@ -35,7 +37,7 @@ hashtree :: ht:head()}). start_link() -> - start_link(?TESTKEYFILE, ?TESTKEYPASSPHRASE). % FIXME: Remove. + start_link(?TESTPRIVKEYFILE, ?TESTPUBKEYFILE). % FIXME: Remove. start_link(Keyfile, Passphrase) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [Keyfile, Passphrase], []). @@ -43,10 +45,16 @@ 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)), +init([PubKeyfile, PrivKeyfile]) -> + %% Read RSA keypair. + %% {Private_key, Public_key} = read_keyfile_rsa(Keyfile, Passphrase), + %% LogID = crypto:hash(sha256, + %% public_key:der_encode('RSAPublicKey', Public_key)), + %% Read EC keypair. + {Private_key, Public_key} = read_keyfiles_ec(PubKeyfile, PrivKeyfile), + LogID = crypto:hash(sha256, public_key:der_encode( + 'ECPoint', + element(2, element(1, Public_key)))), % FIXME! {ok, #state{pubkey = Public_key, privkey = Private_key, logid = LogID, @@ -74,6 +82,8 @@ sth() -> sth(Data) -> gen_server:call(?MODULE, {sth, Data}). +testing_get_pubkey() -> + gen_server:call(?MODULE, {test, pubkey}). %%%%%%%%%%%%%%%%%%%% handle_call(stop, _From, State) -> {stop, normal, stopped, State}; @@ -86,35 +96,46 @@ handle_call({add, #timestamped_entry{} = TimestampedEntry}, 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}. + {reply, sth(PrivKey, Tree, Data), Plop}; + +handle_call({test, pubkey}, _From, + Plop = #state{pubkey = PK}) -> + {reply, PK, Plop}. %%%%%%%%%%%%%%%%%%%% -spec do_add(timestamped_entry(), public_key:rsa_private_key(), binary(), any()) -> {any(), binary()}. -do_add(TimestampedEntry, Privkey, LogID, Tree) -> - MTL = #mtl{entry = TimestampedEntry}, - MTL_text = serialise(MTL), - DB_hash = crypto:hash(sha256, MTL_text), - case db:find(DB_hash) of - #plop{index = I, mtl = M, spt_text = SPT} -> - io:format("Found entry: index=~p, MTL: ~p, SPT: ~p~n", [I, M, SPT]), - %% DB consistency check: - %%Record = #plop{hash = H, spt = serialise(Data)}, % FIXME: Remove. +do_add(TimestampedEntry = #timestamped_entry{entry = PlopEntry}, + Privkey, LogID, Tree) -> + DB_hash = crypto:hash(sha256, serialise(PlopEntry)), + Record = db:find(DB_hash), + case Record of + #plop{index = I, mtl = M = #mtl{entry = E}, spt_text = SPT} -> + io:format("Found entry: index=~p~nMTL: ~p~nSPT: ~p~n", [I, M, SPT]), + Record = Record#plop{ % DB consistency checking. + hash = DB_hash, + mtl = #mtl{entry = + #timestamped_entry{ + timestamp = E#timestamped_entry.timestamp, + entry = PlopEntry} + }}, {Tree, SPT}; % State not changed, cached SPT. _ -> NewSPT = spt(LogID, Privkey, TimestampedEntry), + MTL = #mtl{entry = TimestampedEntry}, + io:format("Creating new entry: index=~p~ndb hash: ~p~nMTL: ~p~nSPT: ~p~n", + [ht:size(Tree) + 1, DB_hash, MTL, NewSPT]), DB_data = #plop{index = ht:size(Tree) + 1, hash = DB_hash, mtl = MTL, spt_text = NewSPT}, db:add(DB_data), - {ht:append(Tree, MTL_text), % New tree. + {ht:append(Tree, serialise(MTL)), % New tree. NewSPT} % New SPT. end. @@ -123,8 +144,7 @@ do_add(TimestampedEntry, Privkey, LogID, Tree) -> -spec spt(binary(), public_key:rsa_private_key(), timestamped_entry()) -> binary(). spt(LogID, PrivKey, #timestamped_entry{ timestamp = Timestamp_in, - entry_type = EntryType, - entry = Entry + entry = #plop_entry{type = EntryType, data = EntryData} }) -> Timestamp = timestamp(Timestamp_in), BinToSign = @@ -133,18 +153,16 @@ spt(LogID, PrivKey, #timestamped_entry{ signature_type = certificate_timestamp, timestamp = Timestamp, entry_type = EntryType, - signed_entry = Entry})), + signed_entry = EntryData})), 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. +%% @doc Signed Tree Head as specified in RFC6962 section 3.2. sth(PrivKey, Tree, []) -> sth(PrivKey, Tree, #sth{timestamp = now}); sth(PrivKey, Tree, #sth{version = Version, timestamp = Timestamp_in}) -> @@ -166,24 +184,64 @@ sth(PrivKey, Tree, #sth{version = Version, timestamp = Timestamp_in}) -> [STH, BinToSign, Signature, Timestamp]), STH. -read_keyfile(Filename, Passphrase) -> +%% TODO: Merge the keyfile reading functions. + +%% Read one password protected PEM file with an RSA keypair. +read_keyfile_rsa(Filename, Passphrase) -> {ok, PemBin} = file:read_file(Filename), - [Entry] = public_key:pem_decode(PemBin), - Privatekey = public_key:pem_entry_decode(Entry, Passphrase), + [KeyPem] = public_key:pem_decode(PemBin), % Use first entry. + Privatekey = decode_key(KeyPem, Passphrase), {Privatekey, public_key(Privatekey)}. +%% Read two PEM files, one with a private EC key and one with the +%% corresponding public EC key. +read_keyfiles_ec(PrivkeyFile, Pubkeyfile) -> + {ok, PemBinPriv} = file:read_file(PrivkeyFile), + [OTPPubParamsPem, PrivkeyPem] = public_key:pem_decode(PemBinPriv), + Privatekey = decode_key(PrivkeyPem), + + {_, ParamsBin, ParamsEnc} = OTPPubParamsPem, + PubParamsPem = {'EcpkParameters', ParamsBin, ParamsEnc}, + Params = public_key:pem_entry_decode(PubParamsPem), + + {ok, PemBinPub} = file:read_file(Pubkeyfile), + [SPKIPem] = public_key:pem_decode(PemBinPub), + %% SPKI is missing #'AlgorithmIdentifier' so pem_entry_decode won't do. + %% Publickey = public_key:pem_entry_decode(SPKIPem), + #'SubjectPublicKeyInfo'{algorithm = AlgoDer} = SPKIPem, + SPKI = public_key:der_decode('SubjectPublicKeyInfo', AlgoDer), + #'SubjectPublicKeyInfo'{subjectPublicKey = {_, Octets}} = SPKI, + Point = #'ECPoint'{point = Octets}, + Publickey = {Point, Params}, + + {Privatekey, Publickey}. + +decode_key(Entry) -> + public_key:pem_entry_decode(Entry). +decode_key(Entry, Passphrase) -> + public_key:pem_entry_decode(Entry, Passphrase). + 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}]). + +%% FIXME: Merge RSA and DC. +signhash(D, K) -> + signhash_ec(D, K). + +-spec signhash_ec(iolist() | binary(), public_key:ec_private_key()) -> binary(). +signhash_ec(Data, PrivKey) -> + public_key:sign(Data, sha256, PrivKey). + +%% -spec signhash_rsa(iolist() | binary(), public_key:rsa_private_key()) -> binary(). +%% signhash_rsa(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. @@ -216,16 +274,20 @@ timestamp(Timestamp) -> _ -> Timestamp end. --spec serialise(timestamped_entry() | spt_on_wire() | spt_signed() | mtl() | sth()) -> iolist(). +-spec serialise(plop_entry() | timestamped_entry() | spt_on_wire() | spt_signed() | mtl() | sth()) -> iolist(). +serialise(#plop_entry{ + type = TypeAtom, + data = Data + }) -> + EntryType = entry_type(TypeAtom), + [<>]; serialise(#timestamped_entry{ timestamp = Timestamp, - entry_type = TypeAtom, - entry = Entry + entry = PlopEntry }) -> - EntryType = entry_type(TypeAtom), - [<>]; + [<>, + serialise(PlopEntry)]; serialise(#spt_on_wire{ version = Version, logid = LogID, @@ -285,14 +347,30 @@ serialise_test_() -> timestamp = 0, entry_type = x509, signed_entry = <<"foo">>})))]. -add_test_() -> - {ok, S} = init([?TESTKEYFILE, ?TESTKEYPASSPHRASE]), +%%add_test_() -> +add_test() -> + {ok, S} = init([?TESTPRIVKEYFILE, ?TESTPUBKEYFILE]), + + Data1 = <<"some data">>, {_Tree, SPT} = do_add(#timestamped_entry{ timestamp = 4711, - entry_type = test, - entry = <<"some data">>}, + entry = #plop_entry{type = test, data = Data1}}, + S#state.privkey, + S#state.logid, + S#state.hashtree), + {_Tree1, SPT1} = + do_add(#timestamped_entry{ + timestamp = 4712, + entry = #plop_entry{type = test, data = Data1}}, S#state.privkey, S#state.logid, S#state.hashtree), - [?_assertEqual(<<"fixme:SPT">>, SPT)]. + ?assertEqual(SPT, SPT1), + + TE = #timestamped_entry{ + timestamp = 0, + entry = #plop_entry{type = test, data = <<"some data">>}}, + SPTeq1 = spt(S#state.logid, S#state.privkey, TE), + SPTeq2 = spt(S#state.logid, S#state.privkey, TE), + ?assertNotEqual(SPTeq1, SPTeq2). % DSA signatures differ! -- cgit v1.1