/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 | 10.6k | void send_msg_kexdh_init() { |
46 | 10.6k | TRACE(("send_msg_kexdh_init()")) |
47 | | |
48 | 10.6k | CHECKCLEARTOWRITE(); |
49 | | |
50 | 10.6k | #if DROPBEAR_FUZZ |
51 | 10.6k | if (fuzz.fuzzing && fuzz.skip_kexmaths) { |
52 | 4.76k | return; |
53 | 4.76k | } |
54 | 5.88k | #endif |
55 | | |
56 | 5.88k | buf_putbyte(ses.writepayload, SSH_MSG_KEXDH_INIT); |
57 | 5.88k | switch (ses.newkeys->algo_kex->mode) { |
58 | 0 | #if DROPBEAR_NORMAL_DH |
59 | 1.27k | case DROPBEAR_KEX_NORMAL_DH: |
60 | 1.27k | if (ses.newkeys->algo_kex != cli_ses.param_kex_algo |
61 | 1.27k | || !cli_ses.dh_param) { |
62 | 1.27k | if (cli_ses.dh_param) { |
63 | 0 | free_kexdh_param(cli_ses.dh_param); |
64 | 0 | } |
65 | 1.27k | cli_ses.dh_param = gen_kexdh_param(); |
66 | 1.27k | } |
67 | 1.27k | buf_putmpint(ses.writepayload, &cli_ses.dh_param->pub); |
68 | 1.27k | break; |
69 | 0 | #endif |
70 | 0 | #if DROPBEAR_ECDH |
71 | 596 | case DROPBEAR_KEX_ECDH: |
72 | 596 | if (ses.newkeys->algo_kex != cli_ses.param_kex_algo |
73 | 596 | || !cli_ses.ecdh_param) { |
74 | 596 | if (cli_ses.ecdh_param) { |
75 | 0 | free_kexecdh_param(cli_ses.ecdh_param); |
76 | 0 | } |
77 | 596 | cli_ses.ecdh_param = gen_kexecdh_param(); |
78 | 596 | } |
79 | 596 | buf_put_ecc_raw_pubkey_string(ses.writepayload, &cli_ses.ecdh_param->key); |
80 | 596 | break; |
81 | 0 | #endif |
82 | 0 | #if DROPBEAR_CURVE25519 |
83 | 4.01k | case DROPBEAR_KEX_CURVE25519: |
84 | 4.01k | if (ses.newkeys->algo_kex != cli_ses.param_kex_algo |
85 | 4.01k | || !cli_ses.curve25519_param) { |
86 | 3.51k | if (cli_ses.curve25519_param) { |
87 | 0 | free_kexcurve25519_param(cli_ses.curve25519_param); |
88 | 0 | } |
89 | 3.51k | cli_ses.curve25519_param = gen_kexcurve25519_param(); |
90 | 3.51k | } |
91 | 4.01k | buf_putstring(ses.writepayload, cli_ses.curve25519_param->pub, CURVE25519_LEN); |
92 | 4.01k | break; |
93 | 5.88k | #endif |
94 | 5.88k | } |
95 | | |
96 | 5.88k | cli_ses.param_kex_algo = ses.newkeys->algo_kex; |
97 | 5.88k | encrypt_packet(); |
98 | 5.88k | } |
99 | | |
100 | | /* Handle a diffie-hellman key exchange reply. */ |
101 | 4.26k | void recv_msg_kexdh_reply() { |
102 | | |
103 | 4.26k | sign_key *hostkey = NULL; |
104 | 4.26k | unsigned int keytype, keybloblen; |
105 | 4.26k | unsigned char* keyblob = NULL; |
106 | | |
107 | 4.26k | TRACE(("enter recv_msg_kexdh_reply")) |
108 | | |
109 | 4.26k | #if DROPBEAR_FUZZ |
110 | 4.26k | if (fuzz.fuzzing && fuzz.skip_kexmaths) { |
111 | 2.15k | return; |
112 | 2.15k | } |
113 | 2.10k | #endif |
114 | | |
115 | 2.10k | if (cli_ses.kex_state != KEXDH_INIT_SENT) { |
116 | 0 | dropbear_exit("Received out-of-order kexdhreply"); |
117 | 0 | } |
118 | 2.10k | keytype = ses.newkeys->algo_hostkey; |
119 | 2.10k | TRACE(("keytype is %d", keytype)) |
120 | | |
121 | 2.10k | hostkey = new_sign_key(); |
122 | 2.10k | keybloblen = buf_getint(ses.payload); |
123 | | |
124 | 2.10k | keyblob = buf_getptr(ses.payload, keybloblen); |
125 | 2.10k | if (!ses.kexstate.donefirstkex) { |
126 | | /* Only makes sense the first time */ |
127 | 0 | checkhostkey(keyblob, keybloblen); |
128 | 0 | } |
129 | | |
130 | 2.10k | if (buf_get_pub_key(ses.payload, hostkey, &keytype) != DROPBEAR_SUCCESS) { |
131 | 376 | TRACE(("failed getting pubkey")) |
132 | 376 | dropbear_exit("Bad KEX packet"); |
133 | 376 | } |
134 | | |
135 | 1.73k | switch (ses.newkeys->algo_kex->mode) { |
136 | 0 | #if DROPBEAR_NORMAL_DH |
137 | 1.06k | case DROPBEAR_KEX_NORMAL_DH: |
138 | 1.06k | { |
139 | 1.06k | DEF_MP_INT(dh_f); |
140 | 1.06k | m_mp_init(&dh_f); |
141 | 1.06k | if (buf_getmpint(ses.payload, &dh_f) != DROPBEAR_SUCCESS) { |
142 | 10 | TRACE(("failed getting mpint")) |
143 | 10 | dropbear_exit("Bad KEX packet"); |
144 | 10 | } |
145 | | |
146 | 1.05k | kexdh_comb_key(cli_ses.dh_param, &dh_f, hostkey); |
147 | 1.05k | mp_clear(&dh_f); |
148 | 1.05k | } |
149 | 0 | break; |
150 | 0 | #endif |
151 | 0 | #if DROPBEAR_ECDH |
152 | 170 | case DROPBEAR_KEX_ECDH: |
153 | 170 | { |
154 | 170 | buffer *ecdh_qs = buf_getstringbuf(ses.payload); |
155 | 170 | kexecdh_comb_key(cli_ses.ecdh_param, ecdh_qs, hostkey); |
156 | 170 | buf_free(ecdh_qs); |
157 | 170 | } |
158 | 170 | break; |
159 | 0 | #endif |
160 | 0 | #if DROPBEAR_CURVE25519 |
161 | 366 | case DROPBEAR_KEX_CURVE25519: |
162 | 366 | { |
163 | 366 | buffer *ecdh_qs = buf_getstringbuf(ses.payload); |
164 | 366 | kexcurve25519_comb_key(cli_ses.curve25519_param, ecdh_qs, hostkey); |
165 | 366 | buf_free(ecdh_qs); |
166 | 366 | } |
167 | 366 | break; |
168 | 1.73k | #endif |
169 | 1.73k | } |
170 | | |
171 | 1.43k | #if DROPBEAR_NORMAL_DH |
172 | 1.43k | if (cli_ses.dh_param) { |
173 | 1.04k | free_kexdh_param(cli_ses.dh_param); |
174 | 1.04k | cli_ses.dh_param = NULL; |
175 | 1.04k | } |
176 | 1.43k | #endif |
177 | 1.43k | #if DROPBEAR_ECDH |
178 | 1.43k | if (cli_ses.ecdh_param) { |
179 | 104 | free_kexecdh_param(cli_ses.ecdh_param); |
180 | 104 | cli_ses.ecdh_param = NULL; |
181 | 104 | } |
182 | 1.43k | #endif |
183 | 1.43k | #if DROPBEAR_CURVE25519 |
184 | 1.43k | if (cli_ses.curve25519_param) { |
185 | 1.43k | free_kexcurve25519_param(cli_ses.curve25519_param); |
186 | 1.43k | cli_ses.curve25519_param = NULL; |
187 | 1.43k | } |
188 | 1.43k | #endif |
189 | | |
190 | 1.43k | cli_ses.param_kex_algo = NULL; |
191 | 1.43k | if (buf_verify(ses.payload, hostkey, ses.newkeys->algo_signature, |
192 | 1.43k | ses.hash) != DROPBEAR_SUCCESS) { |
193 | 951 | dropbear_exit("Bad hostkey signature"); |
194 | 951 | } |
195 | | |
196 | 484 | sign_key_free(hostkey); |
197 | 484 | hostkey = NULL; |
198 | | |
199 | 484 | send_msg_newkeys(); |
200 | 484 | ses.requirenext = SSH_MSG_NEWKEYS; |
201 | 484 | TRACE(("leave recv_msg_kexdh_init")) |
202 | 484 | } |
203 | | |
204 | | static void ask_to_confirm(const unsigned char* keyblob, unsigned int keybloblen, |
205 | 0 | const char* algoname) { |
206 | |
|
207 | 0 | char* fp = NULL; |
208 | 0 | FILE *tty = NULL; |
209 | 0 | int response = 'z'; |
210 | |
|
211 | 0 | fp = sign_key_fingerprint(keyblob, keybloblen); |
212 | 0 | if (cli_opts.always_accept_key) { |
213 | 0 | dropbear_log(LOG_INFO, "\nHost '%s' key accepted unconditionally.\n(%s fingerprint %s)\n", |
214 | 0 | cli_opts.remotehost, |
215 | 0 | algoname, |
216 | 0 | fp); |
217 | 0 | m_free(fp); |
218 | 0 | return; |
219 | 0 | } |
220 | 0 | fprintf(stderr, "\nHost '%s' is not in the trusted hosts file.\n(%s fingerprint %s)\nDo you want to continue connecting? (y/n) ", |
221 | 0 | cli_opts.remotehost, |
222 | 0 | algoname, |
223 | 0 | fp); |
224 | 0 | m_free(fp); |
225 | |
|
226 | 0 | tty = fopen(_PATH_TTY, "r"); |
227 | 0 | if (tty) { |
228 | 0 | response = getc(tty); |
229 | 0 | fclose(tty); |
230 | 0 | } else { |
231 | 0 | response = getc(stdin); |
232 | | /* flush stdin buffer */ |
233 | 0 | while ((getchar()) != '\n'); |
234 | 0 | } |
235 | |
|
236 | 0 | if (response == 'y') { |
237 | 0 | return; |
238 | 0 | } |
239 | | |
240 | 0 | dropbear_exit("Didn't validate host key"); |
241 | 0 | } |
242 | | |
243 | | static FILE* open_known_hosts_file(int * readonly) |
244 | 0 | { |
245 | 0 | FILE * hostsfile = NULL; |
246 | 0 | char * filename = NULL; |
247 | 0 | char * homedir = NULL; |
248 | | |
249 | 0 | homedir = getenv("HOME"); |
250 | |
|
251 | 0 | if (!homedir) { |
252 | 0 | struct passwd * pw = NULL; |
253 | 0 | pw = getpwuid(getuid()); |
254 | 0 | if (pw) { |
255 | 0 | homedir = pw->pw_dir; |
256 | 0 | } |
257 | 0 | } |
258 | |
|
259 | 0 | if (homedir) { |
260 | 0 | unsigned int len; |
261 | 0 | len = strlen(homedir); |
262 | 0 | filename = m_malloc(len + 18); /* "/.ssh/known_hosts" and null-terminator*/ |
263 | |
|
264 | 0 | snprintf(filename, len+18, "%s/.ssh", homedir); |
265 | | /* Check that ~/.ssh exists - easiest way is just to mkdir */ |
266 | 0 | if (mkdir(filename, S_IRWXU) != 0) { |
267 | 0 | if (errno != EEXIST) { |
268 | 0 | dropbear_log(LOG_INFO, "Warning: failed creating %s/.ssh: %s", |
269 | 0 | homedir, strerror(errno)); |
270 | 0 | TRACE(("mkdir didn't work: %s", strerror(errno))) |
271 | 0 | goto out; |
272 | 0 | } |
273 | 0 | } |
274 | | |
275 | 0 | snprintf(filename, len+18, "%s/.ssh/known_hosts", homedir); |
276 | 0 | hostsfile = fopen(filename, "a+"); |
277 | | |
278 | 0 | if (hostsfile != NULL) { |
279 | 0 | *readonly = 0; |
280 | 0 | fseek(hostsfile, 0, SEEK_SET); |
281 | 0 | } else { |
282 | | /* We mightn't have been able to open it if it was read-only */ |
283 | 0 | if (errno == EACCES || errno == EROFS) { |
284 | 0 | TRACE(("trying readonly: %s", strerror(errno))) |
285 | 0 | *readonly = 1; |
286 | 0 | hostsfile = fopen(filename, "r"); |
287 | 0 | } |
288 | 0 | } |
289 | 0 | } |
290 | | |
291 | 0 | if (hostsfile == NULL) { |
292 | 0 | TRACE(("hostsfile didn't open: %s", strerror(errno))) |
293 | 0 | dropbear_log(LOG_WARNING, "Failed to open %s/.ssh/known_hosts", |
294 | 0 | homedir); |
295 | 0 | goto out; |
296 | 0 | } |
297 | | |
298 | 0 | out: |
299 | 0 | m_free(filename); |
300 | 0 | return hostsfile; |
301 | 0 | } |
302 | | |
303 | 0 | static void checkhostkey(const unsigned char* keyblob, unsigned int keybloblen) { |
304 | |
|
305 | 0 | FILE *hostsfile = NULL; |
306 | 0 | int readonly = 0; |
307 | 0 | unsigned int hostlen, algolen; |
308 | 0 | unsigned long len; |
309 | 0 | const char *algoname = NULL; |
310 | 0 | char * fingerprint = NULL; |
311 | 0 | buffer * line = NULL; |
312 | 0 | int ret; |
313 | |
|
314 | 0 | if (cli_opts.no_hostkey_check) { |
315 | 0 | dropbear_log(LOG_INFO, "Caution, skipping hostkey check for %s\n", cli_opts.remotehost); |
316 | 0 | return; |
317 | 0 | } |
318 | | |
319 | 0 | algoname = signkey_name_from_type(ses.newkeys->algo_hostkey, &algolen); |
320 | |
|
321 | 0 | hostsfile = open_known_hosts_file(&readonly); |
322 | 0 | if (!hostsfile) { |
323 | 0 | ask_to_confirm(keyblob, keybloblen, algoname); |
324 | | /* ask_to_confirm will exit upon failure */ |
325 | 0 | return; |
326 | 0 | } |
327 | | |
328 | 0 | line = buf_new(MAX_KNOWNHOSTS_LINE); |
329 | 0 | hostlen = strlen(cli_opts.remotehost); |
330 | |
|
331 | 0 | do { |
332 | 0 | if (buf_getline(line, hostsfile) == DROPBEAR_FAILURE) { |
333 | 0 | TRACE(("failed reading line: prob EOF")) |
334 | 0 | break; |
335 | 0 | } |
336 | | |
337 | | /* The line is too short to be sensible */ |
338 | | /* "30" is 'enough to hold ssh-dss plus the spaces, ie so we don't |
339 | | * buf_getfoo() past the end and die horribly - the base64 parsing |
340 | | * code is what tiptoes up to the end nicely */ |
341 | 0 | if (line->len < (hostlen+30) ) { |
342 | 0 | TRACE(("line is too short to be sensible")) |
343 | 0 | continue; |
344 | 0 | } |
345 | | |
346 | | /* Compare hostnames */ |
347 | 0 | if (strncmp(cli_opts.remotehost, (const char *) buf_getptr(line, hostlen), |
348 | 0 | hostlen) != 0) { |
349 | 0 | continue; |
350 | 0 | } |
351 | | |
352 | 0 | buf_incrpos(line, hostlen); |
353 | 0 | if (buf_getbyte(line) != ' ') { |
354 | | /* there wasn't a space after the hostname, something dodgy */ |
355 | 0 | TRACE(("missing space afte matching hostname")) |
356 | 0 | continue; |
357 | 0 | } |
358 | | |
359 | 0 | if (strncmp((const char *) buf_getptr(line, algolen), algoname, algolen) != 0) { |
360 | 0 | TRACE(("algo doesn't match")) |
361 | 0 | continue; |
362 | 0 | } |
363 | | |
364 | 0 | buf_incrpos(line, algolen); |
365 | 0 | if (buf_getbyte(line) != ' ') { |
366 | 0 | TRACE(("missing space after algo")) |
367 | 0 | continue; |
368 | 0 | } |
369 | | |
370 | | /* Now we're at the interesting hostkey */ |
371 | 0 | ret = cmp_base64_key(keyblob, keybloblen, (const unsigned char *) algoname, algolen, |
372 | 0 | line, &fingerprint); |
373 | |
|
374 | 0 | if (ret == DROPBEAR_SUCCESS) { |
375 | | /* Good matching key */ |
376 | 0 | DEBUG1(("server match %s", fingerprint)) |
377 | 0 | goto out; |
378 | 0 | } |
379 | | |
380 | | /* The keys didn't match. eep. Note that we're "leaking" |
381 | | the fingerprint strings here, but we're exiting anyway */ |
382 | 0 | dropbear_exit("\n\n%s host key mismatch for %s !\n" |
383 | 0 | "Fingerprint is %s\n" |
384 | 0 | "Expected %s\n" |
385 | 0 | "If you know that the host key is correct you can\nremove the bad entry from ~/.ssh/known_hosts", |
386 | 0 | algoname, |
387 | 0 | cli_opts.remotehost, |
388 | 0 | sign_key_fingerprint(keyblob, keybloblen), |
389 | 0 | fingerprint ? fingerprint : "UNKNOWN"); |
390 | 0 | } while (1); /* keep going 'til something happens */ |
391 | | |
392 | | /* Key doesn't exist yet */ |
393 | 0 | ask_to_confirm(keyblob, keybloblen, algoname); |
394 | | |
395 | | /* If we get here, they said yes */ |
396 | |
|
397 | 0 | if (readonly) { |
398 | 0 | TRACE(("readonly")) |
399 | 0 | goto out; |
400 | 0 | } |
401 | | |
402 | 0 | if (!cli_opts.always_accept_key) { |
403 | | /* put the new entry in the file */ |
404 | 0 | fseek(hostsfile, 0, SEEK_END); /* In case it wasn't opened append */ |
405 | 0 | buf_setpos(line, 0); |
406 | 0 | buf_setlen(line, 0); |
407 | 0 | buf_putbytes(line, (const unsigned char *) cli_opts.remotehost, hostlen); |
408 | 0 | buf_putbyte(line, ' '); |
409 | 0 | buf_putbytes(line, (const unsigned char *) algoname, algolen); |
410 | 0 | buf_putbyte(line, ' '); |
411 | 0 | len = line->size - line->pos; |
412 | | /* The only failure with base64 is buffer_overflow, but buf_getwriteptr |
413 | | * will die horribly in the case anyway */ |
414 | 0 | base64_encode(keyblob, keybloblen, buf_getwriteptr(line, len), &len); |
415 | 0 | buf_incrwritepos(line, len); |
416 | 0 | buf_putbyte(line, '\n'); |
417 | 0 | buf_setpos(line, 0); |
418 | 0 | fwrite(buf_getptr(line, line->len), line->len, 1, hostsfile); |
419 | | /* We ignore errors, since there's not much we can do about them */ |
420 | 0 | } |
421 | |
|
422 | 0 | out: |
423 | 0 | if (hostsfile != NULL) { |
424 | 0 | fclose(hostsfile); |
425 | 0 | } |
426 | 0 | if (line != NULL) { |
427 | 0 | buf_free(line); |
428 | 0 | } |
429 | 0 | m_free(fingerprint); |
430 | 0 | } |
431 | | |
432 | 1.45k | void recv_msg_ext_info(void) { |
433 | | /* This message is not client-specific in the protocol but Dropbear only handles |
434 | | a server-sent message at present. */ |
435 | 1.45k | unsigned int num_ext; |
436 | 1.45k | unsigned int i; |
437 | | |
438 | 1.45k | TRACE(("enter recv_msg_ext_info")) |
439 | | |
440 | | /* Must be after the first SSH_MSG_NEWKEYS */ |
441 | 1.45k | TRACE(("last %d, donefirst %d, donescond %d", ses.lastpacket, ses.kexstate.donefirstkex, ses.kexstate.donesecondkex)) |
442 | 1.45k | if (!(ses.lastpacket == SSH_MSG_NEWKEYS && !ses.kexstate.donesecondkex)) { |
443 | 482 | TRACE(("leave recv_msg_ext_info: ignoring packet received at the wrong time")) |
444 | 482 | return; |
445 | 482 | } |
446 | | |
447 | 975 | num_ext = buf_getint(ses.payload); |
448 | 975 | TRACE(("received SSH_MSG_EXT_INFO with %d items", num_ext)) |
449 | | |
450 | 5.62k | for (i = 0; i < num_ext; i++) { |
451 | 4.64k | unsigned int name_len; |
452 | 4.64k | char *ext_name = buf_getstring(ses.payload, &name_len); |
453 | 4.64k | TRACE(("extension %d name '%s'", i, ext_name)) |
454 | 4.64k | if (cli_ses.server_sig_algs == NULL |
455 | 4.64k | && name_len == strlen(SSH_SERVER_SIG_ALGS) |
456 | 4.64k | && strcmp(ext_name, SSH_SERVER_SIG_ALGS) == 0) { |
457 | 112 | cli_ses.server_sig_algs = buf_getbuf(ses.payload); |
458 | 4.53k | } else { |
459 | | /* valid extension values could be >MAX_STRING_LEN */ |
460 | 4.53k | buf_eatstring(ses.payload); |
461 | 4.53k | } |
462 | 4.64k | m_free(ext_name); |
463 | 4.64k | } |
464 | 975 | TRACE(("leave recv_msg_ext_info")) |
465 | 975 | } |