From a9cca146d7aec78515354992d244a50a4505efa6 Mon Sep 17 00:00:00 2001 From: Norman Maurer Date: Mon, 8 Apr 2019 20:17:44 +0200 Subject: [PATCH] Allow to offload / customize key signing operations when using BoringSSL. (#8943) Motivation: BoringSSL allows to customize the way how key signing is done an even offload it from the IO thread. We should provide a way to plugin an own implementation when BoringSSL is used. Modifications: - Introduce OpenSslPrivateKeyMethod that can be used by the user to implement custom signing by using ReferenceCountedOpenSslContext.setPrivateKeyMethod(...) - Introduce static methods to OpenSslKeyManagerFactory which allows to create a KeyManagerFactory which supports to do keyless operations by let the use handle everything in OpenSslPrivateKeyMethod. - Add testcase which verifies that everything works as expected Result: A user is able to customize the way how keys are signed. --- .../handler/ssl/OpenSslPrivateKeyMethod.java | 62 +++ .../ssl/OpenSslX509KeyManagerFactory.java | 60 ++- .../ssl/ReferenceCountedOpenSslContext.java | 66 ++- .../ssl/ReferenceCountedOpenSslEngine.java | 13 +- .../ssl/OpenSslPrivateKeyMethodTest.java | 387 ++++++++++++++++++ pom.xml | 4 +- 6 files changed, 574 insertions(+), 18 deletions(-) create mode 100644 handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKeyMethod.java create mode 100644 handler/src/test/java/io/netty/handler/ssl/OpenSslPrivateKeyMethodTest.java diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKeyMethod.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKeyMethod.java new file mode 100644 index 0000000000..4b1da0b25c --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKeyMethod.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 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: + * + * http://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.internal.tcnative.SSLPrivateKeyMethod; +import io.netty.util.internal.UnstableApi; + +import javax.net.ssl.SSLEngine; + +/** + * Allow to customize private key signing / decrypting (when using RSA). Only supported when using BoringSSL atm. + */ +@UnstableApi +public interface OpenSslPrivateKeyMethod { + int SSL_SIGN_RSA_PKCS1_SHA1 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA1; + int SSL_SIGN_RSA_PKCS1_SHA256 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256; + int SSL_SIGN_RSA_PKCS1_SHA384 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA384; + int SSL_SIGN_RSA_PKCS1_SHA512 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA512; + int SSL_SIGN_ECDSA_SHA1 = SSLPrivateKeyMethod.SSL_SIGN_ECDSA_SHA1; + int SSL_SIGN_ECDSA_SECP256R1_SHA256 = SSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256; + int SSL_SIGN_ECDSA_SECP384R1_SHA384 = SSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP384R1_SHA384; + int SSL_SIGN_ECDSA_SECP521R1_SHA512 = SSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP521R1_SHA512; + int SSL_SIGN_RSA_PSS_RSAE_SHA256 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256; + int SSL_SIGN_RSA_PSS_RSAE_SHA384 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA384; + int SSL_SIGN_RSA_PSS_RSAE_SHA512 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA512; + int SSL_SIGN_ED25519 = SSLPrivateKeyMethod.SSL_SIGN_ED25519; + int SSL_SIGN_RSA_PKCS1_MD5_SHA1 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_MD5_SHA1; + + /** + * Signs the input with the given key and returns the signed bytes. + * + * @param engine the {@link SSLEngine} + * @param signatureAlgorithm the algorithm to use for signing + * @param input the digest itself + * @return the signed data + * @throws Exception thrown if an error is encountered during the signing + */ + byte[] sign(SSLEngine engine, int signatureAlgorithm, byte[] input) throws Exception; + + /** + * Decrypts the input with the given key and returns the decrypted bytes. + * + * @param engine the {@link SSLEngine} + * @param input the input which should be decrypted + * @return the decrypted data + * @throws Exception thrown if an error is encountered during the decrypting + */ + byte[] decrypt(SSLEngine engine, byte[] input) throws Exception; +} diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java index ccd7ef6679..a8a58bc6c2 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java @@ -253,15 +253,47 @@ public final class OpenSslX509KeyManagerFactory extends KeyManagerFactory { public static OpenSslX509KeyManagerFactory newEngineBased(X509Certificate[] certificateChain, String password) throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { - KeyStore store = new OpenSslEngineKeyStore(certificateChain.clone()); + KeyStore store = new OpenSslKeyStore(certificateChain.clone(), false); store.load(null, null); OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory(); factory.init(store, password == null ? null : password.toCharArray()); return factory; } - private static final class OpenSslEngineKeyStore extends KeyStore { - private OpenSslEngineKeyStore(final X509Certificate[] certificateChain) { + /** + * See {@link OpenSslX509KeyManagerFactory#newEngineBased(X509Certificate[], String)}. + */ + public static OpenSslX509KeyManagerFactory newKeyless(File chain) + throws CertificateException, IOException, + KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + return newKeyless(SslContext.toX509Certificates(chain)); + } + + /** + * See {@link OpenSslX509KeyManagerFactory#newEngineBased(X509Certificate[], String)}. + */ + public static OpenSslX509KeyManagerFactory newKeyless(InputStream chain) + throws CertificateException, IOException, + KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + return newKeyless(SslContext.toX509Certificates(chain)); + } + + /** + * Returns a new initialized {@link OpenSslX509KeyManagerFactory} which will provide its private key by using the + * {@link OpenSslPrivateKeyMethod}. + */ + public static OpenSslX509KeyManagerFactory newKeyless(X509Certificate... certificateChain) + throws CertificateException, IOException, + KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + KeyStore store = new OpenSslKeyStore(certificateChain.clone(), true); + store.load(null, null); + OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory(); + factory.init(store, null); + return factory; + } + + private static final class OpenSslKeyStore extends KeyStore { + private OpenSslKeyStore(final X509Certificate[] certificateChain, final boolean keyless) { super(new KeyStoreSpi() { private final Date creationDate = new Date(); @@ -269,15 +301,21 @@ public final class OpenSslX509KeyManagerFactory extends KeyManagerFactory { @Override public Key engineGetKey(String alias, char[] password) throws UnrecoverableKeyException { if (engineContainsAlias(alias)) { - try { - return new OpenSslPrivateKey(SSL.loadPrivateKeyFromEngine( - alias, password == null ? null : new String(password))); - } catch (Exception e) { - UnrecoverableKeyException keyException = - new UnrecoverableKeyException("Unable to load key from engine"); - keyException.initCause(e); - throw keyException; + final long privateKeyAddress; + if (keyless) { + privateKeyAddress = 0; + } else { + try { + privateKeyAddress = SSL.loadPrivateKeyFromEngine( + alias, password == null ? null : new String(password)); + } catch (Exception e) { + UnrecoverableKeyException keyException = + new UnrecoverableKeyException("Unable to load key from engine"); + keyException.initCause(e); + throw keyException; + } } + return new OpenSslPrivateKey(privateKeyAddress); } return null; } diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java index 6b92ad64cf..415cee4004 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java @@ -20,6 +20,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.internal.tcnative.CertificateVerifier; import io.netty.internal.tcnative.SSL; import io.netty.internal.tcnative.SSLContext; +import io.netty.internal.tcnative.SSLPrivateKeyMethod; import io.netty.util.AbstractReferenceCounted; import io.netty.util.ReferenceCounted; import io.netty.util.ResourceLeakDetector; @@ -27,6 +28,7 @@ import io.netty.util.ResourceLeakDetectorFactory; import io.netty.util.ResourceLeakTracker; import io.netty.util.internal.StringUtil; import io.netty.util.internal.SystemPropertyUtil; +import io.netty.util.internal.UnstableApi; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -43,6 +45,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.locks.Lock; @@ -81,7 +84,7 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen AccessController.doPrivileged((PrivilegedAction) () -> Math.max(1, SystemPropertyUtil.getInt("io.netty.handler.ssl.openssl.bioNonApplicationBufferSize", 2048))); - private static final boolean USE_TASKS = + static final boolean USE_TASKS = SystemPropertyUtil.getBoolean("io.netty.handler.ssl.openssl.useTasks", false); private static final Integer DH_KEY_LENGTH; private static final ResourceLeakDetector leakDetector = @@ -508,6 +511,26 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen } } + /** + * Set the {@link OpenSslPrivateKeyMethod} to use. This allows to offload private-key operations + * if needed. + * + * This method is currently only supported when {@code BoringSSL} is used. + * + * @param method method to use. + */ + @UnstableApi + public final void setPrivateKeyMethod(OpenSslPrivateKeyMethod method) { + Objects.requireNonNull(method, "method"); + Lock writerLock = ctxLock.writeLock(); + writerLock.lock(); + try { + SSLContext.setPrivateKeyMethod(ctx, new PrivateKeyMethod(engineMap, method)); + } finally { + writerLock.unlock(); + } + } + // IMPORTANT: This method must only be called from either the constructor or the finalizer as a user MUST never // get access to an OpenSslSessionContext after this method was called to prevent the user from // producing a segfault. @@ -871,4 +894,45 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen // We can not be sure if the material may change at runtime so we will not cache it. return new OpenSslKeyMaterialProvider(keyManager, password); } + + private static final class PrivateKeyMethod implements SSLPrivateKeyMethod { + + private final OpenSslEngineMap engineMap; + private final OpenSslPrivateKeyMethod keyMethod; + PrivateKeyMethod(OpenSslEngineMap engineMap, OpenSslPrivateKeyMethod keyMethod) { + this.engineMap = engineMap; + this.keyMethod = keyMethod; + } + + private ReferenceCountedOpenSslEngine retrieveEngine(long ssl) throws SSLException { + ReferenceCountedOpenSslEngine engine = engineMap.get(ssl); + if (engine == null) { + throw new SSLException("Could not find a " + + StringUtil.simpleClassName(ReferenceCountedOpenSslEngine.class) + " for sslPointer " + ssl); + } + return engine; + } + + @Override + public byte[] sign(long ssl, int signatureAlgorithm, byte[] digest) throws Exception { + ReferenceCountedOpenSslEngine engine = retrieveEngine(ssl); + try { + return keyMethod.sign(engine, signatureAlgorithm, digest); + } catch (Exception e) { + engine.initHandshakeException(e); + throw e; + } + } + + @Override + public byte[] decrypt(long ssl, byte[] input) throws Exception { + ReferenceCountedOpenSslEngine engine = retrieveEngine(ssl); + try { + return keyMethod.decrypt(engine, input); + } catch (Exception e) { + engine.initHandshakeException(e); + throw e; + } + } + } } diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java index 79149a9077..3e731cf8fd 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java @@ -881,7 +881,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc // [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html 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_CERTIFICATE_VERIFY || + sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) { + return newResult(NEED_TASK, bytesConsumed, bytesProduced); } else { // Everything else is considered as error @@ -1175,8 +1177,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc } return newResultMayFinishHandshake(isInboundDone() ? CLOSED : OK, status, bytesConsumed, bytesProduced); - } else if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP - || sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY) { + } 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 { @@ -1738,7 +1741,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc return pendingStatus(SSL.bioLengthNonApplication(networkBIO)); } - if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP || sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY) { + 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; } diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslPrivateKeyMethodTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslPrivateKeyMethodTest.java new file mode 100644 index 0000000000..581f35cecb --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslPrivateKeyMethodTest.java @@ -0,0 +1,387 @@ +/* + * Copyright 2019 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: + * + * http://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.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.MultithreadEventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalHandler; +import io.netty.channel.local.LocalServerChannel; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Promise; +import org.hamcrest.Matchers; +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLHandshakeException; +import java.net.SocketAddress; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +@RunWith(Parameterized.class) +public class OpenSslPrivateKeyMethodTest { + private static final String RFC_CIPHER_NAME = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; + private static EventLoopGroup GROUP; + private static SelfSignedCertificate CERT; + private static ExecutorService EXECUTOR; + + @Parameters(name = "{index}: delegate = {0}") + public static Collection parameters() { + List dst = new ArrayList(); + dst.add(new Object[] { true }); + dst.add(new Object[] { false }); + return dst; + } + + @BeforeClass + public static void init() throws Exception { + Assume.assumeTrue(OpenSsl.isBoringSSL()); + // Check if the cipher is supported at all which may not be the case for various JDK versions and OpenSSL API + // implementations. + assumeCipherAvailable(SslProvider.OPENSSL); + assumeCipherAvailable(SslProvider.JDK); + + GROUP = new MultithreadEventLoopGroup(LocalHandler.newFactory()); + CERT = new SelfSignedCertificate(); + EXECUTOR = Executors.newCachedThreadPool(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new DelegateThread(r); + } + }); + } + + @AfterClass + public static void destroy() { + GROUP.shutdownGracefully(); + CERT.delete(); + EXECUTOR.shutdown(); + } + + private final boolean delegate; + + public OpenSslPrivateKeyMethodTest(boolean delegate) { + this.delegate = delegate; + } + + private static void assumeCipherAvailable(SslProvider provider) throws NoSuchAlgorithmException { + boolean cipherSupported = false; + if (provider == SslProvider.JDK) { + SSLEngine engine = SSLContext.getDefault().createSSLEngine(); + for (String c: engine.getSupportedCipherSuites()) { + if (RFC_CIPHER_NAME.equals(c)) { + cipherSupported = true; + break; + } + } + } else { + cipherSupported = OpenSsl.isCipherSuiteAvailable(RFC_CIPHER_NAME); + } + Assume.assumeTrue("Unsupported cipher: " + RFC_CIPHER_NAME, cipherSupported); + } + + private static SslHandler newSslHandler(SslContext sslCtx, ByteBufAllocator allocator, Executor executor) { + if (executor == null) { + return sslCtx.newHandler(allocator); + } else { + return sslCtx.newHandler(allocator, executor); + } + } + + private SslContext buildServerContext(OpenSslPrivateKeyMethod method) throws Exception { + List ciphers = Collections.singletonList(RFC_CIPHER_NAME); + + final KeyManagerFactory kmf = OpenSslX509KeyManagerFactory.newKeyless(CERT.cert()); + + final SslContext sslServerContext = SslContextBuilder.forServer(kmf) + .sslProvider(SslProvider.OPENSSL) + .ciphers(ciphers) + // As this is not a TLSv1.3 cipher we should ensure we talk something else. + .protocols(SslUtils.PROTOCOL_TLS_V1_2) + .build(); + + ((OpenSslContext) sslServerContext).setPrivateKeyMethod(method); + return sslServerContext; + } + + private SslContext buildClientContext() throws Exception { + return SslContextBuilder.forClient() + .sslProvider(SslProvider.JDK) + .ciphers(Collections.singletonList(RFC_CIPHER_NAME)) + // As this is not a TLSv1.3 cipher we should ensure we talk something else. + .protocols(SslUtils.PROTOCOL_TLS_V1_2) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + } + + private Executor delegateExecutor() { + return delegate ? EXECUTOR : null; + } + + private void assertThread() { + if (delegate && OpenSslContext.USE_TASKS) { + assertEquals(DelegateThread.class, Thread.currentThread().getClass()); + } else { + assertNotEquals(DelegateThread.class, Thread.currentThread().getClass()); + } + } + + @Test + public void testPrivateKeyMethod() throws Exception { + final AtomicBoolean signCalled = new AtomicBoolean(); + final SslContext sslServerContext = buildServerContext(new OpenSslPrivateKeyMethod() { + @Override + public byte[] sign(SSLEngine engine, int signatureAlgorithm, byte[] input) throws Exception { + signCalled.set(true); + assertThread(); + + assertEquals(CERT.cert().getPublicKey(), + engine.getSession().getLocalCertificates()[0].getPublicKey()); + + // Delegate signing to Java implementation. + final Signature signature; + // Depending on the Java version it will pick one or the other. + if (signatureAlgorithm == OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256) { + signature = Signature.getInstance("SHA256withRSA"); + } else if (signatureAlgorithm == OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256) { + signature = Signature.getInstance("RSASSA-PSS"); + signature.setParameter(new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, + 32, 1)); + } else { + throw new AssertionError("Unexpected signature algorithm " + signatureAlgorithm); + } + signature.initSign(CERT.key()); + signature.update(input); + return signature.sign(); + } + + @Override + public byte[] decrypt(SSLEngine engine, byte[] input) { + throw new UnsupportedOperationException(); + } + }); + + final SslContext sslClientContext = buildClientContext(); + try { + try { + final Promise serverPromise = GROUP.next().newPromise(); + final Promise clientPromise = GROUP.next().newPromise(); + + ChannelHandler serverHandler = new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(newSslHandler(sslServerContext, ch.alloc(), delegateExecutor())); + + pipeline.addLast(new SimpleChannelInboundHandler() { + @Override + public void channelInactive(ChannelHandlerContext ctx) { + serverPromise.cancel(true); + ctx.fireChannelInactive(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) { + if (serverPromise.trySuccess(null)) { + ctx.writeAndFlush(Unpooled.wrappedBuffer(new byte[] {'P', 'O', 'N', 'G'})); + } + ctx.close(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (!serverPromise.tryFailure(cause)) { + ctx.fireExceptionCaught(cause); + } + } + }); + } + }; + + LocalAddress address = new LocalAddress("test-" + SslProvider.OPENSSL + + '-' + SslProvider.JDK + '-' + RFC_CIPHER_NAME + '-' + delegate); + + Channel server = server(address, serverHandler); + try { + ChannelHandler clientHandler = new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(newSslHandler(sslClientContext, ch.alloc(), delegateExecutor())); + + pipeline.addLast(new SimpleChannelInboundHandler() { + @Override + public void channelInactive(ChannelHandlerContext ctx) { + clientPromise.cancel(true); + ctx.fireChannelInactive(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) { + clientPromise.trySuccess(null); + ctx.close(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (!clientPromise.tryFailure(cause)) { + ctx.fireExceptionCaught(cause); + } + } + }); + } + }; + + Channel client = client(server, clientHandler); + try { + client.writeAndFlush(Unpooled.wrappedBuffer(new byte[] {'P', 'I', 'N', 'G'})) + .syncUninterruptibly(); + + assertTrue("client timeout", clientPromise.await(5L, TimeUnit.SECONDS)); + assertTrue("server timeout", serverPromise.await(5L, TimeUnit.SECONDS)); + + clientPromise.sync(); + serverPromise.sync(); + assertTrue(signCalled.get()); + } finally { + client.close().sync(); + } + } finally { + server.close().sync(); + } + } finally { + ReferenceCountUtil.release(sslClientContext); + } + } finally { + ReferenceCountUtil.release(sslServerContext); + } + } + + @Test + public void testPrivateKeyMethodFails() throws Exception { + final SslContext sslServerContext = buildServerContext(new OpenSslPrivateKeyMethod() { + @Override + public byte[] sign(SSLEngine engine, int signatureAlgorithm, byte[] input) throws Exception { + assertThread(); + throw new SignatureException(); + } + + @Override + public byte[] decrypt(SSLEngine engine, byte[] input) { + throw new UnsupportedOperationException(); + } + }); + final SslContext sslClientContext = buildClientContext(); + + SslHandler serverSslHandler = newSslHandler( + sslServerContext, UnpooledByteBufAllocator.DEFAULT, delegateExecutor()); + SslHandler clientSslHandler = newSslHandler( + sslClientContext, UnpooledByteBufAllocator.DEFAULT, delegateExecutor()); + + try { + try { + LocalAddress address = new LocalAddress("test-" + SslProvider.OPENSSL + + '-' + SslProvider.JDK + '-' + RFC_CIPHER_NAME + '-' + delegate); + + Channel server = server(address, serverSslHandler); + try { + Channel client = client(server, clientSslHandler); + try { + Throwable clientCause = clientSslHandler.handshakeFuture().await().cause(); + Throwable serverCause = serverSslHandler.handshakeFuture().await().cause(); + assertNotNull(clientCause); + assertThat(serverCause, Matchers.instanceOf(SSLHandshakeException.class)); + } finally { + client.close().sync(); + } + } finally { + server.close().sync(); + } + } finally { + ReferenceCountUtil.release(sslClientContext); + } + } finally { + ReferenceCountUtil.release(sslServerContext); + } + } + + private static Channel server(LocalAddress address, ChannelHandler handler) throws Exception { + ServerBootstrap bootstrap = new ServerBootstrap() + .channel(LocalServerChannel.class) + .group(GROUP) + .childHandler(handler); + + return bootstrap.bind(address).sync().channel(); + } + + private static Channel client(Channel server, ChannelHandler handler) throws Exception { + SocketAddress remoteAddress = server.localAddress(); + + Bootstrap bootstrap = new Bootstrap() + .channel(LocalChannel.class) + .group(GROUP) + .handler(handler); + + return bootstrap.connect(remoteAddress).sync().channel(); + } + + private static final class DelegateThread extends Thread { + DelegateThread(Runnable target) { + super(target); + } + } +} diff --git a/pom.xml b/pom.xml index f646405beb..0385c548c2 100644 --- a/pom.xml +++ b/pom.xml @@ -276,8 +276,8 @@ fedora - netty-tcnative - 2.0.23.Final + netty-tcnative-boringssl-static + 2.0.24.Final ${os.detected.classifier} org.conscrypt conscrypt-openjdk-uber