%% %% ssh_agent %% ssh agent client %% %% Copyright 2020 The University of Queensland %% Author: Alex Wilson %% %% Redistribution and use in source and binary forms, with or without %% modification, are permitted provided that the following conditions %% are met: %% 1. Redistributions of source code must retain the above copyright %% notice, this list of conditions and the following disclaimer. %% 2. Redistributions in binary form must reproduce the above copyright %% notice, this list of conditions and the following disclaimer in the %% documentation and/or other materials provided with the distribution. %% %% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR %% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES %% OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. %% IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, %% INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT %% NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, %% DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY %% THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT %% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF %% THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %% -module(ssh_agent_conn). -include_lib("public_key/include/public_key.hrl"). -include("SSHSignature.hrl"). -behaviour(gen_server). -export([start_link/1]). -export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2]). -type from() :: {pid(), Tag :: term()}. -type cmd() :: {from(), op()}. -spec start_link(ssh_agent:config()) -> {ok, pid()} | {error, term()}. start_link(Config) -> gen_server:start_link(?MODULE, [Config], []). -record(?MODULE, { config :: ssh_agent:config(), path :: string(), socket :: gen_tcp:socket(), cmd = idle :: idle | cmd(), cmdq = queue:new() :: queue:queue(cmd()), closing = false :: boolean() }). -define(SSH_AGENTC_REQUEST_IDENTITIES, 11). -define(SSH_AGENTC_SIGN_REQUEST, 13). -define(SSH_AGENTC_LOCK, 22). -define(SSH_AGENTC_UNLOCK, 23). -define(SSH_AGENTC_EXTENSION, 27). -define(SSH_AGENT_SUCCESS, 6). -define(SSH_AGENT_FAILURE, 5). -define(SSH_AGENT_EXT_FAILURE, 28). -define(SSH_AGENT_IDENTITIES_ANSWER, 12). -define(SSH_AGENT_SIGN_RESPONSE, 14). -define(SSH_AGENT_RSA_SHA2_256, 2). -define(SSH_AGENT_RSA_SHA2_512, 4). -type sign_flag() :: ?SSH_AGENT_RSA_SHA2_256 | ?SSH_AGENT_RSA_SHA2_512. -record(sign_request, { key :: public_key:public_key(), data :: binary(), flags = [] :: [sign_flag()] }). -record(lock_request, { passphrase :: binary() }). -record(unlock_request, { passphrase :: binary() }). -record(extension_request, { extension :: binary(), data :: term() }). -type ecdh_flag() :: none. -record(ext_ecdh, { key :: public_key:public_key(), partner :: public_key:public_key(), flags = [] :: [ecdh_flag()] }). -type op() :: list_keys | #sign_request{} | #lock_request{} | #unlock_request{} | #extension_request{}. init([Config]) -> Path = case Config of #{path := P} -> P; _ -> case os:getenv("SSH_AUTH_SOCK") of false -> error(no_auth_sock_path); P -> P end end, {ok, Socket} = gen_tcp:connect({local, Path}, 0, [binary, {packet, 4}, {active, true}]), {ok, #?MODULE{path = Path, socket = Socket, config = Config}}. terminate(_Why, #?MODULE{socket = S}) -> gen_tcp:close(S), ok. handle_call(list_keys, From, S0 = #?MODULE{}) -> enqueue_and_send({From, request_identities}, S0); handle_call({sign, Key, Data}, From, S0 = #?MODULE{}) -> Flags = case Key of #'RSAPublicKey'{} -> [?SSH_AGENT_RSA_SHA2_256]; _ -> [] end, Req = #sign_request{key = Key, data = Data, flags = Flags}, enqueue_and_send({From, Req}, S0); handle_call({sign, Key, Data, Flags}, From, S0 = #?MODULE{}) -> Req = #sign_request{key = Key, data = Data, flags = Flags}, enqueue_and_send({From, Req}, S0); handle_call({lock, Pw}, From, S0 = #?MODULE{}) -> enqueue_and_send({From, #lock_request{passphrase = Pw}}, S0); handle_call({unlock, Pw}, From, S0 = #?MODULE{}) -> enqueue_and_send({From, #unlock_request{passphrase = Pw}}, S0); handle_call(list_extensions, From, S0 = #?MODULE{}) -> Req = #extension_request{extension = <<"query">>}, enqueue_and_send({From, Req}, S0); handle_call({ecdh, Key, OtherKey}, From, S0 = #?MODULE{}) -> Req = #extension_request{ extension = <<"ecdh@joyent.com">>, data = #ext_ecdh{ key = Key, partner = OtherKey } }, enqueue_and_send({From, Req}, S0); handle_call(close, From, S0 = #?MODULE{}) -> gen_server:reply(From, ok), {stop, normal, S0}. enqueue_and_send(FromOp, S0 = #?MODULE{cmdq = Q0}) -> Q1 = queue:in(FromOp, Q0), S1 = S0#?MODULE{cmdq = Q1}, case S0 of #?MODULE{cmd = idle} -> send_next(S1); _ -> {noreply, S1} end. encode_sign_request(#sign_request{key = K, data = D, flags = F}) -> KBlob = public_key:ssh_encode(K, ssh2_pubkey), Flags = lists:foldl(fun (X, Acc) -> X bor Acc end, 0, F), <>. encode_lock_request(#lock_request{passphrase = Pw}) -> <>; encode_lock_request(#unlock_request{passphrase = Pw}) -> <>. encode_ext_ecdh(#ext_ecdh{key = K, partner = P, flags = F}) -> [] = F, KBlob = public_key:ssh_encode(K, ssh2_pubkey), PBlob = public_key:ssh_encode(P, ssh2_pubkey), <<(byte_size(KBlob)):32/big, KBlob/binary, (byte_size(PBlob)):32/big, PBlob/binary, 0:32/big>>. encode_ext_request(#extension_request{extension = Ext, data = D}) -> Data = case Ext of <<"query">> -> <<>>; <<"ecdh@joyent.com">> -> encode_ext_ecdh(D) end, <>. decode_ext_reply_query(<>) -> {Exts, _Rem1} = lists:foldl(fun (_N, {Acc0, <>}) -> {[Ext | Acc0], IRem0} end, {[], Rem0}, lists:seq(1, NExts)), {ok, lists:reverse(Exts)}. decode_ext_reply_ecdh(<>) -> {ok, Secret}. decode_success_ext(#extension_request{extension = Ext}, Bin) -> case Ext of <<"query">> -> decode_ext_reply_query(Bin); <<"ecdh@joyent.com">> -> decode_ext_reply_ecdh(Bin) end. decode_failure_ext(_Op, Bin) -> {error, Bin}. to_colonhex(Bin) -> Hex = << <> || <> <= Bin, Y <- string:to_lower(integer_to_list(X,16)) >>, Pairs = [ <> || <> <= Hex ], iolist_to_binary(lists:join(<<":">>, Pairs)). decode_identity(<>) -> case (catch public_key:ssh_decode(KeyBlob, ssh2_pubkey)) of {'EXIT', Why} -> {error, {bad_identity, Why}}; PubKey -> Fps = [ iolist_to_binary([ "SHA256:", base64:encode(crypto:hash(sha256, KeyBlob))]), to_colonhex(crypto:hash(md5, KeyBlob)) ], Id = #{pubkey => PubKey, comment => Comment, fingerprints => Fps}, {ok, Id, Rem} end. decode_n_identities(0, SoFar, <<>>) -> {ok, lists:reverse(SoFar)}; decode_n_identities(0, _SoFar, _) -> {error, leftover_bytes}; decode_n_identities(N, SoFar, Bin0) -> case decode_identity(Bin0) of {ok, K, Bin1} -> decode_n_identities(N - 1, [K | SoFar], Bin1); Err -> Err end. decode_identities_answer(<>) -> decode_n_identities(NKeys, [], Rem). decode_sign_response(#sign_request{key = K, flags = _F}, <>) -> <> = Sig, case {K, SigAlg} of {#'RSAPublicKey'{}, <<"ssh-rsa">>} -> {ok, #{hash => sha, signature => SigData}}; {#'RSAPublicKey'{}, <<"rsa-sha2-256">>} -> {ok, #{hash => sha256, signature => SigData}}; {#'RSAPublicKey'{}, <<"rsa-sha2-512">>} -> {ok, #{hash => sha512, signature => SigData}}; {{#'ECPoint'{}, _}, <<"ecdsa-sha2-", Curve/binary>>} -> H = case Curve of <<"nistp256">> -> sha256; <<"nistp384">> -> sha384; <<"nistp521">> -> sha512 end, <> = SigData, {ok, Asn1Sig} = 'SSHSignature':encode('ECDSASignature', #'ECDSASignature'{r = R, s = S}), {ok, #{hash => H, signature => Asn1Sig}}; {{ed_pub, ed25519, _}, <<"ssh-ed25519">>} -> {ok, #{hash => none, signature => SigData}}; _ -> {error, {bad_signature_algo, SigAlg}} end. send_next(S0 = #?MODULE{cmdq = Q0, socket = Socket}) -> case queue:out(Q0) of {{value, {From, Op}}, Q1} -> Msg = case Op of request_identities -> <>; #sign_request{} -> encode_sign_request(Op); #lock_request{} -> encode_lock_request(Op); #unlock_request{} -> encode_lock_request(Op); #extension_request{} -> encode_ext_request(Op) end, ok = gen_tcp:send(Socket, Msg), {noreply, S0#?MODULE{cmdq = Q1, cmd = {From, Op}}}; _ -> {noreply, S0#?MODULE{cmd = idle}} end. handle_info({tcp, Socket, Data}, S0 = #?MODULE{socket = Socket}) -> #?MODULE{cmd = {From, Op}} = S0, case Data of <> -> gen_server:reply(From, ok), send_next(S0); <> -> Rep = decode_success_ext(Op, Rem), gen_server:reply(From, Rep), send_next(S0); <> -> gen_server:reply(From, {error, agent_failure}), send_next(S0); <> -> gen_server:reply(From, {error, {agent_failure, Rem}}), send_next(S0); <> -> gen_server:reply(From, {error, agent_ext_failure}), send_next(S0); <> -> Rep = decode_failure_ext(Op, Rem), gen_server:reply(From, Rep), send_next(S0); <> -> Rep = decode_identities_answer(Rem), gen_server:reply(From, Rep), send_next(S0); <> -> Rep = decode_sign_response(Op, Rem), gen_server:reply(From, Rep), send_next(S0); <> -> gen_server:reply(From, {error, {unsupported_response, Code, Rem}}), send_next(S0) end; handle_info({tcp_closed, Socket}, S0 = #?MODULE{closing = true, socket = Socket}) -> 0 = queue:len(S0#?MODULE.cmdq), {stop, normal, S0}; handle_info({tcp_closed, Socket}, S0 = #?MODULE{closing = false, socket = Socket}) -> lists:foreach(fun ({From, _Op}) -> gen_server:reply(From, {error, closed}) end, queue:to_list(S0#?MODULE.cmdq)), {stop, {error, unexpected_close}, S0}; handle_info({tcp_error, Socket, Reason}, S0 = #?MODULE{socket = Socket}) -> lists:foreach(fun ({From, _Op}) -> gen_server:reply(From, {error, Reason}) end, queue:to_list(S0#?MODULE.cmdq)), {stop, {error, Reason}, S0}. handle_cast(Req, S0 = #?MODULE{}) -> {stop, {unknown_cast, Req}, S0}.