1 /* 2 * Copyright (c) 2006-2011 Christian Plattner. All rights reserved. 3 * Please refer to the LICENSE.txt for licensing details. 4 */ 5 package ch.ethz.ssh2.transport; 6 7 import java.io.IOException; 8 import java.security.SecureRandom; 9 10 import ch.ethz.ssh2.ConnectionInfo; 11 import ch.ethz.ssh2.DHGexParameters; 12 import ch.ethz.ssh2.ServerHostKeyVerifier; 13 import ch.ethz.ssh2.crypto.CryptoWishList; 14 import ch.ethz.ssh2.crypto.KeyMaterial; 15 import ch.ethz.ssh2.crypto.cipher.BlockCipher; 16 import ch.ethz.ssh2.crypto.cipher.BlockCipherFactory; 17 import ch.ethz.ssh2.crypto.dh.DhExchange; 18 import ch.ethz.ssh2.crypto.dh.DhGroupExchange; 19 import ch.ethz.ssh2.crypto.digest.MAC; 20 import ch.ethz.ssh2.log.Logger; 21 import ch.ethz.ssh2.packets.PacketKexDHInit; 22 import ch.ethz.ssh2.packets.PacketKexDHReply; 23 import ch.ethz.ssh2.packets.PacketKexDhGexGroup; 24 import ch.ethz.ssh2.packets.PacketKexDhGexInit; 25 import ch.ethz.ssh2.packets.PacketKexDhGexReply; 26 import ch.ethz.ssh2.packets.PacketKexDhGexRequest; 27 import ch.ethz.ssh2.packets.PacketKexDhGexRequestOld; 28 import ch.ethz.ssh2.packets.PacketKexInit; 29 import ch.ethz.ssh2.packets.PacketNewKeys; 30 import ch.ethz.ssh2.packets.Packets; 31 import ch.ethz.ssh2.signature.DSAPublicKey; 32 import ch.ethz.ssh2.signature.DSASHA1Verify; 33 import ch.ethz.ssh2.signature.DSASignature; 34 import ch.ethz.ssh2.signature.RSAPublicKey; 35 import ch.ethz.ssh2.signature.RSASHA1Verify; 36 import 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 (at) sudo.ch $ 43 */ 44 public 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