1 /* 2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package sun.security.krb5.internal.ssl; 27 28 import sun.security.ssl.ClientKeyExchange; 29 import sun.security.ssl.Debug; 30 import sun.security.ssl.ClientKeyExchangeService; 31 import sun.security.ssl.HandshakeOutStream; 32 33 import sun.security.jgss.GSSCaller; 34 import sun.security.jgss.krb5.Krb5Util; 35 import sun.security.jgss.krb5.ServiceCreds; 36 import sun.security.krb5.EncryptedData; 37 import sun.security.krb5.EncryptionKey; 38 import sun.security.krb5.KrbException; 39 import sun.security.krb5.PrincipalName; 40 import sun.security.krb5.internal.EncTicketPart; 41 import sun.security.krb5.internal.Ticket; 42 import sun.security.krb5.internal.crypto.KeyUsage; 43 import sun.security.ssl.ProtocolVersion; 44 45 import javax.crypto.SecretKey; 46 import javax.crypto.spec.SecretKeySpec; 47 import javax.security.auth.Subject; 48 import javax.security.auth.kerberos.KerberosKey; 49 import javax.security.auth.kerberos.KerberosPrincipal; 50 import javax.security.auth.kerberos.KerberosTicket; 51 import javax.security.auth.kerberos.KeyTab; 52 import javax.security.auth.kerberos.ServicePermission; 53 import java.io.IOException; 54 import java.io.PrintStream; 55 import java.net.InetAddress; 56 import java.security.AccessControlContext; 57 import java.security.AccessController; 58 import java.security.Principal; 59 import java.security.PrivilegedAction; 60 import java.security.PrivilegedActionException; 61 import java.security.PrivilegedExceptionAction; 62 import java.security.SecureRandom; 63 import java.util.Set; 64 65 /** 66 * The provider for TLS_KRB_ cipher suites. 67 * 68 * @since 9 69 */ 70 public class Krb5KeyExchangeService implements ClientKeyExchangeService { 71 72 public static final Debug debug = Debug.getInstance("ssl"); 73 74 @Override 75 public String[] supported() { 76 return new String[] { "KRB5", "KRB5_EXPORT" }; 77 } 78 79 @Override 80 public Object getServiceCreds(AccessControlContext acc) { 81 try { 82 ServiceCreds serviceCreds = AccessController.doPrivileged( 83 (PrivilegedExceptionAction<ServiceCreds>) 84 () -> Krb5Util.getServiceCreds( 85 GSSCaller.CALLER_SSL_SERVER, null, acc)); 86 if (serviceCreds == null) { 87 if (debug != null && Debug.isOn("handshake")) { 88 System.out.println("Kerberos serviceCreds not available"); 89 } 90 return null; 91 } 92 if (debug != null && Debug.isOn("handshake")) { 93 System.out.println("Using Kerberos creds"); 94 } 95 String serverPrincipal = serviceCreds.getName(); 96 if (serverPrincipal != null) { 97 // When service is bound, we check ASAP. Otherwise, 98 // will check after client request is received 99 // in in Kerberos ClientKeyExchange 100 SecurityManager sm = System.getSecurityManager(); 101 try { 102 if (sm != null) { 103 // Eliminate dependency on ServicePermission 104 sm.checkPermission(new ServicePermission( 105 serverPrincipal, "accept"), acc); 106 } 107 } catch (SecurityException se) { 108 if (debug != null && Debug.isOn("handshake")) { 109 System.out.println("Permission to access Kerberos" 110 + " secret key denied"); 111 } 112 return null; 113 } 114 } 115 return serviceCreds; 116 } catch (PrivilegedActionException e) { 117 // Likely exception here is LoginException 118 if (debug != null && Debug.isOn("handshake")) { 119 System.out.println("Attempt to obtain Kerberos key failed: " 120 + e.toString()); 121 } 122 return null; 123 } 124 } 125 126 @Override 127 public String getServiceHostName(Principal principal) { 128 if (principal == null) { 129 return null; 130 } 131 String hostName = null; 132 try { 133 PrincipalName princName = 134 new PrincipalName(principal.getName(), 135 PrincipalName.KRB_NT_SRV_HST); 136 String[] nameParts = princName.getNameStrings(); 137 if (nameParts.length >= 2) { 138 hostName = nameParts[1]; 139 } 140 } catch (Exception e) { 141 // ignore 142 } 143 return hostName; 144 } 145 146 147 @Override 148 public boolean isRelated(boolean isClient, 149 AccessControlContext acc, Principal p) { 150 151 if (p == null) return false; 152 try { 153 Subject subject = AccessController.doPrivileged( 154 (PrivilegedExceptionAction<Subject>) 155 () -> Krb5Util.getSubject( 156 isClient ? GSSCaller.CALLER_SSL_CLIENT 157 : GSSCaller.CALLER_SSL_SERVER, 158 acc)); 159 if (subject == null) { 160 if (debug != null && Debug.isOn("session")) { 161 System.out.println("Kerberos credentials are" + 162 " not present in the current Subject;" + 163 " check if " + 164 " javax.security.auth.useSubjectAsCreds" + 165 " system property has been set to false"); 166 } 167 return false; 168 } 169 Set<Principal> principals = 170 subject.getPrincipals(Principal.class); 171 if (principals.contains(p)) { 172 // bound to this principal 173 return true; 174 } else { 175 if (isClient) { 176 return false; 177 } else { 178 for (KeyTab pc : subject.getPrivateCredentials(KeyTab.class)) { 179 if (!pc.isBound()) { 180 return true; 181 } 182 } 183 return false; 184 } 185 } 186 } catch (PrivilegedActionException pae) { 187 if (debug != null && Debug.isOn("session")) { 188 System.out.println("Attempt to obtain" + 189 " subject failed! " + pae); 190 } 191 return false; 192 } 193 194 } 195 196 public ClientKeyExchange createClientExchange( 197 String serverName, AccessControlContext acc, 198 ProtocolVersion protocolVerson, SecureRandom rand) throws IOException { 199 return new ExchangerImpl(serverName, acc, protocolVerson, rand); 200 } 201 202 public ClientKeyExchange createServerExchange( 203 ProtocolVersion protocolVersion, ProtocolVersion clientVersion, 204 SecureRandom rand, byte[] encodedTicket, byte[] encrypted, 205 AccessControlContext acc, Object serviceCreds) throws IOException { 206 return new ExchangerImpl(protocolVersion, clientVersion, rand, 207 encodedTicket, encrypted, acc, serviceCreds); 208 } 209 210 static class ExchangerImpl extends ClientKeyExchange { 211 212 final private KerberosPreMasterSecret preMaster; 213 final private byte[] encodedTicket; 214 final private KerberosPrincipal peerPrincipal; 215 final private KerberosPrincipal localPrincipal; 216 217 @Override 218 public int messageLength() { 219 return encodedTicket.length + preMaster.getEncrypted().length + 6; 220 } 221 222 @Override 223 public void send(HandshakeOutStream s) throws IOException { 224 s.putBytes16(encodedTicket); 225 s.putBytes16(null); 226 s.putBytes16(preMaster.getEncrypted()); 227 } 228 229 @Override 230 public void print(PrintStream s) throws IOException { 231 s.println("*** ClientKeyExchange, Kerberos"); 232 233 if (debug != null && Debug.isOn("verbose")) { 234 Debug.println(s, "Kerberos service ticket", encodedTicket); 235 Debug.println(s, "Random Secret", preMaster.getUnencrypted()); 236 Debug.println(s, "Encrypted random Secret", preMaster.getEncrypted()); 237 } 238 } 239 240 ExchangerImpl(String serverName, AccessControlContext acc, 241 ProtocolVersion protocolVersion, SecureRandom rand) throws IOException { 242 243 // Get service ticket 244 KerberosTicket ticket = getServiceTicket(serverName, acc); 245 encodedTicket = ticket.getEncoded(); 246 247 // Record the Kerberos principals 248 peerPrincipal = ticket.getServer(); 249 localPrincipal = ticket.getClient(); 250 251 // Optional authenticator, encrypted using session key, 252 // currently ignored 253 254 // Generate premaster secret and encrypt it using session key 255 EncryptionKey sessionKey = new EncryptionKey( 256 ticket.getSessionKeyType(), 257 ticket.getSessionKey().getEncoded()); 258 259 preMaster = new KerberosPreMasterSecret(protocolVersion, 260 rand, sessionKey); 261 } 262 263 ExchangerImpl( 264 ProtocolVersion protocolVersion, ProtocolVersion clientVersion, SecureRandom rand, 265 byte[] encodedTicket, byte[] encrypted, 266 AccessControlContext acc, Object serviceCreds) throws IOException { 267 268 // Read ticket 269 this.encodedTicket = encodedTicket; 270 271 if (debug != null && Debug.isOn("verbose")) { 272 Debug.println(System.out, 273 "encoded Kerberos service ticket", encodedTicket); 274 } 275 276 EncryptionKey sessionKey = null; 277 KerberosPrincipal tmpPeer = null; 278 KerberosPrincipal tmpLocal = null; 279 280 try { 281 Ticket t = new Ticket(encodedTicket); 282 283 EncryptedData encPart = t.encPart; 284 PrincipalName ticketSname = t.sname; 285 286 final ServiceCreds creds = (ServiceCreds)serviceCreds; 287 final KerberosPrincipal princ = 288 new KerberosPrincipal(ticketSname.toString()); 289 290 // For bound service, permission already checked at setup 291 if (creds.getName() == null) { 292 SecurityManager sm = System.getSecurityManager(); 293 try { 294 if (sm != null) { 295 // Eliminate dependency on ServicePermission 296 sm.checkPermission(new ServicePermission( 297 ticketSname.toString(), "accept"), acc); 298 } 299 } catch (SecurityException se) { 300 serviceCreds = null; 301 // Do not destroy keys. Will affect Subject 302 if (debug != null && Debug.isOn("handshake")) { 303 System.out.println("Permission to access Kerberos" 304 + " secret key denied"); 305 se.printStackTrace(System.out); 306 } 307 throw new IOException("Kerberos service not allowedy"); 308 } 309 } 310 KerberosKey[] serverKeys = AccessController.doPrivileged( 311 new PrivilegedAction<KerberosKey[]>() { 312 @Override 313 public KerberosKey[] run() { 314 return creds.getKKeys(princ); 315 } 316 }); 317 if (serverKeys.length == 0) { 318 throw new IOException("Found no key for " + princ + 319 (creds.getName() == null ? "" : 320 (", this keytab is for " + creds.getName() + " only"))); 321 } 322 323 /* 324 * permission to access and use the secret key of the Kerberized 325 * "host" service is done in ServerHandshaker.getKerberosKeys() 326 * to ensure server has the permission to use the secret key 327 * before promising the client 328 */ 329 330 // See if we have the right key to decrypt the ticket to get 331 // the session key. 332 int encPartKeyType = encPart.getEType(); 333 Integer encPartKeyVersion = encPart.getKeyVersionNumber(); 334 KerberosKey dkey = null; 335 try { 336 dkey = findKey(encPartKeyType, encPartKeyVersion, serverKeys); 337 } catch (KrbException ke) { // a kvno mismatch 338 throw new IOException( 339 "Cannot find key matching version number", ke); 340 } 341 if (dkey == null) { 342 // %%% Should print string repr of etype 343 throw new IOException("Cannot find key of appropriate type" + 344 " to decrypt ticket - need etype " + encPartKeyType); 345 } 346 347 EncryptionKey secretKey = new EncryptionKey( 348 encPartKeyType, 349 dkey.getEncoded()); 350 351 // Decrypt encPart using server's secret key 352 byte[] bytes = encPart.decrypt(secretKey, KeyUsage.KU_TICKET); 353 354 // Reset data stream after decryption, remove redundant bytes 355 byte[] temp = encPart.reset(bytes); 356 EncTicketPart encTicketPart = new EncTicketPart(temp); 357 358 // Record the Kerberos Principals 359 tmpPeer = new KerberosPrincipal(encTicketPart.cname.getName()); 360 tmpLocal = new KerberosPrincipal(ticketSname.getName()); 361 362 sessionKey = encTicketPart.key; 363 364 if (debug != null && Debug.isOn("handshake")) { 365 System.out.println("server principal: " + ticketSname); 366 System.out.println("cname: " + encTicketPart.cname.toString()); 367 } 368 } catch (IOException e) { 369 throw e; 370 } catch (Exception e) { 371 if (debug != null && Debug.isOn("handshake")) { 372 System.out.println("KerberosWrapper error getting session key," 373 + " generating random secret (" + e.getMessage() + ")"); 374 } 375 sessionKey = null; 376 } 377 378 //input.getBytes16(); // XXX Read and ignore authenticator 379 380 if (sessionKey != null) { 381 preMaster = new KerberosPreMasterSecret(protocolVersion, 382 clientVersion, rand, encrypted, sessionKey); 383 } else { 384 // Generate bogus premaster secret 385 preMaster = new KerberosPreMasterSecret(clientVersion, rand); 386 } 387 388 peerPrincipal = tmpPeer; 389 localPrincipal = tmpLocal; 390 } 391 392 // Similar to sun.security.jgss.krb5.Krb5InitCredenetial/Krb5Context 393 private static KerberosTicket getServiceTicket(String serverName, 394 final AccessControlContext acc) throws IOException { 395 396 if ("localhost".equals(serverName) || 397 "localhost.localdomain".equals(serverName)) { 398 399 if (debug != null && Debug.isOn("handshake")) { 400 System.out.println("Get the local hostname"); 401 } 402 String localHost = java.security.AccessController.doPrivileged( 403 new java.security.PrivilegedAction<String>() { 404 public String run() { 405 try { 406 return InetAddress.getLocalHost().getHostName(); 407 } catch (java.net.UnknownHostException e) { 408 if (debug != null && Debug.isOn("handshake")) { 409 System.out.println("Warning," 410 + " cannot get the local hostname: " 411 + e.getMessage()); 412 } 413 return null; 414 } 415 } 416 }); 417 if (localHost != null) { 418 serverName = localHost; 419 } 420 } 421 422 // Resolve serverName (possibly in IP addr form) to Kerberos principal 423 // name for service with hostname 424 String serviceName = "host/" + serverName; 425 PrincipalName principal; 426 try { 427 principal = new PrincipalName(serviceName, 428 PrincipalName.KRB_NT_SRV_HST); 429 } catch (SecurityException se) { 430 throw se; 431 } catch (Exception e) { 432 IOException ioe = new IOException("Invalid service principal" + 433 " name: " + serviceName); 434 ioe.initCause(e); 435 throw ioe; 436 } 437 String realm = principal.getRealmAsString(); 438 439 final String serverPrincipal = principal.toString(); 440 final String tgsPrincipal = "krbtgt/" + realm + "@" + realm; 441 final String clientPrincipal = null; // use default 442 443 444 // check permission to obtain a service ticket to initiate a 445 // context with the "host" service 446 SecurityManager sm = System.getSecurityManager(); 447 if (sm != null) { 448 sm.checkPermission(new ServicePermission(serverPrincipal, 449 "initiate"), acc); 450 } 451 452 try { 453 KerberosTicket ticket = AccessController.doPrivileged( 454 new PrivilegedExceptionAction<KerberosTicket>() { 455 public KerberosTicket run() throws Exception { 456 return Krb5Util.getTicketFromSubjectAndTgs( 457 GSSCaller.CALLER_SSL_CLIENT, 458 clientPrincipal, serverPrincipal, 459 tgsPrincipal, acc); 460 }}); 461 462 if (ticket == null) { 463 throw new IOException("Failed to find any kerberos service" + 464 " ticket for " + serverPrincipal); 465 } 466 return ticket; 467 } catch (PrivilegedActionException e) { 468 IOException ioe = new IOException( 469 "Attempt to obtain kerberos service ticket for " + 470 serverPrincipal + " failed!"); 471 ioe.initCause(e); 472 throw ioe; 473 } 474 } 475 476 @Override 477 public SecretKey clientKeyExchange() { 478 byte[] secretBytes = preMaster.getUnencrypted(); 479 return new SecretKeySpec(secretBytes, "TlsPremasterSecret"); 480 } 481 482 @Override 483 public Principal getPeerPrincipal() { 484 return peerPrincipal; 485 } 486 487 @Override 488 public Principal getLocalPrincipal() { 489 return localPrincipal; 490 } 491 492 /** 493 * Determines if a kvno matches another kvno. Used in the method 494 * findKey(etype, version, keys). Always returns true if either input 495 * is null or zero, in case any side does not have kvno info available. 496 * 497 * Note: zero is included because N/A is not a legal value for kvno 498 * in javax.security.auth.kerberos.KerberosKey. Therefore, the info 499 * that the kvno is N/A might be lost when converting between 500 * EncryptionKey and KerberosKey. 501 */ 502 private static boolean versionMatches(Integer v1, int v2) { 503 if (v1 == null || v1 == 0 || v2 == 0) { 504 return true; 505 } 506 return v1.equals(v2); 507 } 508 509 private static KerberosKey findKey(int etype, Integer version, 510 KerberosKey[] keys) throws KrbException { 511 int ktype; 512 boolean etypeFound = false; 513 514 // When no matched kvno is found, returns tke key of the same 515 // etype with the highest kvno 516 int kvno_found = 0; 517 KerberosKey key_found = null; 518 519 for (int i = 0; i < keys.length; i++) { 520 ktype = keys[i].getKeyType(); 521 if (etype == ktype) { 522 int kv = keys[i].getVersionNumber(); 523 etypeFound = true; 524 if (versionMatches(version, kv)) { 525 return keys[i]; 526 } else if (kv > kvno_found) { 527 key_found = keys[i]; 528 kvno_found = kv; 529 } 530 } 531 } 532 // Key not found. 533 // %%% kludge to allow DES keys to be used for diff etypes 534 if ((etype == EncryptedData.ETYPE_DES_CBC_CRC || 535 etype == EncryptedData.ETYPE_DES_CBC_MD5)) { 536 for (int i = 0; i < keys.length; i++) { 537 ktype = keys[i].getKeyType(); 538 if (ktype == EncryptedData.ETYPE_DES_CBC_CRC || 539 ktype == EncryptedData.ETYPE_DES_CBC_MD5) { 540 int kv = keys[i].getVersionNumber(); 541 etypeFound = true; 542 if (versionMatches(version, kv)) { 543 return new KerberosKey(keys[i].getPrincipal(), 544 keys[i].getEncoded(), 545 etype, 546 kv); 547 } else if (kv > kvno_found) { 548 key_found = new KerberosKey(keys[i].getPrincipal(), 549 keys[i].getEncoded(), 550 etype, 551 kv); 552 kvno_found = kv; 553 } 554 } 555 } 556 } 557 if (etypeFound) { 558 return key_found; 559 } 560 return null; 561 } 562 } 563 }