/* * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package sun.security.krb5.internal.ssl; import sun.security.ssl.ClientKeyExchange; import sun.security.ssl.Debug; import sun.security.ssl.ClientKeyExchangeService; import sun.security.ssl.HandshakeOutStream; import sun.security.jgss.GSSCaller; import sun.security.jgss.krb5.Krb5Util; import sun.security.jgss.krb5.ServiceCreds; import sun.security.krb5.EncryptedData; import sun.security.krb5.EncryptionKey; import sun.security.krb5.KrbException; import sun.security.krb5.PrincipalName; import sun.security.krb5.internal.EncTicketPart; import sun.security.krb5.internal.Ticket; import sun.security.krb5.internal.crypto.KeyUsage; import sun.security.ssl.ProtocolVersion; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import javax.security.auth.Subject; import javax.security.auth.kerberos.KerberosKey; import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.kerberos.KerberosTicket; import javax.security.auth.kerberos.KeyTab; import javax.security.auth.kerberos.ServicePermission; import java.io.IOException; import java.io.PrintStream; import java.net.InetAddress; import java.security.AccessControlContext; import java.security.AccessController; import java.security.Principal; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.security.SecureRandom; import java.util.Set; /** * The provider for TLS_KRB_ cipher suites. * * @since 9 */ public class Krb5KeyExchangeService implements ClientKeyExchangeService { public static final Debug debug = Debug.getInstance("ssl"); @Override public String[] supported() { return new String[] { "KRB5", "KRB5_EXPORT" }; } @Override public Object getServiceCreds(AccessControlContext acc) { try { ServiceCreds serviceCreds = AccessController.doPrivileged( (PrivilegedExceptionAction<ServiceCreds>) () -> Krb5Util.getServiceCreds( GSSCaller.CALLER_SSL_SERVER, null, acc)); if (serviceCreds == null) { if (debug != null && Debug.isOn("handshake")) { System.out.println("Kerberos serviceCreds not available"); } return null; } if (debug != null && Debug.isOn("handshake")) { System.out.println("Using Kerberos creds"); } String serverPrincipal = serviceCreds.getName(); if (serverPrincipal != null) { // When service is bound, we check ASAP. Otherwise, // will check after client request is received // in in Kerberos ClientKeyExchange SecurityManager sm = System.getSecurityManager(); try { if (sm != null) { // Eliminate dependency on ServicePermission sm.checkPermission(new ServicePermission( serverPrincipal, "accept"), acc); } } catch (SecurityException se) { if (debug != null && Debug.isOn("handshake")) { System.out.println("Permission to access Kerberos" + " secret key denied"); } return null; } } return serviceCreds; } catch (PrivilegedActionException e) { // Likely exception here is LoginException if (debug != null && Debug.isOn("handshake")) { System.out.println("Attempt to obtain Kerberos key failed: " + e.toString()); } return null; } } @Override public String getServiceHostName(Principal principal) { if (principal == null) { return null; } String hostName = null; try { PrincipalName princName = new PrincipalName(principal.getName(), PrincipalName.KRB_NT_SRV_HST); String[] nameParts = princName.getNameStrings(); if (nameParts.length >= 2) { hostName = nameParts[1]; } } catch (Exception e) { // ignore } return hostName; } @Override public boolean isRelated(boolean isClient, AccessControlContext acc, Principal p) { if (p == null) return false; try { Subject subject = AccessController.doPrivileged( (PrivilegedExceptionAction<Subject>) () -> Krb5Util.getSubject( isClient ? GSSCaller.CALLER_SSL_CLIENT : GSSCaller.CALLER_SSL_SERVER, acc)); if (subject == null) { if (debug != null && Debug.isOn("session")) { System.out.println("Kerberos credentials are" + " not present in the current Subject;" + " check if " + " javax.security.auth.useSubjectAsCreds" + " system property has been set to false"); } return false; } Set<Principal> principals = subject.getPrincipals(Principal.class); if (principals.contains(p)) { // bound to this principal return true; } else { if (isClient) { return false; } else { for (KeyTab pc : subject.getPrivateCredentials(KeyTab.class)) { if (!pc.isBound()) { return true; } } return false; } } } catch (PrivilegedActionException pae) { if (debug != null && Debug.isOn("session")) { System.out.println("Attempt to obtain" + " subject failed! " + pae); } return false; } } public ClientKeyExchange createClientExchange( String serverName, AccessControlContext acc, ProtocolVersion protocolVerson, SecureRandom rand) throws IOException { return new ExchangerImpl(serverName, acc, protocolVerson, rand); } public ClientKeyExchange createServerExchange( ProtocolVersion protocolVersion, ProtocolVersion clientVersion, SecureRandom rand, byte[] encodedTicket, byte[] encrypted, AccessControlContext acc, Object serviceCreds) throws IOException { return new ExchangerImpl(protocolVersion, clientVersion, rand, encodedTicket, encrypted, acc, serviceCreds); } static class ExchangerImpl extends ClientKeyExchange { final private KerberosPreMasterSecret preMaster; final private byte[] encodedTicket; final private KerberosPrincipal peerPrincipal; final private KerberosPrincipal localPrincipal; @Override public int messageLength() { return encodedTicket.length + preMaster.getEncrypted().length + 6; } @Override public void send(HandshakeOutStream s) throws IOException { s.putBytes16(encodedTicket); s.putBytes16(null); s.putBytes16(preMaster.getEncrypted()); } @Override public void print(PrintStream s) throws IOException { s.println("*** ClientKeyExchange, Kerberos"); if (debug != null && Debug.isOn("verbose")) { Debug.println(s, "Kerberos service ticket", encodedTicket); Debug.println(s, "Random Secret", preMaster.getUnencrypted()); Debug.println(s, "Encrypted random Secret", preMaster.getEncrypted()); } } ExchangerImpl(String serverName, AccessControlContext acc, ProtocolVersion protocolVersion, SecureRandom rand) throws IOException { // Get service ticket KerberosTicket ticket = getServiceTicket(serverName, acc); encodedTicket = ticket.getEncoded(); // Record the Kerberos principals peerPrincipal = ticket.getServer(); localPrincipal = ticket.getClient(); // Optional authenticator, encrypted using session key, // currently ignored // Generate premaster secret and encrypt it using session key EncryptionKey sessionKey = new EncryptionKey( ticket.getSessionKeyType(), ticket.getSessionKey().getEncoded()); preMaster = new KerberosPreMasterSecret(protocolVersion, rand, sessionKey); } ExchangerImpl( ProtocolVersion protocolVersion, ProtocolVersion clientVersion, SecureRandom rand, byte[] encodedTicket, byte[] encrypted, AccessControlContext acc, Object serviceCreds) throws IOException { // Read ticket this.encodedTicket = encodedTicket; if (debug != null && Debug.isOn("verbose")) { Debug.println(System.out, "encoded Kerberos service ticket", encodedTicket); } EncryptionKey sessionKey = null; KerberosPrincipal tmpPeer = null; KerberosPrincipal tmpLocal = null; try { Ticket t = new Ticket(encodedTicket); EncryptedData encPart = t.encPart; PrincipalName ticketSname = t.sname; final ServiceCreds creds = (ServiceCreds)serviceCreds; final KerberosPrincipal princ = new KerberosPrincipal(ticketSname.toString()); // For bound service, permission already checked at setup if (creds.getName() == null) { SecurityManager sm = System.getSecurityManager(); try { if (sm != null) { // Eliminate dependency on ServicePermission sm.checkPermission(new ServicePermission( ticketSname.toString(), "accept"), acc); } } catch (SecurityException se) { serviceCreds = null; // Do not destroy keys. Will affect Subject if (debug != null && Debug.isOn("handshake")) { System.out.println("Permission to access Kerberos" + " secret key denied"); se.printStackTrace(System.out); } throw new IOException("Kerberos service not allowedy"); } } KerberosKey[] serverKeys = AccessController.doPrivileged( new PrivilegedAction<KerberosKey[]>() { @Override public KerberosKey[] run() { return creds.getKKeys(princ); } }); if (serverKeys.length == 0) { throw new IOException("Found no key for " + princ + (creds.getName() == null ? "" : (", this keytab is for " + creds.getName() + " only"))); } /* * permission to access and use the secret key of the Kerberized * "host" service is done in ServerHandshaker.getKerberosKeys() * to ensure server has the permission to use the secret key * before promising the client */ // See if we have the right key to decrypt the ticket to get // the session key. int encPartKeyType = encPart.getEType(); Integer encPartKeyVersion = encPart.getKeyVersionNumber(); KerberosKey dkey = null; try { dkey = findKey(encPartKeyType, encPartKeyVersion, serverKeys); } catch (KrbException ke) { // a kvno mismatch throw new IOException( "Cannot find key matching version number", ke); } if (dkey == null) { // %%% Should print string repr of etype throw new IOException("Cannot find key of appropriate type" + " to decrypt ticket - need etype " + encPartKeyType); } EncryptionKey secretKey = new EncryptionKey( encPartKeyType, dkey.getEncoded()); // Decrypt encPart using server's secret key byte[] bytes = encPart.decrypt(secretKey, KeyUsage.KU_TICKET); // Reset data stream after decryption, remove redundant bytes byte[] temp = encPart.reset(bytes); EncTicketPart encTicketPart = new EncTicketPart(temp); // Record the Kerberos Principals tmpPeer = new KerberosPrincipal(encTicketPart.cname.getName()); tmpLocal = new KerberosPrincipal(ticketSname.getName()); sessionKey = encTicketPart.key; if (debug != null && Debug.isOn("handshake")) { System.out.println("server principal: " + ticketSname); System.out.println("cname: " + encTicketPart.cname.toString()); } } catch (IOException e) { throw e; } catch (Exception e) { if (debug != null && Debug.isOn("handshake")) { System.out.println("KerberosWrapper error getting session key," + " generating random secret (" + e.getMessage() + ")"); } sessionKey = null; } //input.getBytes16(); // XXX Read and ignore authenticator if (sessionKey != null) { preMaster = new KerberosPreMasterSecret(protocolVersion, clientVersion, rand, encrypted, sessionKey); } else { // Generate bogus premaster secret preMaster = new KerberosPreMasterSecret(clientVersion, rand); } peerPrincipal = tmpPeer; localPrincipal = tmpLocal; } // Similar to sun.security.jgss.krb5.Krb5InitCredenetial/Krb5Context private static KerberosTicket getServiceTicket(String serverName, final AccessControlContext acc) throws IOException { if ("localhost".equals(serverName) || "localhost.localdomain".equals(serverName)) { if (debug != null && Debug.isOn("handshake")) { System.out.println("Get the local hostname"); } String localHost = java.security.AccessController.doPrivileged( new java.security.PrivilegedAction<String>() { public String run() { try { return InetAddress.getLocalHost().getHostName(); } catch (java.net.UnknownHostException e) { if (debug != null && Debug.isOn("handshake")) { System.out.println("Warning," + " cannot get the local hostname: " + e.getMessage()); } return null; } } }); if (localHost != null) { serverName = localHost; } } // Resolve serverName (possibly in IP addr form) to Kerberos principal // name for service with hostname String serviceName = "host/" + serverName; PrincipalName principal; try { principal = new PrincipalName(serviceName, PrincipalName.KRB_NT_SRV_HST); } catch (SecurityException se) { throw se; } catch (Exception e) { IOException ioe = new IOException("Invalid service principal" + " name: " + serviceName); ioe.initCause(e); throw ioe; } String realm = principal.getRealmAsString(); final String serverPrincipal = principal.toString(); final String tgsPrincipal = "krbtgt/" + realm + "@" + realm; final String clientPrincipal = null; // use default // check permission to obtain a service ticket to initiate a // context with the "host" service SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new ServicePermission(serverPrincipal, "initiate"), acc); } try { KerberosTicket ticket = AccessController.doPrivileged( new PrivilegedExceptionAction<KerberosTicket>() { public KerberosTicket run() throws Exception { return Krb5Util.getTicketFromSubjectAndTgs( GSSCaller.CALLER_SSL_CLIENT, clientPrincipal, serverPrincipal, tgsPrincipal, acc); }}); if (ticket == null) { throw new IOException("Failed to find any kerberos service" + " ticket for " + serverPrincipal); } return ticket; } catch (PrivilegedActionException e) { IOException ioe = new IOException( "Attempt to obtain kerberos service ticket for " + serverPrincipal + " failed!"); ioe.initCause(e); throw ioe; } } @Override public SecretKey clientKeyExchange() { byte[] secretBytes = preMaster.getUnencrypted(); return new SecretKeySpec(secretBytes, "TlsPremasterSecret"); } @Override public Principal getPeerPrincipal() { return peerPrincipal; } @Override public Principal getLocalPrincipal() { return localPrincipal; } /** * Determines if a kvno matches another kvno. Used in the method * findKey(etype, version, keys). Always returns true if either input * is null or zero, in case any side does not have kvno info available. * * Note: zero is included because N/A is not a legal value for kvno * in javax.security.auth.kerberos.KerberosKey. Therefore, the info * that the kvno is N/A might be lost when converting between * EncryptionKey and KerberosKey. */ private static boolean versionMatches(Integer v1, int v2) { if (v1 == null || v1 == 0 || v2 == 0) { return true; } return v1.equals(v2); } private static KerberosKey findKey(int etype, Integer version, KerberosKey[] keys) throws KrbException { int ktype; boolean etypeFound = false; // When no matched kvno is found, returns tke key of the same // etype with the highest kvno int kvno_found = 0; KerberosKey key_found = null; for (int i = 0; i < keys.length; i++) { ktype = keys[i].getKeyType(); if (etype == ktype) { int kv = keys[i].getVersionNumber(); etypeFound = true; if (versionMatches(version, kv)) { return keys[i]; } else if (kv > kvno_found) { key_found = keys[i]; kvno_found = kv; } } } // Key not found. // %%% kludge to allow DES keys to be used for diff etypes if ((etype == EncryptedData.ETYPE_DES_CBC_CRC || etype == EncryptedData.ETYPE_DES_CBC_MD5)) { for (int i = 0; i < keys.length; i++) { ktype = keys[i].getKeyType(); if (ktype == EncryptedData.ETYPE_DES_CBC_CRC || ktype == EncryptedData.ETYPE_DES_CBC_MD5) { int kv = keys[i].getVersionNumber(); etypeFound = true; if (versionMatches(version, kv)) { return new KerberosKey(keys[i].getPrincipal(), keys[i].getEncoded(), etype, kv); } else if (kv > kvno_found) { key_found = new KerberosKey(keys[i].getPrincipal(), keys[i].getEncoded(), etype, kv); kvno_found = kv; } } } } if (etypeFound) { return key_found; } return null; } } }