All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.netty.handler.ssl.ReferenceCountedOpenSslEngine Maven / Gradle / Ivy

There is a newer version: 5.0.0.Alpha2
Show newest version
/*
 * Copyright 2016 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.handler.ssl;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.ssl.util.LazyJavaxX509Certificate;
import io.netty.handler.ssl.util.LazyX509Certificate;
import io.netty.internal.tcnative.AsyncTask;
import io.netty.internal.tcnative.Buffer;
import io.netty.internal.tcnative.SSL;
import io.netty.util.AbstractReferenceCounted;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCounted;
import io.netty.util.ResourceLeakDetector;
import io.netty.util.ResourceLeakDetectorFactory;
import io.netty.util.ResourceLeakTracker;
import io.netty.util.internal.EmptyArrays;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.SuppressJava6Requirement;
import io.netty.util.internal.ThrowableUtil;
import io.netty.util.internal.UnstableApi;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;

import java.nio.ByteBuffer;
import java.nio.ReadOnlyBufferException;
import java.security.Principal;
import java.security.cert.Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;

import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionBindingEvent;
import javax.net.ssl.SSLSessionBindingListener;
import javax.security.cert.X509Certificate;

import static io.netty.handler.ssl.OpenSsl.memoryAddress;
import static io.netty.handler.ssl.SslUtils.SSL_RECORD_HEADER_LENGTH;
import static io.netty.util.internal.EmptyArrays.EMPTY_STRINGS;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static io.netty.util.internal.ObjectUtil.checkNotNullArrayParam;
import static io.netty.util.internal.ObjectUtil.checkNotNullWithIAE;
import static java.lang.Integer.MAX_VALUE;
import static java.lang.Math.min;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_TASK;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_UNWRAP;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_WRAP;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING;
import static javax.net.ssl.SSLEngineResult.Status.BUFFER_OVERFLOW;
import static javax.net.ssl.SSLEngineResult.Status.BUFFER_UNDERFLOW;
import static javax.net.ssl.SSLEngineResult.Status.CLOSED;
import static javax.net.ssl.SSLEngineResult.Status.OK;

/**
 * Implements a {@link SSLEngine} using
 * OpenSSL BIO abstractions.
 * 

Instances of this class must be {@link #release() released} or else native memory will leak! * *

Instances of this class must be released before the {@link ReferenceCountedOpenSslContext} * the instance depends upon are released. Otherwise if any method of this class is called which uses the * the {@link ReferenceCountedOpenSslContext} JNI resources the JVM may crash. */ public class ReferenceCountedOpenSslEngine extends SSLEngine implements ReferenceCounted, ApplicationProtocolAccessor { private static final InternalLogger logger = InternalLoggerFactory.getInstance(ReferenceCountedOpenSslEngine.class); private static final ResourceLeakDetector leakDetector = ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ReferenceCountedOpenSslEngine.class); private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV2 = 0; private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV3 = 1; private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1 = 2; private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_1 = 3; private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_2 = 4; private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_3 = 5; private static final int[] OPENSSL_OP_NO_PROTOCOLS = { SSL.SSL_OP_NO_SSLv2, SSL.SSL_OP_NO_SSLv3, SSL.SSL_OP_NO_TLSv1, SSL.SSL_OP_NO_TLSv1_1, SSL.SSL_OP_NO_TLSv1_2, SSL.SSL_OP_NO_TLSv1_3 }; /** * Depends upon tcnative ... only use if tcnative is available! */ static final int MAX_PLAINTEXT_LENGTH = SSL.SSL_MAX_PLAINTEXT_LENGTH; /** * Depends upon tcnative ... only use if tcnative is available! */ static final int MAX_RECORD_SIZE = SSL.SSL_MAX_RECORD_LENGTH; private static final SSLEngineResult NEED_UNWRAP_OK = new SSLEngineResult(OK, NEED_UNWRAP, 0, 0); private static final SSLEngineResult NEED_UNWRAP_CLOSED = new SSLEngineResult(CLOSED, NEED_UNWRAP, 0, 0); private static final SSLEngineResult NEED_WRAP_OK = new SSLEngineResult(OK, NEED_WRAP, 0, 0); private static final SSLEngineResult NEED_WRAP_CLOSED = new SSLEngineResult(CLOSED, NEED_WRAP, 0, 0); private static final SSLEngineResult CLOSED_NOT_HANDSHAKING = new SSLEngineResult(CLOSED, NOT_HANDSHAKING, 0, 0); // OpenSSL state private long ssl; private long networkBIO; private enum HandshakeState { /** * Not started yet. */ NOT_STARTED, /** * Started via unwrap/wrap. */ STARTED_IMPLICITLY, /** * Started via {@link #beginHandshake()}. */ STARTED_EXPLICITLY, /** * Handshake is finished. */ FINISHED } private HandshakeState handshakeState = HandshakeState.NOT_STARTED; private boolean receivedShutdown; private volatile boolean destroyed; private volatile String applicationProtocol; private volatile boolean needTask; private boolean hasTLSv13Cipher; private boolean sessionSet; // Reference Counting private final ResourceLeakTracker leak; private final AbstractReferenceCounted refCnt = new AbstractReferenceCounted() { @Override public ReferenceCounted touch(Object hint) { if (leak != null) { leak.record(hint); } return ReferenceCountedOpenSslEngine.this; } @Override protected void deallocate() { shutdown(); if (leak != null) { boolean closed = leak.close(ReferenceCountedOpenSslEngine.this); assert closed; } parentContext.release(); } }; private final Set enabledProtocols = new LinkedHashSet(); private volatile ClientAuth clientAuth = ClientAuth.NONE; private String endpointIdentificationAlgorithm; // Store as object as AlgorithmConstraints only exists since java 7. private Object algorithmConstraints; private List sniHostNames; // Mark as volatile as accessed by checkSniHostnameMatch(...) and also not specify the SNIMatcher type to allow us // using it with java7. private volatile Collection matchers; // SSL Engine status variables private boolean isInboundDone; private boolean outboundClosed; final boolean jdkCompatibilityMode; private final boolean clientMode; final ByteBufAllocator alloc; private final OpenSslEngineMap engineMap; private final OpenSslApplicationProtocolNegotiator apn; private final ReferenceCountedOpenSslContext parentContext; private final OpenSslSession session; private final ByteBuffer[] singleSrcBuffer = new ByteBuffer[1]; private final ByteBuffer[] singleDstBuffer = new ByteBuffer[1]; private final boolean enableOcsp; private int maxWrapOverhead; private int maxWrapBufferSize; private Throwable pendingException; /** * Create a new instance. * @param context Reference count release responsibility is not transferred! The callee still owns this object. * @param alloc The allocator to use. * @param peerHost The peer host name. * @param peerPort The peer port. * @param jdkCompatibilityMode {@code true} to behave like described in * https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/SSLEngine.html. * {@code false} allows for partial and/or multiple packets to be process in a single * wrap or unwrap call. * @param leakDetection {@code true} to enable leak detection of this object. */ ReferenceCountedOpenSslEngine(ReferenceCountedOpenSslContext context, final ByteBufAllocator alloc, String peerHost, int peerPort, boolean jdkCompatibilityMode, boolean leakDetection, String endpointIdentificationAlgorithm) { super(peerHost, peerPort); OpenSsl.ensureAvailability(); engineMap = context.engineMap; enableOcsp = context.enableOcsp; this.jdkCompatibilityMode = jdkCompatibilityMode; this.alloc = checkNotNull(alloc, "alloc"); apn = (OpenSslApplicationProtocolNegotiator) context.applicationProtocolNegotiator(); clientMode = context.isClient(); this.endpointIdentificationAlgorithm = endpointIdentificationAlgorithm; if (PlatformDependent.javaVersion() >= 7) { session = new ExtendedOpenSslSession(new DefaultOpenSslSession(context.sessionContext())) { private String[] peerSupportedSignatureAlgorithms; private List requestedServerNames; @Override public List getRequestedServerNames() { if (clientMode) { return Java8SslUtils.getSniHostNames(sniHostNames); } else { synchronized (ReferenceCountedOpenSslEngine.this) { if (requestedServerNames == null) { if (isDestroyed()) { requestedServerNames = Collections.emptyList(); } else { String name = SSL.getSniHostname(ssl); if (name == null) { requestedServerNames = Collections.emptyList(); } else { // Convert to bytes as we do not want to do any strict validation of the // SNIHostName while creating it. requestedServerNames = Java8SslUtils.getSniHostName( SSL.getSniHostname(ssl).getBytes(CharsetUtil.UTF_8)); } } } return requestedServerNames; } } } @Override public String[] getPeerSupportedSignatureAlgorithms() { synchronized (ReferenceCountedOpenSslEngine.this) { if (peerSupportedSignatureAlgorithms == null) { if (isDestroyed()) { peerSupportedSignatureAlgorithms = EMPTY_STRINGS; } else { String[] algs = SSL.getSigAlgs(ssl); if (algs == null) { peerSupportedSignatureAlgorithms = EMPTY_STRINGS; } else { Set algorithmList = new LinkedHashSet(algs.length); for (String alg: algs) { String converted = SignatureAlgorithmConverter.toJavaName(alg); if (converted != null) { algorithmList.add(converted); } } peerSupportedSignatureAlgorithms = algorithmList.toArray(EMPTY_STRINGS); } } } return peerSupportedSignatureAlgorithms.clone(); } } @Override public List getStatusResponses() { byte[] ocspResponse = null; if (enableOcsp && clientMode) { synchronized (ReferenceCountedOpenSslEngine.this) { if (!isDestroyed()) { ocspResponse = SSL.getOcspResponse(ssl); } } } return ocspResponse == null ? Collections.emptyList() : Collections.singletonList(ocspResponse); } }; } else { session = new DefaultOpenSslSession(context.sessionContext()); } if (!context.sessionContext().useKeyManager()) { session.setLocalCertificate(context.keyCertChain); } Lock readerLock = context.ctxLock.readLock(); readerLock.lock(); final long finalSsl; try { finalSsl = SSL.newSSL(context.ctx, !context.isClient()); } finally { readerLock.unlock(); } synchronized (this) { ssl = finalSsl; try { networkBIO = SSL.bioNewByteBuffer(ssl, context.getBioNonApplicationBufferSize()); // Set the client auth mode, this needs to be done via setClientAuth(...) method so we actually call the // needed JNI methods. setClientAuth(clientMode ? ClientAuth.NONE : context.clientAuth); assert context.protocols != null; this.hasTLSv13Cipher = context.hasTLSv13Cipher; setEnabledProtocols(context.protocols); // Use SNI if peerHost was specified and a valid hostname // See https://github.com/netty/netty/issues/4746 if (clientMode && SslUtils.isValidHostNameForSNI(peerHost)) { // If on java8 and later we should do some extra validation to ensure we can construct the // SNIHostName later again. if (PlatformDependent.javaVersion() >= 8) { if (Java8SslUtils.isValidHostNameForSNI(peerHost)) { SSL.setTlsExtHostName(ssl, peerHost); sniHostNames = Collections.singletonList(peerHost); } } else { SSL.setTlsExtHostName(ssl, peerHost); sniHostNames = Collections.singletonList(peerHost); } } if (enableOcsp) { SSL.enableOcsp(ssl); } if (!jdkCompatibilityMode) { SSL.setMode(ssl, SSL.getMode(ssl) | SSL.SSL_MODE_ENABLE_PARTIAL_WRITE); } if (isProtocolEnabled(SSL.getOptions(ssl), SSL.SSL_OP_NO_TLSv1_3, SslProtocols.TLS_v1_3)) { final boolean enableTickets = clientMode ? ReferenceCountedOpenSslContext.CLIENT_ENABLE_SESSION_TICKET_TLSV13 : ReferenceCountedOpenSslContext.SERVER_ENABLE_SESSION_TICKET_TLSV13; if (enableTickets) { // We should enable session tickets for stateless resumption when TLSv1.3 is enabled. This // is also done by OpenJDK and without this session resumption does not work at all with // BoringSSL when TLSv1.3 is used as BoringSSL only supports stateless resumption with TLSv1.3: // // See: // - https://bugs.openjdk.java.net/browse/JDK-8223922 // - https://boringssl.googlesource.com/boringssl/+/refs/heads/master/ssl/tls13_server.cc#104 SSL.clearOptions(ssl, SSL.SSL_OP_NO_TICKET); } } if (OpenSsl.isBoringSSL() && clientMode) { // If in client-mode and BoringSSL let's allow to renegotiate once as the server may use this // for client auth. // // See https://github.com/netty/netty/issues/11529 SSL.setRenegotiateMode(ssl, SSL.SSL_RENEGOTIATE_ONCE); } // setMode may impact the overhead. calculateMaxWrapOverhead(); // Configure any endpoint verification specified by the SslContext. configureEndpointVerification(endpointIdentificationAlgorithm); } catch (Throwable cause) { // Call shutdown so we are sure we correctly release all native memory and also guard against the // case when shutdown() will be called by the finalizer again. shutdown(); PlatformDependent.throwException(cause); } } // Now that everything looks good and we're going to successfully return the // object so we need to retain a reference to the parent context. parentContext = context; parentContext.retain(); // Only create the leak after everything else was executed and so ensure we don't produce a false-positive for // the ResourceLeakDetector. leak = leakDetection ? leakDetector.track(this) : null; } final synchronized String[] authMethods() { if (isDestroyed()) { return EMPTY_STRINGS; } return SSL.authenticationMethods(ssl); } final boolean setKeyMaterial(OpenSslKeyMaterial keyMaterial) throws Exception { synchronized (this) { if (isDestroyed()) { return false; } SSL.setKeyMaterial(ssl, keyMaterial.certificateChainAddress(), keyMaterial.privateKeyAddress()); } session.setLocalCertificate(keyMaterial.certificateChain()); return true; } final synchronized SecretKeySpec masterKey() { if (isDestroyed()) { return null; } return new SecretKeySpec(SSL.getMasterKey(ssl), "AES"); } synchronized boolean isSessionReused() { if (isDestroyed()) { return false; } return SSL.isSessionReused(ssl); } /** * Sets the OCSP response. */ @UnstableApi public void setOcspResponse(byte[] response) { if (!enableOcsp) { throw new IllegalStateException("OCSP stapling is not enabled"); } if (clientMode) { throw new IllegalStateException("Not a server SSLEngine"); } synchronized (this) { if (!isDestroyed()) { SSL.setOcspResponse(ssl, response); } } } /** * Returns the OCSP response or {@code null} if the server didn't provide a stapled OCSP response. */ @UnstableApi public byte[] getOcspResponse() { if (!enableOcsp) { throw new IllegalStateException("OCSP stapling is not enabled"); } if (!clientMode) { throw new IllegalStateException("Not a client SSLEngine"); } synchronized (this) { if (isDestroyed()) { return EmptyArrays.EMPTY_BYTES; } return SSL.getOcspResponse(ssl); } } @Override public final int refCnt() { return refCnt.refCnt(); } @Override public final ReferenceCounted retain() { refCnt.retain(); return this; } @Override public final ReferenceCounted retain(int increment) { refCnt.retain(increment); return this; } @Override public final ReferenceCounted touch() { refCnt.touch(); return this; } @Override public final ReferenceCounted touch(Object hint) { refCnt.touch(hint); return this; } @Override public final boolean release() { return refCnt.release(); } @Override public final boolean release(int decrement) { return refCnt.release(decrement); } // These method will override the method defined by Java 8u251 and later. As we may compile with an earlier // java8 version we don't use @Override annotations here. public String getApplicationProtocol() { return applicationProtocol; } // These method will override the method defined by Java 8u251 and later. As we may compile with an earlier // java8 version we don't use @Override annotations here. public String getHandshakeApplicationProtocol() { return applicationProtocol; } @Override public final synchronized SSLSession getHandshakeSession() { // Javadocs state return value should be: // null if this instance is not currently handshaking, or if the current handshake has not // progressed far enough to create a basic SSLSession. Otherwise, this method returns the // SSLSession currently being negotiated. switch(handshakeState) { case NOT_STARTED: case FINISHED: return null; default: return session; } } /** * Returns the pointer to the {@code SSL} object for this {@link ReferenceCountedOpenSslEngine}. * Be aware that it is freed as soon as the {@link #release()} or {@link #shutdown()} methods are called. * At this point {@code 0} will be returned. */ public final synchronized long sslPointer() { return ssl; } /** * Destroys this engine. */ public final synchronized void shutdown() { if (!destroyed) { destroyed = true; // Let's check if engineMap is null as it could be in theory if we throw an OOME during the construction of // ReferenceCountedOpenSslEngine (before we assign the field). This is needed as shutdown() is called from // the finalizer as well. if (engineMap != null) { engineMap.remove(ssl); } SSL.freeSSL(ssl); ssl = networkBIO = 0; isInboundDone = outboundClosed = true; } // On shutdown clear all errors SSL.clearError(); } /** * Write plaintext data to the OpenSSL internal BIO * * Calling this function with src.remaining == 0 is undefined. */ private int writePlaintextData(final ByteBuffer src, int len) { final int pos = src.position(); final int limit = src.limit(); final int sslWrote; if (src.isDirect()) { sslWrote = SSL.writeToSSL(ssl, bufferAddress(src) + pos, len); if (sslWrote > 0) { src.position(pos + sslWrote); } } else { ByteBuf buf = alloc.directBuffer(len); try { src.limit(pos + len); buf.setBytes(0, src); src.limit(limit); sslWrote = SSL.writeToSSL(ssl, memoryAddress(buf), len); if (sslWrote > 0) { src.position(pos + sslWrote); } else { src.position(pos); } } finally { buf.release(); } } return sslWrote; } synchronized void bioSetFd(int fd) { if (!isDestroyed()) { SSL.bioSetFd(this.ssl, fd); } } /** * Write encrypted data to the OpenSSL network BIO. */ private ByteBuf writeEncryptedData(final ByteBuffer src, int len) throws SSLException { final int pos = src.position(); if (src.isDirect()) { SSL.bioSetByteBuffer(networkBIO, bufferAddress(src) + pos, len, false); } else { final ByteBuf buf = alloc.directBuffer(len); try { final int limit = src.limit(); src.limit(pos + len); buf.writeBytes(src); // Restore the original position and limit because we don't want to consume from `src`. src.position(pos); src.limit(limit); SSL.bioSetByteBuffer(networkBIO, memoryAddress(buf), len, false); return buf; } catch (Throwable cause) { buf.release(); PlatformDependent.throwException(cause); } } return null; } /** * Read plaintext data from the OpenSSL internal BIO */ private int readPlaintextData(final ByteBuffer dst) throws SSLException { final int sslRead; final int pos = dst.position(); if (dst.isDirect()) { sslRead = SSL.readFromSSL(ssl, bufferAddress(dst) + pos, dst.limit() - pos); if (sslRead > 0) { dst.position(pos + sslRead); } } else { final int limit = dst.limit(); final int len = min(maxEncryptedPacketLength0(), limit - pos); final ByteBuf buf = alloc.directBuffer(len); try { sslRead = SSL.readFromSSL(ssl, memoryAddress(buf), len); if (sslRead > 0) { dst.limit(pos + sslRead); buf.getBytes(buf.readerIndex(), dst); dst.limit(limit); } } finally { buf.release(); } } return sslRead; } /** * Visible only for testing! */ final synchronized int maxWrapOverhead() { return maxWrapOverhead; } /** * Visible only for testing! */ final synchronized int maxEncryptedPacketLength() { return maxEncryptedPacketLength0(); } /** * This method is intentionally not synchronized, only use if you know you are in the EventLoop * thread and visibility on {@link #maxWrapOverhead} is achieved via other synchronized blocks. */ final int maxEncryptedPacketLength0() { return maxWrapOverhead + MAX_PLAINTEXT_LENGTH; } /** * This method is intentionally not synchronized, only use if you know you are in the EventLoop * thread and visibility on {@link #maxWrapBufferSize} and {@link #maxWrapOverhead} is achieved * via other synchronized blocks. *
* Calculates the max size of a single wrap operation for the given plaintextLength and * numComponents. */ final int calculateMaxLengthForWrap(int plaintextLength, int numComponents) { return (int) min(maxWrapBufferSize, plaintextLength + (long) maxWrapOverhead * numComponents); } /** * This method is intentionally not synchronized, only use if you know you are in the EventLoop * thread and visibility on {@link #maxWrapOverhead} is achieved via other synchronized blocks. *
* Calculates the size of the out net buf to create for the given plaintextLength and numComponents. * This is not related to the max size per wrap, as we can wrap chunks at a time into one out net buf. */ final int calculateOutNetBufSize(int plaintextLength, int numComponents) { return (int) min(MAX_VALUE, plaintextLength + (long) maxWrapOverhead * numComponents); } final synchronized int sslPending() { return sslPending0(); } /** * It is assumed this method is called in a synchronized block (or the constructor)! */ private void calculateMaxWrapOverhead() { maxWrapOverhead = SSL.getMaxWrapOverhead(ssl); // maxWrapBufferSize must be set after maxWrapOverhead because there is a dependency on this value. // If jdkCompatibility mode is off we allow enough space to encrypt 16 buffers at a time. This could be // configurable in the future if necessary. maxWrapBufferSize = jdkCompatibilityMode ? maxEncryptedPacketLength0() : maxEncryptedPacketLength0() << 4; } private int sslPending0() { // OpenSSL has a limitation where if you call SSL_pending before the handshake is complete OpenSSL will throw a // "called a function you should not call" error. Using the TLS_method instead of SSLv23_method may solve this // issue but this API is only available in 1.1.0+ [1]. // [1] https://www.openssl.org/docs/man1.1.0/ssl/SSL_CTX_new.html return handshakeState != HandshakeState.FINISHED ? 0 : SSL.sslPending(ssl); } private boolean isBytesAvailableEnoughForWrap(int bytesAvailable, int plaintextLength, int numComponents) { return bytesAvailable - (long) maxWrapOverhead * numComponents >= plaintextLength; } @Override public final SSLEngineResult wrap( final ByteBuffer[] srcs, int offset, final int length, final ByteBuffer dst) throws SSLException { // Throw required runtime exceptions checkNotNullWithIAE(srcs, "srcs"); checkNotNullWithIAE(dst, "dst"); if (offset >= srcs.length || offset + length > srcs.length) { throw new IndexOutOfBoundsException( "offset: " + offset + ", length: " + length + " (expected: offset <= offset + length <= srcs.length (" + srcs.length + "))"); } if (dst.isReadOnly()) { throw new ReadOnlyBufferException(); } synchronized (this) { if (isOutboundDone()) { // All drained in the outbound buffer return isInboundDone() || isDestroyed() ? CLOSED_NOT_HANDSHAKING : NEED_UNWRAP_CLOSED; } int bytesProduced = 0; ByteBuf bioReadCopyBuf = null; try { // Setup the BIO buffer so that we directly write the encryption results into dst. if (dst.isDirect()) { SSL.bioSetByteBuffer(networkBIO, bufferAddress(dst) + dst.position(), dst.remaining(), true); } else { bioReadCopyBuf = alloc.directBuffer(dst.remaining()); SSL.bioSetByteBuffer(networkBIO, memoryAddress(bioReadCopyBuf), bioReadCopyBuf.writableBytes(), true); } int bioLengthBefore = SSL.bioLengthByteBuffer(networkBIO); // Explicitly use outboundClosed as we want to drain any bytes that are still present. if (outboundClosed) { // If the outbound was closed we want to ensure we can produce the alert to the destination buffer. // This is true even if we not using jdkCompatibilityMode. // // We use a plaintextLength of 2 as we at least want to have an alert fit into it. // https://tools.ietf.org/html/rfc5246#section-7.2 if (!isBytesAvailableEnoughForWrap(dst.remaining(), 2, 1)) { return new SSLEngineResult(BUFFER_OVERFLOW, getHandshakeStatus(), 0, 0); } // There is something left to drain. // See https://github.com/netty/netty/issues/6260 bytesProduced = SSL.bioFlushByteBuffer(networkBIO); if (bytesProduced <= 0) { return newResultMayFinishHandshake(NOT_HANDSHAKING, 0, 0); } // It is possible when the outbound was closed there was not enough room in the non-application // buffers to hold the close_notify. We should keep trying to close until we consume all the data // OpenSSL can give us. if (!doSSLShutdown()) { return newResultMayFinishHandshake(NOT_HANDSHAKING, 0, bytesProduced); } bytesProduced = bioLengthBefore - SSL.bioLengthByteBuffer(networkBIO); return newResultMayFinishHandshake(NEED_WRAP, 0, bytesProduced); } // Flush any data that may be implicitly generated by OpenSSL (handshake, close, etc..). SSLEngineResult.HandshakeStatus status = NOT_HANDSHAKING; HandshakeState oldHandshakeState = handshakeState; // Prepare OpenSSL to work in server mode and receive handshake if (handshakeState != HandshakeState.FINISHED) { if (handshakeState != HandshakeState.STARTED_EXPLICITLY) { // Update accepted so we know we triggered the handshake via wrap handshakeState = HandshakeState.STARTED_IMPLICITLY; } // Flush any data that may have been written implicitly during the handshake by OpenSSL. bytesProduced = SSL.bioFlushByteBuffer(networkBIO); if (pendingException != null) { // TODO(scott): It is possible that when the handshake failed there was not enough room in the // non-application buffers to hold the alert. We should get all the data before progressing on. // However I'm not aware of a way to do this with the OpenSSL APIs. // See https://github.com/netty/netty/issues/6385. // We produced / consumed some data during the handshake, signal back to the caller. // If there is a handshake exception and we have produced data, we should send the data before // we allow handshake() to throw the handshake exception. // // When the user calls wrap() again we will propagate the handshake error back to the user as // soon as there is no more data to was produced (as part of an alert etc). if (bytesProduced > 0) { return newResult(NEED_WRAP, 0, bytesProduced); } // Nothing was produced see if there is a handshakeException that needs to be propagated // to the caller by calling handshakeException() which will return the right HandshakeStatus // if it can "recover" from the exception for now. return newResult(handshakeException(), 0, 0); } status = handshake(); // Handshake may have generated more data, for example if the internal SSL buffer is small // we may have freed up space by flushing above. bytesProduced = bioLengthBefore - SSL.bioLengthByteBuffer(networkBIO); if (status == NEED_TASK) { return newResult(status, 0, bytesProduced); } if (bytesProduced > 0) { // If we have filled up the dst buffer and we have not finished the handshake we should try to // wrap again. Otherwise we should only try to wrap again if there is still data pending in // SSL buffers. return newResult(mayFinishHandshake(status != FINISHED ? bytesProduced == bioLengthBefore ? NEED_WRAP : getHandshakeStatus(SSL.bioLengthNonApplication(networkBIO)) : FINISHED), 0, bytesProduced); } if (status == NEED_UNWRAP) { // Signal if the outbound is done or not. return isOutboundDone() ? NEED_UNWRAP_CLOSED : NEED_UNWRAP_OK; } // Explicit use outboundClosed and not outboundClosed() as we want to drain any bytes that are // still present. if (outboundClosed) { bytesProduced = SSL.bioFlushByteBuffer(networkBIO); return newResultMayFinishHandshake(status, 0, bytesProduced); } } final int endOffset = offset + length; if (jdkCompatibilityMode || // If the handshake was not finished before we entered the method, we also ensure we only // wrap one record. We do this to ensure we not produce any extra data before the caller // of the method is able to observe handshake completion and react on it. oldHandshakeState != HandshakeState.FINISHED) { int srcsLen = 0; for (int i = offset; i < endOffset; ++i) { final ByteBuffer src = srcs[i]; if (src == null) { throw new IllegalArgumentException("srcs[" + i + "] is null"); } if (srcsLen == MAX_PLAINTEXT_LENGTH) { continue; } srcsLen += src.remaining(); if (srcsLen > MAX_PLAINTEXT_LENGTH || srcsLen < 0) { // If srcLen > MAX_PLAINTEXT_LENGTH or secLen < 0 just set it to MAX_PLAINTEXT_LENGTH. // This also help us to guard against overflow. // We not break out here as we still need to check for null entries in srcs[]. srcsLen = MAX_PLAINTEXT_LENGTH; } } // jdkCompatibilityMode will only produce a single TLS packet, and we don't aggregate src buffers, // so we always fix the number of buffers to 1 when checking if the dst buffer is large enough. if (!isBytesAvailableEnoughForWrap(dst.remaining(), srcsLen, 1)) { return new SSLEngineResult(BUFFER_OVERFLOW, getHandshakeStatus(), 0, 0); } } // There was no pending data in the network BIO -- encrypt any application data int bytesConsumed = 0; assert bytesProduced == 0; // Flush any data that may have been written implicitly by OpenSSL in case a shutdown/alert occurs. bytesProduced = SSL.bioFlushByteBuffer(networkBIO); if (bytesProduced > 0) { return newResultMayFinishHandshake(status, bytesConsumed, bytesProduced); } // There was a pending exception that we just delayed because there was something to produce left. // Throw it now and shutdown the engine. if (pendingException != null) { Throwable error = pendingException; pendingException = null; shutdown(); // Throw a new exception wrapping the pending exception, so the stacktrace is meaningful and // contains all the details. throw new SSLException(error); } for (; offset < endOffset; ++offset) { final ByteBuffer src = srcs[offset]; final int remaining = src.remaining(); if (remaining == 0) { continue; } final int bytesWritten; if (jdkCompatibilityMode) { // Write plaintext application data to the SSL engine. We don't have to worry about checking // if there is enough space if jdkCompatibilityMode because we only wrap at most // MAX_PLAINTEXT_LENGTH and we loop over the input before hand and check if there is space. bytesWritten = writePlaintextData(src, min(remaining, MAX_PLAINTEXT_LENGTH - bytesConsumed)); } else { // OpenSSL's SSL_write keeps state between calls. We should make sure the amount we attempt to // write is guaranteed to succeed so we don't have to worry about keeping state consistent // between calls. final int availableCapacityForWrap = dst.remaining() - bytesProduced - maxWrapOverhead; if (availableCapacityForWrap <= 0) { return new SSLEngineResult(BUFFER_OVERFLOW, getHandshakeStatus(), bytesConsumed, bytesProduced); } bytesWritten = writePlaintextData(src, min(remaining, availableCapacityForWrap)); } // Determine how much encrypted data was generated. // // Even if SSL_write doesn't consume any application data it is possible that OpenSSL will // produce non-application data into the BIO. For example session tickets.... // See https://github.com/netty/netty/issues/10041 final int pendingNow = SSL.bioLengthByteBuffer(networkBIO); bytesProduced += bioLengthBefore - pendingNow; bioLengthBefore = pendingNow; if (bytesWritten > 0) { bytesConsumed += bytesWritten; if (jdkCompatibilityMode || bytesProduced == dst.remaining()) { return newResultMayFinishHandshake(status, bytesConsumed, bytesProduced); } } else { int sslError = SSL.getError(ssl, bytesWritten); if (sslError == SSL.SSL_ERROR_ZERO_RETURN) { // This means the connection was shutdown correctly, close inbound and outbound if (!receivedShutdown) { closeAll(); bytesProduced += bioLengthBefore - SSL.bioLengthByteBuffer(networkBIO); // If we have filled up the dst buffer and we have not finished the handshake we should // try to wrap again. Otherwise we should only try to wrap again if there is still data // pending in SSL buffers. SSLEngineResult.HandshakeStatus hs = mayFinishHandshake( status != FINISHED ? bytesProduced == dst.remaining() ? NEED_WRAP : getHandshakeStatus(SSL.bioLengthNonApplication(networkBIO)) : FINISHED); return newResult(hs, bytesConsumed, bytesProduced); } return newResult(NOT_HANDSHAKING, bytesConsumed, bytesProduced); } else if (sslError == SSL.SSL_ERROR_WANT_READ) { // If there is no pending data to read from BIO we should go back to event loop and try // to read more data [1]. It is also possible that event loop will detect the socket has // been closed. [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html return newResult(NEED_UNWRAP, bytesConsumed, bytesProduced); } else if (sslError == SSL.SSL_ERROR_WANT_WRITE) { // SSL_ERROR_WANT_WRITE typically means that the underlying transport is not writable // and we should set the "want write" flag on the selector and try again when the // underlying transport is writable [1]. However we are not directly writing to the // underlying transport and instead writing to a BIO buffer. The OpenSsl documentation // says we should do the following [1]: // // "When using a buffering BIO, like a BIO pair, data must be written into or retrieved // out of the BIO before being able to continue." // // In practice this means the destination buffer doesn't have enough space for OpenSSL // to write encrypted data to. This is an OVERFLOW condition. // [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html if (bytesProduced > 0) { // If we produced something we should report this back and let the user call // wrap again. return newResult(NEED_WRAP, bytesConsumed, bytesProduced); } return newResult(BUFFER_OVERFLOW, status, bytesConsumed, bytesProduced); } else if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP || sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY || sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) { return newResult(NEED_TASK, bytesConsumed, bytesProduced); } else { // Everything else is considered as error throw shutdownWithError("SSL_write", sslError); } } } return newResultMayFinishHandshake(status, bytesConsumed, bytesProduced); } finally { SSL.bioClearByteBuffer(networkBIO); if (bioReadCopyBuf == null) { dst.position(dst.position() + bytesProduced); } else { assert bioReadCopyBuf.readableBytes() <= dst.remaining() : "The destination buffer " + dst + " didn't have enough remaining space to hold the encrypted content in " + bioReadCopyBuf; dst.put(bioReadCopyBuf.internalNioBuffer(bioReadCopyBuf.readerIndex(), bytesProduced)); bioReadCopyBuf.release(); } } } } private SSLEngineResult newResult(SSLEngineResult.HandshakeStatus hs, int bytesConsumed, int bytesProduced) { return newResult(OK, hs, bytesConsumed, bytesProduced); } private SSLEngineResult newResult(SSLEngineResult.Status status, SSLEngineResult.HandshakeStatus hs, int bytesConsumed, int bytesProduced) { // If isOutboundDone, then the data from the network BIO // was the close_notify message and all was consumed we are not required to wait // for the receipt the peer's close_notify message -- shutdown. if (isOutboundDone()) { if (isInboundDone()) { // If the inbound was done as well, we need to ensure we return NOT_HANDSHAKING to signal we are done. hs = NOT_HANDSHAKING; // As the inbound and the outbound is done we can shutdown the engine now. shutdown(); } return new SSLEngineResult(CLOSED, hs, bytesConsumed, bytesProduced); } if (hs == NEED_TASK) { // Set needTask to true so getHandshakeStatus() will return the correct value. needTask = true; } return new SSLEngineResult(status, hs, bytesConsumed, bytesProduced); } private SSLEngineResult newResultMayFinishHandshake(SSLEngineResult.HandshakeStatus hs, int bytesConsumed, int bytesProduced) throws SSLException { return newResult(mayFinishHandshake(hs, bytesConsumed, bytesProduced), bytesConsumed, bytesProduced); } private SSLEngineResult newResultMayFinishHandshake(SSLEngineResult.Status status, SSLEngineResult.HandshakeStatus hs, int bytesConsumed, int bytesProduced) throws SSLException { return newResult(status, mayFinishHandshake(hs, bytesConsumed, bytesProduced), bytesConsumed, bytesProduced); } /** * Log the error, shutdown the engine and throw an exception. */ private SSLException shutdownWithError(String operations, int sslError) { return shutdownWithError(operations, sslError, SSL.getLastErrorNumber()); } private SSLException shutdownWithError(String operation, int sslError, int error) { if (logger.isDebugEnabled()) { String errorString = SSL.getErrorString(error); logger.debug("{} failed with {}: OpenSSL error: {} {}", operation, sslError, error, errorString); } // There was an internal error -- shutdown shutdown(); SSLException exception = newSSLExceptionForError(error); // If we have a pendingException stored already we should include it as well to help the user debug things. if (pendingException != null) { exception.initCause(pendingException); pendingException = null; } return exception; } private SSLEngineResult handleUnwrapException(int bytesConsumed, int bytesProduced, SSLException e) throws SSLException { int lastError = SSL.getLastErrorNumber(); if (lastError != 0) { return sslReadErrorResult(SSL.SSL_ERROR_SSL, lastError, bytesConsumed, bytesProduced); } throw e; } public final SSLEngineResult unwrap( final ByteBuffer[] srcs, int srcsOffset, final int srcsLength, final ByteBuffer[] dsts, int dstsOffset, final int dstsLength) throws SSLException { // Throw required runtime exceptions checkNotNullWithIAE(srcs, "srcs"); if (srcsOffset >= srcs.length || srcsOffset + srcsLength > srcs.length) { throw new IndexOutOfBoundsException( "offset: " + srcsOffset + ", length: " + srcsLength + " (expected: offset <= offset + length <= srcs.length (" + srcs.length + "))"); } checkNotNullWithIAE(dsts, "dsts"); if (dstsOffset >= dsts.length || dstsOffset + dstsLength > dsts.length) { throw new IndexOutOfBoundsException( "offset: " + dstsOffset + ", length: " + dstsLength + " (expected: offset <= offset + length <= dsts.length (" + dsts.length + "))"); } long capacity = 0; final int dstsEndOffset = dstsOffset + dstsLength; for (int i = dstsOffset; i < dstsEndOffset; i ++) { ByteBuffer dst = checkNotNullArrayParam(dsts[i], i, "dsts"); if (dst.isReadOnly()) { throw new ReadOnlyBufferException(); } capacity += dst.remaining(); } final int srcsEndOffset = srcsOffset + srcsLength; long len = 0; for (int i = srcsOffset; i < srcsEndOffset; i++) { ByteBuffer src = checkNotNullArrayParam(srcs[i], i, "srcs"); len += src.remaining(); } synchronized (this) { if (isInboundDone()) { return isOutboundDone() || isDestroyed() ? CLOSED_NOT_HANDSHAKING : NEED_WRAP_CLOSED; } SSLEngineResult.HandshakeStatus status = NOT_HANDSHAKING; HandshakeState oldHandshakeState = handshakeState; // Prepare OpenSSL to work in server mode and receive handshake if (handshakeState != HandshakeState.FINISHED) { if (handshakeState != HandshakeState.STARTED_EXPLICITLY) { // Update accepted so we know we triggered the handshake via wrap handshakeState = HandshakeState.STARTED_IMPLICITLY; } status = handshake(); if (status == NEED_TASK) { return newResult(status, 0, 0); } if (status == NEED_WRAP) { return NEED_WRAP_OK; } // Check if the inbound is considered to be closed if so let us try to wrap again. if (isInboundDone) { return NEED_WRAP_CLOSED; } } int sslPending = sslPending0(); int packetLength; // The JDK implies that only a single SSL packet should be processed per unwrap call [1]. If we are in // JDK compatibility mode then we should honor this, but if not we just wrap as much as possible. If there // are multiple records or partial records this may reduce thrashing events through the pipeline. // [1] https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/SSLEngine.html if (jdkCompatibilityMode || // If the handshake was not finished before we entered the method, we also ensure we only // unwrap one record. We do this to ensure we not produce any extra data before the caller // of the method is able to observe handshake completion and react on it. oldHandshakeState != HandshakeState.FINISHED) { if (len < SSL_RECORD_HEADER_LENGTH) { return newResultMayFinishHandshake(BUFFER_UNDERFLOW, status, 0, 0); } packetLength = SslUtils.getEncryptedPacketLength(srcs, srcsOffset); if (packetLength == SslUtils.NOT_ENCRYPTED) { throw new NotSslRecordException("not an SSL/TLS record"); } final int packetLengthDataOnly = packetLength - SSL_RECORD_HEADER_LENGTH; if (packetLengthDataOnly > capacity) { // Not enough space in the destination buffer so signal the caller that the buffer needs to be // increased. if (packetLengthDataOnly > MAX_RECORD_SIZE) { // The packet length MUST NOT exceed 2^14 [1]. However we do accommodate more data to support // legacy use cases which may violate this condition (e.g. OpenJDK's SslEngineImpl). If the max // length is exceeded we fail fast here to avoid an infinite loop due to the fact that we // won't allocate a buffer large enough. // [1] https://tools.ietf.org/html/rfc5246#section-6.2.1 throw new SSLException("Illegal packet length: " + packetLengthDataOnly + " > " + session.getApplicationBufferSize()); } else { session.tryExpandApplicationBufferSize(packetLengthDataOnly); } return newResultMayFinishHandshake(BUFFER_OVERFLOW, status, 0, 0); } if (len < packetLength) { // We either don't have enough data to read the packet length or not enough for reading the whole // packet. return newResultMayFinishHandshake(BUFFER_UNDERFLOW, status, 0, 0); } } else if (len == 0 && sslPending <= 0) { return newResultMayFinishHandshake(BUFFER_UNDERFLOW, status, 0, 0); } else if (capacity == 0) { return newResultMayFinishHandshake(BUFFER_OVERFLOW, status, 0, 0); } else { packetLength = (int) min(MAX_VALUE, len); } // This must always be the case when we reached here as if not we returned BUFFER_UNDERFLOW. assert srcsOffset < srcsEndOffset; // This must always be the case if we reached here. assert capacity > 0; // Number of produced bytes int bytesProduced = 0; int bytesConsumed = 0; try { srcLoop: for (;;) { ByteBuffer src = srcs[srcsOffset]; int remaining = src.remaining(); final ByteBuf bioWriteCopyBuf; int pendingEncryptedBytes; if (remaining == 0) { if (sslPending <= 0) { // We must skip empty buffers as BIO_write will return 0 if asked to write something // with length 0. if (++srcsOffset >= srcsEndOffset) { break; } continue; } else { bioWriteCopyBuf = null; pendingEncryptedBytes = SSL.bioLengthByteBuffer(networkBIO); } } else { // Write more encrypted data into the BIO. Ensure we only read one packet at a time as // stated in the SSLEngine javadocs. pendingEncryptedBytes = min(packetLength, remaining); try { bioWriteCopyBuf = writeEncryptedData(src, pendingEncryptedBytes); } catch (SSLException e) { // Ensure we correctly handle the error stack. return handleUnwrapException(bytesConsumed, bytesProduced, e); } } try { for (;;) { ByteBuffer dst = dsts[dstsOffset]; if (!dst.hasRemaining()) { // No space left in the destination buffer, skip it. if (++dstsOffset >= dstsEndOffset) { break srcLoop; } continue; } int bytesRead; try { bytesRead = readPlaintextData(dst); } catch (SSLException e) { // Ensure we correctly handle the error stack. return handleUnwrapException(bytesConsumed, bytesProduced, e); } // We are directly using the ByteBuffer memory for the write, and so we only know what has // been consumed after we let SSL decrypt the data. At this point we should update the // number of bytes consumed, update the ByteBuffer position, and release temp ByteBuf. int localBytesConsumed = pendingEncryptedBytes - SSL.bioLengthByteBuffer(networkBIO); bytesConsumed += localBytesConsumed; packetLength -= localBytesConsumed; pendingEncryptedBytes -= localBytesConsumed; src.position(src.position() + localBytesConsumed); if (bytesRead > 0) { bytesProduced += bytesRead; if (!dst.hasRemaining()) { sslPending = sslPending0(); // Move to the next dst buffer as this one is full. if (++dstsOffset >= dstsEndOffset) { return sslPending > 0 ? newResult(BUFFER_OVERFLOW, status, bytesConsumed, bytesProduced) : newResultMayFinishHandshake(isInboundDone() ? CLOSED : OK, status, bytesConsumed, bytesProduced); } } else if (packetLength == 0 || jdkCompatibilityMode) { // We either consumed all data or we are in jdkCompatibilityMode and have consumed // a single TLS packet and should stop consuming until this method is called again. break srcLoop; } } else { int sslError = SSL.getError(ssl, bytesRead); if (sslError == SSL.SSL_ERROR_WANT_READ || sslError == SSL.SSL_ERROR_WANT_WRITE) { // break to the outer loop as we want to read more data which means we need to // write more to the BIO. break; } else if (sslError == SSL.SSL_ERROR_ZERO_RETURN) { // This means the connection was shutdown correctly, close inbound and outbound if (!receivedShutdown) { closeAll(); } return newResultMayFinishHandshake(isInboundDone() ? CLOSED : OK, status, bytesConsumed, bytesProduced); } else if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP || sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY || sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) { return newResult(isInboundDone() ? CLOSED : OK, NEED_TASK, bytesConsumed, bytesProduced); } else { return sslReadErrorResult(sslError, SSL.getLastErrorNumber(), bytesConsumed, bytesProduced); } } } if (++srcsOffset >= srcsEndOffset) { break; } } finally { if (bioWriteCopyBuf != null) { bioWriteCopyBuf.release(); } } } } finally { SSL.bioClearByteBuffer(networkBIO); rejectRemoteInitiatedRenegotiation(); } // Check to see if we received a close_notify message from the peer. if (!receivedShutdown && (SSL.getShutdown(ssl) & SSL.SSL_RECEIVED_SHUTDOWN) == SSL.SSL_RECEIVED_SHUTDOWN) { closeAll(); } return newResultMayFinishHandshake(isInboundDone() ? CLOSED : OK, status, bytesConsumed, bytesProduced); } } private boolean needWrapAgain(int stackError) { // Check if we have a pending handshakeException and if so see if we need to consume all pending data from the // BIO first or can just shutdown and throw it now. // This is needed so we ensure close_notify etc is correctly send to the remote peer. // See https://github.com/netty/netty/issues/3900 if (SSL.bioLengthNonApplication(networkBIO) > 0) { // we seem to have data left that needs to be transferred and so the user needs // call wrap(...). Store the error so we can pick it up later. if (pendingException == null) { pendingException = newSSLExceptionForError(stackError); } else if (shouldAddSuppressed(pendingException, stackError)) { ThrowableUtil.addSuppressed(pendingException, newSSLExceptionForError(stackError)); } // We need to clear all errors so we not pick up anything that was left on the stack on the next // operation. Note that shutdownWithError(...) will cleanup the stack as well so its only needed here. SSL.clearError(); return true; } return false; } private SSLException newSSLExceptionForError(int stackError) { String message = SSL.getErrorString(stackError); return handshakeState == HandshakeState.FINISHED ? new OpenSslException(message, stackError) : new OpenSslHandshakeException(message, stackError); } private static boolean shouldAddSuppressed(Throwable target, int errorCode) { for (Throwable suppressed: ThrowableUtil.getSuppressed(target)) { if (suppressed instanceof NativeSslException && ((NativeSslException) suppressed).errorCode() == errorCode) { /// An exception with this errorCode was already added before. return false; } } return true; } private SSLEngineResult sslReadErrorResult(int error, int stackError, int bytesConsumed, int bytesProduced) throws SSLException { if (needWrapAgain(stackError)) { // There is something that needs to be send to the remote peer before we can teardown. // This is most likely some alert. return new SSLEngineResult(OK, NEED_WRAP, bytesConsumed, bytesProduced); } throw shutdownWithError("SSL_read", error, stackError); } private void closeAll() throws SSLException { receivedShutdown = true; closeOutbound(); closeInbound(); } private void rejectRemoteInitiatedRenegotiation() throws SSLHandshakeException { // As rejectRemoteInitiatedRenegotiation() is called in a finally block we also need to check if we shutdown // the engine before as otherwise SSL.getHandshakeCount(ssl) will throw an NPE if the passed in ssl is 0. // See https://github.com/netty/netty/issues/7353 if (!isDestroyed() && (!clientMode && SSL.getHandshakeCount(ssl) > 1 || // Let's allow to renegotiate once for client auth. clientMode && SSL.getHandshakeCount(ssl) > 2) && // As we may count multiple handshakes when TLSv1.3 is used we should just ignore this here as // renegotiation is not supported in TLSv1.3 as per spec. !SslProtocols.TLS_v1_3.equals(session.getProtocol()) && handshakeState == HandshakeState.FINISHED) { // TODO: In future versions me may also want to send a fatal_alert to the client and so notify it // that the renegotiation failed. shutdown(); throw new SSLHandshakeException("remote-initiated renegotiation not allowed"); } } public final SSLEngineResult unwrap(final ByteBuffer[] srcs, final ByteBuffer[] dsts) throws SSLException { return unwrap(srcs, 0, srcs.length, dsts, 0, dsts.length); } private ByteBuffer[] singleSrcBuffer(ByteBuffer src) { singleSrcBuffer[0] = src; return singleSrcBuffer; } private void resetSingleSrcBuffer() { singleSrcBuffer[0] = null; } private ByteBuffer[] singleDstBuffer(ByteBuffer src) { singleDstBuffer[0] = src; return singleDstBuffer; } private void resetSingleDstBuffer() { singleDstBuffer[0] = null; } @Override public final synchronized SSLEngineResult unwrap( final ByteBuffer src, final ByteBuffer[] dsts, final int offset, final int length) throws SSLException { try { return unwrap(singleSrcBuffer(src), 0, 1, dsts, offset, length); } finally { resetSingleSrcBuffer(); } } @Override public final synchronized SSLEngineResult wrap(ByteBuffer src, ByteBuffer dst) throws SSLException { try { return wrap(singleSrcBuffer(src), dst); } finally { resetSingleSrcBuffer(); } } @Override public final synchronized SSLEngineResult unwrap(ByteBuffer src, ByteBuffer dst) throws SSLException { try { return unwrap(singleSrcBuffer(src), singleDstBuffer(dst)); } finally { resetSingleSrcBuffer(); resetSingleDstBuffer(); } } @Override public final synchronized SSLEngineResult unwrap(ByteBuffer src, ByteBuffer[] dsts) throws SSLException { try { return unwrap(singleSrcBuffer(src), dsts); } finally { resetSingleSrcBuffer(); } } private class TaskDecorator implements Runnable { protected final R task; TaskDecorator(R task) { this.task = task; } @Override public void run() { runAndResetNeedTask(task); } } private final class AsyncTaskDecorator extends TaskDecorator implements AsyncRunnable { AsyncTaskDecorator(AsyncTask task) { super(task); } @Override public void run(final Runnable runnable) { if (isDestroyed()) { // The engine was destroyed in the meantime, just return. return; } task.runAsync(new TaskDecorator(runnable)); } } private void runAndResetNeedTask(Runnable task) { // We need to synchronize on the ReferenceCountedOpenSslEngine, we are sure the SSL object // will not be freed by the user calling for example shutdown() concurrently. synchronized (ReferenceCountedOpenSslEngine.this) { try { if (isDestroyed()) { // The engine was destroyed in the meantime, just return. return; } task.run(); if (handshakeState != HandshakeState.FINISHED && !isDestroyed()) { // Call SSL.doHandshake(...) If the handshake was not finished yet. This might be needed // to fill the application buffer and so have getHandshakeStatus() return the right value // in this case. if (SSL.doHandshake(ssl) <= 0) { SSL.clearError(); } } } finally { // The task was run, reset needTask to false so getHandshakeStatus() returns the correct value. needTask = false; } } } @Override public final synchronized Runnable getDelegatedTask() { if (isDestroyed()) { return null; } final Runnable task = SSL.getTask(ssl); if (task == null) { return null; } if (task instanceof AsyncTask) { return new AsyncTaskDecorator((AsyncTask) task); } return new TaskDecorator(task); } @Override public final synchronized void closeInbound() throws SSLException { if (isInboundDone) { return; } isInboundDone = true; if (isOutboundDone()) { // Only call shutdown if there is no outbound data pending. // See https://github.com/netty/netty/issues/6167 shutdown(); } if (handshakeState != HandshakeState.NOT_STARTED && !receivedShutdown) { throw new SSLException( "Inbound closed before receiving peer's close_notify: possible truncation attack?"); } } @Override public final synchronized boolean isInboundDone() { return isInboundDone; } @Override public final synchronized void closeOutbound() { if (outboundClosed) { return; } outboundClosed = true; if (handshakeState != HandshakeState.NOT_STARTED && !isDestroyed()) { int mode = SSL.getShutdown(ssl); if ((mode & SSL.SSL_SENT_SHUTDOWN) != SSL.SSL_SENT_SHUTDOWN) { doSSLShutdown(); } } else { // engine closing before initial handshake shutdown(); } } /** * Attempt to call {@link SSL#shutdownSSL(long)}. * @return {@code false} if the call to {@link SSL#shutdownSSL(long)} was not attempted or returned an error. */ private boolean doSSLShutdown() { if (SSL.isInInit(ssl) != 0) { // Only try to call SSL_shutdown if we are not in the init state anymore. // Otherwise we will see 'error:140E0197:SSL routines:SSL_shutdown:shutdown while in init' in our logs. // // See also https://hg.nginx.org/nginx/rev/062c189fee20 return false; } int err = SSL.shutdownSSL(ssl); if (err < 0) { int sslErr = SSL.getError(ssl, err); if (sslErr == SSL.SSL_ERROR_SYSCALL || sslErr == SSL.SSL_ERROR_SSL) { if (logger.isDebugEnabled()) { int error = SSL.getLastErrorNumber(); logger.debug("SSL_shutdown failed: OpenSSL error: {} {}", error, SSL.getErrorString(error)); } // There was an internal error -- shutdown shutdown(); return false; } SSL.clearError(); } return true; } @Override public final synchronized boolean isOutboundDone() { // Check if there is anything left in the outbound buffer. // We need to ensure we only call SSL.pendingWrittenBytesInBIO(...) if the engine was not destroyed yet. return outboundClosed && (networkBIO == 0 || SSL.bioLengthNonApplication(networkBIO) == 0); } @Override public final String[] getSupportedCipherSuites() { return OpenSsl.AVAILABLE_CIPHER_SUITES.toArray(EMPTY_STRINGS); } @Override public final String[] getEnabledCipherSuites() { final String[] extraCiphers; final String[] enabled; final boolean tls13Enabled; synchronized (this) { if (!isDestroyed()) { enabled = SSL.getCiphers(ssl); int opts = SSL.getOptions(ssl); if (isProtocolEnabled(opts, SSL.SSL_OP_NO_TLSv1_3, SslProtocols.TLS_v1_3)) { extraCiphers = OpenSsl.EXTRA_SUPPORTED_TLS_1_3_CIPHERS; tls13Enabled = true; } else { extraCiphers = EMPTY_STRINGS; tls13Enabled = false; } } else { return EMPTY_STRINGS; } } if (enabled == null) { return EMPTY_STRINGS; } else { Set enabledSet = new LinkedHashSet(enabled.length + extraCiphers.length); synchronized (this) { for (int i = 0; i < enabled.length; i++) { String mapped = toJavaCipherSuite(enabled[i]); final String cipher = mapped == null ? enabled[i] : mapped; if ((!tls13Enabled || !OpenSsl.isTlsv13Supported()) && SslUtils.isTLSv13Cipher(cipher)) { continue; } enabledSet.add(cipher); } Collections.addAll(enabledSet, extraCiphers); } return enabledSet.toArray(EMPTY_STRINGS); } } @Override public final void setEnabledCipherSuites(String[] cipherSuites) { checkNotNull(cipherSuites, "cipherSuites"); final StringBuilder buf = new StringBuilder(); final StringBuilder bufTLSv13 = new StringBuilder(); CipherSuiteConverter.convertToCipherStrings(Arrays.asList(cipherSuites), buf, bufTLSv13, OpenSsl.isBoringSSL()); final String cipherSuiteSpec = buf.toString(); final String cipherSuiteSpecTLSv13 = bufTLSv13.toString(); if (!OpenSsl.isTlsv13Supported() && !cipherSuiteSpecTLSv13.isEmpty()) { throw new IllegalArgumentException("TLSv1.3 is not supported by this java version."); } synchronized (this) { hasTLSv13Cipher = !cipherSuiteSpecTLSv13.isEmpty(); if (!isDestroyed()) { try { // Set non TLSv1.3 ciphers. SSL.setCipherSuites(ssl, cipherSuiteSpec, false); if (OpenSsl.isTlsv13Supported()) { // Set TLSv1.3 ciphers. SSL.setCipherSuites(ssl, OpenSsl.checkTls13Ciphers(logger, cipherSuiteSpecTLSv13), true); } // We also need to update the enabled protocols to ensure we disable the protocol if there are // no compatible ciphers left. Set protocols = new HashSet(enabledProtocols); // We have no ciphers that are compatible with none-TLSv1.3, let us explicit disable all other // protocols. if (cipherSuiteSpec.isEmpty()) { protocols.remove(SslProtocols.TLS_v1); protocols.remove(SslProtocols.TLS_v1_1); protocols.remove(SslProtocols.TLS_v1_2); protocols.remove(SslProtocols.SSL_v3); protocols.remove(SslProtocols.SSL_v2); protocols.remove(SslProtocols.SSL_v2_HELLO); } // We have no ciphers that are compatible with TLSv1.3, let us explicit disable it. if (cipherSuiteSpecTLSv13.isEmpty()) { protocols.remove(SslProtocols.TLS_v1_3); } // Update the protocols but not cache the value. We only cache when we call it from the user // code or when we construct the engine. setEnabledProtocols0(protocols.toArray(EMPTY_STRINGS), !hasTLSv13Cipher); } catch (Exception e) { throw new IllegalStateException("failed to enable cipher suites: " + cipherSuiteSpec, e); } } else { throw new IllegalStateException("failed to enable cipher suites: " + cipherSuiteSpec); } } } @Override public final String[] getSupportedProtocols() { return OpenSsl.SUPPORTED_PROTOCOLS_SET.toArray(EMPTY_STRINGS); } @Override public final String[] getEnabledProtocols() { return enabledProtocols.toArray(EMPTY_STRINGS); } private static boolean isProtocolEnabled(int opts, int disableMask, String protocolString) { // We also need to check if the actual protocolString is supported as depending on the openssl API // implementations it may use a disableMask of 0 (BoringSSL is doing this for example). return (opts & disableMask) == 0 && OpenSsl.SUPPORTED_PROTOCOLS_SET.contains(protocolString); } /** * {@inheritDoc} * TLS doesn't support a way to advertise non-contiguous versions from the client's perspective, and the client * just advertises the max supported version. The TLS protocol also doesn't support all different combinations of * discrete protocols, and instead assumes contiguous ranges. OpenSSL has some unexpected behavior * (e.g. handshake failures) if non-contiguous protocols are used even where there is a compatible set of protocols * and ciphers. For these reasons this method will determine the minimum protocol and the maximum protocol and * enabled a contiguous range from [min protocol, max protocol] in OpenSSL. */ @Override public final void setEnabledProtocols(String[] protocols) { checkNotNullWithIAE(protocols, "protocols"); synchronized (this) { enabledProtocols.clear(); // Seems like there is no way to explicit disable SSLv2Hello in openssl, so it is always enabled enabledProtocols.add(SslProtocols.SSL_v2_HELLO); Collections.addAll(enabledProtocols, protocols); setEnabledProtocols0(protocols, !hasTLSv13Cipher); } } private void setEnabledProtocols0(String[] protocols, boolean explicitDisableTLSv13) { assert Thread.holdsLock(this); // This is correct from the API docs int minProtocolIndex = OPENSSL_OP_NO_PROTOCOLS.length; int maxProtocolIndex = 0; for (String p: protocols) { if (!OpenSsl.SUPPORTED_PROTOCOLS_SET.contains(p)) { throw new IllegalArgumentException("Protocol " + p + " is not supported."); } if (p.equals(SslProtocols.SSL_v2)) { if (minProtocolIndex > OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV2) { minProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV2; } if (maxProtocolIndex < OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV2) { maxProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV2; } } else if (p.equals(SslProtocols.SSL_v3)) { if (minProtocolIndex > OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV3) { minProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV3; } if (maxProtocolIndex < OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV3) { maxProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV3; } } else if (p.equals(SslProtocols.TLS_v1)) { if (minProtocolIndex > OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1) { minProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1; } if (maxProtocolIndex < OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1) { maxProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1; } } else if (p.equals(SslProtocols.TLS_v1_1)) { if (minProtocolIndex > OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_1) { minProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_1; } if (maxProtocolIndex < OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_1) { maxProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_1; } } else if (p.equals(SslProtocols.TLS_v1_2)) { if (minProtocolIndex > OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_2) { minProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_2; } if (maxProtocolIndex < OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_2) { maxProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_2; } } else if (!explicitDisableTLSv13 && p.equals(SslProtocols.TLS_v1_3)) { if (minProtocolIndex > OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_3) { minProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_3; } if (maxProtocolIndex < OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_3) { maxProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_3; } } } if (!isDestroyed()) { // Clear out options which disable protocols SSL.clearOptions(ssl, SSL.SSL_OP_NO_SSLv2 | SSL.SSL_OP_NO_SSLv3 | SSL.SSL_OP_NO_TLSv1 | SSL.SSL_OP_NO_TLSv1_1 | SSL.SSL_OP_NO_TLSv1_2 | SSL.SSL_OP_NO_TLSv1_3); int opts = 0; for (int i = 0; i < minProtocolIndex; ++i) { opts |= OPENSSL_OP_NO_PROTOCOLS[i]; } assert maxProtocolIndex != MAX_VALUE; for (int i = maxProtocolIndex + 1; i < OPENSSL_OP_NO_PROTOCOLS.length; ++i) { opts |= OPENSSL_OP_NO_PROTOCOLS[i]; } // Disable protocols we do not want SSL.setOptions(ssl, opts); } else { throw new IllegalStateException("failed to enable protocols: " + Arrays.asList(protocols)); } } @Override public final SSLSession getSession() { return session; } @Override public final synchronized void beginHandshake() throws SSLException { switch (handshakeState) { case STARTED_IMPLICITLY: checkEngineClosed(); // A user did not start handshake by calling this method by him/herself, // but handshake has been started already by wrap() or unwrap() implicitly. // Because it's the user's first time to call this method, it is unfair to // raise an exception. From the user's standpoint, he or she never asked // for renegotiation. handshakeState = HandshakeState.STARTED_EXPLICITLY; // Next time this method is invoked by the user, calculateMaxWrapOverhead(); // we should raise an exception. break; case STARTED_EXPLICITLY: // Nothing to do as the handshake is not done yet. break; case FINISHED: throw new SSLException("renegotiation unsupported"); case NOT_STARTED: handshakeState = HandshakeState.STARTED_EXPLICITLY; if (handshake() == NEED_TASK) { // Set needTask to true so getHandshakeStatus() will return the correct value. needTask = true; } calculateMaxWrapOverhead(); break; default: throw new Error(); } } private void checkEngineClosed() throws SSLException { if (isDestroyed()) { throw new SSLException("engine closed"); } } private static SSLEngineResult.HandshakeStatus pendingStatus(int pendingStatus) { // Depending on if there is something left in the BIO we need to WRAP or UNWRAP return pendingStatus > 0 ? NEED_WRAP : NEED_UNWRAP; } private static boolean isEmpty(Object[] arr) { return arr == null || arr.length == 0; } private static boolean isEmpty(byte[] cert) { return cert == null || cert.length == 0; } private SSLEngineResult.HandshakeStatus handshakeException() throws SSLException { if (SSL.bioLengthNonApplication(networkBIO) > 0) { // There is something pending, we need to consume it first via a WRAP so we don't loose anything. return NEED_WRAP; } Throwable exception = pendingException; assert exception != null; pendingException = null; shutdown(); if (exception instanceof SSLHandshakeException) { throw (SSLHandshakeException) exception; } SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem"); e.initCause(exception); throw e; } /** * Should be called if the handshake will be failed due a callback that throws an exception. * This cause will then be used to give more details as part of the {@link SSLHandshakeException}. */ final void initHandshakeException(Throwable cause) { if (pendingException == null) { pendingException = cause; } else { ThrowableUtil.addSuppressed(pendingException, cause); } } private SSLEngineResult.HandshakeStatus handshake() throws SSLException { if (needTask) { return NEED_TASK; } if (handshakeState == HandshakeState.FINISHED) { return FINISHED; } checkEngineClosed(); if (pendingException != null) { // Let's call SSL.doHandshake(...) again in case there is some async operation pending that would fill the // outbound buffer. if (SSL.doHandshake(ssl) <= 0) { // Clear any error that was put on the stack by the handshake SSL.clearError(); } return handshakeException(); } // Adding the OpenSslEngine to the OpenSslEngineMap so it can be used in the AbstractCertificateVerifier. engineMap.add(this); if (!sessionSet) { if (!parentContext.sessionContext().setSessionFromCache(ssl, session, getPeerHost(), getPeerPort())) { // The session was not reused via the cache. Call prepareHandshake() to ensure we remove all previous // stored key-value pairs. session.prepareHandshake(); } sessionSet = true; } int code = SSL.doHandshake(ssl); if (code <= 0) { int sslError = SSL.getError(ssl, code); if (sslError == SSL.SSL_ERROR_WANT_READ || sslError == SSL.SSL_ERROR_WANT_WRITE) { return pendingStatus(SSL.bioLengthNonApplication(networkBIO)); } if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP || sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY || sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) { return NEED_TASK; } if (needWrapAgain(SSL.getLastErrorNumber())) { // There is something that needs to be send to the remote peer before we can teardown. // This is most likely some alert. return NEED_WRAP; } // Check if we have a pending exception that was created during the handshake and if so throw it after // shutdown the connection. if (pendingException != null) { return handshakeException(); } // Everything else is considered as error throw shutdownWithError("SSL_do_handshake", sslError); } // We have produced more data as part of the handshake if this is the case the user should call wrap(...) if (SSL.bioLengthNonApplication(networkBIO) > 0) { return NEED_WRAP; } // if SSL_do_handshake returns > 0 or sslError == SSL.SSL_ERROR_NAME it means the handshake was finished. session.handshakeFinished(SSL.getSessionId(ssl), SSL.getCipherForSSL(ssl), SSL.getVersion(ssl), SSL.getPeerCertificate(ssl), SSL.getPeerCertChain(ssl), SSL.getTime(ssl) * 1000L, parentContext.sessionTimeout() * 1000L); selectApplicationProtocol(); return FINISHED; } private SSLEngineResult.HandshakeStatus mayFinishHandshake( SSLEngineResult.HandshakeStatus hs, int bytesConsumed, int bytesProduced) throws SSLException { return hs == NEED_UNWRAP && bytesProduced > 0 || hs == NEED_WRAP && bytesConsumed > 0 ? handshake() : mayFinishHandshake(hs != FINISHED ? getHandshakeStatus() : FINISHED); } private SSLEngineResult.HandshakeStatus mayFinishHandshake(SSLEngineResult.HandshakeStatus status) throws SSLException { if (status == NOT_HANDSHAKING) { if (handshakeState != HandshakeState.FINISHED) { // If the status was NOT_HANDSHAKING and we not finished the handshake we need to call // SSL_do_handshake() again return handshake(); } if (!isDestroyed() && SSL.bioLengthNonApplication(networkBIO) > 0) { // We have something left that needs to be wrapped. return NEED_WRAP; } } return status; } @Override public final synchronized SSLEngineResult.HandshakeStatus getHandshakeStatus() { // Check if we are in the initial handshake phase or shutdown phase if (needPendingStatus()) { if (needTask) { // There is a task outstanding return NEED_TASK; } return pendingStatus(SSL.bioLengthNonApplication(networkBIO)); } return NOT_HANDSHAKING; } private SSLEngineResult.HandshakeStatus getHandshakeStatus(int pending) { // Check if we are in the initial handshake phase or shutdown phase if (needPendingStatus()) { if (needTask) { // There is a task outstanding return NEED_TASK; } return pendingStatus(pending); } return NOT_HANDSHAKING; } private boolean needPendingStatus() { return handshakeState != HandshakeState.NOT_STARTED && !isDestroyed() && (handshakeState != HandshakeState.FINISHED || isInboundDone() || isOutboundDone()); } /** * Converts the specified OpenSSL cipher suite to the Java cipher suite. */ private String toJavaCipherSuite(String openSslCipherSuite) { if (openSslCipherSuite == null) { return null; } String version = SSL.getVersion(ssl); String prefix = toJavaCipherSuitePrefix(version); return CipherSuiteConverter.toJava(openSslCipherSuite, prefix); } /** * Converts the protocol version string returned by {@link SSL#getVersion(long)} to protocol family string. */ private static String toJavaCipherSuitePrefix(String protocolVersion) { final char c; if (protocolVersion == null || protocolVersion.isEmpty()) { c = 0; } else { c = protocolVersion.charAt(0); } switch (c) { case 'T': return "TLS"; case 'S': return "SSL"; default: return "UNKNOWN"; } } @Override public final void setUseClientMode(boolean clientMode) { if (clientMode != this.clientMode) { throw new UnsupportedOperationException(); } } @Override public final boolean getUseClientMode() { return clientMode; } @Override public final void setNeedClientAuth(boolean b) { setClientAuth(b ? ClientAuth.REQUIRE : ClientAuth.NONE); } @Override public final boolean getNeedClientAuth() { return clientAuth == ClientAuth.REQUIRE; } @Override public final void setWantClientAuth(boolean b) { setClientAuth(b ? ClientAuth.OPTIONAL : ClientAuth.NONE); } @Override public final boolean getWantClientAuth() { return clientAuth == ClientAuth.OPTIONAL; } /** * See SSL_set_verify and * {@link SSL#setVerify(long, int, int)}. */ @UnstableApi public final synchronized void setVerify(int verifyMode, int depth) { if (!isDestroyed()) { SSL.setVerify(ssl, verifyMode, depth); } } private void setClientAuth(ClientAuth mode) { if (clientMode) { return; } synchronized (this) { if (clientAuth == mode) { // No need to issue any JNI calls if the mode is the same return; } if (!isDestroyed()) { switch (mode) { case NONE: SSL.setVerify(ssl, SSL.SSL_CVERIFY_NONE, ReferenceCountedOpenSslContext.VERIFY_DEPTH); break; case REQUIRE: SSL.setVerify(ssl, SSL.SSL_CVERIFY_REQUIRED, ReferenceCountedOpenSslContext.VERIFY_DEPTH); break; case OPTIONAL: SSL.setVerify(ssl, SSL.SSL_CVERIFY_OPTIONAL, ReferenceCountedOpenSslContext.VERIFY_DEPTH); break; default: throw new Error(mode.toString()); } } clientAuth = mode; } } @Override public final void setEnableSessionCreation(boolean b) { if (b) { throw new UnsupportedOperationException(); } } @Override public final boolean getEnableSessionCreation() { return false; } @SuppressJava6Requirement(reason = "Usage guarded by java version check") @Override public final synchronized SSLParameters getSSLParameters() { SSLParameters sslParameters = super.getSSLParameters(); int version = PlatformDependent.javaVersion(); if (version >= 7) { Java7SslParametersUtils.setEndpointIdentificationAlgorithm(sslParameters, endpointIdentificationAlgorithm); Java7SslParametersUtils.setAlgorithmConstraints(sslParameters, algorithmConstraints); if (version >= 8) { if (sniHostNames != null) { Java8SslUtils.setSniHostNames(sslParameters, sniHostNames); } if (!isDestroyed()) { Java8SslUtils.setUseCipherSuitesOrder( sslParameters, (SSL.getOptions(ssl) & SSL.SSL_OP_CIPHER_SERVER_PREFERENCE) != 0); } Java8SslUtils.setSNIMatchers(sslParameters, matchers); } } return sslParameters; } @SuppressJava6Requirement(reason = "Usage guarded by java version check") @Override public final synchronized void setSSLParameters(SSLParameters sslParameters) { int version = PlatformDependent.javaVersion(); if (version >= 7) { if (sslParameters.getAlgorithmConstraints() != null) { throw new IllegalArgumentException("AlgorithmConstraints are not supported."); } boolean isDestroyed = isDestroyed(); if (version >= 8) { if (!isDestroyed) { if (clientMode) { final List sniHostNames = Java8SslUtils.getSniHostNames(sslParameters); for (String name: sniHostNames) { SSL.setTlsExtHostName(ssl, name); } this.sniHostNames = sniHostNames; } if (Java8SslUtils.getUseCipherSuitesOrder(sslParameters)) { SSL.setOptions(ssl, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); } else { SSL.clearOptions(ssl, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); } } matchers = sslParameters.getSNIMatchers(); } final String endpointIdentificationAlgorithm = sslParameters.getEndpointIdentificationAlgorithm(); if (!isDestroyed) { configureEndpointVerification(endpointIdentificationAlgorithm); } this.endpointIdentificationAlgorithm = endpointIdentificationAlgorithm; algorithmConstraints = sslParameters.getAlgorithmConstraints(); } super.setSSLParameters(sslParameters); } private void configureEndpointVerification(String endpointIdentificationAlgorithm) { // If the user asks for hostname verification we must ensure we verify the peer. // If the user disables hostname verification we leave it up to the user to change the mode manually. if (clientMode && isEndPointVerificationEnabled(endpointIdentificationAlgorithm)) { SSL.setVerify(ssl, SSL.SSL_CVERIFY_REQUIRED, -1); } } private static boolean isEndPointVerificationEnabled(String endPointIdentificationAlgorithm) { return endPointIdentificationAlgorithm != null && !endPointIdentificationAlgorithm.isEmpty(); } private boolean isDestroyed() { return destroyed; } final boolean checkSniHostnameMatch(byte[] hostname) { return Java8SslUtils.checkSniHostnameMatch(matchers, hostname); } @Override public String getNegotiatedApplicationProtocol() { return applicationProtocol; } private static long bufferAddress(ByteBuffer b) { assert b.isDirect(); if (PlatformDependent.hasUnsafe()) { return PlatformDependent.directBufferAddress(b); } return Buffer.address(b); } /** * Select the application protocol used. */ private void selectApplicationProtocol() throws SSLException { ApplicationProtocolConfig.SelectedListenerFailureBehavior behavior = apn.selectedListenerFailureBehavior(); List protocols = apn.protocols(); String applicationProtocol; switch (apn.protocol()) { case NONE: break; // We always need to check for applicationProtocol == null as the remote peer may not support // the TLS extension or may have returned an empty selection. case ALPN: applicationProtocol = SSL.getAlpnSelected(ssl); if (applicationProtocol != null) { ReferenceCountedOpenSslEngine.this.applicationProtocol = selectApplicationProtocol( protocols, behavior, applicationProtocol); } break; case NPN: applicationProtocol = SSL.getNextProtoNegotiated(ssl); if (applicationProtocol != null) { ReferenceCountedOpenSslEngine.this.applicationProtocol = selectApplicationProtocol( protocols, behavior, applicationProtocol); } break; case NPN_AND_ALPN: applicationProtocol = SSL.getAlpnSelected(ssl); if (applicationProtocol == null) { applicationProtocol = SSL.getNextProtoNegotiated(ssl); } if (applicationProtocol != null) { ReferenceCountedOpenSslEngine.this.applicationProtocol = selectApplicationProtocol( protocols, behavior, applicationProtocol); } break; default: throw new Error(); } } private String selectApplicationProtocol(List protocols, ApplicationProtocolConfig.SelectedListenerFailureBehavior behavior, String applicationProtocol) throws SSLException { if (behavior == ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT) { return applicationProtocol; } else { int size = protocols.size(); assert size > 0; if (protocols.contains(applicationProtocol)) { return applicationProtocol; } else { if (behavior == ApplicationProtocolConfig.SelectedListenerFailureBehavior.CHOOSE_MY_LAST_PROTOCOL) { return protocols.get(size - 1); } else { throw new SSLException("unknown protocol " + applicationProtocol); } } } } private static final X509Certificate[] JAVAX_CERTS_NOT_SUPPORTED = new X509Certificate[0]; private final class DefaultOpenSslSession implements OpenSslSession { private final OpenSslSessionContext sessionContext; // These are guarded by synchronized(OpenSslEngine.this) as handshakeFinished() may be triggered by any // thread. private X509Certificate[] x509PeerCerts; private Certificate[] peerCerts; private boolean valid = true; private String protocol; private String cipher; private OpenSslSessionId id = OpenSslSessionId.NULL_ID; private long creationTime; // Updated once a new handshake is started and so the SSLSession reused. private long lastAccessed = -1; private volatile int applicationBufferSize = MAX_PLAINTEXT_LENGTH; private volatile Certificate[] localCertificateChain; private volatile Map keyValueStorage = new ConcurrentHashMap(); DefaultOpenSslSession(OpenSslSessionContext sessionContext) { this.sessionContext = sessionContext; } private SSLSessionBindingEvent newSSLSessionBindingEvent(String name) { return new SSLSessionBindingEvent(session, name); } @Override public void prepareHandshake() { keyValueStorage.clear(); } @Override public void setSessionDetails( long creationTime, long lastAccessedTime, OpenSslSessionId sessionId, Map keyValueStorage) { synchronized (ReferenceCountedOpenSslEngine.this) { if (this.id == OpenSslSessionId.NULL_ID) { this.id = sessionId; this.creationTime = creationTime; this.lastAccessed = lastAccessedTime; // Update the key value storage. It's fine to just drop the previous stored values on the floor // as the JDK does the same in the sense that it will use a new SSLSessionImpl instance once the // handshake was done this.keyValueStorage = keyValueStorage; } } } @Override public Map keyValueStorage() { return keyValueStorage; } @Override public OpenSslSessionId sessionId() { synchronized (ReferenceCountedOpenSslEngine.this) { if (this.id == OpenSslSessionId.NULL_ID && !isDestroyed()) { byte[] sessionId = SSL.getSessionId(ssl); if (sessionId != null) { id = new OpenSslSessionId(sessionId); } } return id; } } @Override public void setLocalCertificate(Certificate[] localCertificate) { this.localCertificateChain = localCertificate; } @Override public byte[] getId() { return sessionId().cloneBytes(); } @Override public OpenSslSessionContext getSessionContext() { return sessionContext; } @Override public long getCreationTime() { synchronized (ReferenceCountedOpenSslEngine.this) { return creationTime; } } @Override public void setLastAccessedTime(long time) { synchronized (ReferenceCountedOpenSslEngine.this) { this.lastAccessed = time; } } @Override public long getLastAccessedTime() { // if lastAccessed is -1 we will just return the creation time as the handshake was not started yet. synchronized (ReferenceCountedOpenSslEngine.this) { return lastAccessed == -1 ? creationTime : lastAccessed; } } @Override public void invalidate() { synchronized (ReferenceCountedOpenSslEngine.this) { valid = false; sessionContext.removeFromCache(id); } } @Override public boolean isValid() { synchronized (ReferenceCountedOpenSslEngine.this) { return valid || sessionContext.isInCache(id); } } @Override public void putValue(String name, Object value) { checkNotNull(name, "name"); checkNotNull(value, "value"); final Object old = keyValueStorage.put(name, value); if (value instanceof SSLSessionBindingListener) { // Use newSSLSessionBindingEvent so we always use the wrapper if needed. ((SSLSessionBindingListener) value).valueBound(newSSLSessionBindingEvent(name)); } notifyUnbound(old, name); } @Override public Object getValue(String name) { checkNotNull(name, "name"); return keyValueStorage.get(name); } @Override public void removeValue(String name) { checkNotNull(name, "name"); final Object old = keyValueStorage.remove(name); notifyUnbound(old, name); } @Override public String[] getValueNames() { return keyValueStorage.keySet().toArray(EMPTY_STRINGS); } private void notifyUnbound(Object value, String name) { if (value instanceof SSLSessionBindingListener) { // Use newSSLSessionBindingEvent so we always use the wrapper if needed. ((SSLSessionBindingListener) value).valueUnbound(newSSLSessionBindingEvent(name)); } } /** * Finish the handshake and so init everything in the {@link OpenSslSession} that should be accessible by * the user. */ @Override public void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCertificate, byte[][] peerCertificateChain, long creationTime, long timeout) throws SSLException { synchronized (ReferenceCountedOpenSslEngine.this) { if (!isDestroyed()) { if (this.id == OpenSslSessionId.NULL_ID) { // if the handshake finished and it was not a resumption let ensure we try to set the id this.id = id == null ? OpenSslSessionId.NULL_ID : new OpenSslSessionId(id); // Once the handshake was done the lastAccessed and creationTime should be the same if we // did not set it earlier via setSessionDetails(...) this.creationTime = lastAccessed = creationTime; } this.cipher = toJavaCipherSuite(cipher); this.protocol = protocol; if (clientMode) { if (isEmpty(peerCertificateChain)) { peerCerts = EmptyArrays.EMPTY_CERTIFICATES; if (OpenSsl.JAVAX_CERTIFICATE_CREATION_SUPPORTED) { x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; } else { x509PeerCerts = JAVAX_CERTS_NOT_SUPPORTED; } } else { peerCerts = new Certificate[peerCertificateChain.length]; if (OpenSsl.JAVAX_CERTIFICATE_CREATION_SUPPORTED) { x509PeerCerts = new X509Certificate[peerCertificateChain.length]; } else { x509PeerCerts = JAVAX_CERTS_NOT_SUPPORTED; } initCerts(peerCertificateChain, 0); } } else { // if used on the server side SSL_get_peer_cert_chain(...) will not include the remote peer // certificate. We use SSL_get_peer_certificate to get it in this case and add it to our // array later. // // See https://www.openssl.org/docs/ssl/SSL_get_peer_cert_chain.html if (isEmpty(peerCertificate)) { peerCerts = EmptyArrays.EMPTY_CERTIFICATES; x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; } else { if (isEmpty(peerCertificateChain)) { peerCerts = new Certificate[] {new LazyX509Certificate(peerCertificate)}; if (OpenSsl.JAVAX_CERTIFICATE_CREATION_SUPPORTED) { x509PeerCerts = new X509Certificate[] { new LazyJavaxX509Certificate(peerCertificate) }; } else { x509PeerCerts = JAVAX_CERTS_NOT_SUPPORTED; } } else { peerCerts = new Certificate[peerCertificateChain.length + 1]; peerCerts[0] = new LazyX509Certificate(peerCertificate); if (OpenSsl.JAVAX_CERTIFICATE_CREATION_SUPPORTED) { x509PeerCerts = new X509Certificate[peerCertificateChain.length + 1]; x509PeerCerts[0] = new LazyJavaxX509Certificate(peerCertificate); } else { x509PeerCerts = JAVAX_CERTS_NOT_SUPPORTED; } initCerts(peerCertificateChain, 1); } } } calculateMaxWrapOverhead(); handshakeState = HandshakeState.FINISHED; } else { throw new SSLException("Already closed"); } } } private void initCerts(byte[][] chain, int startPos) { for (int i = 0; i < chain.length; i++) { int certPos = startPos + i; peerCerts[certPos] = new LazyX509Certificate(chain[i]); if (x509PeerCerts != JAVAX_CERTS_NOT_SUPPORTED) { x509PeerCerts[certPos] = new LazyJavaxX509Certificate(chain[i]); } } } @Override public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { synchronized (ReferenceCountedOpenSslEngine.this) { if (isEmpty(peerCerts)) { throw new SSLPeerUnverifiedException("peer not verified"); } return peerCerts.clone(); } } @Override public Certificate[] getLocalCertificates() { Certificate[] localCerts = this.localCertificateChain; if (localCerts == null) { return null; } return localCerts.clone(); } @Override public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException { synchronized (ReferenceCountedOpenSslEngine.this) { if (x509PeerCerts == JAVAX_CERTS_NOT_SUPPORTED) { // Not supported by the underlying JDK, so just throw. This is fine in terms of the API // contract. See SSLSession.html#getPeerCertificateChain(). throw new UnsupportedOperationException(); } if (isEmpty(x509PeerCerts)) { throw new SSLPeerUnverifiedException("peer not verified"); } return x509PeerCerts.clone(); } } @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { Certificate[] peer = getPeerCertificates(); // No need for null or length > 0 is needed as this is done in getPeerCertificates() // already. return ((java.security.cert.X509Certificate) peer[0]).getSubjectX500Principal(); } @Override public Principal getLocalPrincipal() { Certificate[] local = this.localCertificateChain; if (local == null || local.length == 0) { return null; } return ((java.security.cert.X509Certificate) local[0]).getSubjectX500Principal(); } @Override public String getCipherSuite() { synchronized (ReferenceCountedOpenSslEngine.this) { if (cipher == null) { return SslUtils.INVALID_CIPHER; } return cipher; } } @Override public String getProtocol() { String protocol = this.protocol; if (protocol == null) { synchronized (ReferenceCountedOpenSslEngine.this) { if (!isDestroyed()) { protocol = SSL.getVersion(ssl); } else { protocol = StringUtil.EMPTY_STRING; } } } return protocol; } @Override public String getPeerHost() { return ReferenceCountedOpenSslEngine.this.getPeerHost(); } @Override public int getPeerPort() { return ReferenceCountedOpenSslEngine.this.getPeerPort(); } @Override public int getPacketBufferSize() { return SSL.SSL_MAX_ENCRYPTED_LENGTH; } @Override public int getApplicationBufferSize() { return applicationBufferSize; } @Override public void tryExpandApplicationBufferSize(int packetLengthDataOnly) { if (packetLengthDataOnly > MAX_PLAINTEXT_LENGTH && applicationBufferSize != MAX_RECORD_SIZE) { applicationBufferSize = MAX_RECORD_SIZE; } } @Override public String toString() { return "DefaultOpenSslSession{" + "sessionContext=" + sessionContext + ", id=" + id + '}'; } @Override public int hashCode() { return sessionId().hashCode(); } @Override public boolean equals(Object o) { if (o == this) { return true; } // We trust all sub-types as we use different types but the interface is package-private if (!(o instanceof OpenSslSession)) { return false; } return sessionId().equals(((OpenSslSession) o).sessionId()); } } private interface NativeSslException { int errorCode(); } private static final class OpenSslException extends SSLException implements NativeSslException { private final int errorCode; OpenSslException(String reason, int errorCode) { super(reason); this.errorCode = errorCode; } @Override public int errorCode() { return errorCode; } } private static final class OpenSslHandshakeException extends SSLHandshakeException implements NativeSslException { private final int errorCode; OpenSslHandshakeException(String reason, int errorCode) { super(reason); this.errorCode = errorCode; } @Override public int errorCode() { return errorCode; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy