%%% Copyright (c) 2014, NORDUnet A/S. %%% See LICENSE for licensing information. %%% %%% @doc Signing service -module(sign). -behaviour(gen_server). %% API. -export([start_link/0, stop/0]). -export([sign_sct/1, sign_sth/1, get_pubkey/0, get_logid/0, verify_sth/2]). -export([read_keyfile_ec/1]). %% API for tests. -export([read_keyfile_rsa/2]). %% gen_server callbacks. -export([init/1, handle_call/3, terminate/2, handle_cast/2, handle_info/2, code_change/3]). -define(CERTIFICATE_TIMESTAMP, 0). -define(TREE_HASH, 1). -import(stacktrace, [call/2]). -include_lib("public_key/include/public_key.hrl"). -include_lib("eunit/include/eunit.hrl"). -record(state, {pubkey :: public_key:rsa_public_key(), privkey :: public_key:rsa_private_key(), logid :: binary() }). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). stop() -> call(?MODULE, stop). init([]) -> %% 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. PrivKeyfile = application:get_env(plop, log_private_key, none), PubKeyfile = application:get_env(plop, log_public_key, none), Private_key = read_keyfile_ec(PrivKeyfile), Public_key = read_keyfile_ec(PubKeyfile), LogID = read_keyfile_ec_logid(PubKeyfile), {ok, #state{pubkey = Public_key, privkey = Private_key, logid = LogID}}. %% TODO: Merge the keyfile reading functions. %% @doc Read one password protected PEM file with an RSA keypair. read_keyfile_rsa(Filename, Passphrase) -> {ok, PemBin} = file:read_file(Filename), [KeyPem] = public_key:pem_decode(PemBin), % Use first entry. Privatekey = decode_key(KeyPem, Passphrase), {Privatekey, public_key(Privatekey)}. filter_pem_types(ParsedPem, Types) -> [E || E <- ParsedPem, lists:member(element(1, E), Types)]. read_keyfile_ec(KeyFile) -> lager:debug("reading file ~p", [KeyFile]), {ok, PemBin} = file:read_file(KeyFile), [KeyPem] = filter_pem_types(public_key:pem_decode(PemBin), ['ECPrivateKey', 'SubjectPublicKeyInfo']), decode_key(KeyPem). read_keyfile_ec_logid(KeyFile) -> lager:debug("reading file ~p", [KeyFile]), {ok, PemBin} = file:read_file(KeyFile), [{'SubjectPublicKeyInfo', Der, _}] = filter_pem_types(public_key:pem_decode(PemBin), ['SubjectPublicKeyInfo']), crypto:hash(sha256, Der). pem_entry_decode({'SubjectPublicKeyInfo', Der, _}) -> SPKI = public_key:der_decode('SubjectPublicKeyInfo', Der), #'SubjectPublicKeyInfo'{subjectPublicKey = {_, Octets}, algorithm = Algorithm} = SPKI, #'AlgorithmIdentifier'{parameters = ECParams} = Algorithm, Params = public_key:der_decode('EcpkParameters', ECParams), Point = #'ECPoint'{point = Octets}, {Point, Params}; pem_entry_decode(Entry) -> public_key:pem_entry_decode(Entry). %% -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}]). -spec signhash_ec(iolist() | binary(), public_key:ec_private_key()) -> binary(). signhash_ec(Data, PrivKey) -> public_key:sign(Data, sha256, PrivKey). decode_key(Entry) -> 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}. remote_sign_request([], _Request) -> none; remote_sign_request([URL|RestURLs], Request) -> case plop_httputil:request("signing", URL, [{"Content-Type", "text/json"}], list_to_binary(mochijson2:encode(Request))) of {error, Error} -> lager:info("request error: ~p", [Error]), remote_sign_request(RestURLs, Request); {failure, _StatusLine, _RespHeaders, _Body} -> lager:debug("auth check failed"), remote_sign_request(RestURLs, Request); {success, {_HttpVersion, StatusCode, _ReasonPhrase}, _RespHeaders, Body} when StatusCode == 200 -> lager:debug("auth check succeeded"), case (catch mochijson2:decode(Body)) of {error, E} -> lager:error("json parse error: ~p", [E]), remote_sign_request(RestURLs, Request); {struct, PropList} -> base64:decode(proplists:get_value(<<"result">>, PropList)) end; {noauth, _StatusLine, _RespHeaders, _Body} -> lager:debug("no auth"), remote_sign_request(RestURLs, Request); _ -> remote_sign_request(RestURLs, Request) end. %%%%%%%%%%%%%%%%%%%% %% Public API. sign_sct(Data = <<_Version:8, ?CERTIFICATE_TIMESTAMP:8, _/binary>>) -> case application:get_env(plop, signing_nodes) of {ok, URLBases} -> Request = {[{plop_version, 1}, {data, base64:encode(Data)} ]}, remote_sign_request([URLBase ++ "sct" || URLBase <- URLBases], Request); undefined -> call(?MODULE, {sign, Data}) end. sign_sth(Data = <<_Version:8, ?TREE_HASH:8, _/binary>>) -> case application:get_env(plop, signing_nodes) of {ok, URLBases} -> Request = {[{plop_version, 1}, {data, base64:encode(Data)} ]}, remote_sign_request([URLBase ++ "sth" || URLBase <- URLBases], Request); undefined -> call(?MODULE, {sign, Data}) end. get_pubkey() -> call(?MODULE, {get, pubkey}). get_logid() -> PubKeyfile = application:get_env(plop, log_public_key, none), read_keyfile_ec_logid(PubKeyfile). verify_sth(STH, Signature) -> lager:debug("verifying ~p: ~p", [STH, Signature]), PubKeyfile = application:get_env(plop, log_public_key, none), PublicKey = read_keyfile_ec(PubKeyfile), public_key:verify(STH, sha256, Signature, PublicKey). %%%%%%%%%%%%%%%%%%%% %% gen_server callbacks. 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. handle_call(stop, _From, State) -> {stop, normal, stopped, State}; handle_call({get, logid}, _From, State) -> {reply, State#state.logid, State}; handle_call({get, pubkey}, _From, State) -> {reply, State#state.pubkey, State}; handle_call({sign, Data}, _From, State) -> %% FIXME: Merge RSA and DC. Signature = signhash_ec(Data, State#state.privkey), lager:debug("signing ~p: ~p", [Data, Signature]), {reply, Signature, State}.