1/*
2 * Copyright (c) 2006-2011 Christian Plattner. All rights reserved.
3 * Please refer to the LICENSE.txt for licensing details.
4 */
5package ch.ethz.ssh2.transport;
6
7import java.io.IOException;
8import java.security.SecureRandom;
9
10import ch.ethz.ssh2.ConnectionInfo;
11import ch.ethz.ssh2.DHGexParameters;
12import ch.ethz.ssh2.ServerHostKeyVerifier;
13import ch.ethz.ssh2.crypto.CryptoWishList;
14import ch.ethz.ssh2.crypto.KeyMaterial;
15import ch.ethz.ssh2.crypto.cipher.BlockCipher;
16import ch.ethz.ssh2.crypto.cipher.BlockCipherFactory;
17import ch.ethz.ssh2.crypto.dh.DhExchange;
18import ch.ethz.ssh2.crypto.dh.DhGroupExchange;
19import ch.ethz.ssh2.crypto.digest.MAC;
20import ch.ethz.ssh2.log.Logger;
21import ch.ethz.ssh2.packets.PacketKexDHInit;
22import ch.ethz.ssh2.packets.PacketKexDHReply;
23import ch.ethz.ssh2.packets.PacketKexDhGexGroup;
24import ch.ethz.ssh2.packets.PacketKexDhGexInit;
25import ch.ethz.ssh2.packets.PacketKexDhGexReply;
26import ch.ethz.ssh2.packets.PacketKexDhGexRequest;
27import ch.ethz.ssh2.packets.PacketKexDhGexRequestOld;
28import ch.ethz.ssh2.packets.PacketKexInit;
29import ch.ethz.ssh2.packets.PacketNewKeys;
30import ch.ethz.ssh2.packets.Packets;
31import ch.ethz.ssh2.signature.DSAPublicKey;
32import ch.ethz.ssh2.signature.DSASHA1Verify;
33import ch.ethz.ssh2.signature.DSASignature;
34import ch.ethz.ssh2.signature.RSAPublicKey;
35import ch.ethz.ssh2.signature.RSASHA1Verify;
36import ch.ethz.ssh2.signature.RSASignature;
37
38/**
39 * KexManager.
40 *
41 * @author Christian Plattner
42 * @version $Id: KexManager.java 45 2011-07-01 15:09:41Z dkocher@sudo.ch $
43 */
44public class KexManager
45{
46	private static final Logger log = Logger.getLogger(KexManager.class);
47
48	KexState kxs;
49	int kexCount = 0;
50	KeyMaterial km;
51	byte[] sessionId;
52	ClientServerHello csh;
53
54	final Object accessLock = new Object();
55	ConnectionInfo lastConnInfo = null;
56
57	boolean connectionClosed = false;
58
59	boolean ignore_next_kex_packet = false;
60
61	final TransportManager tm;
62
63	CryptoWishList nextKEXcryptoWishList;
64	DHGexParameters nextKEXdhgexParameters;
65
66	ServerHostKeyVerifier verifier;
67	final String hostname;
68	final int port;
69	final SecureRandom rnd;
70
71	public KexManager(TransportManager tm, ClientServerHello csh, CryptoWishList initialCwl, String hostname, int port,
72			ServerHostKeyVerifier keyVerifier, SecureRandom rnd)
73	{
74		this.tm = tm;
75		this.csh = csh;
76		this.nextKEXcryptoWishList = initialCwl;
77		this.nextKEXdhgexParameters = new DHGexParameters();
78		this.hostname = hostname;
79		this.port = port;
80		this.verifier = keyVerifier;
81		this.rnd = rnd;
82	}
83
84	public ConnectionInfo getOrWaitForConnectionInfo(int minKexCount) throws IOException
85	{
86		boolean wasInterrupted = false;
87
88		try
89		{
90			synchronized (accessLock)
91			{
92				while (true)
93				{
94					if ((lastConnInfo != null) && (lastConnInfo.keyExchangeCounter >= minKexCount))
95						return lastConnInfo;
96
97					if (connectionClosed)
98						throw (IOException) new IOException("Key exchange was not finished, connection is closed.")
99								.initCause(tm.getReasonClosedCause());
100
101					try
102					{
103						accessLock.wait();
104					}
105					catch (InterruptedException e)
106					{
107						wasInterrupted = true;
108					}
109				}
110			}
111		}
112		finally
113		{
114			if (wasInterrupted)
115				Thread.currentThread().interrupt();
116		}
117	}
118
119	private String getFirstMatch(String[] client, String[] server) throws NegotiateException
120	{
121		if (client == null || server == null)
122			throw new IllegalArgumentException();
123
124		if (client.length == 0)
125			return null;
126
127		for (int i = 0; i < client.length; i++)
128		{
129			for (int j = 0; j < server.length; j++)
130			{
131				if (client[i].equals(server[j]))
132					return client[i];
133			}
134		}
135		throw new NegotiateException();
136	}
137
138	private boolean compareFirstOfNameList(String[] a, String[] b)
139	{
140		if (a == null || b == null)
141			throw new IllegalArgumentException();
142
143		if ((a.length == 0) && (b.length == 0))
144			return true;
145
146		if ((a.length == 0) || (b.length == 0))
147			return false;
148
149		return (a[0].equals(b[0]));
150	}
151
152	private boolean isGuessOK(KexParameters cpar, KexParameters spar)
153	{
154		if (cpar == null || spar == null)
155			throw new IllegalArgumentException();
156
157		if (compareFirstOfNameList(cpar.kex_algorithms, spar.kex_algorithms) == false)
158		{
159			return false;
160		}
161
162		if (compareFirstOfNameList(cpar.server_host_key_algorithms, spar.server_host_key_algorithms) == false)
163		{
164			return false;
165		}
166
167		/*
168		 * We do NOT check here if the other algorithms can be agreed on, this
169		 * is just a check if kex_algorithms and server_host_key_algorithms were
170		 * guessed right!
171		 */
172
173		return true;
174	}
175
176	private NegotiatedParameters mergeKexParameters(KexParameters client, KexParameters server)
177	{
178		NegotiatedParameters np = new NegotiatedParameters();
179
180		try
181		{
182			np.kex_algo = getFirstMatch(client.kex_algorithms, server.kex_algorithms);
183
184			log.info("kex_algo=" + np.kex_algo);
185
186			np.server_host_key_algo = getFirstMatch(client.server_host_key_algorithms,
187					server.server_host_key_algorithms);
188
189			log.info("server_host_key_algo=" + np.server_host_key_algo);
190
191			np.enc_algo_client_to_server = getFirstMatch(client.encryption_algorithms_client_to_server,
192					server.encryption_algorithms_client_to_server);
193			np.enc_algo_server_to_client = getFirstMatch(client.encryption_algorithms_server_to_client,
194					server.encryption_algorithms_server_to_client);
195
196			log.info("enc_algo_client_to_server=" + np.enc_algo_client_to_server);
197			log.info("enc_algo_server_to_client=" + np.enc_algo_server_to_client);
198
199			np.mac_algo_client_to_server = getFirstMatch(client.mac_algorithms_client_to_server,
200					server.mac_algorithms_client_to_server);
201			np.mac_algo_server_to_client = getFirstMatch(client.mac_algorithms_server_to_client,
202					server.mac_algorithms_server_to_client);
203
204			log.info("mac_algo_client_to_server=" + np.mac_algo_client_to_server);
205			log.info("mac_algo_server_to_client=" + np.mac_algo_server_to_client);
206
207			np.comp_algo_client_to_server = getFirstMatch(client.compression_algorithms_client_to_server,
208					server.compression_algorithms_client_to_server);
209			np.comp_algo_server_to_client = getFirstMatch(client.compression_algorithms_server_to_client,
210					server.compression_algorithms_server_to_client);
211
212			log.info("comp_algo_client_to_server=" + np.comp_algo_client_to_server);
213			log.info("comp_algo_server_to_client=" + np.comp_algo_server_to_client);
214
215		}
216		catch (NegotiateException e)
217		{
218			return null;
219		}
220
221		try
222		{
223			np.lang_client_to_server = getFirstMatch(client.languages_client_to_server,
224					server.languages_client_to_server);
225		}
226		catch (NegotiateException e1)
227		{
228			np.lang_client_to_server = null;
229		}
230
231		try
232		{
233			np.lang_server_to_client = getFirstMatch(client.languages_server_to_client,
234					server.languages_server_to_client);
235		}
236		catch (NegotiateException e2)
237		{
238			np.lang_server_to_client = null;
239		}
240
241		if (isGuessOK(client, server))
242			np.guessOK = true;
243
244		return np;
245	}
246
247	public synchronized void initiateKEX(CryptoWishList cwl, DHGexParameters dhgex) throws IOException
248	{
249		nextKEXcryptoWishList = cwl;
250		nextKEXdhgexParameters = dhgex;
251
252		if (kxs == null)
253		{
254			kxs = new KexState();
255
256			kxs.dhgexParameters = nextKEXdhgexParameters;
257			PacketKexInit kp = new PacketKexInit(nextKEXcryptoWishList, rnd);
258			kxs.localKEX = kp;
259			tm.sendKexMessage(kp.getPayload());
260		}
261	}
262
263	private boolean establishKeyMaterial()
264	{
265		try
266		{
267			int mac_cs_key_len = MAC.getKeyLen(kxs.np.mac_algo_client_to_server);
268			int enc_cs_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_client_to_server);
269			int enc_cs_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_client_to_server);
270
271			int mac_sc_key_len = MAC.getKeyLen(kxs.np.mac_algo_server_to_client);
272			int enc_sc_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_server_to_client);
273			int enc_sc_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_server_to_client);
274
275			km = KeyMaterial.create("SHA1", kxs.H, kxs.K, sessionId, enc_cs_key_len, enc_cs_block_len, mac_cs_key_len,
276					enc_sc_key_len, enc_sc_block_len, mac_sc_key_len);
277		}
278		catch (IllegalArgumentException e)
279		{
280			return false;
281		}
282		return true;
283	}
284
285	private void finishKex() throws IOException
286	{
287		if (sessionId == null)
288			sessionId = kxs.H;
289
290		establishKeyMaterial();
291
292		/* Tell the other side that we start using the new material */
293
294		PacketNewKeys ign = new PacketNewKeys();
295		tm.sendKexMessage(ign.getPayload());
296
297		BlockCipher cbc;
298		MAC mac;
299
300		try
301		{
302			cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_client_to_server, true, km.enc_key_client_to_server,
303					km.initial_iv_client_to_server);
304
305			mac = new MAC(kxs.np.mac_algo_client_to_server, km.integrity_key_client_to_server);
306
307		}
308		catch (IllegalArgumentException e1)
309		{
310			throw new IOException("Fatal error during MAC startup!");
311		}
312
313		tm.changeSendCipher(cbc, mac);
314		tm.kexFinished();
315	}
316
317	public static String[] getDefaultServerHostkeyAlgorithmList()
318	{
319		return new String[] { "ssh-rsa", "ssh-dss" };
320	}
321
322	public static void checkServerHostkeyAlgorithmsList(String[] algos)
323	{
324		for (int i = 0; i < algos.length; i++)
325		{
326			if (("ssh-rsa".equals(algos[i]) == false) && ("ssh-dss".equals(algos[i]) == false))
327				throw new IllegalArgumentException("Unknown server host key algorithm '" + algos[i] + "'");
328		}
329	}
330
331	public static String[] getDefaultKexAlgorithmList()
332	{
333		return new String[] { "diffie-hellman-group-exchange-sha1", "diffie-hellman-group14-sha1",
334				"diffie-hellman-group1-sha1" };
335	}
336
337	public static void checkKexAlgorithmList(String[] algos)
338	{
339		for (int i = 0; i < algos.length; i++)
340		{
341			if ("diffie-hellman-group-exchange-sha1".equals(algos[i]))
342				continue;
343
344			if ("diffie-hellman-group14-sha1".equals(algos[i]))
345				continue;
346
347			if ("diffie-hellman-group1-sha1".equals(algos[i]))
348				continue;
349
350			throw new IllegalArgumentException("Unknown kex algorithm '" + algos[i] + "'");
351		}
352	}
353
354	private boolean verifySignature(byte[] sig, byte[] hostkey) throws IOException
355	{
356		if (kxs.np.server_host_key_algo.equals("ssh-rsa"))
357		{
358			RSASignature rs = RSASHA1Verify.decodeSSHRSASignature(sig);
359			RSAPublicKey rpk = RSASHA1Verify.decodeSSHRSAPublicKey(hostkey);
360
361			log.debug("Verifying ssh-rsa signature");
362
363			return RSASHA1Verify.verifySignature(kxs.H, rs, rpk);
364		}
365
366		if (kxs.np.server_host_key_algo.equals("ssh-dss"))
367		{
368			DSASignature ds = DSASHA1Verify.decodeSSHDSASignature(sig);
369			DSAPublicKey dpk = DSASHA1Verify.decodeSSHDSAPublicKey(hostkey);
370
371			log.debug("Verifying ssh-dss signature");
372
373			return DSASHA1Verify.verifySignature(kxs.H, ds, dpk);
374		}
375
376		throw new IOException("Unknown server host key algorithm '" + kxs.np.server_host_key_algo + "'");
377	}
378
379	public synchronized void handleMessage(byte[] msg, int msglen) throws IOException
380	{
381		PacketKexInit kip;
382
383		if (msg == null)
384		{
385			synchronized (accessLock)
386			{
387				connectionClosed = true;
388				accessLock.notifyAll();
389				return;
390			}
391		}
392
393		if ((kxs == null) && (msg[0] != Packets.SSH_MSG_KEXINIT))
394			throw new IOException("Unexpected KEX message (type " + msg[0] + ")");
395
396		if (ignore_next_kex_packet)
397		{
398			ignore_next_kex_packet = false;
399			return;
400		}
401
402		if (msg[0] == Packets.SSH_MSG_KEXINIT)
403		{
404			if ((kxs != null) && (kxs.state != 0))
405				throw new IOException("Unexpected SSH_MSG_KEXINIT message during on-going kex exchange!");
406
407			if (kxs == null)
408			{
409				/*
410				 * Ah, OK, peer wants to do KEX. Let's be nice and play
411				 * together.
412				 */
413				kxs = new KexState();
414				kxs.dhgexParameters = nextKEXdhgexParameters;
415				kip = new PacketKexInit(nextKEXcryptoWishList, rnd);
416				kxs.localKEX = kip;
417				tm.sendKexMessage(kip.getPayload());
418			}
419
420			kip = new PacketKexInit(msg, 0, msglen);
421			kxs.remoteKEX = kip;
422
423			kxs.np = mergeKexParameters(kxs.localKEX.getKexParameters(), kxs.remoteKEX.getKexParameters());
424
425			if (kxs.np == null)
426				throw new IOException("Cannot negotiate, proposals do not match.");
427
428			if (kxs.remoteKEX.isFirst_kex_packet_follows() && (kxs.np.guessOK == false))
429			{
430				/*
431				 * Guess was wrong, we need to ignore the next kex packet.
432				 */
433
434				ignore_next_kex_packet = true;
435			}
436
437			if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1"))
438			{
439				if (kxs.dhgexParameters.getMin_group_len() == 0)
440				{
441					PacketKexDhGexRequestOld dhgexreq = new PacketKexDhGexRequestOld(kxs.dhgexParameters);
442					tm.sendKexMessage(dhgexreq.getPayload());
443
444				}
445				else
446				{
447					PacketKexDhGexRequest dhgexreq = new PacketKexDhGexRequest(kxs.dhgexParameters);
448					tm.sendKexMessage(dhgexreq.getPayload());
449				}
450				kxs.state = 1;
451				return;
452			}
453
454			if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1")
455					|| kxs.np.kex_algo.equals("diffie-hellman-group14-sha1"))
456			{
457				kxs.dhx = new DhExchange();
458
459				if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1"))
460					kxs.dhx.init(1, rnd);
461				else
462					kxs.dhx.init(14, rnd);
463
464				PacketKexDHInit kp = new PacketKexDHInit(kxs.dhx.getE());
465				tm.sendKexMessage(kp.getPayload());
466				kxs.state = 1;
467				return;
468			}
469
470			throw new IllegalStateException("Unkown KEX method!");
471		}
472
473		if (msg[0] == Packets.SSH_MSG_NEWKEYS)
474		{
475			if (km == null)
476				throw new IOException("Peer sent SSH_MSG_NEWKEYS, but I have no key material ready!");
477
478			BlockCipher cbc;
479			MAC mac;
480
481			try
482			{
483				cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_server_to_client, false,
484						km.enc_key_server_to_client, km.initial_iv_server_to_client);
485
486				mac = new MAC(kxs.np.mac_algo_server_to_client, km.integrity_key_server_to_client);
487
488			}
489			catch (IllegalArgumentException e1)
490			{
491				throw new IOException("Fatal error during MAC startup!");
492			}
493
494			tm.changeRecvCipher(cbc, mac);
495
496			ConnectionInfo sci = new ConnectionInfo();
497
498			kexCount++;
499
500			sci.keyExchangeAlgorithm = kxs.np.kex_algo;
501			sci.keyExchangeCounter = kexCount;
502			sci.clientToServerCryptoAlgorithm = kxs.np.enc_algo_client_to_server;
503			sci.serverToClientCryptoAlgorithm = kxs.np.enc_algo_server_to_client;
504			sci.clientToServerMACAlgorithm = kxs.np.mac_algo_client_to_server;
505			sci.serverToClientMACAlgorithm = kxs.np.mac_algo_server_to_client;
506			sci.serverHostKeyAlgorithm = kxs.np.server_host_key_algo;
507			sci.serverHostKey = kxs.hostkey;
508
509			synchronized (accessLock)
510			{
511				lastConnInfo = sci;
512				accessLock.notifyAll();
513			}
514
515			kxs = null;
516			return;
517		}
518
519		if ((kxs == null) || (kxs.state == 0))
520			throw new IOException("Unexpected Kex submessage!");
521
522		if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1"))
523		{
524			if (kxs.state == 1)
525			{
526				PacketKexDhGexGroup dhgexgrp = new PacketKexDhGexGroup(msg, 0, msglen);
527				kxs.dhgx = new DhGroupExchange(dhgexgrp.getP(), dhgexgrp.getG());
528				kxs.dhgx.init(rnd);
529				PacketKexDhGexInit dhgexinit = new PacketKexDhGexInit(kxs.dhgx.getE());
530				tm.sendKexMessage(dhgexinit.getPayload());
531				kxs.state = 2;
532				return;
533			}
534
535			if (kxs.state == 2)
536			{
537				PacketKexDhGexReply dhgexrpl = new PacketKexDhGexReply(msg, 0, msglen);
538
539				kxs.hostkey = dhgexrpl.getHostKey();
540
541				if (verifier != null)
542				{
543					boolean vres = false;
544
545					try
546					{
547						vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey);
548					}
549					catch (Exception e)
550					{
551						throw (IOException) new IOException(
552								"The server hostkey was not accepted by the verifier callback.").initCause(e);
553					}
554
555					if (vres == false)
556						throw new IOException("The server hostkey was not accepted by the verifier callback");
557				}
558
559				kxs.dhgx.setF(dhgexrpl.getF());
560
561				try
562				{
563					kxs.H = kxs.dhgx.calculateH(csh.getClientString(), csh.getServerString(),
564							kxs.localKEX.getPayload(), kxs.remoteKEX.getPayload(), dhgexrpl.getHostKey(),
565							kxs.dhgexParameters);
566				}
567				catch (IllegalArgumentException e)
568				{
569					throw (IOException) new IOException("KEX error.").initCause(e);
570				}
571
572				boolean res = verifySignature(dhgexrpl.getSignature(), kxs.hostkey);
573
574				if (res == false)
575					throw new IOException("Hostkey signature sent by remote is wrong!");
576
577				kxs.K = kxs.dhgx.getK();
578
579				finishKex();
580				kxs.state = -1;
581				return;
582			}
583
584			throw new IllegalStateException("Illegal State in KEX Exchange!");
585		}
586
587		if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1")
588				|| kxs.np.kex_algo.equals("diffie-hellman-group14-sha1"))
589		{
590			if (kxs.state == 1)
591			{
592
593				PacketKexDHReply dhr = new PacketKexDHReply(msg, 0, msglen);
594
595				kxs.hostkey = dhr.getHostKey();
596
597				if (verifier != null)
598				{
599					boolean vres = false;
600
601					try
602					{
603						vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey);
604					}
605					catch (Exception e)
606					{
607						throw (IOException) new IOException(
608								"The server hostkey was not accepted by the verifier callback.").initCause(e);
609					}
610
611					if (vres == false)
612						throw new IOException("The server hostkey was not accepted by the verifier callback");
613				}
614
615				kxs.dhx.setF(dhr.getF());
616
617				try
618				{
619					kxs.H = kxs.dhx.calculateH(csh.getClientString(), csh.getServerString(), kxs.localKEX.getPayload(),
620							kxs.remoteKEX.getPayload(), dhr.getHostKey());
621				}
622				catch (IllegalArgumentException e)
623				{
624					throw (IOException) new IOException("KEX error.").initCause(e);
625				}
626
627				boolean res = verifySignature(dhr.getSignature(), kxs.hostkey);
628
629				if (res == false)
630					throw new IOException("Hostkey signature sent by remote is wrong!");
631
632				kxs.K = kxs.dhx.getK();
633
634				finishKex();
635				kxs.state = -1;
636				return;
637			}
638		}
639
640		throw new IllegalStateException("Unkown KEX method! (" + kxs.np.kex_algo + ")");
641	}
642}
643