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.
This commit is contained in:
parent
5e8fdf06bc
commit
a9cca146d7
|
@ -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;
|
||||||
|
}
|
|
@ -253,15 +253,47 @@ public final class OpenSslX509KeyManagerFactory extends KeyManagerFactory {
|
||||||
public static OpenSslX509KeyManagerFactory newEngineBased(X509Certificate[] certificateChain, String password)
|
public static OpenSslX509KeyManagerFactory newEngineBased(X509Certificate[] certificateChain, String password)
|
||||||
throws CertificateException, IOException,
|
throws CertificateException, IOException,
|
||||||
KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||||
KeyStore store = new OpenSslEngineKeyStore(certificateChain.clone());
|
KeyStore store = new OpenSslKeyStore(certificateChain.clone(), false);
|
||||||
store.load(null, null);
|
store.load(null, null);
|
||||||
OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory();
|
OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory();
|
||||||
factory.init(store, password == null ? null : password.toCharArray());
|
factory.init(store, password == null ? null : password.toCharArray());
|
||||||
return factory;
|
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() {
|
super(new KeyStoreSpi() {
|
||||||
|
|
||||||
private final Date creationDate = new Date();
|
private final Date creationDate = new Date();
|
||||||
|
@ -269,15 +301,21 @@ public final class OpenSslX509KeyManagerFactory extends KeyManagerFactory {
|
||||||
@Override
|
@Override
|
||||||
public Key engineGetKey(String alias, char[] password) throws UnrecoverableKeyException {
|
public Key engineGetKey(String alias, char[] password) throws UnrecoverableKeyException {
|
||||||
if (engineContainsAlias(alias)) {
|
if (engineContainsAlias(alias)) {
|
||||||
try {
|
final long privateKeyAddress;
|
||||||
return new OpenSslPrivateKey(SSL.loadPrivateKeyFromEngine(
|
if (keyless) {
|
||||||
alias, password == null ? null : new String(password)));
|
privateKeyAddress = 0;
|
||||||
} catch (Exception e) {
|
} else {
|
||||||
UnrecoverableKeyException keyException =
|
try {
|
||||||
new UnrecoverableKeyException("Unable to load key from engine");
|
privateKeyAddress = SSL.loadPrivateKeyFromEngine(
|
||||||
keyException.initCause(e);
|
alias, password == null ? null : new String(password));
|
||||||
throw keyException;
|
} catch (Exception e) {
|
||||||
|
UnrecoverableKeyException keyException =
|
||||||
|
new UnrecoverableKeyException("Unable to load key from engine");
|
||||||
|
keyException.initCause(e);
|
||||||
|
throw keyException;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return new OpenSslPrivateKey(privateKeyAddress);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import io.netty.buffer.ByteBufAllocator;
|
||||||
import io.netty.internal.tcnative.CertificateVerifier;
|
import io.netty.internal.tcnative.CertificateVerifier;
|
||||||
import io.netty.internal.tcnative.SSL;
|
import io.netty.internal.tcnative.SSL;
|
||||||
import io.netty.internal.tcnative.SSLContext;
|
import io.netty.internal.tcnative.SSLContext;
|
||||||
|
import io.netty.internal.tcnative.SSLPrivateKeyMethod;
|
||||||
import io.netty.util.AbstractReferenceCounted;
|
import io.netty.util.AbstractReferenceCounted;
|
||||||
import io.netty.util.ReferenceCounted;
|
import io.netty.util.ReferenceCounted;
|
||||||
import io.netty.util.ResourceLeakDetector;
|
import io.netty.util.ResourceLeakDetector;
|
||||||
|
@ -27,6 +28,7 @@ import io.netty.util.ResourceLeakDetectorFactory;
|
||||||
import io.netty.util.ResourceLeakTracker;
|
import io.netty.util.ResourceLeakTracker;
|
||||||
import io.netty.util.internal.StringUtil;
|
import io.netty.util.internal.StringUtil;
|
||||||
import io.netty.util.internal.SystemPropertyUtil;
|
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.InternalLogger;
|
||||||
import io.netty.util.internal.logging.InternalLoggerFactory;
|
import io.netty.util.internal.logging.InternalLoggerFactory;
|
||||||
|
|
||||||
|
@ -43,6 +45,7 @@ import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
|
@ -81,7 +84,7 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen
|
||||||
AccessController.doPrivileged((PrivilegedAction<Integer>) () -> Math.max(1,
|
AccessController.doPrivileged((PrivilegedAction<Integer>) () -> Math.max(1,
|
||||||
SystemPropertyUtil.getInt("io.netty.handler.ssl.openssl.bioNonApplicationBufferSize",
|
SystemPropertyUtil.getInt("io.netty.handler.ssl.openssl.bioNonApplicationBufferSize",
|
||||||
2048)));
|
2048)));
|
||||||
private static final boolean USE_TASKS =
|
static final boolean USE_TASKS =
|
||||||
SystemPropertyUtil.getBoolean("io.netty.handler.ssl.openssl.useTasks", false);
|
SystemPropertyUtil.getBoolean("io.netty.handler.ssl.openssl.useTasks", false);
|
||||||
private static final Integer DH_KEY_LENGTH;
|
private static final Integer DH_KEY_LENGTH;
|
||||||
private static final ResourceLeakDetector<ReferenceCountedOpenSslContext> leakDetector =
|
private static final ResourceLeakDetector<ReferenceCountedOpenSslContext> 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
|
// 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
|
// get access to an OpenSslSessionContext after this method was called to prevent the user from
|
||||||
// producing a segfault.
|
// 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.
|
// We can not be sure if the material may change at runtime so we will not cache it.
|
||||||
return new OpenSslKeyMaterialProvider(keyManager, password);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -881,7 +881,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
|
||||||
// [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html
|
// [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html
|
||||||
return newResult(BUFFER_OVERFLOW, status, bytesConsumed, bytesProduced);
|
return newResult(BUFFER_OVERFLOW, status, bytesConsumed, bytesProduced);
|
||||||
} else if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP ||
|
} 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);
|
return newResult(NEED_TASK, bytesConsumed, bytesProduced);
|
||||||
} else {
|
} else {
|
||||||
// Everything else is considered as error
|
// Everything else is considered as error
|
||||||
|
@ -1175,8 +1177,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
|
||||||
}
|
}
|
||||||
return newResultMayFinishHandshake(isInboundDone() ? CLOSED : OK, status,
|
return newResultMayFinishHandshake(isInboundDone() ? CLOSED : OK, status,
|
||||||
bytesConsumed, bytesProduced);
|
bytesConsumed, bytesProduced);
|
||||||
} else if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP
|
} 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(isInboundDone() ? CLOSED : OK,
|
return newResult(isInboundDone() ? CLOSED : OK,
|
||||||
NEED_TASK, bytesConsumed, bytesProduced);
|
NEED_TASK, bytesConsumed, bytesProduced);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1738,7 +1741,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
|
||||||
return pendingStatus(SSL.bioLengthNonApplication(networkBIO));
|
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;
|
return NEED_TASK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<Object[]> parameters() {
|
||||||
|
List<Object[]> dst = new ArrayList<Object[]>();
|
||||||
|
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<String> 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<Object> serverPromise = GROUP.next().newPromise();
|
||||||
|
final Promise<Object> clientPromise = GROUP.next().newPromise();
|
||||||
|
|
||||||
|
ChannelHandler serverHandler = new ChannelInitializer<Channel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(Channel ch) {
|
||||||
|
ChannelPipeline pipeline = ch.pipeline();
|
||||||
|
pipeline.addLast(newSslHandler(sslServerContext, ch.alloc(), delegateExecutor()));
|
||||||
|
|
||||||
|
pipeline.addLast(new SimpleChannelInboundHandler<Object>() {
|
||||||
|
@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<Channel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(Channel ch) {
|
||||||
|
ChannelPipeline pipeline = ch.pipeline();
|
||||||
|
pipeline.addLast(newSslHandler(sslClientContext, ch.alloc(), delegateExecutor()));
|
||||||
|
|
||||||
|
pipeline.addLast(new SimpleChannelInboundHandler<Object>() {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
pom.xml
4
pom.xml
|
@ -276,8 +276,8 @@
|
||||||
<!-- Configure the os-maven-plugin extension to expand the classifier on -->
|
<!-- Configure the os-maven-plugin extension to expand the classifier on -->
|
||||||
<!-- Fedora-"like" systems. This is currently only used for the netty-tcnative dependency -->
|
<!-- Fedora-"like" systems. This is currently only used for the netty-tcnative dependency -->
|
||||||
<os.detection.classifierWithLikes>fedora</os.detection.classifierWithLikes>
|
<os.detection.classifierWithLikes>fedora</os.detection.classifierWithLikes>
|
||||||
<tcnative.artifactId>netty-tcnative</tcnative.artifactId>
|
<tcnative.artifactId>netty-tcnative-boringssl-static</tcnative.artifactId>
|
||||||
<tcnative.version>2.0.23.Final</tcnative.version>
|
<tcnative.version>2.0.24.Final</tcnative.version>
|
||||||
<tcnative.classifier>${os.detected.classifier}</tcnative.classifier>
|
<tcnative.classifier>${os.detected.classifier}</tcnative.classifier>
|
||||||
<conscrypt.groupId>org.conscrypt</conscrypt.groupId>
|
<conscrypt.groupId>org.conscrypt</conscrypt.groupId>
|
||||||
<conscrypt.artifactId>conscrypt-openjdk-uber</conscrypt.artifactId>
|
<conscrypt.artifactId>conscrypt-openjdk-uber</conscrypt.artifactId>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user