diff --git a/handler/src/main/java/io/netty/handler/ssl/KeyManagerFactoryWrapper.java b/handler/src/main/java/io/netty/handler/ssl/KeyManagerFactoryWrapper.java new file mode 100644 index 0000000000..715b780fb4 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/KeyManagerFactoryWrapper.java @@ -0,0 +1,44 @@ +/* + * 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.handler.ssl.util.SimpleKeyManagerFactory; +import io.netty.util.internal.ObjectUtil; + +import java.security.KeyStore; +import javax.net.ssl.KeyManager; +import javax.net.ssl.ManagerFactoryParameters; + +final class KeyManagerFactoryWrapper extends SimpleKeyManagerFactory { + private final KeyManager km; + + KeyManagerFactoryWrapper(KeyManager km) { + this.km = ObjectUtil.checkNotNull(km, "km"); + } + + @Override + protected void engineInit(KeyStore keyStore, char[] var2) throws Exception { } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) + throws Exception { } + + @Override + protected KeyManager[] engineGetKeyManagers() { + return new KeyManager[] {km}; + } +} 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 f17169238d..2b67ab7b48 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java @@ -18,9 +18,11 @@ package io.netty.handler.ssl; import io.netty.util.internal.UnstableApi; +import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import java.io.File; import java.io.InputStream; @@ -160,6 +162,15 @@ public final class SslContextBuilder { return new SslContextBuilder(true).keyManager(keyManagerFactory); } + /** + * Creates a builder for new server-side {@link SslContext} with {@link KeyManager}. + * + * @param KeyManager non-{@code null} KeyManager for server's private key + */ + public static SslContextBuilder forServer(KeyManager keyManager) { + return new SslContextBuilder(true).keyManager(keyManager); + } + private final boolean forServer; private SslProvider provider; private Provider sslContextProvider; @@ -259,6 +270,19 @@ public final class SslContextBuilder { return this; } + /** + * A single trusted manager for verifying the remote endpoint's certificate. + * This is helpful when custom implementation of {@link TrustManager} is needed. + * Internally, a simple wrapper of {@link TrustManagerFactory} that only produces this + * specified {@link TrustManager} will be created, thus all the requirements specified in + * {@link #trustManager(TrustManagerFactory trustManagerFactory)} also apply here. + */ + public SslContextBuilder trustManager(TrustManager trustManager) { + this.trustManagerFactory = new TrustManagerFactoryWrapper(trustManager); + trustCertCollection = null; + return this; + } + /** * Identifying certificate for this host. {@code keyCertChainFile} and {@code keyFile} may * be {@code null} for client contexts, which disables mutual authentication. @@ -423,6 +447,28 @@ public final class SslContextBuilder { return this; } + /** + * A single key manager managing the identity information of this host. + * This is helpful when custom implementation of {@link KeyManager} is needed. + * Internally, a wrapper of {@link KeyManagerFactory} that only produces this specified + * {@link KeyManager} will be created, thus all the requirements specified in + * {@link #keyManager(KeyManagerFactory keyManagerFactory)} also apply here. + */ + public SslContextBuilder keyManager(KeyManager keyManager) { + if (forServer) { + checkNotNull(keyManager, "keyManager required for servers"); + } + if (keyManager != null) { + this.keyManagerFactory = new KeyManagerFactoryWrapper(keyManager); + } else { + this.keyManagerFactory = null; + } + keyCertChain = null; + key = null; + keyPassword = null; + return this; + } + /** * The cipher suites to enable, in the order of preference. {@code null} to use default * cipher suites. diff --git a/handler/src/main/java/io/netty/handler/ssl/TrustManagerFactoryWrapper.java b/handler/src/main/java/io/netty/handler/ssl/TrustManagerFactoryWrapper.java new file mode 100644 index 0000000000..5abf8aaf25 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/TrustManagerFactoryWrapper.java @@ -0,0 +1,44 @@ +/* + * 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.handler.ssl.util.SimpleTrustManagerFactory; +import io.netty.util.internal.ObjectUtil; + +import java.security.KeyStore; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.TrustManager; + +final class TrustManagerFactoryWrapper extends SimpleTrustManagerFactory { + private final TrustManager tm; + + TrustManagerFactoryWrapper(TrustManager tm) { + this.tm = ObjectUtil.checkNotNull(tm, "tm"); + } + + @Override + protected void engineInit(KeyStore keyStore) throws Exception { } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) + throws Exception { } + + @Override + protected TrustManager[] engineGetTrustManagers() { + return new TrustManager[] {tm}; + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/util/SimpleKeyManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/util/SimpleKeyManagerFactory.java new file mode 100644 index 0000000000..31b28f80e1 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/util/SimpleKeyManagerFactory.java @@ -0,0 +1,154 @@ +/* + * 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.util; + +import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.SuppressJava6Requirement; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Provider; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.KeyManagerFactorySpi; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; + +/** + * Helps to implement a custom {@link KeyManagerFactory}. + */ +public abstract class SimpleKeyManagerFactory extends KeyManagerFactory { + + private static final Provider PROVIDER = new Provider("", 0.0, "") { + private static final long serialVersionUID = -2680540247105807895L; + }; + + /** + * {@link SimpleKeyManagerFactorySpi} must have a reference to {@link SimpleKeyManagerFactory} + * to delegate its callbacks back to {@link SimpleKeyManagerFactory}. However, it is impossible to do so, + * because {@link KeyManagerFactory} requires {@link KeyManagerFactorySpi} at construction time and + * does not provide a way to access it later. + * + * To work around this issue, we use an ugly hack which uses a {@link FastThreadLocal }. + */ + private static final FastThreadLocal CURRENT_SPI = + new FastThreadLocal() { + @Override + protected SimpleKeyManagerFactorySpi initialValue() { + return new SimpleKeyManagerFactorySpi(); + } + }; + + /** + * Creates a new instance. + */ + protected SimpleKeyManagerFactory() { + this(StringUtil.EMPTY_STRING); + } + + /** + * Creates a new instance. + * + * @param name the name of this {@link KeyManagerFactory} + */ + protected SimpleKeyManagerFactory(String name) { + super(CURRENT_SPI.get(), PROVIDER, ObjectUtil.checkNotNull(name, "name")); + CURRENT_SPI.get().init(this); + CURRENT_SPI.remove(); + } + + /** + * Initializes this factory with a source of certificate authorities and related key material. + * + * @see KeyManagerFactorySpi#engineInit(KeyStore, char[]) + */ + protected abstract void engineInit(KeyStore keyStore, char[] var2) throws Exception; + + /** + * Initializes this factory with a source of provider-specific key material. + * + * @see KeyManagerFactorySpi#engineInit(ManagerFactoryParameters) + */ + protected abstract void engineInit(ManagerFactoryParameters managerFactoryParameters) throws Exception; + + /** + * Returns one key manager for each type of key material. + * + * @see KeyManagerFactorySpi#engineGetKeyManagers() + */ + protected abstract KeyManager[] engineGetKeyManagers(); + + private static final class SimpleKeyManagerFactorySpi extends KeyManagerFactorySpi { + + private SimpleKeyManagerFactory parent; + private volatile KeyManager[] keyManagers; + + void init(SimpleKeyManagerFactory parent) { + this.parent = parent; + } + + @Override + protected void engineInit(KeyStore keyStore, char[] pwd) throws KeyStoreException { + try { + parent.engineInit(keyStore, pwd); + } catch (KeyStoreException e) { + throw e; + } catch (Exception e) { + throw new KeyStoreException(e); + } + } + + @Override + protected void engineInit( + ManagerFactoryParameters managerFactoryParameters) throws InvalidAlgorithmParameterException { + try { + parent.engineInit(managerFactoryParameters); + } catch (InvalidAlgorithmParameterException e) { + throw e; + } catch (Exception e) { + throw new InvalidAlgorithmParameterException(e); + } + } + + @Override + protected KeyManager[] engineGetKeyManagers() { + KeyManager[] keyManagers = this.keyManagers; + if (keyManagers == null) { + keyManagers = parent.engineGetKeyManagers(); + if (PlatformDependent.javaVersion() >= 7) { + wrapIfNeeded(keyManagers); + } + this.keyManagers = keyManagers; + } + return keyManagers.clone(); + } + + @SuppressJava6Requirement(reason = "Usage guarded by java version check") + private static void wrapIfNeeded(KeyManager[] keyManagers) { + for (int i = 0; i < keyManagers.length; i++) { + final KeyManager tm = keyManagers[i]; + if (tm instanceof X509KeyManager && !(tm instanceof X509ExtendedKeyManager)) { + keyManagers[i] = new X509KeyManagerWrapper((X509KeyManager) tm); + } + } + } + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/util/X509KeyManagerWrapper.java b/handler/src/main/java/io/netty/handler/ssl/util/X509KeyManagerWrapper.java new file mode 100644 index 0000000000..20c1f26f8a --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/util/X509KeyManagerWrapper.java @@ -0,0 +1,78 @@ +/* + * 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.util; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import io.netty.util.internal.SuppressJava6Requirement; +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; + +@SuppressJava6Requirement(reason = "Usage guarded by java version check") +final class X509KeyManagerWrapper extends X509ExtendedKeyManager { + + private final X509KeyManager delegate; + + X509KeyManagerWrapper(X509KeyManager delegate) { + this.delegate = checkNotNull(delegate, "delegate"); + } + + @Override + public String[] getClientAliases(String var1, Principal[] var2) { + return delegate.getClientAliases(var1, var2); + } + + @Override + public String chooseClientAlias(String[] var1, Principal[] var2, Socket var3) { + return delegate.chooseClientAlias(var1, var2, var3); + } + + @Override + public String[] getServerAliases(String var1, Principal[] var2) { + return delegate.getServerAliases(var1, var2); + } + + @Override + public String chooseServerAlias(String var1, Principal[] var2, Socket var3) { + return delegate.chooseServerAlias(var1, var2, var3); + } + + @Override + public X509Certificate[] getCertificateChain(String var1) { + return delegate.getCertificateChain(var1); + } + + @Override + public PrivateKey getPrivateKey(String var1) { + return delegate.getPrivateKey(var1); + } + + @Override + public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { + return delegate.chooseClientAlias(keyType, issuers, null); + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) { + return delegate.chooseServerAlias(keyType, issuers, null); + } +} diff --git a/handler/src/test/java/io/netty/handler/ssl/SslContextBuilderTest.java b/handler/src/test/java/io/netty/handler/ssl/SslContextBuilderTest.java index ee53f97ed2..e3bc1f4400 100644 --- a/handler/src/test/java/io/netty/handler/ssl/SslContextBuilderTest.java +++ b/handler/src/test/java/io/netty/handler/ssl/SslContextBuilderTest.java @@ -21,9 +21,18 @@ import io.netty.util.CharsetUtil; import org.junit.Assume; import org.junit.Test; +import javax.net.ssl.KeyManager; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; import java.io.ByteArrayInputStream; +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.Collections; import static org.junit.Assert.*; @@ -85,6 +94,17 @@ public class SslContextBuilderTest { testServerContext(SslProvider.OPENSSL); } + @Test + public void testContextFromManagersJdk() throws Exception { + testContextFromManagers(SslProvider.JDK); + } + + @Test + public void testContextFromManagersOpenssl() throws Exception { + Assume.assumeTrue(OpenSsl.isAvailable()); + testContextFromManagers(SslProvider.OPENSSL); + } + @Test(expected = SSLException.class) public void testUnsupportedPrivateKeyFailsFastForServer() throws Exception { Assume.assumeTrue(OpenSsl.isBoringSSL()); @@ -233,4 +253,104 @@ public class SslContextBuilderTest { engine.closeInbound(); engine.closeOutbound(); } + + private static void testContextFromManagers(SslProvider provider) throws Exception { + final SelfSignedCertificate cert = new SelfSignedCertificate(); + KeyManager customKeyManager = new X509ExtendedKeyManager() { + @Override + public String[] getClientAliases(String s, + Principal[] principals) { + return new String[0]; + } + + @Override + public String chooseClientAlias(String[] strings, + Principal[] principals, + Socket socket) { + return "cert_sent_to_server"; + } + + @Override + public String[] getServerAliases(String s, + Principal[] principals) { + return new String[0]; + } + + @Override + public String chooseServerAlias(String s, + Principal[] principals, + Socket socket) { + return null; + } + + @Override + public X509Certificate[] getCertificateChain(String s) { + X509Certificate[] certificates = new X509Certificate[1]; + certificates[0] = cert.cert(); + return new X509Certificate[0]; + } + + @Override + public PrivateKey getPrivateKey(String s) { + return cert.key(); + } + }; + TrustManager customTrustManager = new X509ExtendedTrustManager() { + @Override + public void checkClientTrusted( + X509Certificate[] x509Certificates, String s, + Socket socket) throws CertificateException { } + + @Override + public void checkServerTrusted( + X509Certificate[] x509Certificates, String s, + Socket socket) throws CertificateException { } + + @Override + public void checkClientTrusted( + X509Certificate[] x509Certificates, String s, + SSLEngine sslEngine) throws CertificateException { } + + @Override + public void checkServerTrusted( + X509Certificate[] x509Certificates, String s, + SSLEngine sslEngine) throws CertificateException { } + + @Override + public void checkClientTrusted( + X509Certificate[] x509Certificates, String s) + throws CertificateException { } + + @Override + public void checkServerTrusted( + X509Certificate[] x509Certificates, String s) + throws CertificateException { } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + SslContextBuilder client_builder = SslContextBuilder.forClient() + .sslProvider(provider) + .keyManager(customKeyManager) + .trustManager(customTrustManager) + .clientAuth(ClientAuth.OPTIONAL); + SslContext client_context = client_builder.build(); + SSLEngine client_engine = client_context.newEngine(UnpooledByteBufAllocator.DEFAULT); + assertFalse(client_engine.getWantClientAuth()); + assertFalse(client_engine.getNeedClientAuth()); + client_engine.closeInbound(); + client_engine.closeOutbound(); + SslContextBuilder server_builder = SslContextBuilder.forServer(customKeyManager) + .sslProvider(provider) + .trustManager(customTrustManager) + .clientAuth(ClientAuth.REQUIRE); + SslContext server_context = server_builder.build(); + SSLEngine server_engine = server_context.newEngine(UnpooledByteBufAllocator.DEFAULT); + assertFalse(server_engine.getWantClientAuth()); + assertTrue(server_engine.getNeedClientAuth()); + server_engine.closeInbound(); + server_engine.closeOutbound(); + } }