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