%%% Copyright (c) 2014-2015, 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, pem_entry_decode/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]). -include("timeouts.hrl"). -define(CERTIFICATE_TIMESTAMP, 0). -define(TREE_HASH, 1). -import(stacktrace, [call/2]). -include_lib("public_key/include/public_key.hrl"). -record(state, {pubkey :: public_key:rsa_public_key(), privkey :: public_key:rsa_private_key(), hsmport :: port(), logid :: binary() }). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). stop() -> call(?MODULE, stop). get_log_public_key() -> Der = application:get_env(plop, log_public_key, none), pem_entry_decode({'SubjectPublicKeyInfo', Der, []}). 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. Public_key = get_log_public_key(), LogID = get_logid(), case application:get_env(plop, hsm) of {ok, Args} -> Port = open_port({spawn_executable, code:priv_dir(plop) ++ "/hsmhelper"}, [{args, Args}, exit_status, {packet, 4}]), {ok, #state{pubkey = Public_key, hsmport = Port, logid = LogID}}; undefined -> PrivKeyfile = application:get_env(plop, log_private_key, none), Private_key = read_keyfile_ec(PrivKeyfile), {ok, #state{pubkey = Public_key, privkey = Private_key, logid = LogID}} end. %% 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). pem_entry_decode({'SubjectPublicKeyInfo', Der, _}) -> SPKI = public_key:der_decode('SubjectPublicKeyInfo', Der), {Octets, Algorithm} = plop_compat:unpack_spki(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() -> Der = application:get_env(plop, log_public_key, none), crypto:hash(sha256, Der). verify_sth(STH, Signature) -> lager:debug("verifying ~p: ~p", [STH, Signature]), PublicKey = get_log_public_key(), public_key:verify(STH, sha256, Signature, PublicKey). encode_ec_signature(RawSignature, SignatureLength) -> <> = RawSignature, {ok, Signature} = 'Dss':encode('Dss-Sig-Value', #'Dss-Sig-Value'{r = R, s = S}), Signature. %%%%%%%%%%%%%%%%%%%% %% 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. case State#state.hsmport of undefined -> Signature = signhash_ec(Data, State#state.privkey), lager:debug("signing ~p: ~p", [Data, Signature]), {reply, Signature, State}; Port -> lager:debug("sending signing request to HSM"), Port ! {self(), {command, crypto:hash(sha256, Data)}}, receive {Port, {data, RawSignature}} when is_port(Port) -> Signature = encode_ec_signature(list_to_binary(RawSignature), 256), lager:debug("received signing reply from HSM: ~p", [Signature]), {reply, Signature, State}; {Port, {exit_status, ExitStatus}} -> lager:error("hsmhelper port ~p exiting with status ~p", [Port, ExitStatus]), {stop, portexit, State} after ?SIGN_HSM_PORT_TIMEOUT -> lager:error("HSM timeout"), {stop, timeout, State} end end.