8207317: SSLEngine negotiation fail exception behavior changed from fail-fast to fail-lazy
Reviewed-by: xuelei
/*
* 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.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.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 {
final SSLTransport transport;
// registered plaintext consumers
final Map<Byte, SSLConsumer> consumers;
final AccessControlContext acc;
final SSLContextImpl sslContext;
final SSLConfiguration sslConfig;
final InputRecord inputRecord;
final OutputRecord outputRecord;
// connection status
boolean isUnsureMode;
boolean isNegotiated = false;
boolean isBroken = false;
boolean isInputCloseNotified = false;
boolean peerUserCanceled = false;
Exception closeReason = null;
Exception delegatedThrown = null;
// negotiated security parameters
SSLSessionImpl conSession;
ProtocolVersion protocolVersion;
String applicationProtocol= null;
// handshake context
HandshakeContext handshakeContext = null;
// connection reserved status for handshake.
boolean secureRenegotiation = false;
byte[] clientVerifyData;
byte[] serverVerifyData;
// connection sensitive configuration
List<NamedGroup> serverRequestedNamedGroups;
CipherSuite cipherSuite;
private static final byte[] emptyByteArray = new byte[0];
// Please never use the transport parameter other than storing a
// reference to this object.
//
// Called by SSLEngineImpl
TransportContext(SSLContextImpl sslContext, SSLTransport transport,
InputRecord inputRecord, OutputRecord outputRecord) {
this(sslContext, transport, new SSLConfiguration(sslContext, true),
inputRecord, outputRecord, true);
}
// Please never use the transport parameter other than storing a
// reference to this object.
//
// Called by SSLSocketImpl
TransportContext(SSLContextImpl sslContext, SSLTransport transport,
InputRecord inputRecord, OutputRecord outputRecord,
boolean isClientMode) {
this(sslContext, transport,
new SSLConfiguration(sslContext, isClientMode),
inputRecord, outputRecord, false);
}
// Please never use the transport parameter other than storing a
// reference to this object.
//
// Called by SSLSocketImpl with an existing SSLConfig
TransportContext(SSLContextImpl sslContext, SSLTransport transport,
SSLConfiguration sslConfig,
InputRecord inputRecord, OutputRecord outputRecord) {
this(sslContext, transport, (SSLConfiguration)sslConfig.clone(),
inputRecord, outputRecord, false);
}
private TransportContext(SSLContextImpl sslContext, SSLTransport transport,
SSLConfiguration sslConfig, InputRecord inputRecord,
OutputRecord outputRecord, boolean isUnsureMode) {
this.transport = transport;
this.sslContext = sslContext;
this.inputRecord = inputRecord;
this.outputRecord = outputRecord;
this.sslConfig = sslConfig;
if (this.sslConfig.maximumPacketSize == 0) {
this.sslConfig.maximumPacketSize = outputRecord.getMaxPacketSize();
}
this.isUnsureMode = isUnsureMode;
// initial security parameters
this.conSession = SSLSessionImpl.nullSession;
this.protocolVersion = this.sslConfig.maximumProtocolVersion;
this.clientVerifyData = emptyByteArray;
this.serverVerifyData = emptyByteArray;
this.acc = AccessController.getContext();
this.consumers = new HashMap<>();
}
// 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;
}
switch (ct) {
case HANDSHAKE:
byte type = HandshakeContext.getHandshakeType(this,
plaintext);
if (handshakeContext == null) {
if (type == SSLHandshake.KEY_UPDATE.id ||
type == SSLHandshake.NEW_SESSION_TICKET.id) {
if (isNegotiated &&
protocolVersion.useTLS13PlusSpec()) {
handshakeContext = new PostHandshakeContext(this);
} else {
fatal(Alert.UNEXPECTED_MESSAGE,
"Unexpected post-handshake message: " +
SSLHandshake.nameOf(type));
}
} else {
handshakeContext = sslConfig.isClientMode ?
new ClientHandshakeContext(sslContext, this) :
new ServerHandshakeContext(sslContext, this);
outputRecord.initHandshaker();
}
}
handshakeContext.dispatch(type, 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.");
}
if (outputRecord.isClosed() || inputRecord.isClosed() || isBroken) {
if (closeReason != null) {
throw new SSLException(
"Cannot kickstart, the connection is broken or closed",
closeReason);
} else {
throw new SSLException(
"Cannot kickstart, the connection is broken or closed");
}
}
// initialize the handshaker if necessary
if (handshakeContext == null) {
// TLS1.3 post-handshake
if (isNegotiated && protocolVersion.useTLS13PlusSpec()) {
handshakeContext = new PostHandshakeContext(this);
} else {
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 established.
if(isNegotiated || sslConfig.isClientMode) {
handshakeContext.kickstart();
}
}
boolean isPostHandshakeContext() {
return handshakeContext != null &&
(handshakeContext instanceof PostHandshakeContext);
}
// Note: close_notify is delivered as a warning alert.
void warning(Alert alert) {
// For initial handshaking, don't send a warning 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, unexpected rethrowing", 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 && !isOutboundClosed() && !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: output record closure failed", ioe);
}
}
// terminate the handshake context
if (handshakeContext != null) {
handshakeContext = null;
}
// terminate 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) {
// 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 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) {
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;
}
// The OutputRecord is closed and not buffered output record.
boolean isOutboundDone() {
return outputRecord.isClosed() && outputRecord.isEmpty();
}
// The OutputRecord is closed, but buffered output record may be still
// waiting for delivery to the underlying connection.
boolean isOutboundClosed() {
return outputRecord.isClosed();
}
boolean isInboundClosed() {
return inputRecord.isClosed();
}
// Close inbound, no more data should be delivered to the underlying
// transportation connection.
void closeInbound() throws SSLException {
if (isInboundClosed()) {
return;
}
try {
// Important note: check if the initial handshake is started at
// first so that the passiveInboundClose() implementation need not
// to consider the case any more.
if (!isInputCloseNotified) {
// the initial handshake is not started
initiateInboundClose();
} else {
passiveInboundClose();
}
} catch (IOException ioe) {
if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
SSLLogger.warning("inbound 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.
//
// Note that this method is called only if the initial handshake has
// started or completed.
private void passiveInboundClose() throws IOException {
if (!isInboundClosed()) {
inputRecord.close();
}
// 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 (!isOutboundClosed()) {
boolean needCloseNotify = SSLConfiguration.acknowledgeCloseNotify;
if (!needCloseNotify) {
if (isNegotiated) {
if (!protocolVersion.useTLS13PlusSpec()) {
needCloseNotify = true;
}
} else if (handshakeContext != null) { // initial handshake
ProtocolVersion pv = handshakeContext.negotiatedProtocol;
if (pv == null || (!pv.useTLS13PlusSpec())) {
needCloseNotify = true;
}
}
}
if (needCloseNotify) {
synchronized (outputRecord) {
try {
// send a close_notify alert
warning(Alert.CLOSE_NOTIFY);
} finally {
outputRecord.close();
}
}
}
}
}
// Initiate a inbound close when the handshake is not started.
private void initiateInboundClose() throws IOException {
if (!isInboundClosed()) {
inputRecord.close();
}
}
// Close outbound, no more data should be received from the underlying
// transportation connection.
void closeOutbound() {
if (isOutboundClosed()) {
return;
}
try {
initiateOutboundClose();
} catch (IOException ioe) {
if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
SSLLogger.warning("outbound closure failed", ioe);
}
}
}
// Initiate a close by sending a close_notify alert.
private void initiateOutboundClose() throws IOException {
boolean useUserCanceled = false;
if (!isNegotiated && (handshakeContext != null) && !peerUserCanceled) {
// initial handshake
useUserCanceled = true;
}
// Need a lock here so that the user_canceled alert and the
// close_notify alert can be delivered together.
synchronized (outputRecord) {
try {
// send a user_canceled alert if needed.
if (useUserCanceled) {
warning(Alert.USER_CANCELED);
}
// send a close_notify alert
warning(Alert.CLOSE_NOTIFY);
} finally {
outputRecord.close();
}
}
}
// Note; HandshakeStatus.FINISHED status is retrieved in other places.
HandshakeStatus getHandshakeStatus() {
if (!outputRecord.isEmpty()) {
// If no handshaking, special case to wrap alters or
// post-handshake messages.
return HandshakeStatus.NEED_WRAP;
} else if (isOutboundClosed() && isInboundClosed()) {
return HandshakeStatus.NOT_HANDSHAKING;
} else if (handshakeContext != null) {
if (!handshakeContext.delegatedActions.isEmpty()) {
return HandshakeStatus.NEED_TASK;
} else if (!isInboundClosed()) {
if (sslContext.isDTLS() &&
!inputRecord.isEmpty()) {
return HandshakeStatus.NEED_UNWRAP_AGAIN;
} else {
return HandshakeStatus.NEED_UNWRAP;
}
} else if (!isOutboundClosed()) {
// Special case that the inbound was closed, but outbound open.
return HandshakeStatus.NEED_WRAP;
}
} else if (isOutboundClosed() && !isInboundClosed()) {
// Special case that the outbound was closed, but inbound open.
return HandshakeStatus.NEED_UNWRAP;
} else if (!isOutboundClosed() && isInboundClosed()) {
// Special case that the inbound was closed, but outbound open.
return HandshakeStatus.NEED_WRAP;
}
return HandshakeStatus.NOT_HANDSHAKING;
}
HandshakeStatus finishHandshake() {
if (protocolVersion.useTLS13PlusSpec()) {
outputRecord.tc = this;
inputRecord.tc = this;
cipherSuite = handshakeContext.negotiatedCipherSuite;
inputRecord.readCipher.baseSecret =
handshakeContext.baseReadSecret;
outputRecord.writeCipher.baseSecret =
handshakeContext.baseWriteSecret;
}
handshakeContext = null;
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;
}
HandshakeStatus finishPostHandshake() {
handshakeContext = null;
// Note: May need trigger handshake completion even for post-handshake
// authentication in the future.
return HandshakeStatus.FINISHED;
}
// A separate thread is allocated to deliver handshake completion
// events.
private static class NotifyHandshake implements Runnable {
private final Set<Map.Entry<HandshakeCompletedListener,
AccessControlContext>> targets; // who gets notified
private final HandshakeCompletedEvent event; // the notification
NotifyHandshake(
Map<HandshakeCompletedListener,AccessControlContext> 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<HandshakeCompletedListener,
AccessControlContext> entry : targets) {
final HandshakeCompletedListener listener = entry.getKey();
AccessControlContext acc = entry.getValue();
AccessController.doPrivileged(new PrivilegedAction<Void>() {
@Override
public Void run() {
listener.handshakeCompleted(event);
return null;
}
}, acc);
}
}
}
}