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