diff --git a/http-netty/src/main/java/io/micronaut/http/netty/NettyTlsUtils.java b/http-netty/src/main/java/io/micronaut/http/netty/NettyTlsUtils.java index ea69d625d69..fa38dce8a34 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/NettyTlsUtils.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/NettyTlsUtils.java @@ -24,7 +24,9 @@ import io.netty.handler.ssl.SslProvider; import javax.net.ssl.KeyManagerFactory; +import java.security.Key; import java.security.KeyStore; +import java.security.cert.Certificate; import java.util.Optional; /** @@ -79,7 +81,34 @@ public static KeyManagerFactory storeToFactory(@NonNull SslConfiguration ssl, @N if (keyPassword == null && pwd.isPresent()) { keyPassword = pwd.get().toCharArray(); } + if (keyStore != null && ssl.getKey().getAlias().isPresent()) { + keyStore = extractKeystoreAlias(keyStore, ssl.getKey().getAlias().get(), keyPassword); + } keyManagerFactory.init(keyStore, keyPassword); return keyManagerFactory; } + + /** + * Creates a new {@link KeyStore} from the original containing only the selected alias. + * + * @param rootKeystore The original keystore + * @param alias The selected alias + * @param password Password of Alias + * @return {@link KeyStore} containing only the selected alias + */ + @NonNull + private static KeyStore extractKeystoreAlias(@NonNull KeyStore rootKeystore, @NonNull String alias, @Nullable char[] password) throws Exception { + if (!rootKeystore.containsAlias(alias)) { + throw new IllegalArgumentException("Alias " + alias + " not found in keystore"); + } + Key key = rootKeystore.getKey(alias, password); + if (key == null) { + throw new IllegalStateException("There are no keys associated with the alias " + alias); + } + Certificate[] certChain = rootKeystore.getCertificateChain(alias); + KeyStore aliasKeystore = KeyStore.getInstance(rootKeystore.getType()); + aliasKeystore.load(null, null); + aliasKeystore.setKeyEntry(alias, key, password, certChain); + return aliasKeystore; + } } diff --git a/http-netty/src/test/groovy/io/micronaut/http/netty/NettyTlsUtilsSpec.groovy b/http-netty/src/test/groovy/io/micronaut/http/netty/NettyTlsUtilsSpec.groovy new file mode 100644 index 00000000000..bf72adfe282 --- /dev/null +++ b/http-netty/src/test/groovy/io/micronaut/http/netty/NettyTlsUtilsSpec.groovy @@ -0,0 +1,131 @@ +package io.micronaut.http.netty + +import io.micronaut.http.ssl.SslConfiguration +import spock.lang.Specification + +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.X509KeyManager +import java.security.KeyStore +import java.security.cert.Certificate + +class NettyTlsUtilsSpec extends Specification { + def "storeToFactory should return a KeyManagerFactory with only the selected alias"() { + given: + + SslConfiguration sslConfig = Mock(SslConfiguration); + SslConfiguration.KeyConfiguration keyConfiguration = Mock(SslConfiguration.KeyConfiguration); + SslConfiguration.KeyStoreConfiguration keyStoreConfiguration = Mock(SslConfiguration.KeyStoreConfiguration); + sslConfig.isPreferOpenssl() >> false + sslConfig.getKey() >> keyConfiguration + sslConfig.getKeyStore() >> keyStoreConfiguration + keyConfiguration.getAlias() >> Optional.of("alias2") + keyConfiguration.getPassword() >> Optional.of("passwordAlias2") + + String keystorePath = "src/test/resources/keystoreWithMultipleAlias.jks"; + char[] keystorePassword = "password".toCharArray(); + + KeyStore rootKeyStore = KeyStore.getInstance("JKS"); + try (FileInputStream fis = new FileInputStream(keystorePath)) { + rootKeyStore.load(fis, keystorePassword); + } + + when: + KeyManagerFactory resultKeyStore = NettyTlsUtils.storeToFactory(sslConfig, rootKeyStore) + + then: + resultKeyStore.getKeyManagers().size() == 1 + def manager = (X509KeyManager) resultKeyStore.getKeyManagers().first() + def certificate = manager.getCertificateChain("alias2") + certificate[0].getSubjectX500Principal().toString() == "CN=localhost2, OU=Micronaut2, O=My Company, L=City, ST=State, C=BR"; + } + + def "storeToFactory should throw a exception if selected alias not exists"() { + given: + + SslConfiguration sslConfig = Mock(SslConfiguration); + SslConfiguration.KeyConfiguration keyConfiguration = Mock(SslConfiguration.KeyConfiguration); + SslConfiguration.KeyStoreConfiguration keyStoreConfiguration = Mock(SslConfiguration.KeyStoreConfiguration); + sslConfig.isPreferOpenssl() >> false + sslConfig.getKey() >> keyConfiguration + sslConfig.getKeyStore() >> keyStoreConfiguration + keyConfiguration.getAlias() >> Optional.of("alias5") + keyConfiguration.getPassword() >> Optional.of("passwordAlias2") + KeyStore keystore = KeyStore.getInstance("JKS"); + keystore.load(null, null) + keystore.containsAlias("any") >> false; + + when: + NettyTlsUtils.storeToFactory(sslConfig, keystore) + + then: + def e = thrown(IllegalArgumentException) + e.message == "Alias alias5 not found in keystore" + } + + def "storeToFactory should throw a exception if key of alias is null"() { + given: + + SslConfiguration sslConfig = Mock(SslConfiguration); + SslConfiguration.KeyConfiguration keyConfiguration = Mock(SslConfiguration.KeyConfiguration); + SslConfiguration.KeyStoreConfiguration keyStoreConfiguration = Mock(SslConfiguration.KeyStoreConfiguration); + sslConfig.isPreferOpenssl() >> false + sslConfig.getKey() >> keyConfiguration + sslConfig.getKeyStore() >> keyStoreConfiguration + keyConfiguration.getAlias() >> Optional.of("any") + keyConfiguration.getPassword() >> Optional.of("any") + + KeyStore keystore = KeyStore.getInstance("JKS"); + keystore.load(null, null) + keystore.containsAlias("any") >> false; + keystore.setCertificateEntry("any", Mock(Certificate)) + + when: + NettyTlsUtils.storeToFactory(sslConfig, keystore) + + then: + def e = thrown(IllegalStateException) + e.message == "There are no keys associated with the alias any" + } + + def "storeToFactory should not extract alias if Keystore is null"() { + given: + + SslConfiguration sslConfig = Mock(SslConfiguration); + SslConfiguration.KeyConfiguration keyConfiguration = Mock(SslConfiguration.KeyConfiguration); + SslConfiguration.KeyStoreConfiguration keyStoreConfiguration = Mock(SslConfiguration.KeyStoreConfiguration); + sslConfig.isPreferOpenssl() >> false + sslConfig.getKey() >> keyConfiguration + sslConfig.getKeyStore() >> keyStoreConfiguration + keyConfiguration.getAlias() >> Optional.of("any") + keyConfiguration.getPassword() >> Optional.of("any") + + when: + NettyTlsUtils.storeToFactory(sslConfig, null) + + then: + 0 * NettyTlsUtils.extractKeystoreAlias(_, _, _) + } + + def "storeToFactory should not extract alias if alias is not defined"() { + given: + + SslConfiguration sslConfig = Mock(SslConfiguration); + SslConfiguration.KeyConfiguration keyConfiguration = Mock(SslConfiguration.KeyConfiguration); + SslConfiguration.KeyStoreConfiguration keyStoreConfiguration = Mock(SslConfiguration.KeyStoreConfiguration); + sslConfig.isPreferOpenssl() >> false + sslConfig.getKey() >> keyConfiguration + sslConfig.getKeyStore() >> keyStoreConfiguration + keyConfiguration.getAlias() >> Optional.empty() + keyConfiguration.getPassword() >> Optional.empty() + keyStoreConfiguration.getPassword() >> Optional.of("any") + + KeyStore keystore = KeyStore.getInstance("JKS"); + keystore.load(null, null) + + when: + NettyTlsUtils.storeToFactory(sslConfig, keystore) + + then: + 0 * NettyTlsUtils.extractKeystoreAlias(_, _, _) + } +} diff --git a/http-netty/src/test/resources/kesytoreGenerateCommands.md b/http-netty/src/test/resources/kesytoreGenerateCommands.md new file mode 100644 index 00000000000..36d63c7ff06 --- /dev/null +++ b/http-netty/src/test/resources/kesytoreGenerateCommands.md @@ -0,0 +1,9 @@ +## Commands used to generate keystore for testing + +**keystoreWithMultipleAlias.jks** + +```sh +keytool -genkeypair -v -keystore keystoreWithMultipleAlias.jks -storetype JKS -alias alias1 -keyalg RSA -keysize 2048 -validity 36500 -dname "CN=localhost1, OU=Micronaut1, O=My Company, L=City, ST=State, C=BR" -storepass password -keypass passwordAlias1 +keytool -genkeypair -v -keystore keystoreWithMultipleAlias.jks -storetype JKS -alias alias2 -keyalg RSA -keysize 2048 -validity 36500 -dname "CN=localhost2, OU=Micronaut2, O=My Company, L=City, ST=State, C=BR" -storepass password -keypass passwordAlias2 +keytool -genkeypair -v -keystore keystoreWithMultipleAlias.jks -storetype JKS -alias alias3 -keyalg RSA -keysize 2048 -validity 36500 -dname "CN=localhost3, OU=Micronaut3, O=My Company, L=City, ST=State, C=BR" -storepass password -keypass passwordAlias3 +``` diff --git a/http-netty/src/test/resources/keystoreWithMultipleAlias.jks b/http-netty/src/test/resources/keystoreWithMultipleAlias.jks new file mode 100644 index 00000000000..24d5b14f5cb Binary files /dev/null and b/http-netty/src/test/resources/keystoreWithMultipleAlias.jks differ