1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
open Util
type event =
| Channel_exec of (int32 * string)
| Channel_subsystem of (int32 * string)
| Channel_data of (int32 * Cstruct.t)
| Channel_eof of int32
| Disconnected of string
type t = {
client_version : string option;
server_version : string;
client_kexinit : Ssh.kexinit option;
server_kexinit : Ssh.kexinit;
neg_kex : Kex.negotiation option;
host_key : Hostkey.priv;
session_id : Cstruct.t option;
keys_ctos : Kex.keys;
keys_stoc : Kex.keys;
new_keys_ctos : Kex.keys option;
new_keys_stoc : Kex.keys option;
keying : bool;
key_eol : Mtime.t option;
expect : Ssh.message_id option;
auth_state : Auth.state;
user_db : Auth.db;
channels : Channel.db;
ignore_next_packet : bool;
}
let guard_msg t msg =
let open Ssh in
match t.expect with
| None -> Ok ()
| Some MSG_DISCONNECT -> Ok ()
| Some MSG_IGNORE -> Ok ()
| Some MSG_DEBUG -> Ok ()
| Some id ->
let msgid = message_to_id msg in
guard (id = msgid) ("Unexpected message " ^ (message_id_to_string msgid))
let make host_key user_db =
let open Ssh in
let server_kexinit =
Kex.make_kexinit Hostkey.preferred_algs Kex.server_supported ()
in
let banner_msg = Ssh.Msg_version version_banner in
let kex_msg = Ssh.Msg_kexinit server_kexinit in
{ client_version = None;
server_version = version_banner;
server_kexinit;
client_kexinit = None;
neg_kex = None;
host_key;
session_id = None;
keys_ctos = Kex.make_plaintext ();
keys_stoc = Kex.make_plaintext ();
new_keys_ctos = None;
new_keys_stoc = None;
keying = true;
key_eol = None;
expect = Some MSG_VERSION;
auth_state = Auth.Preauth;
user_db;
channels = Channel.empty_db;
ignore_next_packet = false },
[ banner_msg; kex_msg ]
let of_new_keys_ctos t =
let open Kex in
let* new_keys_ctos = guard_some t.new_keys_ctos "No new_keys_ctos" in
let* () = guard (is_keyed new_keys_ctos) "Plaintext new keys" in
let new_keys_ctos = { new_keys_ctos with seq = t.keys_ctos.seq } in
Ok { t with keys_ctos = new_keys_ctos; new_keys_ctos = None }
let of_new_keys_stoc t =
let open Kex in
let* new_keys_stoc = guard_some t.new_keys_stoc "No new_keys_stoc" in
let* () = guard (is_keyed new_keys_stoc) "Plaintext new keys" in
let new_keys_stoc = { new_keys_stoc with seq = t.keys_stoc.seq } in
Ok { t with keys_stoc = new_keys_stoc; new_keys_stoc = None; keying = false }
let rekey t =
match t.keying, (Kex.is_keyed t.keys_stoc) with
| false, true ->
let server_kexinit =
Kex.make_kexinit Hostkey.preferred_algs Kex.server_supported ()
in
let t = { t with server_kexinit; keying = true } in
Some (t, Ssh.Msg_kexinit server_kexinit)
| _ -> None
let should_rekey t now =
match t.key_eol with
| None -> false
| Some eol ->
not t.keying &&
Kex.should_rekey t.keys_stoc.Kex.tx_rx eol now
let maybe_rekey t now = if should_rekey t now then rekey t else None
let pop_msg2 t buf =
let version t buf =
let* v, i = Common.version buf in
Ok (t, v, i)
in
let decrypt t buf =
let* keys_ctos, msg, buf =
Common.decrypt ~ignore_packet:t.ignore_next_packet t.keys_ctos buf
in
Ok ({ t with keys_ctos; ignore_next_packet = false }, msg, buf)
in
match t.client_version with
| None -> version t buf
| Some _ -> decrypt t buf
let make_noreply t = Ok (t, [], None)
let make_reply t msg = Ok (t, [ msg ], None)
let make_replies t msgs = Ok (t, msgs, None)
let make_event t e = Ok (t, [], Some e)
let make_reply_with_event t msg e = Ok (t, [ msg ], Some e)
let make_disconnect t code s =
Ok (t, [ Ssh.disconnect_msg code s ], Some (Disconnected s))
let rec input_userauth_request t username service auth_method =
let open Ssh in
let open Auth in
let inc_nfailed t =
match t.auth_state with
| Preauth | Done -> Error "Unexpected auth_state"
| Inprogress (u, s, nfailed) ->
Ok ({ t with auth_state = Inprogress (u, s, succ nfailed) })
in
let disconnect t code s =
let* t = inc_nfailed t in
make_disconnect t code s
in
let failure t =
let* t = inc_nfailed t in
make_reply t (Msg_userauth_failure ([ "publickey"; "password" ], false))
in
let discard t = make_noreply t in
let success t =
make_reply { t with auth_state = Done; expect = None } Msg_userauth_success
in
let try_probe t pubkey =
make_reply t (Msg_userauth_pk_ok pubkey)
in
let try_auth t b = if b then success t else failure t in
let handle_auth t =
let* session_id = guard_some t.session_id "No session_id" in
let* () = guard (service = "ssh-connection") "Bad service" in
match auth_method with
| Pubkey (pubkey, None) ->
try_probe t pubkey
| Pubkey (pubkey, Some (alg, signed)) ->
try_auth t (by_pubkey username alg pubkey session_id service signed t.user_db)
| Password (password, None) ->
try_auth t (by_password username password t.user_db)
| Password (_, Some _) | Hostbased _ | Authnone -> failure t
in
match t.auth_state with
| Done -> discard t
| Preauth ->
let t = { t with auth_state = Inprogress (username, service, 0) } in
input_userauth_request t username service auth_method
| Inprogress (prev_username, prev_service, nfailed) ->
if service <> "ssh-connection" then
disconnect t DISCONNECT_SERVICE_NOT_AVAILABLE
(sprintf "Don't know service `%s`" service)
else if prev_username <> username || prev_service <> service then
disconnect t DISCONNECT_PROTOCOL_ERROR
"Username or service changed during authentication"
else if nfailed = 10 then
disconnect t DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE
"Maximum authentication attempts reached"
else if nfailed > 10 then
Error "Maximum authentication attempts reached, already sent disconnect"
else
handle_auth t
let input_channel_open t send_channel init_win_size max_pkt_size data =
let open Ssh in
let fail t code s =
make_reply t
(Msg_channel_open_failure
(send_channel, channel_open_code_to_int code, s, ""))
in
let known = function
| Session -> true
| X11 _ -> true
| Forwarded_tcpip _ -> true
| Direct_tcpip _ -> true
| Raw_data _ -> false
in
let allowed = function
| Session -> true
| X11 _ -> false
| Forwarded_tcpip _ -> false
| Direct_tcpip _ -> false
| Raw_data _ -> false
in
let do_open t send_channel init_win_size max_pkt_size data =
match
Channel.add ~id:send_channel ~win:init_win_size
~max_pkt:max_pkt_size t.channels
with
| Error `No_channels_left ->
fail t OPEN_RESOURCE_SHORTAGE "Maximum number of channels reached"
| Ok (c, channels) ->
let open Channel in
make_reply { t with channels }
(Msg_channel_open_confirmation
(send_channel,
c.us.id,
c.us.win,
c.us.max_pkt,
Wire.blob_of_channel_data data))
in
if not (known data) then
fail t OPEN_UNKNOWN_CHANNEL_TYPE ""
else if not (allowed data) then
fail t OPEN_ADMINISTRATIVELY_PROHIBITED ""
else
do_open t send_channel init_win_size max_pkt_size data
let input_channel_request t recp_channel want_reply data =
let open Ssh in
let fail t =
if want_reply then
make_reply t (Msg_channel_failure recp_channel)
else
make_noreply t
in
let success t =
if want_reply then
make_reply t (Msg_channel_success recp_channel)
else
make_noreply t
in
let event t event =
if want_reply then
make_reply_with_event t (Msg_channel_success recp_channel) event
else
make_event t event
in
let handle t c = function
| Pty_req _ -> success t
| X11_req _ -> fail t
| Env (_key, _value) -> success t
| Shell -> fail t
| Exec cmd -> event t (Channel_exec (c, cmd))
| Subsystem cmd -> event t (Channel_subsystem (c, cmd))
| Window_change _ -> fail t
| Xon_xoff _ -> fail t
| Signal _ -> fail t
| Exit_status _ -> fail t
| Exit_signal _ -> fail t
| Raw_data _ -> fail t
in
match Channel.lookup recp_channel t.channels with
| None -> fail t
| Some c -> handle t (Channel.id c) data
let input_msg t msg now =
let open Ssh in
let* () = guard_msg t msg in
match msg with
| Msg_kexinit kex ->
let* neg = Kex.negotiate ~s:t.server_kexinit ~c:kex in
let ignore_next_packet =
kex.first_kex_packet_follows &&
not (Kex.guessed_right ~s:t.server_kexinit ~c:kex)
in
let t = { t with client_kexinit = Some kex;
neg_kex = Some neg;
expect = Some MSG_KEX_0;
ignore_next_packet }
in
(match rekey t with
| None -> make_noreply t
| Some (t, kexinit) -> make_reply t kexinit)
| Msg_kex (id, data) ->
begin
let* m = Wire.dh_kexdh_of_kex id data in
match m with
| Msg_kexdh_init e ->
let* neg = guard_some t.neg_kex "No negotiated kex" in
let* client_version = guard_some t.client_version "No client version" in
let* () = guard_none t.new_keys_stoc "Already got new_keys_stoc" in
let* () = guard_none t.new_keys_ctos "Already got new_keys_ctos" in
let* c = guard_some t.client_kexinit "No client kex" in
let* f, k = Kex.(Dh.generate neg.kex_alg e) in
let pub_host_key = Hostkey.pub_of_priv t.host_key in
let h = Kex.Dh.compute_hash ~signed:true neg
~v_c:client_version
~v_s:t.server_version
~i_c:c.rawkex
~i_s:(Wire.blob_of_kexinit t.server_kexinit)
~k_s:pub_host_key
~e ~f ~k
in
let signature = Hostkey.sign neg.server_host_key_alg t.host_key h in
Format.printf "shared is %a signature is %a (hash %a)\n%!"
Cstruct.hexdump_pp (Mirage_crypto_pk.Z_extra.to_cstruct_be f)
Cstruct.hexdump_pp signature Cstruct.hexdump_pp h;
let session_id = match t.session_id with None -> h | Some x -> x in
let* new_keys_ctos, new_keys_stoc, key_eol =
Kex.Dh.derive_keys k h session_id neg now
in
let signature = neg.server_host_key_alg, signature in
make_replies { t with session_id = Some session_id;
new_keys_ctos = Some new_keys_ctos;
new_keys_stoc = Some new_keys_stoc;
key_eol = Some key_eol;
expect = Some MSG_NEWKEYS }
[ Msg_kexdh_reply (pub_host_key, f, signature); Msg_newkeys ]
| _ ->
Error "unexpected KEX message"
end
| Msg_newkeys ->
let expect = if not (Kex.is_keyed t.keys_ctos) then
Some MSG_SERVICE_REQUEST
else
None
in
let* t = of_new_keys_ctos t in
make_noreply { t with expect }
| Msg_service_request service ->
if service = "ssh-userauth" then
make_reply { t with expect = Some MSG_USERAUTH_REQUEST }
(Msg_service_accept service)
else
make_disconnect t DISCONNECT_SERVICE_NOT_AVAILABLE
(sprintf "service %s not available" service)
| Msg_userauth_request (username, service, auth_method) ->
input_userauth_request t username service auth_method
| Msg_channel_open (send_channel, init_win_size, max_pkt_size, data) ->
input_channel_open t send_channel init_win_size max_pkt_size data
| Msg_channel_request (recp_channel, want_reply, data) ->
input_channel_request t recp_channel want_reply data
| Msg_channel_close recp_channel ->
let open Channel in
(match lookup recp_channel t.channels with
| None -> make_noreply t
| Some c ->
let t = { t with channels = remove recp_channel t.channels } in
(match c.state with
| Open -> make_reply_with_event
t (Msg_channel_close c.them.id) (Channel_eof recp_channel)
| Sent_close -> make_noreply t))
| Msg_channel_data (recp_channel, data) ->
let* c =
guard_some (Channel.lookup recp_channel t.channels) "no such channel"
in
let* c, data, adjust = Channel.input_data c data in
let channels = Channel.update c t.channels in
let t = { t with channels } in
let e = (Channel_data (Channel.id c, data)) in
(match adjust with
| None -> make_event t (Channel_data (Channel.id c, data))
| Some adjust -> make_reply_with_event t adjust e)
| Msg_channel_window_adjust (recp_channel, len) ->
let* c =
guard_some (Channel.lookup recp_channel t.channels) "no such channel"
in
let* c, msgs = Channel.adjust_window c len in
let channels = Channel.update c t.channels in
make_replies { t with channels } msgs
| Msg_channel_eof recp_channel ->
let* c =
guard_some (Channel.lookup recp_channel t.channels) "no such channel"
in
make_event t (Channel_eof (Channel.id c))
| Msg_disconnect (_, s, _) -> make_event t (Disconnected s)
| Msg_version v -> make_noreply { t with client_version = Some v;
expect = Some MSG_KEXINIT }
| msg -> Error ("unhandled msg: " ^ (message_to_string msg))
let output_msg t msg =
let buf, keys_stoc = Common.output_msg t.keys_stoc msg in
let t = { t with keys_stoc } in
match msg with
| Ssh.Msg_newkeys ->
let* t = of_new_keys_stoc t in
Ok (t, buf)
| _ -> Ok (t, buf)
let output_channel_data t id data =
let* () = guard (Cstruct.length data > 0) "empty data" in
let* c = guard_some (Channel.lookup id t.channels) "no such channel" in
let* c, frags = Channel.output_data c data in
Ok ({ t with channels = Channel.update c t.channels }, frags)