jdk/src/java.security.jgss/share/classes/sun/security/krb5/internal/ssl/Krb5KeyExchangeService.java
author weijun
Tue, 02 Jun 2015 22:26:36 +0800
changeset 30905 bba6fefdd660
child 31800 450f972e0488
permissions -rw-r--r--
8038089: TLS optional support for Kerberos cipher suites needs to be re-examine Reviewed-by: xuelei

/*
 * 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 1.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 (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;
        }
    }
}