From a137291ad161c970259d9a5d7cc206c026f08ee7 Mon Sep 17 00:00:00 2001 From: Norman Maurer Date: Tue, 10 Jul 2018 00:42:37 -0400 Subject: [PATCH] =?UTF-8?q?Add=20OpenSslX509KeyManagerFactory=20which=20ma?= =?UTF-8?q?kes=20it=20even=20easier=20for=20peopl=E2=80=A6=20(#8084)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add OpenSslX509KeyManagerFactory which makes it even easier for people to get the maximum performance when using OpenSSL / LibreSSL / BoringSSL with netty. Motivation: To make it even easier for people to get the maximum performance when using native SSL we should provide our own KeyManagerFactory implementation that people can just use to configure their key material. Modifications: - Add OpenSslX509KeyManagerFactory which users can use for maximum performance with native SSL - Refactor some internal code to re-use logic and not duplicate it. Result: Easier to get the max performance out of native SSL implementation. --- .../ssl/OpenSslX509KeyManagerFactory.java | 218 ++++++++++++++++++ .../ReferenceCountedOpenSslClientContext.java | 13 +- .../ssl/ReferenceCountedOpenSslContext.java | 4 + .../ReferenceCountedOpenSslServerContext.java | 15 +- .../java/io/netty/handler/ssl/SslContext.java | 13 +- .../netty/handler/ssl/SslContextBuilder.java | 4 +- 6 files changed, 256 insertions(+), 11 deletions(-) create mode 100644 handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java new file mode 100644 index 0000000000..2d87aade7e --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java @@ -0,0 +1,218 @@ +/* + * Copyright 2018 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.buffer.ByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.ObjectUtil; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.KeyManagerFactorySpi; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.X509KeyManager; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.UnrecoverableKeyException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Special {@link KeyManagerFactory} that pre-compute the keymaterial used when {@link SslProvider#OPENSSL} or + * {@link SslProvider#OPENSSL_REFCNT} is used and so will improve handshake times and its performance. + * + * Because the keymaterial is pre-computed any modification to the {@link KeyStore} is ignored after + * {@link #init(KeyStore, char[])} is called. + * + * {@link #init(ManagerFactoryParameters)} is not supported by this implementation and so a call to it will always + * result in an {@link InvalidAlgorithmParameterException}. + */ +public final class OpenSslX509KeyManagerFactory extends KeyManagerFactory { + + private final OpenSslKeyManagerFactorySpi spi; + + public OpenSslX509KeyManagerFactory() { + this(newOpenSslKeyManagerFactorySpi(null)); + } + + public OpenSslX509KeyManagerFactory(Provider provider) { + this(newOpenSslKeyManagerFactorySpi(provider)); + } + + public OpenSslX509KeyManagerFactory(String algorithm, Provider provider) throws NoSuchAlgorithmException { + this(newOpenSslKeyManagerFactorySpi(algorithm, provider)); + } + + private OpenSslX509KeyManagerFactory(OpenSslKeyManagerFactorySpi spi) { + super(spi, spi.kmf.getProvider(), spi.kmf.getAlgorithm()); + this.spi = spi; + } + + private static OpenSslKeyManagerFactorySpi newOpenSslKeyManagerFactorySpi(Provider provider) { + try { + return newOpenSslKeyManagerFactorySpi(null, provider); + } catch (NoSuchAlgorithmException e) { + // This should never happen as we use the default algorithm. + throw new IllegalStateException(e); + } + } + + private static OpenSslKeyManagerFactorySpi newOpenSslKeyManagerFactorySpi(String algorithm, Provider provider) + throws NoSuchAlgorithmException { + if (algorithm == null) { + algorithm = KeyManagerFactory.getDefaultAlgorithm(); + } + return new OpenSslKeyManagerFactorySpi( + provider == null ? KeyManagerFactory.getInstance(algorithm) : + KeyManagerFactory.getInstance(algorithm, provider)); + } + + OpenSslKeyMaterialProvider newProvider() { + return spi.newProvider(); + } + + private static final class OpenSslKeyManagerFactorySpi extends KeyManagerFactorySpi { + final KeyManagerFactory kmf; + private volatile ProviderFactory providerFactory; + + OpenSslKeyManagerFactorySpi(KeyManagerFactory kmf) { + this.kmf = ObjectUtil.checkNotNull(kmf, "kmf"); + } + + @Override + protected synchronized void engineInit(KeyStore keyStore, char[] chars) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + if (providerFactory != null) { + throw new KeyStoreException("Already initialized"); + } + if (!keyStore.aliases().hasMoreElements()) { + throw new KeyStoreException("No aliases found"); + } + + kmf.init(keyStore, chars); + providerFactory = new ProviderFactory( + ReferenceCountedOpenSslContext.chooseX509KeyManager(kmf.getKeyManagers()), + password(chars), Collections.list(keyStore.aliases())); + } + + private static String password(char[] password) { + if (password == null || password.length == 0) { + return null; + } + return new String(password); + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) + throws InvalidAlgorithmParameterException { + throw new InvalidAlgorithmParameterException("Not supported"); + } + + @Override + protected KeyManager[] engineGetKeyManagers() { + ProviderFactory providerFactory = this.providerFactory; + if (providerFactory == null) { + throw new IllegalStateException("engineInit(...) not called yet"); + } + return new KeyManager[] { providerFactory.keyManager }; + } + + OpenSslKeyMaterialProvider newProvider() { + ProviderFactory providerFactory = this.providerFactory; + if (providerFactory == null) { + throw new IllegalStateException("engineInit(...) not called yet"); + } + return providerFactory.newProvider(); + } + + private static final class ProviderFactory { + private final X509KeyManager keyManager; + private final String password; + private final Iterable aliases; + + ProviderFactory(X509KeyManager keyManager, String password, Iterable aliases) { + this.keyManager = keyManager; + this.password = password; + this.aliases = aliases; + } + + OpenSslKeyMaterialProvider newProvider() { + return new OpenSslPopulatedKeyMaterialProvider(keyManager, + password, aliases); + } + + /** + * {@link OpenSslKeyMaterialProvider} implementation that pre-compute the {@link OpenSslKeyMaterial} for + * all aliases. + */ + private static final class OpenSslPopulatedKeyMaterialProvider extends OpenSslKeyMaterialProvider { + private final Map materialMap; + + OpenSslPopulatedKeyMaterialProvider( + X509KeyManager keyManager, String password, Iterable aliases) { + super(keyManager, password); + materialMap = new HashMap(); + boolean initComplete = false; + try { + for (String alias: aliases) { + if (alias != null && !materialMap.containsKey(alias)) { + try { + materialMap.put(alias, super.chooseKeyMaterial( + UnpooledByteBufAllocator.DEFAULT, alias)); + } catch (Exception e) { + // Just store the exception and rethrow it when we try to choose the keymaterial + // for this alias later on. + materialMap.put(alias, e); + } + } + } + initComplete = true; + } finally { + if (!initComplete) { + destroy(); + } + } + if (materialMap.isEmpty()) { + throw new IllegalArgumentException("aliases must be non-empty"); + } + } + + @Override + OpenSslKeyMaterial chooseKeyMaterial(ByteBufAllocator allocator, String alias) throws Exception { + Object value = materialMap.get(alias); + if (value instanceof OpenSslKeyMaterial) { + return ((OpenSslKeyMaterial) value).retain(); + } else { + throw (Exception) value; + } + } + + @Override + void destroy() { + for (Object material: materialMap.values()) { + ReferenceCountUtil.release(material); + } + materialMap.clear(); + } + } + } + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java index 749c540a3a..29815c429e 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java @@ -102,9 +102,16 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted } else { // javadocs state that keyManagerFactory has precedent over keyCertChain if (keyManagerFactory == null && keyCertChain != null) { - keyMaterialProvider = new OpenSslCachingKeyMaterialProvider( - chooseX509KeyManager(buildKeyManagerFactory(keyCertChain, key, keyPassword, null) - .getKeyManagers()), keyPassword); + char[] keyPasswordChars = keyStorePassword(keyPassword); + KeyStore ks = buildKeyStore(keyCertChain, key, keyPasswordChars); + if (ks.aliases().hasMoreElements()) { + keyManagerFactory = new OpenSslX509KeyManagerFactory(); + } else { + keyManagerFactory = new OpenSslCachingX509KeyManagerFactory( + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())); + } + keyManagerFactory.init(ks, keyPasswordChars); + keyMaterialProvider = providerFor(keyManagerFactory, keyPassword); } else if (keyManagerFactory != null) { keyMaterialProvider = providerFor(keyManagerFactory, keyPassword); } 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 2a961ca024..4a02022677 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java @@ -823,6 +823,10 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen * ensure that the same material is always returned for the same alias. */ static OpenSslKeyMaterialProvider providerFor(KeyManagerFactory factory, String password) { + if (factory instanceof OpenSslX509KeyManagerFactory) { + return ((OpenSslX509KeyManagerFactory) factory).newProvider(); + } + X509KeyManager keyManager = chooseX509KeyManager(factory.getKeyManagers()); if (factory instanceof OpenSslCachingX509KeyManagerFactory) { // The user explicit used OpenSslCachingX509KeyManagerFactory which signals us that its fine to cache. diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java index b8f0254ef5..9f09393f01 100644 --- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java @@ -121,12 +121,17 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted // javadocs state that keyManagerFactory has precedent over keyCertChain, and we must have a // keyManagerFactory for the server so build one if it is not specified. if (keyManagerFactory == null) { - keyMaterialProvider = new OpenSslCachingKeyMaterialProvider( - chooseX509KeyManager(buildKeyManagerFactory(keyCertChain, key, keyPassword, null) - .getKeyManagers()), keyPassword); - } else { - keyMaterialProvider = providerFor(keyManagerFactory, keyPassword); + char[] keyPasswordChars = keyStorePassword(keyPassword); + KeyStore ks = buildKeyStore(keyCertChain, key, keyPasswordChars); + if (ks.aliases().hasMoreElements()) { + keyManagerFactory = new OpenSslX509KeyManagerFactory(); + } else { + keyManagerFactory = new OpenSslCachingX509KeyManagerFactory( + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())); + } + keyManagerFactory.init(ks, keyPasswordChars); } + keyMaterialProvider = providerFor(keyManagerFactory, keyPassword); result.keyMaterialManager = new OpenSslKeyMaterialManager(keyMaterialProvider); } diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContext.java b/handler/src/main/java/io/netty/handler/ssl/SslContext.java index ef5c4bfdca..c842a14615 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContext.java @@ -1154,8 +1154,15 @@ public abstract class SslContext { String keyPassword, KeyManagerFactory kmf) throws KeyStoreException, NoSuchAlgorithmException, IOException, CertificateException, UnrecoverableKeyException { - char[] keyPasswordChars = keyPassword == null ? EmptyArrays.EMPTY_CHARS : keyPassword.toCharArray(); + char[] keyPasswordChars = keyStorePassword(keyPassword); KeyStore ks = buildKeyStore(certChainFile, key, keyPasswordChars); + return buildKeyManagerFactory(ks, keyAlgorithm, keyPasswordChars, kmf); + } + + static KeyManagerFactory buildKeyManagerFactory(KeyStore ks, + String keyAlgorithm, + char[] keyPasswordChars, KeyManagerFactory kmf) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { // Set up key manager factory to use our key store if (kmf == null) { kmf = KeyManagerFactory.getInstance(keyAlgorithm); @@ -1164,4 +1171,8 @@ public abstract class SslContext { return kmf; } + + static char[] keyStorePassword(String keyPassword) { + return keyPassword == null ? EmptyArrays.EMPTY_CHARS : keyPassword.toCharArray(); + } } diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java index 48617505af..ae21440ce0 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java @@ -122,7 +122,7 @@ public final class SslContextBuilder { * Creates a builder for new server-side {@link SslContext}. * * If you use {@link SslProvider#OPENSSL} or {@link SslProvider#OPENSSL_REFCNT} consider using - * {@link OpenSslCachingX509KeyManagerFactory}. + * {@link OpenSslX509KeyManagerFactory} or {@link OpenSslCachingX509KeyManagerFactory}. * * @param keyManagerFactory non-{@code null} factory for server's private key * @see #keyManager(KeyManagerFactory) @@ -340,7 +340,7 @@ public final class SslContextBuilder { * you must use {@link #keyManager(File, File)} or {@link #keyManager(File, File, String)}. * * If you use {@link SslProvider#OPENSSL} or {@link SslProvider#OPENSSL_REFCNT} consider using - * {@link OpenSslCachingX509KeyManagerFactory}. + * {@link OpenSslX509KeyManagerFactory} or {@link OpenSslCachingX509KeyManagerFactory}. */ public SslContextBuilder keyManager(KeyManagerFactory keyManagerFactory) { if (forServer) {