| /* |
| * Copyright (c) 2015, 2019, 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; |
| |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.security.*; |
| import java.text.MessageFormat; |
| import java.util.List; |
| import java.util.ArrayList; |
| import java.util.Locale; |
| import java.util.Objects; |
| import java.util.Collection; |
| import javax.crypto.Mac; |
| import javax.crypto.SecretKey; |
| import javax.net.ssl.SSLPeerUnverifiedException; |
| import static sun.security.ssl.ClientAuthType.CLIENT_AUTH_REQUIRED; |
| import sun.security.ssl.ClientHello.ClientHelloMessage; |
| import sun.security.ssl.SSLExtension.ExtensionConsumer; |
| import sun.security.ssl.SSLExtension.SSLExtensionSpec; |
| import sun.security.ssl.SSLHandshake.HandshakeMessage; |
| import static sun.security.ssl.SSLExtension.*; |
| |
| /** |
| * Pack of the "pre_shared_key" extension. |
| */ |
| final class PreSharedKeyExtension { |
| static final HandshakeProducer chNetworkProducer = |
| new CHPreSharedKeyProducer(); |
| static final ExtensionConsumer chOnLoadConsumer = |
| new CHPreSharedKeyConsumer(); |
| static final HandshakeAbsence chOnLoadAbsence = |
| new CHPreSharedKeyOnLoadAbsence(); |
| static final HandshakeConsumer chOnTradeConsumer = |
| new CHPreSharedKeyUpdate(); |
| static final HandshakeAbsence chOnTradAbsence = |
| new CHPreSharedKeyOnTradeAbsence(); |
| static final SSLStringizer chStringizer = |
| new CHPreSharedKeyStringizer(); |
| |
| static final HandshakeProducer shNetworkProducer = |
| new SHPreSharedKeyProducer(); |
| static final ExtensionConsumer shOnLoadConsumer = |
| new SHPreSharedKeyConsumer(); |
| static final HandshakeAbsence shOnLoadAbsence = |
| new SHPreSharedKeyAbsence(); |
| static final SSLStringizer shStringizer = |
| new SHPreSharedKeyStringizer(); |
| |
| private static final class PskIdentity { |
| final byte[] identity; |
| final int obfuscatedAge; |
| |
| PskIdentity(byte[] identity, int obfuscatedAge) { |
| this.identity = identity; |
| this.obfuscatedAge = obfuscatedAge; |
| } |
| |
| int getEncodedLength() { |
| return 2 + identity.length + 4; |
| } |
| |
| void writeEncoded(ByteBuffer m) throws IOException { |
| Record.putBytes16(m, identity); |
| Record.putInt32(m, obfuscatedAge); |
| } |
| |
| @Override |
| public String toString() { |
| return "{" + Utilities.toHexString(identity) + "," + |
| obfuscatedAge + "}"; |
| } |
| } |
| |
| private static final |
| class CHPreSharedKeySpec implements SSLExtensionSpec { |
| final List<PskIdentity> identities; |
| final List<byte[]> binders; |
| |
| CHPreSharedKeySpec(List<PskIdentity> identities, List<byte[]> binders) { |
| this.identities = identities; |
| this.binders = binders; |
| } |
| |
| CHPreSharedKeySpec(HandshakeContext context, |
| ByteBuffer m) throws IOException { |
| // struct { |
| // PskIdentity identities<7..2^16-1>; |
| // PskBinderEntry binders<33..2^16-1>; |
| // } OfferedPsks; |
| if (m.remaining() < 44) { |
| throw context.conContext.fatal(Alert.ILLEGAL_PARAMETER, |
| "Invalid pre_shared_key extension: " + |
| "insufficient data (length=" + m.remaining() + ")"); |
| } |
| |
| int idEncodedLength = Record.getInt16(m); |
| if (idEncodedLength < 7) { |
| throw context.conContext.fatal(Alert.ILLEGAL_PARAMETER, |
| "Invalid pre_shared_key extension: " + |
| "insufficient identities (length=" + idEncodedLength + ")"); |
| } |
| |
| identities = new ArrayList<>(); |
| int idReadLength = 0; |
| while (idReadLength < idEncodedLength) { |
| byte[] id = Record.getBytes16(m); |
| if (id.length < 1) { |
| throw context.conContext.fatal(Alert.ILLEGAL_PARAMETER, |
| "Invalid pre_shared_key extension: " + |
| "insufficient identity (length=" + id.length + ")"); |
| } |
| int obfuscatedTicketAge = Record.getInt32(m); |
| |
| PskIdentity pskId = new PskIdentity(id, obfuscatedTicketAge); |
| identities.add(pskId); |
| idReadLength += pskId.getEncodedLength(); |
| } |
| |
| if (m.remaining() < 35) { |
| throw context.conContext.fatal(Alert.ILLEGAL_PARAMETER, |
| "Invalid pre_shared_key extension: " + |
| "insufficient binders data (length=" + |
| m.remaining() + ")"); |
| } |
| |
| int bindersEncodedLen = Record.getInt16(m); |
| if (bindersEncodedLen < 33) { |
| throw context.conContext.fatal(Alert.ILLEGAL_PARAMETER, |
| "Invalid pre_shared_key extension: " + |
| "insufficient binders (length=" + |
| bindersEncodedLen + ")"); |
| } |
| |
| binders = new ArrayList<>(); |
| int bindersReadLength = 0; |
| while (bindersReadLength < bindersEncodedLen) { |
| byte[] binder = Record.getBytes8(m); |
| if (binder.length < 32) { |
| throw context.conContext.fatal(Alert.ILLEGAL_PARAMETER, |
| "Invalid pre_shared_key extension: " + |
| "insufficient binder entry (length=" + |
| binder.length + ")"); |
| } |
| binders.add(binder); |
| bindersReadLength += 1 + binder.length; |
| } |
| } |
| |
| int getIdsEncodedLength() { |
| int idEncodedLength = 0; |
| for (PskIdentity curId : identities) { |
| idEncodedLength += curId.getEncodedLength(); |
| } |
| |
| return idEncodedLength; |
| } |
| |
| int getBindersEncodedLength() { |
| int binderEncodedLength = 0; |
| for (byte[] curBinder : binders) { |
| binderEncodedLength += 1 + curBinder.length; |
| } |
| |
| return binderEncodedLength; |
| } |
| |
| byte[] getEncoded() throws IOException { |
| int idsEncodedLength = getIdsEncodedLength(); |
| int bindersEncodedLength = getBindersEncodedLength(); |
| int encodedLength = 4 + idsEncodedLength + bindersEncodedLength; |
| byte[] buffer = new byte[encodedLength]; |
| ByteBuffer m = ByteBuffer.wrap(buffer); |
| Record.putInt16(m, idsEncodedLength); |
| for (PskIdentity curId : identities) { |
| curId.writeEncoded(m); |
| } |
| Record.putInt16(m, bindersEncodedLength); |
| for (byte[] curBinder : binders) { |
| Record.putBytes8(m, curBinder); |
| } |
| |
| return buffer; |
| } |
| |
| @Override |
| public String toString() { |
| MessageFormat messageFormat = new MessageFormat( |
| "\"PreSharedKey\": '{'\n" + |
| " \"identities\" : \"{0}\",\n" + |
| " \"binders\" : \"{1}\",\n" + |
| "'}'", |
| Locale.ENGLISH); |
| |
| Object[] messageFields = { |
| Utilities.indent(identitiesString()), |
| Utilities.indent(bindersString()) |
| }; |
| |
| return messageFormat.format(messageFields); |
| } |
| |
| String identitiesString() { |
| StringBuilder result = new StringBuilder(); |
| for (PskIdentity curId : identities) { |
| result.append(curId.toString() + "\n"); |
| } |
| |
| return result.toString(); |
| } |
| |
| String bindersString() { |
| StringBuilder result = new StringBuilder(); |
| for (byte[] curBinder : binders) { |
| result.append("{" + Utilities.toHexString(curBinder) + "}\n"); |
| } |
| |
| return result.toString(); |
| } |
| } |
| |
| private static final |
| class CHPreSharedKeyStringizer implements SSLStringizer { |
| @Override |
| public String toString(ByteBuffer buffer) { |
| try { |
| // As the HandshakeContext parameter of CHPreSharedKeySpec |
| // constructor is used for fatal alert only, we can use |
| // null HandshakeContext here as we don't care about exception. |
| // |
| // Please take care of this code if the CHPreSharedKeySpec |
| // constructor is updated in the future. |
| return (new CHPreSharedKeySpec(null, buffer)).toString(); |
| } catch (Exception ex) { |
| // For debug logging only, so please swallow exceptions. |
| return ex.getMessage(); |
| } |
| } |
| } |
| |
| private static final |
| class SHPreSharedKeySpec implements SSLExtensionSpec { |
| final int selectedIdentity; |
| |
| SHPreSharedKeySpec(int selectedIdentity) { |
| this.selectedIdentity = selectedIdentity; |
| } |
| |
| SHPreSharedKeySpec(HandshakeContext context, |
| ByteBuffer m) throws IOException { |
| if (m.remaining() < 2) { |
| throw context.conContext.fatal(Alert.ILLEGAL_PARAMETER, |
| "Invalid pre_shared_key extension: " + |
| "insufficient selected_identity (length=" + |
| m.remaining() + ")"); |
| } |
| this.selectedIdentity = Record.getInt16(m); |
| } |
| |
| byte[] getEncoded() throws IOException { |
| return new byte[] { |
| (byte)((selectedIdentity >> 8) & 0xFF), |
| (byte)(selectedIdentity & 0xFF) |
| }; |
| } |
| |
| @Override |
| public String toString() { |
| MessageFormat messageFormat = new MessageFormat( |
| "\"PreSharedKey\": '{'\n" + |
| " \"selected_identity\" : \"{0}\",\n" + |
| "'}'", |
| Locale.ENGLISH); |
| |
| Object[] messageFields = { |
| Utilities.byte16HexString(selectedIdentity) |
| }; |
| |
| return messageFormat.format(messageFields); |
| } |
| } |
| |
| private static final |
| class SHPreSharedKeyStringizer implements SSLStringizer { |
| @Override |
| public String toString(ByteBuffer buffer) { |
| try { |
| // As the HandshakeContext parameter of SHPreSharedKeySpec |
| // constructor is used for fatal alert only, we can use |
| // null HandshakeContext here as we don't care about exception. |
| // |
| // Please take care of this code if the SHPreSharedKeySpec |
| // constructor is updated in the future. |
| return (new SHPreSharedKeySpec(null, buffer)).toString(); |
| } catch (Exception ex) { |
| // For debug logging only, so please swallow exceptions. |
| return ex.getMessage(); |
| } |
| } |
| } |
| |
| private static final |
| class CHPreSharedKeyConsumer implements ExtensionConsumer { |
| // Prevent instantiation of this class. |
| private CHPreSharedKeyConsumer() { |
| // blank |
| } |
| |
| @Override |
| public void consume(ConnectionContext context, |
| HandshakeMessage message, |
| ByteBuffer buffer) throws IOException { |
| ClientHelloMessage clientHello = (ClientHelloMessage) message; |
| ServerHandshakeContext shc = (ServerHandshakeContext)context; |
| // Is it a supported and enabled extension? |
| if (!shc.sslConfig.isAvailable(SSLExtension.CH_PRE_SHARED_KEY)) { |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
| SSLLogger.fine( |
| "Ignore unavailable pre_shared_key extension"); |
| } |
| return; // ignore the extension |
| } |
| |
| // Parse the extension. |
| CHPreSharedKeySpec pskSpec = null; |
| try { |
| pskSpec = new CHPreSharedKeySpec(shc, buffer); |
| } catch (IOException ioe) { |
| throw shc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, ioe); |
| } |
| |
| // The "psk_key_exchange_modes" extension should have been loaded. |
| if (!shc.handshakeExtensions.containsKey( |
| SSLExtension.PSK_KEY_EXCHANGE_MODES)) { |
| throw shc.conContext.fatal(Alert.ILLEGAL_PARAMETER, |
| "Client sent PSK but not PSK modes, or the PSK " + |
| "extension is not the last extension"); |
| } |
| |
| // error if id and binder lists are not the same length |
| if (pskSpec.identities.size() != pskSpec.binders.size()) { |
| throw shc.conContext.fatal(Alert.ILLEGAL_PARAMETER, |
| "PSK extension has incorrect number of binders"); |
| } |
| |
| if (shc.isResumption) { // resumingSession may not be set |
| SSLSessionContextImpl sessionCache = (SSLSessionContextImpl) |
| shc.sslContext.engineGetServerSessionContext(); |
| int idIndex = 0; |
| for (PskIdentity requestedId : pskSpec.identities) { |
| SSLSessionImpl s = sessionCache.pull(requestedId.identity); |
| if (s != null && canRejoin(clientHello, shc, s)) { |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
| SSLLogger.fine("Resuming session: ", s); |
| } |
| |
| // binder will be checked later |
| shc.resumingSession = s; |
| shc.handshakeExtensions.put(SH_PRE_SHARED_KEY, |
| new SHPreSharedKeySpec(idIndex)); // for the index |
| break; |
| } |
| |
| ++idIndex; |
| } |
| |
| if (idIndex == pskSpec.identities.size()) { |
| // no resumable session |
| shc.isResumption = false; |
| shc.resumingSession = null; |
| } |
| } |
| |
| // update the context |
| shc.handshakeExtensions.put( |
| SSLExtension.CH_PRE_SHARED_KEY, pskSpec); |
| } |
| } |
| |
| private static boolean canRejoin(ClientHelloMessage clientHello, |
| ServerHandshakeContext shc, SSLSessionImpl s) { |
| |
| boolean result = s.isRejoinable() && (s.getPreSharedKey() != null); |
| |
| // Check protocol version |
| if (result && s.getProtocolVersion() != shc.negotiatedProtocol) { |
| if (SSLLogger.isOn && |
| SSLLogger.isOn("ssl,handshake,verbose")) { |
| |
| SSLLogger.finest("Can't resume, incorrect protocol version"); |
| } |
| result = false; |
| } |
| |
| // Make sure that the server handshake context's localSupportedSignAlgs |
| // field is populated. This is particularly important when |
| // client authentication was used in an initial session and it is |
| // now being resumed. |
| if (shc.localSupportedSignAlgs == null) { |
| shc.localSupportedSignAlgs = |
| SignatureScheme.getSupportedAlgorithms( |
| shc.sslConfig, |
| shc.algorithmConstraints, shc.activeProtocols); |
| } |
| |
| // Validate the required client authentication. |
| if (result && |
| (shc.sslConfig.clientAuthType == CLIENT_AUTH_REQUIRED)) { |
| try { |
| s.getPeerPrincipal(); |
| } catch (SSLPeerUnverifiedException e) { |
| if (SSLLogger.isOn && |
| SSLLogger.isOn("ssl,handshake,verbose")) { |
| SSLLogger.finest( |
| "Can't resume, " + |
| "client authentication is required"); |
| } |
| result = false; |
| } |
| |
| // Make sure the list of supported signature algorithms matches |
| Collection<SignatureScheme> sessionSigAlgs = |
| s.getLocalSupportedSignatureSchemes(); |
| if (result && |
| !shc.localSupportedSignAlgs.containsAll(sessionSigAlgs)) { |
| |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
| SSLLogger.fine("Can't resume. Session uses different " + |
| "signature algorithms"); |
| } |
| result = false; |
| } |
| } |
| |
| // ensure that the endpoint identification algorithm matches the |
| // one in the session |
| String identityAlg = shc.sslConfig.identificationProtocol; |
| if (result && identityAlg != null) { |
| String sessionIdentityAlg = s.getIdentificationProtocol(); |
| if (!identityAlg.equalsIgnoreCase(sessionIdentityAlg)) { |
| if (SSLLogger.isOn && |
| SSLLogger.isOn("ssl,handshake,verbose")) { |
| |
| SSLLogger.finest("Can't resume, endpoint id" + |
| " algorithm does not match, requested: " + |
| identityAlg + ", cached: " + sessionIdentityAlg); |
| } |
| result = false; |
| } |
| } |
| |
| // Ensure cipher suite can be negotiated |
| if (result && (!shc.isNegotiable(s.getSuite()) || |
| !clientHello.cipherSuites.contains(s.getSuite()))) { |
| if (SSLLogger.isOn && |
| SSLLogger.isOn("ssl,handshake,verbose")) { |
| SSLLogger.finest( |
| "Can't resume, unavailable session cipher suite"); |
| } |
| result = false; |
| } |
| |
| return result; |
| } |
| |
| private static final |
| class CHPreSharedKeyUpdate implements HandshakeConsumer { |
| // Prevent instantiation of this class. |
| private CHPreSharedKeyUpdate() { |
| // blank |
| } |
| |
| @Override |
| public void consume(ConnectionContext context, |
| HandshakeMessage message) throws IOException { |
| ServerHandshakeContext shc = (ServerHandshakeContext)context; |
| if (!shc.isResumption || shc.resumingSession == null) { |
| // not resuming---nothing to do |
| return; |
| } |
| |
| CHPreSharedKeySpec chPsk = (CHPreSharedKeySpec) |
| shc.handshakeExtensions.get(SSLExtension.CH_PRE_SHARED_KEY); |
| SHPreSharedKeySpec shPsk = (SHPreSharedKeySpec) |
| shc.handshakeExtensions.get(SSLExtension.SH_PRE_SHARED_KEY); |
| if (chPsk == null || shPsk == null) { |
| throw shc.conContext.fatal(Alert.INTERNAL_ERROR, |
| "Required extensions are unavailable"); |
| } |
| |
| byte[] binder = chPsk.binders.get(shPsk.selectedIdentity); |
| |
| // set up PSK binder hash |
| HandshakeHash pskBinderHash = shc.handshakeHash.copy(); |
| byte[] lastMessage = pskBinderHash.removeLastReceived(); |
| ByteBuffer messageBuf = ByteBuffer.wrap(lastMessage); |
| // skip the type and length |
| messageBuf.position(4); |
| // read to find the beginning of the binders |
| ClientHelloMessage.readPartial(shc.conContext, messageBuf); |
| int length = messageBuf.position(); |
| messageBuf.position(0); |
| pskBinderHash.receive(messageBuf, length); |
| |
| checkBinder(shc, shc.resumingSession, pskBinderHash, binder); |
| } |
| } |
| |
| private static void checkBinder(ServerHandshakeContext shc, |
| SSLSessionImpl session, |
| HandshakeHash pskBinderHash, byte[] binder) throws IOException { |
| SecretKey psk = session.getPreSharedKey(); |
| if (psk == null) { |
| throw shc.conContext.fatal(Alert.INTERNAL_ERROR, |
| "Session has no PSK"); |
| } |
| |
| SecretKey binderKey = deriveBinderKey(shc, psk, session); |
| byte[] computedBinder = |
| computeBinder(shc, binderKey, session, pskBinderHash); |
| if (!MessageDigest.isEqual(binder, computedBinder)) { |
| throw shc.conContext.fatal(Alert.ILLEGAL_PARAMETER, |
| "Incorect PSK binder value"); |
| } |
| } |
| |
| // Class that produces partial messages used to compute binder hash |
| static final class PartialClientHelloMessage extends HandshakeMessage { |
| |
| private final ClientHello.ClientHelloMessage msg; |
| private final CHPreSharedKeySpec psk; |
| |
| PartialClientHelloMessage(HandshakeContext ctx, |
| ClientHello.ClientHelloMessage msg, |
| CHPreSharedKeySpec psk) { |
| super(ctx); |
| |
| this.msg = msg; |
| this.psk = psk; |
| } |
| |
| @Override |
| SSLHandshake handshakeType() { |
| return msg.handshakeType(); |
| } |
| |
| private int pskTotalLength() { |
| return psk.getIdsEncodedLength() + |
| psk.getBindersEncodedLength() + 8; |
| } |
| |
| @Override |
| int messageLength() { |
| |
| if (msg.extensions.get(SSLExtension.CH_PRE_SHARED_KEY) != null) { |
| return msg.messageLength(); |
| } else { |
| return msg.messageLength() + pskTotalLength(); |
| } |
| } |
| |
| @Override |
| void send(HandshakeOutStream hos) throws IOException { |
| msg.sendCore(hos); |
| |
| // complete extensions |
| int extsLen = msg.extensions.length(); |
| if (msg.extensions.get(SSLExtension.CH_PRE_SHARED_KEY) == null) { |
| extsLen += pskTotalLength(); |
| } |
| hos.putInt16(extsLen - 2); |
| // write the complete extensions |
| for (SSLExtension ext : SSLExtension.values()) { |
| byte[] extData = msg.extensions.get(ext); |
| if (extData == null) { |
| continue; |
| } |
| // the PSK could be there from an earlier round |
| if (ext == SSLExtension.CH_PRE_SHARED_KEY) { |
| continue; |
| } |
| int extID = ext.id; |
| hos.putInt16(extID); |
| hos.putBytes16(extData); |
| } |
| |
| // partial PSK extension |
| int extID = SSLExtension.CH_PRE_SHARED_KEY.id; |
| hos.putInt16(extID); |
| byte[] encodedPsk = psk.getEncoded(); |
| hos.putInt16(encodedPsk.length); |
| hos.write(encodedPsk, 0, psk.getIdsEncodedLength() + 2); |
| } |
| } |
| |
| private static final |
| class CHPreSharedKeyProducer implements HandshakeProducer { |
| // Prevent instantiation of this class. |
| private CHPreSharedKeyProducer() { |
| // blank |
| } |
| |
| @Override |
| public byte[] produce(ConnectionContext context, |
| HandshakeMessage message) throws IOException { |
| |
| // The producing happens in client side only. |
| ClientHandshakeContext chc = (ClientHandshakeContext)context; |
| if (!chc.isResumption || chc.resumingSession == null) { |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
| SSLLogger.fine("No session to resume."); |
| } |
| return null; |
| } |
| |
| // Make sure the list of supported signature algorithms matches |
| Collection<SignatureScheme> sessionSigAlgs = |
| chc.resumingSession.getLocalSupportedSignatureSchemes(); |
| if (!chc.localSupportedSignAlgs.containsAll(sessionSigAlgs)) { |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
| SSLLogger.fine("Existing session uses different " + |
| "signature algorithms"); |
| } |
| return null; |
| } |
| |
| // The session must have a pre-shared key |
| SecretKey psk = chc.resumingSession.getPreSharedKey(); |
| if (psk == null) { |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
| SSLLogger.fine("Existing session has no PSK."); |
| } |
| return null; |
| } |
| |
| // The PSK ID can only be used in one connections, but this method |
| // may be called twice in a connection if the server sends HRR. |
| // ID is saved in the context so it can be used in the second call. |
| if (chc.pskIdentity == null) { |
| chc.pskIdentity = chc.resumingSession.consumePskIdentity(); |
| } |
| |
| if (chc.pskIdentity == null) { |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
| SSLLogger.fine( |
| "PSK has no identity, or identity was already used"); |
| } |
| return null; |
| } |
| |
| //The session cannot be used again. Remove it from the cache. |
| SSLSessionContextImpl sessionCache = (SSLSessionContextImpl) |
| chc.sslContext.engineGetClientSessionContext(); |
| sessionCache.remove(chc.resumingSession.getSessionId()); |
| |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
| SSLLogger.fine( |
| "Found resumable session. Preparing PSK message."); |
| } |
| |
| List<PskIdentity> identities = new ArrayList<>(); |
| int ageMillis = (int)(System.currentTimeMillis() - |
| chc.resumingSession.getTicketCreationTime()); |
| int obfuscatedAge = |
| ageMillis + chc.resumingSession.getTicketAgeAdd(); |
| identities.add(new PskIdentity(chc.pskIdentity, obfuscatedAge)); |
| |
| SecretKey binderKey = |
| deriveBinderKey(chc, psk, chc.resumingSession); |
| ClientHelloMessage clientHello = (ClientHelloMessage)message; |
| CHPreSharedKeySpec pskPrototype = createPskPrototype( |
| chc.resumingSession.getSuite().hashAlg.hashLength, identities); |
| HandshakeHash pskBinderHash = chc.handshakeHash.copy(); |
| |
| byte[] binder = computeBinder(chc, binderKey, pskBinderHash, |
| chc.resumingSession, chc, clientHello, pskPrototype); |
| |
| List<byte[]> binders = new ArrayList<>(); |
| binders.add(binder); |
| |
| CHPreSharedKeySpec pskMessage = |
| new CHPreSharedKeySpec(identities, binders); |
| chc.handshakeExtensions.put(CH_PRE_SHARED_KEY, pskMessage); |
| return pskMessage.getEncoded(); |
| } |
| |
| private CHPreSharedKeySpec createPskPrototype( |
| int hashLength, List<PskIdentity> identities) { |
| List<byte[]> binders = new ArrayList<>(); |
| byte[] binderProto = new byte[hashLength]; |
| for (PskIdentity curId : identities) { |
| binders.add(binderProto); |
| } |
| |
| return new CHPreSharedKeySpec(identities, binders); |
| } |
| } |
| |
| private static byte[] computeBinder( |
| HandshakeContext context, SecretKey binderKey, |
| SSLSessionImpl session, |
| HandshakeHash pskBinderHash) throws IOException { |
| |
| pskBinderHash.determine( |
| session.getProtocolVersion(), session.getSuite()); |
| pskBinderHash.update(); |
| byte[] digest = pskBinderHash.digest(); |
| |
| return computeBinder(context, binderKey, session, digest); |
| } |
| |
| private static byte[] computeBinder( |
| HandshakeContext context, SecretKey binderKey, |
| HandshakeHash hash, SSLSessionImpl session, |
| HandshakeContext ctx, ClientHello.ClientHelloMessage hello, |
| CHPreSharedKeySpec pskPrototype) throws IOException { |
| |
| PartialClientHelloMessage partialMsg = |
| new PartialClientHelloMessage(ctx, hello, pskPrototype); |
| |
| SSLEngineOutputRecord record = new SSLEngineOutputRecord(hash); |
| HandshakeOutStream hos = new HandshakeOutStream(record); |
| partialMsg.write(hos); |
| |
| hash.determine(session.getProtocolVersion(), session.getSuite()); |
| hash.update(); |
| byte[] digest = hash.digest(); |
| |
| return computeBinder(context, binderKey, session, digest); |
| } |
| |
| private static byte[] computeBinder(HandshakeContext context, |
| SecretKey binderKey, |
| SSLSessionImpl session, byte[] digest) throws IOException { |
| try { |
| CipherSuite.HashAlg hashAlg = session.getSuite().hashAlg; |
| HKDF hkdf = new HKDF(hashAlg.name); |
| byte[] label = ("tls13 finished").getBytes(); |
| byte[] hkdfInfo = SSLSecretDerivation.createHkdfInfo( |
| label, new byte[0], hashAlg.hashLength); |
| SecretKey finishedKey = hkdf.expand( |
| binderKey, hkdfInfo, hashAlg.hashLength, "TlsBinderKey"); |
| |
| String hmacAlg = |
| "Hmac" + hashAlg.name.replace("-", ""); |
| try { |
| Mac hmac = JsseJce.getMac(hmacAlg); |
| hmac.init(finishedKey); |
| return hmac.doFinal(digest); |
| } catch (NoSuchAlgorithmException | InvalidKeyException ex) { |
| throw context.conContext.fatal(Alert.INTERNAL_ERROR, ex); |
| } |
| } catch (GeneralSecurityException ex) { |
| throw context.conContext.fatal(Alert.INTERNAL_ERROR, ex); |
| } |
| } |
| |
| private static SecretKey deriveBinderKey(HandshakeContext context, |
| SecretKey psk, SSLSessionImpl session) throws IOException { |
| try { |
| CipherSuite.HashAlg hashAlg = session.getSuite().hashAlg; |
| HKDF hkdf = new HKDF(hashAlg.name); |
| byte[] zeros = new byte[hashAlg.hashLength]; |
| SecretKey earlySecret = hkdf.extract(zeros, psk, "TlsEarlySecret"); |
| |
| byte[] label = ("tls13 res binder").getBytes(); |
| MessageDigest md = MessageDigest.getInstance(hashAlg.name); |
| byte[] hkdfInfo = SSLSecretDerivation.createHkdfInfo( |
| label, md.digest(new byte[0]), hashAlg.hashLength); |
| return hkdf.expand(earlySecret, |
| hkdfInfo, hashAlg.hashLength, "TlsBinderKey"); |
| } catch (GeneralSecurityException ex) { |
| throw context.conContext.fatal(Alert.INTERNAL_ERROR, ex); |
| } |
| } |
| |
| private static final |
| class CHPreSharedKeyOnLoadAbsence implements HandshakeAbsence { |
| @Override |
| public void absent(ConnectionContext context, |
| HandshakeMessage message) throws IOException { |
| |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
| SSLLogger.fine( |
| "Handling pre_shared_key absence."); |
| } |
| |
| ServerHandshakeContext shc = (ServerHandshakeContext)context; |
| |
| // Resumption is only determined by PSK, when enabled |
| shc.resumingSession = null; |
| shc.isResumption = false; |
| } |
| } |
| |
| /** |
| * The absence processing if the extension is not present in |
| * a ClientHello handshake message. |
| */ |
| private static final class CHPreSharedKeyOnTradeAbsence |
| implements HandshakeAbsence { |
| @Override |
| public void absent(ConnectionContext context, |
| HandshakeMessage message) throws IOException { |
| // The producing happens in server side only. |
| ServerHandshakeContext shc = (ServerHandshakeContext)context; |
| |
| // A client is considered to be attempting to negotiate using this |
| // specification if the ClientHello contains a "supported_versions" |
| // extension with 0x0304 contained in its body. Such a ClientHello |
| // message MUST meet the following requirements: |
| // - If not containing a "pre_shared_key" extension, it MUST |
| // contain both a "signature_algorithms" extension and a |
| // "supported_groups" extension. |
| if (shc.negotiatedProtocol.useTLS13PlusSpec() && |
| (!shc.handshakeExtensions.containsKey( |
| SSLExtension.CH_SIGNATURE_ALGORITHMS) || |
| !shc.handshakeExtensions.containsKey( |
| SSLExtension.CH_SUPPORTED_GROUPS))) { |
| throw shc.conContext.fatal(Alert.MISSING_EXTENSION, |
| "No supported_groups or signature_algorithms extension " + |
| "when pre_shared_key extension is not present"); |
| } |
| } |
| } |
| |
| private static final |
| class SHPreSharedKeyConsumer implements ExtensionConsumer { |
| // Prevent instantiation of this class. |
| private SHPreSharedKeyConsumer() { |
| // blank |
| } |
| |
| @Override |
| public void consume(ConnectionContext context, |
| HandshakeMessage message, ByteBuffer buffer) throws IOException { |
| // The consuming happens in client side only. |
| ClientHandshakeContext chc = (ClientHandshakeContext)context; |
| |
| // Is it a response of the specific request? |
| if (!chc.handshakeExtensions.containsKey( |
| SSLExtension.CH_PRE_SHARED_KEY)) { |
| throw chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, |
| "Server sent unexpected pre_shared_key extension"); |
| } |
| |
| SHPreSharedKeySpec shPsk = new SHPreSharedKeySpec(chc, buffer); |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
| SSLLogger.fine( |
| "Received pre_shared_key extension: ", shPsk); |
| } |
| |
| if (shPsk.selectedIdentity != 0) { |
| throw chc.conContext.fatal(Alert.ILLEGAL_PARAMETER, |
| "Selected identity index is not in correct range."); |
| } |
| |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
| SSLLogger.fine( |
| "Resuming session: ", chc.resumingSession); |
| } |
| } |
| } |
| |
| private static final |
| class SHPreSharedKeyAbsence implements HandshakeAbsence { |
| @Override |
| public void absent(ConnectionContext context, |
| HandshakeMessage message) throws IOException { |
| ClientHandshakeContext chc = (ClientHandshakeContext)context; |
| |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
| SSLLogger.fine("Handling pre_shared_key absence."); |
| } |
| |
| // The server refused to resume, or the client did not |
| // request 1.3 resumption. |
| chc.resumingSession = null; |
| chc.isResumption = false; |
| } |
| } |
| |
| private static final |
| class SHPreSharedKeyProducer implements HandshakeProducer { |
| // Prevent instantiation of this class. |
| private SHPreSharedKeyProducer() { |
| // blank |
| } |
| |
| @Override |
| public byte[] produce(ConnectionContext context, |
| HandshakeMessage message) throws IOException { |
| ServerHandshakeContext shc = (ServerHandshakeContext)context; |
| SHPreSharedKeySpec psk = (SHPreSharedKeySpec) |
| shc.handshakeExtensions.get(SH_PRE_SHARED_KEY); |
| if (psk == null) { |
| return null; |
| } |
| |
| return psk.getEncoded(); |
| } |
| } |
| } |