313 lines
14 KiB
Java
313 lines
14 KiB
Java
/*
|
|
* 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.bootstrap.Bootstrap;
|
|
import io.netty.bootstrap.ServerBootstrap;
|
|
import io.netty.channel.Channel;
|
|
import io.netty.channel.ChannelHandler;
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
import io.netty.channel.ChannelInitializer;
|
|
import io.netty.channel.EventLoopGroup;
|
|
import io.netty.channel.MultithreadEventLoopGroup;
|
|
import io.netty.channel.nio.NioHandler;
|
|
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
|
import io.netty.handler.logging.LogLevel;
|
|
import io.netty.handler.logging.LoggingHandler;
|
|
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
|
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
|
import io.netty.handler.ssl.util.SimpleTrustManagerFactory;
|
|
import io.netty.util.ReferenceCountUtil;
|
|
import io.netty.util.concurrent.Promise;
|
|
import io.netty.util.internal.EmptyArrays;
|
|
import org.junit.jupiter.api.Timeout;
|
|
import org.junit.jupiter.params.ParameterizedTest;
|
|
import org.junit.jupiter.params.provider.MethodSource;
|
|
|
|
import javax.net.ssl.ManagerFactoryParameters;
|
|
import javax.net.ssl.SSLException;
|
|
import javax.net.ssl.TrustManager;
|
|
import javax.net.ssl.X509TrustManager;
|
|
import javax.security.auth.x500.X500Principal;
|
|
import java.io.File;
|
|
import java.security.KeyStore;
|
|
import java.security.cert.CRLReason;
|
|
import java.security.cert.CertPathValidatorException;
|
|
import java.security.cert.CertificateException;
|
|
import java.security.cert.CertificateExpiredException;
|
|
import java.security.cert.CertificateNotYetValidException;
|
|
import java.security.cert.CertificateRevokedException;
|
|
import java.security.cert.X509Certificate;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
public class SslErrorTest {
|
|
|
|
static Collection<Object[]> data() {
|
|
List<SslProvider> serverProviders = new ArrayList<>(2);
|
|
List<SslProvider> clientProviders = new ArrayList<>(3);
|
|
|
|
if (OpenSsl.isAvailable()) {
|
|
serverProviders.add(SslProvider.OPENSSL);
|
|
serverProviders.add(SslProvider.OPENSSL_REFCNT);
|
|
clientProviders.add(SslProvider.OPENSSL);
|
|
clientProviders.add(SslProvider.OPENSSL_REFCNT);
|
|
}
|
|
// We not test with SslProvider.JDK on the server side as the JDK implementation currently just send the same
|
|
// alert all the time, sigh.....
|
|
clientProviders.add(SslProvider.JDK);
|
|
|
|
List<CertificateException> exceptions = new ArrayList<>(6);
|
|
exceptions.add(new CertificateExpiredException());
|
|
exceptions.add(new CertificateNotYetValidException());
|
|
exceptions.add(new CertificateRevokedException(
|
|
new Date(), CRLReason.AA_COMPROMISE, new X500Principal(""),
|
|
Collections.emptyMap()));
|
|
|
|
// Also use wrapped exceptions as this is what the JDK implementation of X509TrustManagerFactory is doing.
|
|
exceptions.add(newCertificateException(CertPathValidatorException.BasicReason.EXPIRED));
|
|
exceptions.add(newCertificateException(CertPathValidatorException.BasicReason.NOT_YET_VALID));
|
|
exceptions.add(newCertificateException(CertPathValidatorException.BasicReason.REVOKED));
|
|
|
|
List<Object[]> params = new ArrayList<>();
|
|
for (SslProvider serverProvider: serverProviders) {
|
|
for (SslProvider clientProvider: clientProviders) {
|
|
for (CertificateException exception: exceptions) {
|
|
params.add(new Object[] { serverProvider, clientProvider, exception, true });
|
|
params.add(new Object[] { serverProvider, clientProvider, exception, false });
|
|
}
|
|
}
|
|
}
|
|
return params;
|
|
}
|
|
|
|
private static CertificateException newCertificateException(CertPathValidatorException.Reason reason) {
|
|
return new TestCertificateException(
|
|
new CertPathValidatorException("x", null, null, -1, reason));
|
|
}
|
|
|
|
@ParameterizedTest(
|
|
name = "{index}: serverProvider = {0}, clientProvider = {1}, exception = {2}, serverProduceError = {3}")
|
|
@MethodSource("data")
|
|
@Timeout(value = 30000, unit = TimeUnit.MILLISECONDS)
|
|
public void testCorrectAlert(SslProvider serverProvider, final SslProvider clientProvider,
|
|
final CertificateException exception, final boolean serverProduceError)
|
|
throws Exception {
|
|
// As this only works correctly at the moment when OpenSslEngine is used on the server-side there is
|
|
// no need to run it if there is no openssl is available at all.
|
|
OpenSsl.ensureAvailability();
|
|
|
|
SelfSignedCertificate ssc = new SelfSignedCertificate();
|
|
|
|
SslContextBuilder sslServerCtxBuilder = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
|
|
.sslProvider(serverProvider)
|
|
.clientAuth(ClientAuth.REQUIRE);
|
|
SslContextBuilder sslClientCtxBuilder = SslContextBuilder.forClient()
|
|
.keyManager(new File(getClass().getResource("test.crt").getFile()),
|
|
new File(getClass().getResource("test_unencrypted.pem").getFile()))
|
|
.sslProvider(clientProvider);
|
|
|
|
if (serverProduceError) {
|
|
sslServerCtxBuilder.trustManager(new ExceptionTrustManagerFactory(exception));
|
|
sslClientCtxBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
|
|
} else {
|
|
sslServerCtxBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
|
|
sslClientCtxBuilder.trustManager(new ExceptionTrustManagerFactory(exception));
|
|
}
|
|
|
|
final SslContext sslServerCtx = sslServerCtxBuilder.build();
|
|
final SslContext sslClientCtx = sslClientCtxBuilder.build();
|
|
|
|
Channel serverChannel = null;
|
|
Channel clientChannel = null;
|
|
EventLoopGroup group = new MultithreadEventLoopGroup(NioHandler.newFactory());
|
|
final Promise<Void> promise = group.next().newPromise();
|
|
try {
|
|
serverChannel = new ServerBootstrap().group(group)
|
|
.channel(NioServerSocketChannel.class)
|
|
.handler(new LoggingHandler(LogLevel.INFO))
|
|
.childHandler(new ChannelInitializer<Channel>() {
|
|
@Override
|
|
protected void initChannel(Channel ch) {
|
|
ch.pipeline().addLast(sslServerCtx.newHandler(ch.alloc()));
|
|
if (!serverProduceError) {
|
|
ch.pipeline().addLast(new AlertValidationHandler(clientProvider, serverProduceError,
|
|
exception, promise));
|
|
}
|
|
ch.pipeline().addLast(new ChannelHandler() {
|
|
|
|
@Override
|
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
|
ctx.close();
|
|
}
|
|
});
|
|
}
|
|
}).bind(0).get();
|
|
|
|
clientChannel = new Bootstrap().group(group)
|
|
.channel(NioSocketChannel.class)
|
|
.handler(new ChannelInitializer<Channel>() {
|
|
@Override
|
|
protected void initChannel(Channel ch) {
|
|
ch.pipeline().addLast(sslClientCtx.newHandler(ch.alloc()));
|
|
|
|
if (serverProduceError) {
|
|
ch.pipeline().addLast(new AlertValidationHandler(clientProvider, serverProduceError,
|
|
exception, promise));
|
|
}
|
|
ch.pipeline().addLast(new ChannelHandler() {
|
|
|
|
@Override
|
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
|
ctx.close();
|
|
}
|
|
});
|
|
}
|
|
}).connect(serverChannel.localAddress()).get();
|
|
// Block until we received the correct exception
|
|
promise.asFuture().syncUninterruptibly();
|
|
} finally {
|
|
if (clientChannel != null) {
|
|
clientChannel.close().syncUninterruptibly();
|
|
}
|
|
if (serverChannel != null) {
|
|
serverChannel.close().syncUninterruptibly();
|
|
}
|
|
group.shutdownGracefully();
|
|
|
|
ReferenceCountUtil.release(sslServerCtx);
|
|
ReferenceCountUtil.release(sslClientCtx);
|
|
}
|
|
}
|
|
|
|
private static final class ExceptionTrustManagerFactory extends SimpleTrustManagerFactory {
|
|
private final CertificateException exception;
|
|
|
|
ExceptionTrustManagerFactory(CertificateException exception) {
|
|
this.exception = exception;
|
|
}
|
|
|
|
@Override
|
|
protected void engineInit(KeyStore keyStore) { }
|
|
@Override
|
|
protected void engineInit(ManagerFactoryParameters managerFactoryParameters) { }
|
|
|
|
@Override
|
|
protected TrustManager[] engineGetTrustManagers() {
|
|
return new TrustManager[] { new X509TrustManager() {
|
|
|
|
@Override
|
|
public void checkClientTrusted(X509Certificate[] x509Certificates, String s)
|
|
throws CertificateException {
|
|
throw exception;
|
|
}
|
|
|
|
@Override
|
|
public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
|
|
throws CertificateException {
|
|
throw exception;
|
|
}
|
|
|
|
@Override
|
|
public X509Certificate[] getAcceptedIssuers() {
|
|
return EmptyArrays.EMPTY_X509_CERTIFICATES;
|
|
}
|
|
} };
|
|
}
|
|
}
|
|
|
|
private static final class AlertValidationHandler implements ChannelHandler {
|
|
private final SslProvider clientProvider;
|
|
private final boolean serverProduceError;
|
|
private final CertificateException exception;
|
|
private final Promise<Void> promise;
|
|
|
|
AlertValidationHandler(SslProvider clientProvider, boolean serverProduceError,
|
|
CertificateException exception, Promise<Void> promise) {
|
|
this.clientProvider = clientProvider;
|
|
this.serverProduceError = serverProduceError;
|
|
this.exception = exception;
|
|
this.promise = promise;
|
|
}
|
|
|
|
@Override
|
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
|
// Unwrap as its wrapped by a DecoderException
|
|
Throwable unwrappedCause = cause.getCause();
|
|
if (unwrappedCause instanceof SSLException) {
|
|
if (exception instanceof TestCertificateException) {
|
|
CertPathValidatorException.Reason reason =
|
|
((CertPathValidatorException) exception.getCause()).getReason();
|
|
if (reason == CertPathValidatorException.BasicReason.EXPIRED) {
|
|
verifyException(clientProvider, serverProduceError, unwrappedCause, promise, "expired");
|
|
} else if (reason == CertPathValidatorException.BasicReason.NOT_YET_VALID) {
|
|
// BoringSSL may use "expired" in this case while others use "bad"
|
|
verifyException(clientProvider, serverProduceError, unwrappedCause, promise, "expired", "bad");
|
|
} else if (reason == CertPathValidatorException.BasicReason.REVOKED) {
|
|
verifyException(clientProvider, serverProduceError, unwrappedCause, promise, "revoked");
|
|
}
|
|
} else if (exception instanceof CertificateExpiredException) {
|
|
verifyException(clientProvider, serverProduceError, unwrappedCause, promise, "expired");
|
|
} else if (exception instanceof CertificateNotYetValidException) {
|
|
// BoringSSL may use "expired" in this case while others use "bad"
|
|
verifyException(clientProvider, serverProduceError, unwrappedCause, promise, "expired", "bad");
|
|
} else if (exception instanceof CertificateRevokedException) {
|
|
verifyException(clientProvider, serverProduceError, unwrappedCause, promise, "revoked");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Its a bit hacky to verify against the message that is part of the exception but there is no other way
|
|
// at the moment as there are no different exceptions for the different alerts.
|
|
private static void verifyException(SslProvider clientProvider, boolean serverProduceError,
|
|
Throwable cause, Promise<Void> promise, String... messageParts) {
|
|
String message = cause.getMessage();
|
|
// When the error is produced on the client side and the client side uses JDK as provider it will always
|
|
// use "certificate unknown".
|
|
if (!serverProduceError && clientProvider == SslProvider.JDK &&
|
|
message.toLowerCase(Locale.UK).contains("unknown")) {
|
|
promise.setSuccess(null);
|
|
return;
|
|
}
|
|
|
|
for (String m: messageParts) {
|
|
if (message.toLowerCase(Locale.UK).contains(m.toLowerCase(Locale.UK))) {
|
|
promise.setSuccess(null);
|
|
return;
|
|
}
|
|
}
|
|
Throwable error = new AssertionError("message not contains any of '"
|
|
+ Arrays.toString(messageParts) + "': " + message, cause);
|
|
promise.setFailure(error);
|
|
}
|
|
|
|
private static final class TestCertificateException extends CertificateException {
|
|
private static final long serialVersionUID = -5816338303868751410L;
|
|
|
|
TestCertificateException(Throwable cause) {
|
|
super(cause);
|
|
}
|
|
}
|
|
}
|