/src/dropbear/src/cli-kex.c
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Dropbear - a SSH2 server |
3 | | * |
4 | | * Copyright (c) 2002-2004 Matt Johnston |
5 | | * Copyright (c) 2004 by Mihnea Stoenescu |
6 | | * All rights reserved. |
7 | | * |
8 | | * Permission is hereby granted, free of charge, to any person obtaining a copy |
9 | | * of this software and associated documentation files (the "Software"), to deal |
10 | | * in the Software without restriction, including without limitation the rights |
11 | | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
12 | | * copies of the Software, and to permit persons to whom the Software is |
13 | | * furnished to do so, subject to the following conditions: |
14 | | * |
15 | | * The above copyright notice and this permission notice shall be included in |
16 | | * all copies or substantial portions of the Software. |
17 | | * |
18 | | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
19 | | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
20 | | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
21 | | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
22 | | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
23 | | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
24 | | * SOFTWARE. */ |
25 | | |
26 | | #include "includes.h" |
27 | | #include "session.h" |
28 | | #include "dbutil.h" |
29 | | #include "algo.h" |
30 | | #include "buffer.h" |
31 | | #include "session.h" |
32 | | #include "kex.h" |
33 | | #include "ssh.h" |
34 | | #include "packet.h" |
35 | | #include "bignum.h" |
36 | | #include "dbrandom.h" |
37 | | #include "runopts.h" |
38 | | #include "signkey.h" |
39 | | #include "ecc.h" |
40 | | |
41 | | |
42 | | static void checkhostkey(const unsigned char* keyblob, unsigned int keybloblen); |
43 | 0 | #define MAX_KNOWNHOSTS_LINE 4500 |
44 | | |
45 | 0 | static void cli_kex_free_param(void) { |
46 | 0 | #if DROPBEAR_NORMAL_DH |
47 | 0 | if (cli_ses.dh_param) { |
48 | 0 | free_kexdh_param(cli_ses.dh_param); |
49 | 0 | cli_ses.dh_param = NULL; |
50 | 0 | } |
51 | 0 | #endif |
52 | 0 | #if DROPBEAR_ECDH |
53 | 0 | if (cli_ses.ecdh_param) { |
54 | 0 | free_kexecdh_param(cli_ses.ecdh_param); |
55 | 0 | cli_ses.ecdh_param = NULL; |
56 | 0 | } |
57 | 0 | #endif |
58 | 0 | #if DROPBEAR_CURVE25519 |
59 | 0 | if (cli_ses.curve25519_param) { |
60 | 0 | free_kexcurve25519_param(cli_ses.curve25519_param); |
61 | 0 | cli_ses.curve25519_param = NULL; |
62 | 0 | } |
63 | 0 | #endif |
64 | 0 | #if DROPBEAR_PQHYBRID |
65 | 0 | if (cli_ses.pqhybrid_param) { |
66 | 0 | free_kexpqhybrid_param(cli_ses.pqhybrid_param); |
67 | 0 | cli_ses.pqhybrid_param = NULL; |
68 | 0 | } |
69 | 0 | #endif |
70 | 0 | } |
71 | | |
72 | 0 | void send_msg_kexdh_init() { |
73 | 0 | TRACE(("send_msg_kexdh_init()")) |
74 | |
|
75 | 0 | CHECKCLEARTOWRITE(); |
76 | |
|
77 | 0 | #if DROPBEAR_FUZZ |
78 | 0 | if (fuzz.fuzzing && fuzz.skip_kexmaths) { |
79 | 0 | return; |
80 | 0 | } |
81 | 0 | #endif |
82 | | |
83 | 0 | cli_kex_free_param(); |
84 | |
|
85 | 0 | buf_putbyte(ses.writepayload, SSH_MSG_KEXDH_INIT); |
86 | 0 | switch (ses.newkeys->algo_kex->mode) { |
87 | 0 | #if DROPBEAR_NORMAL_DH |
88 | 0 | case DROPBEAR_KEX_NORMAL_DH: |
89 | 0 | cli_ses.dh_param = gen_kexdh_param(); |
90 | 0 | buf_putmpint(ses.writepayload, &cli_ses.dh_param->pub); |
91 | 0 | break; |
92 | 0 | #endif |
93 | 0 | #if DROPBEAR_ECDH |
94 | 0 | case DROPBEAR_KEX_ECDH: |
95 | 0 | cli_ses.ecdh_param = gen_kexecdh_param(); |
96 | 0 | buf_put_ecc_raw_pubkey_string(ses.writepayload, &cli_ses.ecdh_param->key); |
97 | 0 | break; |
98 | 0 | #endif |
99 | 0 | #if DROPBEAR_CURVE25519 |
100 | 0 | case DROPBEAR_KEX_CURVE25519: |
101 | 0 | cli_ses.curve25519_param = gen_kexcurve25519_param(); |
102 | 0 | buf_putstring(ses.writepayload, cli_ses.curve25519_param->pub, CURVE25519_LEN); |
103 | 0 | break; |
104 | 0 | #endif |
105 | 0 | #if DROPBEAR_PQHYBRID |
106 | 0 | case DROPBEAR_KEX_PQHYBRID: |
107 | 0 | cli_ses.pqhybrid_param = gen_kexpqhybrid_param(); |
108 | 0 | buf_putbufstring(ses.writepayload, cli_ses.pqhybrid_param->concat_public); |
109 | 0 | break; |
110 | 0 | #endif |
111 | 0 | } |
112 | | |
113 | 0 | encrypt_packet(); |
114 | 0 | } |
115 | | |
116 | | /* Handle a diffie-hellman key exchange reply. */ |
117 | 0 | void recv_msg_kexdh_reply() { |
118 | |
|
119 | 0 | sign_key *hostkey = NULL; |
120 | 0 | unsigned int keytype, keybloblen; |
121 | 0 | unsigned char* keyblob = NULL; |
122 | |
|
123 | 0 | TRACE(("enter recv_msg_kexdh_reply")) |
124 | | |
125 | 0 | #if DROPBEAR_FUZZ |
126 | 0 | if (fuzz.fuzzing && fuzz.skip_kexmaths) { |
127 | 0 | return; |
128 | 0 | } |
129 | 0 | #endif |
130 | | |
131 | 0 | if (cli_ses.kex_state != KEXDH_INIT_SENT) { |
132 | 0 | dropbear_exit("Received out-of-order kexdhreply"); |
133 | 0 | } |
134 | 0 | keytype = ses.newkeys->algo_hostkey; |
135 | 0 | TRACE(("keytype is %d", keytype)) |
136 | |
|
137 | 0 | hostkey = new_sign_key(); |
138 | 0 | keybloblen = buf_getint(ses.payload); |
139 | |
|
140 | 0 | keyblob = buf_getptr(ses.payload, keybloblen); |
141 | 0 | if (!ses.kexstate.donefirstkex) { |
142 | | /* Only makes sense the first time */ |
143 | 0 | checkhostkey(keyblob, keybloblen); |
144 | 0 | } |
145 | |
|
146 | 0 | if (buf_get_pub_key(ses.payload, hostkey, &keytype) != DROPBEAR_SUCCESS) { |
147 | 0 | TRACE(("failed getting pubkey")) |
148 | 0 | dropbear_exit("Bad KEX packet"); |
149 | 0 | } |
150 | | |
151 | | /* Derive the shared secret */ |
152 | 0 | switch (ses.newkeys->algo_kex->mode) { |
153 | 0 | #if DROPBEAR_NORMAL_DH |
154 | 0 | case DROPBEAR_KEX_NORMAL_DH: |
155 | 0 | { |
156 | 0 | DEF_MP_INT(dh_f); |
157 | 0 | m_mp_init(&dh_f); |
158 | 0 | if (buf_getmpint(ses.payload, &dh_f) != DROPBEAR_SUCCESS) { |
159 | 0 | TRACE(("failed getting mpint")) |
160 | 0 | dropbear_exit("Bad KEX packet"); |
161 | 0 | } |
162 | | |
163 | 0 | kexdh_comb_key(cli_ses.dh_param, &dh_f, hostkey); |
164 | 0 | mp_clear(&dh_f); |
165 | 0 | } |
166 | 0 | break; |
167 | 0 | #endif |
168 | 0 | #if DROPBEAR_ECDH |
169 | 0 | case DROPBEAR_KEX_ECDH: |
170 | 0 | { |
171 | 0 | buffer *ecdh_qs = buf_getstringbuf(ses.payload); |
172 | 0 | kexecdh_comb_key(cli_ses.ecdh_param, ecdh_qs, hostkey); |
173 | 0 | buf_free(ecdh_qs); |
174 | 0 | } |
175 | 0 | break; |
176 | 0 | #endif |
177 | 0 | #if DROPBEAR_CURVE25519 |
178 | 0 | case DROPBEAR_KEX_CURVE25519: |
179 | 0 | { |
180 | 0 | buffer *ecdh_qs = buf_getstringbuf(ses.payload); |
181 | 0 | kexcurve25519_comb_key(cli_ses.curve25519_param, ecdh_qs, hostkey); |
182 | 0 | buf_free(ecdh_qs); |
183 | 0 | } |
184 | 0 | break; |
185 | 0 | #endif |
186 | 0 | #if DROPBEAR_PQHYBRID |
187 | 0 | case DROPBEAR_KEX_PQHYBRID: |
188 | 0 | { |
189 | 0 | buffer *q_s = buf_getstringbuf(ses.payload); |
190 | 0 | kexpqhybrid_comb_key(cli_ses.pqhybrid_param, q_s, hostkey); |
191 | 0 | buf_free(q_s); |
192 | 0 | } |
193 | 0 | break; |
194 | 0 | #endif |
195 | 0 | } |
196 | | |
197 | | /* Clear the local parameter */ |
198 | 0 | cli_kex_free_param(); |
199 | |
|
200 | 0 | if (buf_verify(ses.payload, hostkey, ses.newkeys->algo_signature, |
201 | 0 | ses.hash) != DROPBEAR_SUCCESS) { |
202 | 0 | dropbear_exit("Bad hostkey signature"); |
203 | 0 | } |
204 | | |
205 | 0 | sign_key_free(hostkey); |
206 | 0 | hostkey = NULL; |
207 | |
|
208 | 0 | send_msg_newkeys(); |
209 | 0 | ses.requirenext = SSH_MSG_NEWKEYS; |
210 | 0 | TRACE(("leave recv_msg_kexdh_init")) |
211 | 0 | } |
212 | | |
213 | | static void ask_to_confirm(const unsigned char* keyblob, unsigned int keybloblen, |
214 | 0 | const char* algoname) { |
215 | |
|
216 | 0 | char* fp = NULL; |
217 | 0 | FILE *tty = NULL; |
218 | 0 | int response = 'z'; |
219 | |
|
220 | 0 | fp = sign_key_fingerprint(keyblob, keybloblen); |
221 | |
|
222 | 0 | if (!cli_opts.ask_hostkey) { |
223 | 0 | dropbear_log(LOG_INFO, "\nHost '%s' key unknown.\n(%s fingerprint %s)", |
224 | 0 | cli_opts.remotehost, |
225 | 0 | algoname, |
226 | 0 | fp); |
227 | 0 | dropbear_exit("Not accepted automatically"); |
228 | 0 | } |
229 | | |
230 | 0 | if (cli_opts.always_accept_key) { |
231 | 0 | dropbear_log(LOG_INFO, "\nHost '%s' key accepted unconditionally.\n(%s fingerprint %s)\n", |
232 | 0 | cli_opts.remotehost, |
233 | 0 | algoname, |
234 | 0 | fp); |
235 | 0 | m_free(fp); |
236 | 0 | return; |
237 | 0 | } |
238 | | |
239 | 0 | fprintf(stderr, "\nHost '%s' is not in the trusted hosts file.\n(%s fingerprint %s)\n", |
240 | 0 | cli_opts.remotehost, |
241 | 0 | algoname, |
242 | 0 | fp); |
243 | 0 | m_free(fp); |
244 | 0 | if (cli_opts.batch_mode) { |
245 | 0 | dropbear_exit("Didn't validate host key"); |
246 | 0 | } |
247 | | |
248 | 0 | fprintf(stderr, "Do you want to continue connecting? (y/n) "); |
249 | 0 | tty = fopen(_PATH_TTY, "r"); |
250 | 0 | if (tty) { |
251 | 0 | response = getc(tty); |
252 | 0 | fclose(tty); |
253 | 0 | } else { |
254 | 0 | response = getc(stdin); |
255 | | /* flush stdin buffer */ |
256 | 0 | while ((getchar()) != '\n'); |
257 | 0 | } |
258 | |
|
259 | 0 | if (response == 'y') { |
260 | 0 | return; |
261 | 0 | } |
262 | | |
263 | 0 | dropbear_exit("Didn't validate host key"); |
264 | 0 | } |
265 | | |
266 | | static FILE* open_known_hosts_file(int * readonly) |
267 | 0 | { |
268 | 0 | FILE * hostsfile = NULL; |
269 | 0 | char * filename = NULL; |
270 | 0 | char * homedir = NULL; |
271 | | |
272 | 0 | homedir = getenv("HOME"); |
273 | |
|
274 | 0 | if (!homedir) { |
275 | 0 | struct passwd * pw = NULL; |
276 | 0 | pw = getpwuid(getuid()); |
277 | 0 | if (pw) { |
278 | 0 | homedir = pw->pw_dir; |
279 | 0 | } |
280 | 0 | } |
281 | |
|
282 | 0 | if (homedir) { |
283 | 0 | unsigned int len; |
284 | 0 | len = strlen(homedir); |
285 | 0 | filename = m_malloc(len + 18); /* "/.ssh/known_hosts" and null-terminator*/ |
286 | |
|
287 | 0 | snprintf(filename, len+18, "%s/.ssh", homedir); |
288 | | /* Check that ~/.ssh exists - easiest way is just to mkdir */ |
289 | 0 | if (mkdir(filename, S_IRWXU) != 0) { |
290 | 0 | if (errno != EEXIST) { |
291 | 0 | dropbear_log(LOG_INFO, "Warning: failed creating %s/.ssh: %s", |
292 | 0 | homedir, strerror(errno)); |
293 | 0 | TRACE(("mkdir didn't work: %s", strerror(errno))) |
294 | 0 | goto out; |
295 | 0 | } |
296 | 0 | } |
297 | | |
298 | 0 | snprintf(filename, len+18, "%s/.ssh/known_hosts", homedir); |
299 | 0 | hostsfile = fopen(filename, "a+"); |
300 | | |
301 | 0 | if (hostsfile != NULL) { |
302 | 0 | *readonly = 0; |
303 | 0 | fseek(hostsfile, 0, SEEK_SET); |
304 | 0 | } else { |
305 | | /* We mightn't have been able to open it if it was read-only */ |
306 | 0 | if (errno == EACCES || errno == EROFS) { |
307 | 0 | TRACE(("trying readonly: %s", strerror(errno))) |
308 | 0 | *readonly = 1; |
309 | 0 | hostsfile = fopen(filename, "r"); |
310 | 0 | } |
311 | 0 | } |
312 | 0 | } |
313 | | |
314 | 0 | if (hostsfile == NULL) { |
315 | 0 | TRACE(("hostsfile didn't open: %s", strerror(errno))) |
316 | 0 | dropbear_log(LOG_WARNING, "Failed to open %s/.ssh/known_hosts", |
317 | 0 | homedir); |
318 | 0 | goto out; |
319 | 0 | } |
320 | | |
321 | 0 | out: |
322 | 0 | m_free(filename); |
323 | 0 | return hostsfile; |
324 | 0 | } |
325 | | |
326 | 0 | static void checkhostkey(const unsigned char* keyblob, unsigned int keybloblen) { |
327 | |
|
328 | 0 | FILE *hostsfile = NULL; |
329 | 0 | int readonly = 0; |
330 | 0 | unsigned int hostlen, algolen; |
331 | 0 | unsigned long len; |
332 | 0 | const char *algoname = NULL; |
333 | 0 | char * fingerprint = NULL; |
334 | 0 | buffer * line = NULL; |
335 | 0 | int ret; |
336 | |
|
337 | 0 | if (cli_opts.no_hostkey_check) { |
338 | 0 | dropbear_log(LOG_INFO, "Caution, skipping hostkey check for %s\n", cli_opts.remotehost); |
339 | 0 | return; |
340 | 0 | } |
341 | | |
342 | 0 | algoname = signkey_name_from_type(ses.newkeys->algo_hostkey, &algolen); |
343 | |
|
344 | 0 | hostsfile = open_known_hosts_file(&readonly); |
345 | 0 | if (!hostsfile) { |
346 | 0 | ask_to_confirm(keyblob, keybloblen, algoname); |
347 | | /* ask_to_confirm will exit upon failure */ |
348 | 0 | return; |
349 | 0 | } |
350 | | |
351 | 0 | line = buf_new(MAX_KNOWNHOSTS_LINE); |
352 | 0 | hostlen = strlen(cli_opts.remotehost); |
353 | |
|
354 | 0 | do { |
355 | 0 | if (buf_getline(line, hostsfile) == DROPBEAR_FAILURE) { |
356 | 0 | TRACE(("failed reading line: prob EOF")) |
357 | 0 | break; |
358 | 0 | } |
359 | | |
360 | | /* The line is too short to be sensible */ |
361 | | /* "30" is 'enough to hold ssh-dss plus the spaces, ie so we don't |
362 | | * buf_getfoo() past the end and die horribly - the base64 parsing |
363 | | * code is what tiptoes up to the end nicely */ |
364 | 0 | if (line->len < (hostlen+30) ) { |
365 | 0 | TRACE(("line is too short to be sensible")) |
366 | 0 | continue; |
367 | 0 | } |
368 | | |
369 | | /* Compare hostnames */ |
370 | 0 | if (strncmp(cli_opts.remotehost, (const char *) buf_getptr(line, hostlen), |
371 | 0 | hostlen) != 0) { |
372 | 0 | continue; |
373 | 0 | } |
374 | | |
375 | 0 | buf_incrpos(line, hostlen); |
376 | 0 | if (buf_getbyte(line) != ' ') { |
377 | | /* there wasn't a space after the hostname, something dodgy */ |
378 | 0 | TRACE(("missing space afte matching hostname")) |
379 | 0 | continue; |
380 | 0 | } |
381 | | |
382 | 0 | if (strncmp((const char *) buf_getptr(line, algolen), algoname, algolen) != 0) { |
383 | 0 | TRACE(("algo doesn't match")) |
384 | 0 | continue; |
385 | 0 | } |
386 | | |
387 | 0 | buf_incrpos(line, algolen); |
388 | 0 | if (buf_getbyte(line) != ' ') { |
389 | 0 | TRACE(("missing space after algo")) |
390 | 0 | continue; |
391 | 0 | } |
392 | | |
393 | | /* Now we're at the interesting hostkey */ |
394 | 0 | ret = cmp_base64_key(keyblob, keybloblen, (const unsigned char *) algoname, algolen, |
395 | 0 | line, &fingerprint); |
396 | |
|
397 | 0 | if (ret == DROPBEAR_SUCCESS) { |
398 | | /* Good matching key */ |
399 | 0 | DEBUG1(("server match %s", fingerprint)) |
400 | 0 | goto out; |
401 | 0 | } |
402 | | |
403 | | /* The keys didn't match. eep. Note that we're "leaking" |
404 | | the fingerprint strings here, but we're exiting anyway */ |
405 | 0 | dropbear_exit("\n\n%s host key mismatch for %s !\n" |
406 | 0 | "Fingerprint is %s\n" |
407 | 0 | "Expected %s\n" |
408 | 0 | "If you know that the host key is correct you can\nremove the bad entry from ~/.ssh/known_hosts", |
409 | 0 | algoname, |
410 | 0 | cli_opts.remotehost, |
411 | 0 | sign_key_fingerprint(keyblob, keybloblen), |
412 | 0 | fingerprint ? fingerprint : "UNKNOWN"); |
413 | 0 | } while (1); /* keep going 'til something happens */ |
414 | | |
415 | | /* Key doesn't exist yet */ |
416 | 0 | ask_to_confirm(keyblob, keybloblen, algoname); |
417 | | |
418 | | /* If we get here, they said yes */ |
419 | |
|
420 | 0 | if (readonly) { |
421 | 0 | TRACE(("readonly")) |
422 | 0 | goto out; |
423 | 0 | } |
424 | | |
425 | 0 | if (!cli_opts.no_hostkey_check) { |
426 | | /* put the new entry in the file */ |
427 | 0 | fseek(hostsfile, 0, SEEK_END); /* In case it wasn't opened append */ |
428 | 0 | buf_setpos(line, 0); |
429 | 0 | buf_setlen(line, 0); |
430 | 0 | buf_putbytes(line, (const unsigned char *) cli_opts.remotehost, hostlen); |
431 | 0 | buf_putbyte(line, ' '); |
432 | 0 | buf_putbytes(line, (const unsigned char *) algoname, algolen); |
433 | 0 | buf_putbyte(line, ' '); |
434 | 0 | len = line->size - line->pos; |
435 | | /* The only failure with base64 is buffer_overflow, but buf_getwriteptr |
436 | | * will die horribly in the case anyway */ |
437 | 0 | base64_encode(keyblob, keybloblen, buf_getwriteptr(line, len), &len); |
438 | 0 | buf_incrwritepos(line, len); |
439 | 0 | buf_putbyte(line, '\n'); |
440 | 0 | buf_setpos(line, 0); |
441 | 0 | fwrite(buf_getptr(line, line->len), line->len, 1, hostsfile); |
442 | | /* We ignore errors, since there's not much we can do about them */ |
443 | 0 | } |
444 | |
|
445 | 0 | out: |
446 | 0 | if (hostsfile != NULL) { |
447 | 0 | fclose(hostsfile); |
448 | 0 | } |
449 | 0 | if (line != NULL) { |
450 | 0 | buf_free(line); |
451 | 0 | } |
452 | 0 | m_free(fingerprint); |
453 | 0 | } |
454 | | |
455 | 0 | void recv_msg_ext_info(void) { |
456 | | /* This message is not client-specific in the protocol but Dropbear only handles |
457 | | a server-sent message at present. */ |
458 | 0 | unsigned int num_ext; |
459 | 0 | unsigned int i; |
460 | |
|
461 | 0 | TRACE(("enter recv_msg_ext_info")) |
462 | | |
463 | | /* Must be after the first SSH_MSG_NEWKEYS */ |
464 | 0 | TRACE(("last %d, donefirst %d, donescond %d", ses.lastpacket, ses.kexstate.donefirstkex, ses.kexstate.donesecondkex)) |
465 | 0 | if (!(ses.lastpacket == SSH_MSG_NEWKEYS && !ses.kexstate.donesecondkex)) { |
466 | 0 | TRACE(("leave recv_msg_ext_info: ignoring packet received at the wrong time")) |
467 | 0 | return; |
468 | 0 | } |
469 | | |
470 | 0 | num_ext = buf_getint(ses.payload); |
471 | 0 | TRACE(("received SSH_MSG_EXT_INFO with %d items", num_ext)) |
472 | |
|
473 | 0 | for (i = 0; i < num_ext; i++) { |
474 | 0 | unsigned int name_len; |
475 | 0 | char *ext_name = buf_getstring(ses.payload, &name_len); |
476 | 0 | TRACE(("extension %d name '%s'", i, ext_name)) |
477 | 0 | if (cli_ses.server_sig_algs == NULL |
478 | 0 | && name_len == strlen(SSH_SERVER_SIG_ALGS) |
479 | 0 | && strcmp(ext_name, SSH_SERVER_SIG_ALGS) == 0) { |
480 | 0 | cli_ses.server_sig_algs = buf_getbuf(ses.payload); |
481 | 0 | } else { |
482 | | /* valid extension values could be >MAX_STRING_LEN */ |
483 | 0 | buf_eatstring(ses.payload); |
484 | 0 | } |
485 | 0 | m_free(ext_name); |
486 | 0 | } |
487 | 0 | TRACE(("leave recv_msg_ext_info")) |
488 | 0 | } |