1%% 2%% %CopyrightBegin% 3%% 4%% Copyright Ericsson AB 2019. All Rights Reserved. 5%% 6%% Licensed under the Apache License, Version 2.0 (the "License"); 7%% you may not use this file except in compliance with the License. 8%% You may obtain a copy of the License at 9%% 10%% http://www.apache.org/licenses/LICENSE-2.0 11%% 12%% Unless required by applicable law or agreed to in writing, software 13%% distributed under the License is distributed on an "AS IS" BASIS, 14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15%% See the License for the specific language governing permissions and 16%% limitations under the License. 17%% 18%% %CopyrightEnd% 19%% 20 21%% Reference: https://tools.ietf.org/html/draft-miller-ssh-agent-02 22 23-module(ssh_agent). 24 25-behaviour(ssh_client_key_api). 26 27-include("ssh.hrl"). 28-include("ssh_agent.hrl"). 29 30-export([send/2]). 31-export([add_host_key/3, add_host_key/4, is_host_key/4, is_host_key/5, user_key/2, sign/3]). 32 33-type socket_path_option() :: {socket_path, string()}. 34-type timeout_option() :: {timeout, integer()}. 35-type call_ssh_file_option() :: {call_ssh_file, atom()}. 36 37%% ssh_client_key_api implementation 38 39%% Old (compatibility) version 40-spec add_host_key(string(), 41 public_key:public_key(), 42 Options 43 ) -> 44 ok | {error, Error :: term()} when 45 Options :: ssh_client_key_api:client_key_cb_options(call_ssh_file_option()). 46 47add_host_key(Host, PublicKey, Options) -> 48 KeyCbOpts = proplists:get_value(key_cb_private, Options, []), 49 SshFileCb = proplists:get_value(call_ssh_file, KeyCbOpts, ssh_file), 50 SshFileCb:add_host_key(Host, PublicKey, Options). 51 52 53-spec is_host_key(Key :: public_key:public_key(), 54 Host :: string(), 55 Algorithm :: ssh:pubkey_alg(), 56 Options 57 ) -> 58 boolean() when 59 Options :: ssh_client_key_api:client_key_cb_options(call_ssh_file_option()) . 60 61is_host_key(Key, PeerName, Algorithm, Opts) -> 62 KeyCbOpts = proplists:get_value(key_cb_private, Opts, []), 63 SshFileCb = proplists:get_value(call_ssh_file, KeyCbOpts, ssh_file), 64 SshFileCb:is_host_key(Key, PeerName, Algorithm, Opts). 65 66%% New version 67-spec add_host_key(Host, 68 inet:port_number(), 69 public_key:public_key(), 70 Options 71 ) -> Result when 72 Host :: inet:ip_address() | inet:hostname() | [inet:ip_address() | inet:hostname()], 73 Options :: ssh_client_key_api:client_key_cb_options(call_ssh_file_option()), 74 Result :: ok | {error, Error :: term()}. 75 76add_host_key(Host, Port, PublicKey, Options) -> 77 KeyCbOpts = proplists:get_value(key_cb_private, Options, []), 78 SshFileCb = proplists:get_value(call_ssh_file, KeyCbOpts, ssh_file), 79 SshFileCb:add_host_key(Host, Port, PublicKey, Options). 80 81 82-spec is_host_key(public_key:public_key(), 83 Host, 84 inet:port_number(), 85 ssh:pubkey_alg(), 86 Options 87 ) -> 88 boolean() when 89 Host :: inet:ip_address() | inet:hostname() | [inet:ip_address() | inet:hostname()], 90 Options :: ssh_client_key_api:client_key_cb_options(call_ssh_file_option()). 91 92is_host_key(Key, PeerName, Port, Algorithm, Opts) -> 93 KeyCbOpts = proplists:get_value(key_cb_private, Opts, []), 94 SshFileCb = proplists:get_value(call_ssh_file, KeyCbOpts, ssh_file), 95 SshFileCb:is_host_key(Key, PeerName, Port, Algorithm, Opts). 96 97 98-spec user_key(Algorithm :: ssh:pubkey_alg(), 99 Options) -> Result when 100 Result :: {ok, public_key:private_key()} | 101 {ok, {ssh2_pubkey, PubKeyBlob :: binary()}} | 102 {error, string()}, 103 Options :: ssh_client_key_api:client_key_cb_options(socket_path_option() 104 | timeout_option()). 105 106user_key(Algorithm, Opts) -> 107 KeyCbOpts = proplists:get_value(key_cb_private, Opts, []), 108 109 Request = #ssh_agent_identities_request{}, 110 Response = ssh_agent:send(Request, KeyCbOpts), 111 112 #ssh_agent_identities_response{keys = Keys} = Response, 113 114 AlgorithmStr = atom_to_list(Algorithm), 115 MatchingKeys = lists:filter(fun(Key) -> has_key_type(Key, AlgorithmStr) end, Keys), 116 117 % The "client_key_api" behaviour only allows returning a single user key, 118 % so we simply select the first one returned from the SSH agent here. This 119 % means that if a user adds multiple keys for the same algorithm, only the 120 % first one added will be used. 121 case MatchingKeys of 122 [#ssh_agent_key{blob = PubKeyBlob} | _OtherKeys] -> 123 {ok, {ssh2_pubkey, PubKeyBlob}}; 124 _ -> 125 {error, enoent} 126 end. 127 128-spec sign(binary(), 129 binary(), 130 Options 131 ) -> 132 Blob :: binary() when 133 Options :: ssh_client_key_api:client_key_cb_options(socket_path_option() 134 | timeout_option()). 135 136sign(PubKeyBlob, SigData, Opts) -> 137 KeyCbOpts = proplists:get_value(key_cb_private, Opts, []), 138 % OpenSSH does not seem to care when these flags are set for 139 % signature algorithms other than RSA, so we always send them. 140 SignFlags = ?SSH_AGENT_RSA_SHA2_256 bor ?SSH_AGENT_RSA_SHA2_512, 141 SignRequest = #ssh_agent_sign_request{key_blob = PubKeyBlob, data = SigData, flags = SignFlags}, 142 SignResponse = ssh_agent:send(SignRequest, KeyCbOpts), 143 #ssh_agent_sign_response{signature = #ssh_agent_signature{blob = Blob}} = SignResponse, 144 Blob. 145 146%% Utility functions 147 148has_key_type(#ssh_agent_key{blob = KeyBlob}, Type) -> 149 <<?DEC_BIN(KeyType, _KeyTypeLen), _KeyBlobRest/binary>> = KeyBlob, 150 binary_to_list(KeyType) == Type. 151 152%% Agent communication 153 154send(Request, Opts) -> 155 SocketPath = proplists:get_value(socket_path, Opts, os:getenv("SSH_AUTH_SOCK")), 156 Timeout = proplists:get_value(timeout, Opts, 1000), 157 158 ConnectOpts = [binary, {packet, 0}, {active, false}], 159 {ok, Socket} = gen_tcp:connect({local, SocketPath}, 0, ConnectOpts, Timeout), 160 161 BinRequest = pack(encode(Request)), 162 ok = gen_tcp:send(Socket, BinRequest), 163 164 {ok, <<Len:32/unsigned-big-integer>>} = gen_tcp:recv(Socket, 4, Timeout), 165 {ok, BinResponse} = gen_tcp:recv(Socket, Len, Timeout), 166 167 ok = gen_tcp:close(Socket), 168 169 Response = decode(BinResponse), 170 171 Response. 172 173%% Message packing 174 175pack(Data) -> 176 <<(size(Data)):32/unsigned-big-integer, Data/binary>>. 177 178%% SSH Agent message encoding 179 180encode(#ssh_agent_identities_request{}) -> 181 <<?Ebyte(?SSH_AGENTC_REQUEST_IDENTITIES)>>; 182 183encode(#ssh_agent_sign_request{key_blob = KeyBlob, data = Data, flags = Flags}) -> 184 <<?Ebyte(?SSH_AGENTC_SIGN_REQUEST), ?Estring(KeyBlob), ?Estring(Data), ?Euint32(Flags)>>. 185 186%% SSH Agent message decoding 187 188decode_keys(<<>>, Acc, 0) -> 189 lists:reverse(Acc); 190 191decode_keys(<<?DEC_BIN(KeyBlob, _KeyBlobLen), ?DEC_BIN(Comment, _CommentLen), Rest/binary>>, Acc, N) -> 192 Key = #ssh_agent_key{blob = KeyBlob, comment = Comment}, 193 decode_keys(Rest, [Key | Acc], N - 1). 194 195decode_signature(<<?DEC_BIN(Format, _FormatLen), Blob/binary>>) -> 196 % Decode signature according to https://tools.ietf.org/html/rfc4253#section-6.6 197 <<?DEC_BIN(SignatureBlob, _SignatureBlobLen)>> = Blob, 198 #ssh_agent_signature{format = Format, blob = SignatureBlob}. 199 200decode(<<?BYTE(?SSH_AGENT_SUCCESS)>>) -> 201 #ssh_agent_success{}; 202 203decode(<<?BYTE(?SSH_AGENT_FAILURE)>>) -> 204 #ssh_agent_failure{}; 205 206decode(<<?BYTE(?SSH_AGENT_IDENTITIES_ANSWER), ?UINT32(NumKeys), KeyData/binary>>) -> 207 #ssh_agent_identities_response{keys = decode_keys(KeyData, [], NumKeys)}; 208 209decode(<<?BYTE(?SSH_AGENT_SIGN_RESPONSE), ?DEC_BIN(Signature, _SignatureLen)>>) -> 210 #ssh_agent_sign_response{signature = decode_signature(Signature)}. 211