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