467 lines
18 KiB
Java
467 lines
18 KiB
Java
/*
|
|
* Copyright (c) 2003, 2020, 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.ssl.krb5;
|
|
|
|
import java.io.IOException;
|
|
import java.net.InetAddress;
|
|
import java.net.UnknownHostException;
|
|
import java.security.AccessController;
|
|
import java.security.AccessControlContext;
|
|
import java.security.PrivilegedExceptionAction;
|
|
import java.security.PrivilegedActionException;
|
|
import java.util.Arrays;
|
|
import java.security.PrivilegedAction;
|
|
|
|
import javax.security.auth.kerberos.KerberosTicket;
|
|
import javax.net.ssl.SSLKeyException;
|
|
import javax.security.auth.kerberos.KerberosKey;
|
|
import javax.security.auth.kerberos.KerberosPrincipal;
|
|
import javax.security.auth.kerberos.ServicePermission;
|
|
import sun.security.jgss.GSSCaller;
|
|
|
|
import sun.security.krb5.EncryptionKey;
|
|
import sun.security.krb5.EncryptedData;
|
|
import sun.security.krb5.PrincipalName;
|
|
import sun.security.krb5.internal.Ticket;
|
|
import sun.security.krb5.internal.EncTicketPart;
|
|
import sun.security.krb5.internal.crypto.KeyUsage;
|
|
|
|
import sun.security.jgss.krb5.Krb5Util;
|
|
import sun.security.jgss.krb5.ServiceCreds;
|
|
import sun.security.krb5.KrbException;
|
|
|
|
import sun.security.ssl.Krb5Helper;
|
|
import sun.security.ssl.SSLLogger;
|
|
|
|
public final class KrbClientKeyExchangeHelperImpl
|
|
implements sun.security.ssl.KrbClientKeyExchangeHelper {
|
|
|
|
private byte[] preMaster;
|
|
private byte[] preMasterEnc;
|
|
private byte[] encodedTicket;
|
|
private KerberosPrincipal peerPrincipal;
|
|
private KerberosPrincipal localPrincipal;
|
|
|
|
/**
|
|
* Initialises an instance of KrbClientKeyExchangeHelperImpl.
|
|
* Used by the TLS client to create a Kerberos Client Key Exchange
|
|
* message.
|
|
*
|
|
* @param preMaster plain pre-master secret
|
|
* @param serverName name of the TLS server to perform the handshake;
|
|
* used by the TLS client to get a Kerberos service
|
|
* ticket which contains the session key to encrypt
|
|
* the pre-master secret
|
|
* @param acc the TLS client security context for the handshake
|
|
*/
|
|
@Override
|
|
public void init(byte[] preMaster, String serverName,
|
|
AccessControlContext acc) throws IOException {
|
|
this.preMaster = preMaster;
|
|
|
|
// Get service ticket
|
|
KerberosTicket ticket = getServiceTicket(serverName, acc);
|
|
encodedTicket = ticket.getEncoded();
|
|
|
|
// Record the Kerberos principals
|
|
peerPrincipal = ticket.getServer();
|
|
localPrincipal = ticket.getClient();
|
|
|
|
// Encrypt the pre-master secret with the Kerberos session key
|
|
EncryptionKey sessionKey = new EncryptionKey(
|
|
ticket.getSessionKeyType(),
|
|
ticket.getSessionKey().getEncoded());
|
|
encryptPremasterSecret(sessionKey);
|
|
}
|
|
|
|
/**
|
|
* Initialises an instance of KrbClientKeyExchangeHelperImpl.
|
|
* Used by the TLS server to process the content of a Kerberos Client Key
|
|
* Exchange message (received from a TLS client).
|
|
*
|
|
* @param encodedTicket the encoded Kerberos ticket (TGS) received from the
|
|
* TLS client. This ticket seals the Kerberos session
|
|
* key.
|
|
* @param preMasterEnc the pre-master secret encrypted with the Kerberos
|
|
* session key
|
|
* @param serviceCreds the TLS server Kerberos credentials used to process
|
|
* the received Kerberos ticket.
|
|
* @param acc the TLS server security context for the handshake
|
|
*/
|
|
@Override
|
|
public void init(byte[] encodedTicket, byte[] preMasterEnc,
|
|
Object serviceCreds, AccessControlContext acc) throws IOException {
|
|
this.encodedTicket = encodedTicket;
|
|
this.preMasterEnc = preMasterEnc;
|
|
EncryptionKey sessionKey = 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(Krb5Helper.getServicePermission(
|
|
ticketSname.toString(), "accept"), acc);
|
|
}
|
|
} catch (SecurityException se) {
|
|
// Do not destroy keys. Will affect Subject
|
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
|
SSLLogger.fine("Permission to access Kerberos"
|
|
+ " secret key denied");
|
|
}
|
|
throw new IOException("Kerberos service not allowed");
|
|
}
|
|
}
|
|
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")));
|
|
}
|
|
|
|
// 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
|
|
peerPrincipal =
|
|
new KerberosPrincipal(encTicketPart.cname.getName());
|
|
localPrincipal = new KerberosPrincipal(ticketSname.getName());
|
|
|
|
sessionKey = encTicketPart.key;
|
|
|
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
|
SSLLogger.fine("server principal: " + ticketSname);
|
|
SSLLogger.fine("cname: " + encTicketPart.cname.toString());
|
|
}
|
|
} catch (Exception e) {
|
|
sessionKey = null;
|
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
|
SSLLogger.fine("Error getting the Kerberos session key" +
|
|
" to decrypt the pre-master secret");
|
|
}
|
|
}
|
|
if (sessionKey != null)
|
|
decryptPremasterSecret(sessionKey);
|
|
}
|
|
|
|
@Override
|
|
public byte[] getEncodedTicket() {
|
|
return encodedTicket;
|
|
}
|
|
|
|
@Override
|
|
public byte[] getEncryptedPreMasterSecret() {
|
|
return preMasterEnc;
|
|
}
|
|
|
|
@Override
|
|
public byte[] getPlainPreMasterSecret() {
|
|
return preMaster;
|
|
}
|
|
|
|
@Override
|
|
public KerberosPrincipal getPeerPrincipal() {
|
|
return peerPrincipal;
|
|
}
|
|
|
|
@Override
|
|
public KerberosPrincipal getLocalPrincipal() {
|
|
return localPrincipal;
|
|
}
|
|
|
|
private void encryptPremasterSecret(EncryptionKey sessionKey)
|
|
throws IOException {
|
|
if (sessionKey.getEType() ==
|
|
EncryptedData.ETYPE_DES3_CBC_HMAC_SHA1_KD) {
|
|
throw new IOException(
|
|
"session keys with des3-cbc-hmac-sha1-kd encryption type " +
|
|
"are not supported for TLS Kerberos cipher suites");
|
|
}
|
|
try {
|
|
EncryptedData eData = new EncryptedData(sessionKey, preMaster,
|
|
KeyUsage.KU_UNKNOWN);
|
|
preMasterEnc = eData.getBytes(); // not ASN.1 encoded.
|
|
} catch (KrbException e) {
|
|
throw (IOException) new SSLKeyException("Kerberos pre-master" +
|
|
" secret error").initCause(e);
|
|
}
|
|
}
|
|
|
|
private void decryptPremasterSecret(EncryptionKey sessionKey)
|
|
throws IOException {
|
|
if (sessionKey.getEType() ==
|
|
EncryptedData.ETYPE_DES3_CBC_HMAC_SHA1_KD) {
|
|
throw new IOException(
|
|
"session keys with des3-cbc-hmac-sha1-kd encryption type " +
|
|
"are not supported for TLS Kerberos cipher suites");
|
|
}
|
|
try {
|
|
EncryptedData data = new EncryptedData(sessionKey.getEType(),
|
|
null /* optional kvno */, preMasterEnc);
|
|
byte[] temp = data.decrypt(sessionKey, KeyUsage.KU_UNKNOWN);
|
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
|
if (preMasterEnc != null) {
|
|
SSLLogger.fine("decrypted premaster secret", temp);
|
|
}
|
|
}
|
|
|
|
// Remove padding bytes after decryption. Only DES and DES3 have
|
|
// paddings and we don't support DES3 in TLS (see above)
|
|
if (temp.length == 52 &&
|
|
data.getEType() == EncryptedData.ETYPE_DES_CBC_CRC) {
|
|
// For des-cbc-crc, 4 paddings. Value can be 0x04 or 0x00.
|
|
if (paddingByteIs(temp, 52, (byte)4) ||
|
|
paddingByteIs(temp, 52, (byte)0)) {
|
|
temp = Arrays.copyOf(temp, 48);
|
|
}
|
|
} else if (temp.length == 56 &&
|
|
data.getEType() == EncryptedData.ETYPE_DES_CBC_MD5) {
|
|
// For des-cbc-md5, 8 paddings with 0x08, or no padding
|
|
if (paddingByteIs(temp, 56, (byte)8)) {
|
|
temp = Arrays.copyOf(temp, 48);
|
|
}
|
|
}
|
|
|
|
preMaster = temp;
|
|
} catch (Exception e) {
|
|
// Decrypting the pre-master secret was not possible.
|
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
|
SSLLogger.fine("Error decrypting the pre-master secret");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if all paddings of data are b
|
|
* @param data the block with padding
|
|
* @param len length of data, >= 48
|
|
* @param b expected padding byte
|
|
*/
|
|
private static boolean paddingByteIs(byte[] data, int len, byte b) {
|
|
for (int i=48; i<len; i++) {
|
|
if (data[i] != b) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// 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 (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
|
SSLLogger.fine("Get the local hostname");
|
|
}
|
|
String localHost = java.security.AccessController.doPrivileged(
|
|
new java.security.PrivilegedAction<String>() {
|
|
public String run() {
|
|
try {
|
|
return InetAddress.getLocalHost().getHostName();
|
|
} catch (UnknownHostException e) {
|
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
|
SSLLogger.fine("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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|