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
26package sun.security.krb5.internal.ssl;
27
28import sun.security.ssl.ClientKeyExchange;
29import sun.security.ssl.Debug;
30import sun.security.ssl.ClientKeyExchangeService;
31import sun.security.ssl.HandshakeOutStream;
32
33import sun.security.jgss.GSSCaller;
34import sun.security.jgss.krb5.Krb5Util;
35import sun.security.jgss.krb5.ServiceCreds;
36import sun.security.krb5.EncryptedData;
37import sun.security.krb5.EncryptionKey;
38import sun.security.krb5.KrbException;
39import sun.security.krb5.PrincipalName;
40import sun.security.krb5.internal.EncTicketPart;
41import sun.security.krb5.internal.Ticket;
42import sun.security.krb5.internal.crypto.KeyUsage;
43import sun.security.ssl.ProtocolVersion;
44
45import javax.crypto.SecretKey;
46import javax.crypto.spec.SecretKeySpec;
47import javax.security.auth.Subject;
48import javax.security.auth.kerberos.KerberosKey;
49import javax.security.auth.kerberos.KerberosPrincipal;
50import javax.security.auth.kerberos.KerberosTicket;
51import javax.security.auth.kerberos.KeyTab;
52import javax.security.auth.kerberos.ServicePermission;
53import java.io.IOException;
54import java.io.PrintStream;
55import java.net.InetAddress;
56import java.security.AccessControlContext;
57import java.security.AccessController;
58import java.security.Principal;
59import java.security.PrivilegedAction;
60import java.security.PrivilegedActionException;
61import java.security.PrivilegedExceptionAction;
62import java.security.SecureRandom;
63import java.util.Set;
64
65/**
66 * The provider for TLS_KRB_ cipher suites.
67 *
68 * @since 9
69 */
70public 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}
564