/* * Copyright (c) 2018, 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.Closeable; import java.io.IOException; import java.security.AccessControlContext; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.crypto.SecretKey; import javax.net.ssl.HandshakeCompletedEvent; import javax.net.ssl.HandshakeCompletedListener; import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocket; import sun.security.ssl.SupportedGroupsExtension.NamedGroup; /** * SSL/(D)TLS transportation context. */ class TransportContext implements ConnectionContext, Closeable { final SSLTransport transport; // registered plaintext consumers final Map consumers; final AccessControlContext acc; final SSLContextImpl sslContext; final SSLConfiguration sslConfig; final InputRecord inputRecord; final OutputRecord outputRecord; // connection status boolean isUnsureMode; boolean isNegotiated; boolean isBroken; boolean isInputCloseNotified; boolean isOutputCloseNotified; Exception closeReason; //SSLException or RuntimeException // negotiated security parameters SSLSessionImpl conSession; ProtocolVersion protocolVersion; String applicationProtocol; // handshake context HandshakeContext handshakeContext; // connection reserved status for handshake. boolean secureRenegotiation; byte[] clientVerifyData; byte[] serverVerifyData; // connection sensitive configuration List serverRequestedNamedGroups; SecretKey baseWriteSecret, baseReadSecret; CipherSuite cipherSuite; // Please never use the transport parameter other than storing a // reference to this object. TransportContext(SSLContextImpl sslContext, SSLTransport transport, InputRecord inputRecord, OutputRecord outputRecord) { this.transport = transport; this.sslContext = sslContext; this.inputRecord = inputRecord; this.outputRecord = outputRecord; this.sslConfig = new SSLConfiguration(sslContext, true); this.sslConfig.maximumPacketSize = outputRecord.getMaxPacketSize(); this.isUnsureMode = true; initialize(); this.acc = AccessController.getContext(); this.consumers = new HashMap<>(); } // Please never use the transport parameter other than storing a // reference to this object. TransportContext(SSLContextImpl sslContext, SSLTransport transport, InputRecord inputRecord, OutputRecord outputRecord, boolean isClientMode) { this.transport = transport; this.sslContext = sslContext; this.inputRecord = inputRecord; this.outputRecord = outputRecord; this.sslConfig = new SSLConfiguration(sslContext, isClientMode); this.sslConfig.maximumPacketSize = outputRecord.getMaxPacketSize(); this.isUnsureMode = false; initialize(); this.acc = AccessController.getContext(); this.consumers = new HashMap<>(); } // Please never use the transport parameter other than storing a // reference to this object. TransportContext(SSLContextImpl sslContext, SSLTransport transport, SSLConfiguration sslConfig, InputRecord inputRecord, OutputRecord outputRecord) { this.transport = transport; this.sslContext = sslContext; this.inputRecord = inputRecord; this.outputRecord = outputRecord; this.sslConfig = (SSLConfiguration)sslConfig.clone(); if (this.sslConfig.maximumPacketSize == 0) { this.sslConfig.maximumPacketSize = outputRecord.getMaxPacketSize(); } this.isUnsureMode = false; initialize(); this.acc = AccessController.getContext(); this.consumers = new HashMap<>(); } // Initialize the non-final class variables. private void initialize() { // initial security parameters this.conSession = SSLSessionImpl.nullSession; this.protocolVersion = this.sslConfig.maximumProtocolVersion; this.applicationProtocol = null; // initial handshake context this.handshakeContext = null; // initial security parameters for secure renegotiation this.secureRenegotiation = false; this.clientVerifyData = new byte[0]; this.serverVerifyData = new byte[0]; this.isNegotiated = false; this.isBroken = false; this.isInputCloseNotified = false; this.isOutputCloseNotified = false; this.closeReason = null; } // Dispatch plaintext to a specific consumer. void dispatch(Plaintext plaintext) throws IOException { if (plaintext == null) { return; } ContentType ct = ContentType.valueOf(plaintext.contentType); if (ct == null) { fatal(Alert.UNEXPECTED_MESSAGE, "Unknown content type: " + plaintext.contentType); return; // make compiler happy } switch (ct) { case HANDSHAKE: if (handshakeContext == null) { handshakeContext = sslConfig.isClientMode ? new ClientHandshakeContext(sslContext, this) : new ServerHandshakeContext(sslContext, this); outputRecord.initHandshaker(); } handshakeContext.dispatch(plaintext); break; case ALERT: Alert.alertConsumer.consume(this, plaintext.fragment); break; default: SSLConsumer consumer = consumers.get(plaintext.contentType); if (consumer != null) { consumer.consume(this, plaintext.fragment); } else { fatal(Alert.UNEXPECTED_MESSAGE, "Unexpected content: " + plaintext.contentType); } } } void kickstart() throws IOException { if (isUnsureMode) { throw new IllegalStateException("Client/Server mode not yet set."); } // initialize the handshaker if necessary if (handshakeContext == null) { handshakeContext = sslConfig.isClientMode ? new ClientHandshakeContext(sslContext, this) : new ServerHandshakeContext(sslContext, this); outputRecord.initHandshaker(); } // kickstart the handshake if needed // // Need no kickstart message on server side unless the connection // has been estabilished. if(isNegotiated || sslConfig.isClientMode) { handshakeContext.kickstart(); } } void keyUpdate() throws IOException { // TODO: TLS 1.3 kickstart(); } // Note: close_notify is delivered as awarning alert. void warning(Alert alert) { // For initial handshaking, don't send awarning alert message to peer // if handshaker has not started. if (isNegotiated || handshakeContext != null) { try { outputRecord.encodeAlert(Alert.Level.WARNING.level, alert.id); } catch (IOException ioe) { if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning( "Warning: failed to send warning alert " + alert, ioe); } } } } void fatal(Alert alert, String diagnostic) throws SSLException { fatal(alert, diagnostic, null); } void fatal(Alert alert, Throwable cause) throws SSLException { fatal(alert, null, cause); } void fatal(Alert alert, String diagnostic, Throwable cause) throws SSLException { fatal(alert, diagnostic, false, cause); } // Note: close_notify is not delivered via fatal() methods. void fatal(Alert alert, String diagnostic, boolean recvFatalAlert, Throwable cause) throws SSLException { // If we've already shutdown because of an error, there is nothing we // can do except rethrow the exception. // // Most exceptions seen here will be SSLExceptions. We may find the // occasional Exception which hasn't been converted to a SSLException, // so we'll do it here. if (closeReason != null) { if (cause == null) { if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning( "Closed transport, general or untracked problem"); } throw alert.createSSLException( "Closed transport, general or untracked problem"); } if (cause instanceof SSLException) { throw (SSLException)cause; } else { // unlikely, but just in case. if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning( "Closed transport, rethrowing (unexpected)", cause); } throw alert.createSSLException("Unexpected rethrowing", cause); } } // If we have no further information, make a general-purpose // message for folks to see. We generally have one or the other. if (diagnostic == null) { if (cause == null) { diagnostic = "General/Untracked problem"; } else { diagnostic = cause.getMessage(); } } if (cause == null) { cause = alert.createSSLException(diagnostic); } // shutdown the transport if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.severe("Fatal (" + alert + "): " + diagnostic, cause); } // remember the close reason if (cause instanceof SSLException) { closeReason = (SSLException)cause; } else { // Including RuntimeException, but we'll throw those down below. closeReason = alert.createSSLException(diagnostic, cause); } // close inbound try { inputRecord.close(); } catch (IOException ioe) { if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning("Fatal: input record closure failed", ioe); } } // invalidate the session if (conSession != null) { conSession.invalidate(); } if (handshakeContext != null && handshakeContext.handshakeSession != null) { handshakeContext.handshakeSession.invalidate(); } // send fatal alert // // If we haven't even started handshaking yet, or we are the recipient // of a fatal alert, no need to generate a fatal close alert. if (!recvFatalAlert && !isOutboundDone() && !isBroken && (isNegotiated || handshakeContext != null)) { try { outputRecord.encodeAlert(Alert.Level.FATAL.level, alert.id); } catch (IOException ioe) { if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning( "Fatal: failed to send fatal alert " + alert, ioe); } } } // close outbound try { outputRecord.close(); } catch (IOException ioe) { if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning("Fatal: ouput record closure failed", ioe); } } // terminal handshake context if (handshakeContext != null) { handshakeContext = null; } // terminal the transport try { transport.shutdown(); } catch (IOException ioe) { if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning("Fatal: transport closure failed", ioe); } } finally { isBroken = true; } if (closeReason instanceof SSLException) { throw (SSLException)closeReason; } else { throw (RuntimeException)closeReason; } } void setUseClientMode(boolean useClientMode) { /* * If we need to change the client mode and the enabled * protocols and cipher suites haven't specifically been * set by the user, change them to the corresponding * default ones. */ if (sslConfig.isClientMode != useClientMode) { // Once handshaking has begun, the mode can not be reset for the // life of this engine. if (handshakeContext != null || isNegotiated) { throw new IllegalArgumentException( "Cannot change mode after SSL traffic has started"); } if (sslContext.isDefaultProtocolVesions( sslConfig.enabledProtocols)) { sslConfig.enabledProtocols = sslContext.getDefaultProtocolVersions(!useClientMode); } if (sslContext.isDefaultCipherSuiteList( sslConfig.enabledCipherSuites)) { sslConfig.enabledCipherSuites = sslContext.getDefaultCipherSuites(!useClientMode); } sslConfig.isClientMode = useClientMode; } isUnsureMode = false; } boolean isOutboundDone() { return outputRecord.isClosed(); } boolean isInboundDone() { return inputRecord.isClosed(); } boolean isClosed() { return isOutboundDone() && isInboundDone(); } @Override public void close() throws IOException { if (!isOutboundDone()) { closeOutbound(); } if (!isInboundDone()) { closeInbound(); } } void closeInbound() throws SSLException { if (isInboundDone()) { return; } try { if (isInputCloseNotified) { // passive close passiveInboundClose(); } else { // initiative close initiateInboundClose(); } } catch (IOException ioe) { if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning("inbound closure failed", ioe); } } } void closeOutbound() { if (isOutboundDone()) { return; } try { initiateOutboundClose(); } catch (IOException ioe) { if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { SSLLogger.warning("outbound closure failed", ioe); } } } // Close the connection passively. The closure could be kickoff by // receiving a close_notify alert or reaching end_of_file of the socket. private void passiveInboundClose() throws IOException { if (!isInboundDone()) { inputRecord.close(); } // For TLS 1.3, output closure is independent from input closure. // if (isNegotiated && protocolVersion.useTLS13PlusSpec()) { // return; // } // For TLS 1.2 and prior version, it is required to respond with // a close_notify alert of its own and close down the connection // immediately, discarding any pending writes. if (!isOutboundDone() && !isOutputCloseNotified) { try { // send a close_notify alert warning(Alert.CLOSE_NOTIFY); } finally { // any data received after a closure alert is ignored. isOutputCloseNotified = true; outputRecord.close(); } } transport.shutdown(); } // Initiate a close by sending a close_notify alert. private void initiateInboundClose() throws IOException { // TLS 1.3 does not define how to initiate and close a TLS connection // gracefully. We will always send a close_notify alert, and close // the underlying transportation layer if needed. if (!isOutboundDone() && !isOutputCloseNotified) { try { // send a close_notify alert warning(Alert.CLOSE_NOTIFY); } finally { // any data received after a closure alert is ignored. isOutputCloseNotified = true; outputRecord.close(); } } // For TLS 1.3, input closure is independent from output closure. Both // parties need not wait to receive a "close_notify" alert before // closing their read side of the connection. // // For TLS 1.2 and prior version, it is not required for the initiator // of the close to wait for the responding close_notify alert before // closing the read side of the connection. try { transport.shutdown(); } finally { if (!isInboundDone()) { inputRecord.close(); } } } // Initiate a close by sending a close_notify alert. private void initiateOutboundClose() throws IOException { if (!isOutboundDone() && !isOutputCloseNotified) { try { // close outputRecord // send a close_notify alert warning(Alert.CLOSE_NOTIFY); } finally { // any data received after a closure alert is ignored. isOutputCloseNotified = true; outputRecord.close(); } } // For TLS 1.3, output closure is independent from input closure. // // if (isNegotiated && protocolVersion.useTLS13PlusSpec()) { // return; // } // // It is not required for the initiator of the close to wait for the // responding close_notify alert before closing the read side of the // connection. However, if the application protocol using TLS // provides that any data may be carried over the underlying transport // after the TLS connection is closed, the TLS implementation MUST // receive a "close_notify" alert before indicating end-of-data to the // application-layer. try { transport.shutdown(); } finally { if (!isInboundDone()) { inputRecord.close(); } } } // Note; HandshakeStatus.FINISHED status is retrieved in other places. HandshakeStatus getHandshakeStatus() { if (!outputRecord.isEmpty()) { // If no handshaking, special case to wrap alters. return HandshakeStatus.NEED_WRAP; } else if (handshakeContext != null) { if (!handshakeContext.delegatedActions.isEmpty()) { return HandshakeStatus.NEED_TASK; } else if (sslContext.isDTLS() && !inputRecord.isEmpty()) { return HandshakeStatus.NEED_UNWRAP_AGAIN; } else { return HandshakeStatus.NEED_UNWRAP; } } else if (isOutboundDone() && !isInboundDone()) { /* * Special case where we're closing, but * still need the close_notify before we * can officially be closed. * * Note isOutboundDone is taken care of by * hasOutboundData() above. */ return HandshakeStatus.NEED_UNWRAP; } return HandshakeStatus.NOT_HANDSHAKING; } HandshakeStatus finishHandshake() { if (protocolVersion.useTLS13PlusSpec()) { outputRecord.tc = this; cipherSuite = handshakeContext.negotiatedCipherSuite; inputRecord.readCipher.baseSecret = handshakeContext.baseReadSecret; outputRecord.writeCipher.baseSecret = handshakeContext.baseWriteSecret; } handshakeContext = null; // inputRecord and outputRecord shares the same handshakeHash // inputRecord.handshakeHash.finish(); outputRecord.handshakeHash.finish(); inputRecord.finishHandshake(); outputRecord.finishHandshake(); isNegotiated = true; // Tell folk about handshake completion, but do it in a separate thread. if (transport instanceof SSLSocket && sslConfig.handshakeListeners != null && !sslConfig.handshakeListeners.isEmpty()) { HandshakeCompletedEvent hce = new HandshakeCompletedEvent((SSLSocket)transport, conSession); Thread thread = new Thread( null, new NotifyHandshake(sslConfig.handshakeListeners, hce), "HandshakeCompletedNotify-Thread", 0, false); thread.start(); } return HandshakeStatus.FINISHED; } // A separate thread is allocated to deliver handshake completion // events. private static class NotifyHandshake implements Runnable { private Set> targets; // who gets notified private HandshakeCompletedEvent event; // the notification NotifyHandshake( Map listeners, HandshakeCompletedEvent event) { this.targets = new HashSet<>(listeners.entrySet()); // clone this.event = event; } @Override public void run() { // Don't need to synchronize, as it only runs in one thread. for (Map.Entry entry : targets) { final HandshakeCompletedListener listener = entry.getKey(); AccessControlContext acc = entry.getValue(); AccessController.doPrivileged(new PrivilegedAction() { @Override public Void run() { listener.handshakeCompleted(event); return null; } }, acc); } } } }