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 1.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 }