From 716a839d545a26566ea16fb713ad24f594fbc8f6 Mon Sep 17 00:00:00 2001 From: cdanger Date: Sat, 27 Aug 2022 19:02:56 +0200 Subject: [PATCH 1/2] Fix bug in webserver start when loading PKCS#11 KeyStore See gh-32179 --- .../embedded/jetty/SslServerCustomizer.java | 27 +++- .../embedded/netty/SslServerCustomizer.java | 25 +++- .../tomcat/SslConnectorCustomizer.java | 25 +++- .../undertow/SslBuilderCustomizer.java | 26 +++- .../jetty/SslServerCustomizerTests.java | 57 ++++++- .../web/embedded/netty/MockKeyStoreSpi.java | 139 ++++++++++++++++++ .../netty/MockPkcs11SecurityProvider.java | 48 ++++++ .../netty/SslServerCustomizerTests.java | 59 +++++++- .../tomcat/SslConnectorCustomizerTests.java | 56 ++++++- .../undertow/SslBuilderCustomizerTests.java | 58 +++++++- .../build.gradle | 52 +++++++ .../build.gradle | 31 ++++ .../settings.gradle | 15 ++ ...NettySslServerWithPkcs11KeystoreTests.java | 122 +++++++++++++++ .../src/test/resources/logback.xml | 14 ++ .../Dockerfile | 14 ++ .../docker-entrypoint.sh | 3 + 17 files changed, 738 insertions(+), 33 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/MockKeyStoreSpi.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/MockPkcs11SecurityProvider.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/build.gradle create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/spring-boot-starter-webflux-tests-app/build.gradle create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/spring-boot-starter-webflux-tests-app/settings.gradle create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/java/smoketest.webflux.ssl/EmbeddedNettySslServerWithPkcs11KeystoreTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/resources/logback.xml create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/resources/ssl-server-with-pkcs11-keystore/Dockerfile create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/resources/ssl-server-with-pkcs11-keystore/docker-entrypoint.sh diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java index 4c6015d5f300..f2fb7458fb16 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.URL; +import java.util.Objects; import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; import org.eclipse.jetty.http.HttpVersion; @@ -51,6 +52,7 @@ * @author Brian Clozel * @author Olivier Lamy * @author Chris Bono + * @author Cyril Dangerville */ class SslServerCustomizer implements JettyServerCustomizer { @@ -220,16 +222,25 @@ private void configureSslPasswords(SslContextFactory.Server factory, Ssl ssl) { } private void configureSslKeyStore(SslContextFactory.Server factory, Ssl ssl) { - try { - URL url = ResourceUtils.getURL(ssl.getKeyStore()); - factory.setKeyStoreResource(Resource.newResource(url)); - } - catch (Exception ex) { - throw new WebServerException("Could not load key store '" + ssl.getKeyStore() + "'", ex); + final String keystoreType = Objects.requireNonNullElse(ssl.getKeyStoreType(), "JKS"); + final String keystoreLocation = ssl.getKeyStore(); + if (keystoreType.equalsIgnoreCase("PKCS11")) { + if (keystoreLocation != null && !keystoreLocation.isBlank()) { + throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '" + + keystoreLocation + "'. Must be undefined / null."); + } } - if (ssl.getKeyStoreType() != null) { - factory.setKeyStoreType(ssl.getKeyStoreType()); + else { + try { + URL url = ResourceUtils.getURL(keystoreLocation); + factory.setKeyStoreResource(Resource.newResource(url)); + } + catch (Exception ex) { + throw new WebServerException("Could not load key store '" + keystoreLocation + "'", ex); + } } + + factory.setKeyStoreType(keystoreType); if (ssl.getKeyStoreProvider() != null) { factory.setKeyStoreProvider(ssl.getKeyStoreProvider()); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java index 821bd9577fb1..cfa809ab9586 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java @@ -57,6 +57,7 @@ * @author Brian Clozel * @author Raheela Aslam * @author Chris Bono + * @author Cyril Dangerville * @since 2.0.0 * @deprecated this class is meant for Spring Boot internal use only. */ @@ -171,17 +172,27 @@ private KeyStore loadTrustStore(String type, String provider, String resource, S private KeyStore loadStore(String type, String provider, String resource, String password) throws Exception { type = (type != null) ? type : "JKS"; KeyStore store = (provider != null) ? KeyStore.getInstance(type, provider) : KeyStore.getInstance(type); - try { - URL url = ResourceUtils.getURL(resource); - try (InputStream stream = url.openStream()) { - store.load(stream, (password != null) ? password.toCharArray() : null); + if (type.equalsIgnoreCase("PKCS11")) { + if (resource != null && !resource.isBlank()) { + throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '" + + resource + "'. Must be undefined / null."); } - return store; + + store.load(null, (password != null) ? password.toCharArray() : null); } - catch (Exception ex) { - throw new WebServerException("Could not load key store '" + resource + "'", ex); + else { + try { + URL url = ResourceUtils.getURL(resource); + try (InputStream stream = url.openStream()) { + store.load(stream, (password != null) ? password.toCharArray() : null); + } + } + catch (Exception ex) { + throw new WebServerException("Could not load key store '" + resource + "'", ex); + } } + return store; } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java index c7fe1a530a7d..134b2e8883f1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java @@ -17,6 +17,7 @@ package org.springframework.boot.web.embedded.tomcat; import java.io.FileNotFoundException; +import java.util.Objects; import org.apache.catalina.connector.Connector; import org.apache.coyote.ProtocolHandler; @@ -39,6 +40,7 @@ * @author Brian Clozel * @author Andy Wilkinson * @author Scott Frederick + * @author Cyril Dangerville */ class SslConnectorCustomizer implements TomcatConnectorCustomizer { @@ -139,15 +141,24 @@ protected void configureSslStoreProvider(AbstractHttp11JsseProtocol protocol, } private void configureSslKeyStore(SSLHostConfigCertificate certificate, Ssl ssl) { - try { - certificate.setCertificateKeystoreFile(ResourceUtils.getURL(ssl.getKeyStore()).toString()); - } - catch (Exception ex) { - throw new WebServerException("Could not load key store '" + ssl.getKeyStore() + "'", ex); + final String keystoreType = Objects.requireNonNullElse(ssl.getKeyStoreType(), "JKS"); + final String keystoreLocation = ssl.getKeyStore(); + if (keystoreType.equalsIgnoreCase("PKCS11")) { + if (keystoreLocation != null && !keystoreLocation.isBlank()) { + throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '" + + keystoreLocation + "'. Must be undefined / null."); + } } - if (ssl.getKeyStoreType() != null) { - certificate.setCertificateKeystoreType(ssl.getKeyStoreType()); + else { + try { + certificate.setCertificateKeystoreFile(ResourceUtils.getURL(keystoreLocation).toString()); + } + catch (Exception ex) { + throw new WebServerException("Could not load key store '" + keystoreLocation + "'", ex); + } } + + certificate.setCertificateKeystoreType(keystoreType); if (ssl.getKeyStoreProvider() != null) { certificate.setCertificateKeystoreProvider(ssl.getKeyStoreProvider()); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java index a4674e8b397d..3cfd1b817b6e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java @@ -51,6 +51,7 @@ * * @author Brian Clozel * @author Raheela Aslam + * @author Cyril Dangerville */ class SslBuilderCustomizer implements UndertowBuilderCustomizer { @@ -180,16 +181,27 @@ private KeyStore loadTrustStore(String type, String provider, String resource, S private KeyStore loadStore(String type, String provider, String resource, String password) throws Exception { type = (type != null) ? type : "JKS"; KeyStore store = (provider != null) ? KeyStore.getInstance(type, provider) : KeyStore.getInstance(type); - try { - URL url = ResourceUtils.getURL(resource); - try (InputStream stream = url.openStream()) { - store.load(stream, (password != null) ? password.toCharArray() : null); + if (type.equalsIgnoreCase("PKCS11")) { + if (resource != null && !resource.isBlank()) { + throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '" + + resource + "'. Must be undefined / null."); } - return store; + + store.load(null, (password != null) ? password.toCharArray() : null); } - catch (Exception ex) { - throw new WebServerException("Could not load key store '" + resource + "'", ex); + else { + try { + URL url = ResourceUtils.getURL(resource); + try (InputStream stream = url.openStream()) { + store.load(stream, (password != null) ? password.toCharArray() : null); + } + } + catch (Exception ex) { + throw new WebServerException("Could not load key store '" + resource + "'", ex); + } } + + return store; } /** diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizerTests.java index 5cc9815eb46a..c63d3ba1fb7f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizerTests.java @@ -17,6 +17,8 @@ package org.springframework.boot.web.embedded.jetty; import java.net.InetSocketAddress; +import java.security.Provider; +import java.security.Security; import java.util.ArrayList; import java.util.List; @@ -27,24 +29,47 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.OS; import org.springframework.boot.testsupport.junit.DisabledOnOs; +import org.springframework.boot.web.embedded.netty.MockPkcs11SecurityProvider; import org.springframework.boot.web.server.Http2; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServerException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * Tests for {@link SslServerCustomizer}. * * @author Andy Wilkinson + * @author Cyril Dangerville */ class SslServerCustomizerTests { + private static final Provider PKCS11_PROVIDER = new MockPkcs11SecurityProvider(); + + @BeforeAll + static void beforeAllTests() { + /* + * Add the mock Java security provider for PKCS#11-related unit tests. + * + */ + Security.addProvider(PKCS11_PROVIDER); + } + + @AfterAll + static void afterAllTests() { + // Remove the provider previously added in setup() + Security.removeProvider(PKCS11_PROVIDER.getName()); + } + @Test @SuppressWarnings("rawtypes") void whenHttp2IsNotEnabledServerConnectorHasSslAndHttpConnectionFactories() { @@ -82,8 +107,11 @@ void alpnConnectionFactoryHasNullDefaultProtocolToAllowNegotiationToHttp11() { assertThat(((ALPNServerConnectionFactory) factories.get(1)).getDefaultProtocol()).isNull(); } + /** + * Null/undefined keystore is invalid unless keystore type is PKCS11. + */ @Test - void configureSslWhenSslIsEnabledWithNoKeyStoreThrowsWebServerException() { + void configureSslWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsWebServerException() { Ssl ssl = new Ssl(); SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null); assertThatExceptionOfType(Exception.class) @@ -94,6 +122,33 @@ void configureSslWhenSslIsEnabledWithNoKeyStoreThrowsWebServerException() { }); } + /** + * No keystore path should be defined if keystore type is PKCS#11. + */ + @Test + void configureSslWhenSslIsEnabledWithPkcs11AndKeyStoreThrowsIllegalArgumentException() { + Ssl ssl = new Ssl(); + ssl.setKeyStoreType("PKCS11"); + ssl.setKeyStoreProvider(PKCS11_PROVIDER.getName()); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyPassword("password"); + SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null); + assertThatIllegalArgumentException() + .isThrownBy(() -> customizer.configureSsl(new SslContextFactory.Server(), ssl, null)) + .withMessageContaining("Input keystore location is not valid for keystore type 'PKCS11'"); + } + + @Test + void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() { + Ssl ssl = new Ssl(); + ssl.setKeyStoreType("PKCS11"); + ssl.setKeyStoreProvider(PKCS11_PROVIDER.getName()); + ssl.setKeyStorePassword("1234"); + SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null); + // Loading the KeyManagerFactory should be successful + assertThatNoException().isThrownBy(() -> customizer.configureSsl(new SslContextFactory.Server(), ssl, null)); + } + private Server createCustomizedServer() { return createCustomizedServer(new Http2()); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/MockKeyStoreSpi.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/MockKeyStoreSpi.java new file mode 100644 index 000000000000..9f9aee61ca71 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/MockKeyStoreSpi.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed 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 + * + * https://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 org.springframework.boot.web.embedded.netty; + +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStoreSpi; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +/** + * Mock Security Provider for testing purposes only (e.g. SslServerCustomizerTests class) + * + * @author Cyril Dangerville + */ +public class MockKeyStoreSpi extends KeyStoreSpi { + + private static final KeyPairGenerator KEYGEN; + + static { + try { + KEYGEN = KeyPairGenerator.getInstance("RSA"); + KEYGEN.initialize(2048); + } + catch (NoSuchAlgorithmException ex) { + throw new RuntimeException(ex); + } + } + + private final Map aliases = new HashMap<>(); + + @Override + public Key engineGetKey(String alias, char[] password) { + final KeyPair keyPair = this.aliases.get(alias); + return (keyPair != null) ? keyPair.getPrivate() : null; + } + + @Override + public Certificate[] engineGetCertificateChain(String alias) { + return new Certificate[0]; + } + + @Override + public Certificate engineGetCertificate(String alias) { + throw new UnsupportedOperationException(); + } + + @Override + public Date engineGetCreationDate(String alias) { + throw new UnsupportedOperationException(); + } + + @Override + public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) { + throw new UnsupportedOperationException(); + } + + @Override + public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) { + throw new UnsupportedOperationException(); + } + + @Override + public void engineSetCertificateEntry(String alias, Certificate cert) { + throw new UnsupportedOperationException(); + } + + @Override + public void engineDeleteEntry(String alias) { + throw new UnsupportedOperationException(); + } + + @Override + public Enumeration engineAliases() { + return Collections.enumeration(this.aliases.keySet()); + } + + @Override + public boolean engineContainsAlias(String alias) { + // contains any required alias, for testing purposes + // Add alias to aliases list on the fly + this.aliases.put(alias, KEYGEN.generateKeyPair()); + return true; + } + + @Override + public int engineSize() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean engineIsKeyEntry(String alias) { + // Handle all keystore entries as key entries + return this.aliases.containsKey(alias); + } + + @Override + public boolean engineIsCertificateEntry(String alias) { + return false; + } + + @Override + public String engineGetCertificateAlias(Certificate cert) { + throw new UnsupportedOperationException(); + } + + @Override + public void engineStore(OutputStream stream, char[] password) { + throw new UnsupportedOperationException(); + } + + @Override + public void engineLoad(InputStream stream, char[] password) { + // Nothing to do, this is a mock keystore implementation, for testing only. + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/MockPkcs11SecurityProvider.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/MockPkcs11SecurityProvider.java new file mode 100644 index 000000000000..31bc824e6294 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/MockPkcs11SecurityProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed 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 + * + * https://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 org.springframework.boot.web.embedded.netty; + +import java.security.KeyStoreSpi; +import java.security.Provider; + +/** + * Mock PKCS#11 Security Provider for testing purposes only (e.g. SslServerCustomizerTests + * class) + * + * @author Cyril Dangerville + */ +public class MockPkcs11SecurityProvider extends Provider { + + private static final String DEFAULT_PROVIDER_NAME = "Mock-PKCS11"; + + private static final String VERSION = "0.1"; + + private static final String DESCRIPTION = "Mock PKCS11 Provider"; + + /** + * Create Security Provider named {@value #DEFAULT_PROVIDER_NAME}, version + * {@value #VERSION} and providing PKCS11 KeyStores with {@link MockKeyStoreSpi} as + * {@link KeyStoreSpi} implementation. + */ + public MockPkcs11SecurityProvider() { + super(DEFAULT_PROVIDER_NAME, VERSION, DESCRIPTION); + + putService(new Service(this, "KeyStore", "PKCS11", + "org.springframework.boot.web.embedded.netty.MockKeyStoreSpi", null, null)); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/SslServerCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/SslServerCustomizerTests.java index e1eace5595db..1c2a17612ad0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/SslServerCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/SslServerCustomizerTests.java @@ -17,23 +17,50 @@ package org.springframework.boot.web.embedded.netty; import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.Security; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServerException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * Tests for {@link SslServerCustomizer}. * * @author Andy Wilkinson * @author Raheela Aslam + * @author Cyril Dangerville */ @SuppressWarnings("deprecation") class SslServerCustomizerTests { + private static final Provider PKCS11_PROVIDER = new MockPkcs11SecurityProvider(); + + @BeforeAll + static void setup() { + /* + * Add the mock Java security provider for PKCS#11-related unit tests. + * + * For an integration test with an actual PKCS#11 library - SoftHSM - properly + * installed and configured on the system (inside a container), used via Java + * built-in SunPKCS11 provider, see the 'spring-boot-smoke-test-webflux-ssl' + * project in 'spring-boot-tests/spring-boot-smoke-tests' folder. + */ + Security.addProvider(PKCS11_PROVIDER); + } + + @AfterAll + static void shutdown() { + // Remove the provider previously added in setup() + Security.removeProvider(PKCS11_PROVIDER.getName()); + } + @Test void keyStoreProviderIsUsedWhenCreatingKeyStore() { Ssl ssl = new Ssl(); @@ -58,12 +85,42 @@ void trustStoreProviderIsUsedWhenCreatingTrustStore() { .withMessageContaining("com.example.TrustStoreProvider"); } + /** + * Null/undefined keystore is not valid unless keystore type is PKCS11. + */ @Test - void getKeyManagerFactoryWhenSslIsEnabledWithNoKeyStoreThrowsWebServerException() { + void getKeyManagerFactoryWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsWebServerException() { Ssl ssl = new Ssl(); SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null); assertThatIllegalStateException().isThrownBy(() -> customizer.getKeyManagerFactory(ssl, null)) .withCauseInstanceOf(WebServerException.class).withMessageContaining("Could not load key store 'null'"); } + /** + * No keystore path should be defined if keystore type is PKCS#11. + */ + @Test + void getKeyManagerFactoryWhenSslIsEnabledWithPkcs11AndKeyStoreThrowsIllegalArgumentException() { + Ssl ssl = new Ssl(); + ssl.setKeyStoreType("PKCS11"); + ssl.setKeyStoreProvider(PKCS11_PROVIDER.getName()); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyPassword("password"); + SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null); + assertThatIllegalStateException().isThrownBy(() -> customizer.getKeyManagerFactory(ssl, null)) + .withCauseInstanceOf(IllegalArgumentException.class) + .withMessageContaining("Input keystore location is not valid for keystore type 'PKCS11'"); + } + + @Test + void getKeyManagerFactoryWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() { + Ssl ssl = new Ssl(); + ssl.setKeyStoreType("PKCS11"); + ssl.setKeyStoreProvider(PKCS11_PROVIDER.getName()); + ssl.setKeyStorePassword("1234"); + SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null); + // Loading the KeyManagerFactory should be successful + assertThatNoException().isThrownBy(() -> customizer.getKeyManagerFactory(ssl, null)); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java index f1d7716fcae4..be18d9ee4e23 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java @@ -21,6 +21,8 @@ import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; import java.security.cert.CertificateException; import java.util.Set; @@ -29,13 +31,16 @@ import org.apache.catalina.startup.Tomcat; import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; +import org.springframework.boot.web.embedded.netty.MockPkcs11SecurityProvider; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.SslStoreProvider; @@ -45,6 +50,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -54,15 +61,33 @@ * @author Brian Clozel * @author Andy Wilkinson * @author Scott Frederick + * @author Cyril Dangerville */ @ExtendWith(OutputCaptureExtension.class) @DirtiesUrlFactories class SslConnectorCustomizerTests { + private static final Provider PKCS11_PROVIDER = new MockPkcs11SecurityProvider(); + private Tomcat tomcat; private Connector connector; + @BeforeAll + static void beforeAllTests() { + /* + * Add the mock Java security provider for PKCS#11-related unit tests. + * + */ + Security.addProvider(PKCS11_PROVIDER); + } + + @AfterAll + static void afterAllTests() { + // Remove the provider previously added in setup() + Security.removeProvider(PKCS11_PROVIDER.getName()); + } + @BeforeEach void setup() { this.tomcat = new Tomcat(); @@ -176,13 +201,42 @@ void customizeWhenSslStoreProviderPresentShouldIgnorePasswordFromSsl(CapturedOut assertThat(output).doesNotContain("Password verification failed"); } + /** + * Null/undefined keystore is invalid unless keystore type is PKCS11. + */ @Test - void customizeWhenSslIsEnabledWithNoKeyStoreThrowsWebServerException() { + void customizeWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsWebServerException() { assertThatExceptionOfType(WebServerException.class) .isThrownBy(() -> new SslConnectorCustomizer(new Ssl(), null).customize(this.tomcat.getConnector())) .withMessageContaining("Could not load key store 'null'"); } + /** + * No keystore path should be defined if keystore type is PKCS#11. + */ + @Test + void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreThrowsIllegalArgumentException() { + Ssl ssl = new Ssl(); + ssl.setKeyStoreType("PKCS11"); + ssl.setKeyStoreProvider(PKCS11_PROVIDER.getName()); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyPassword("password"); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, null); + assertThatIllegalArgumentException().isThrownBy(() -> customizer.customize(this.tomcat.getConnector())) + .withMessageContaining("Input keystore location is not valid for keystore type 'PKCS11'"); + } + + @Test + void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() { + Ssl ssl = new Ssl(); + ssl.setKeyStoreType("PKCS11"); + ssl.setKeyStoreProvider(PKCS11_PROVIDER.getName()); + ssl.setKeyStorePassword("1234"); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, null); + // Loading the KeyManagerFactory should be successful + assertThatNoException().isThrownBy(() -> customizer.customize(this.tomcat.getConnector())); + } + private KeyStore loadStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { KeyStore keyStore = KeyStore.getInstance("JKS"); Resource resource = new ClassPathResource("test.jks"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizerTests.java index 5b876bc6b8f3..1be04a384291 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizerTests.java @@ -18,26 +18,50 @@ import java.net.InetAddress; import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.Security; import javax.net.ssl.KeyManager; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.springframework.boot.web.embedded.netty.MockPkcs11SecurityProvider; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServerException; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * Tests for {@link SslBuilderCustomizer} * * @author Brian Clozel * @author Raheela Aslam + * @author Cyril Dangerville */ class SslBuilderCustomizerTests { + private static final Provider PKCS11_PROVIDER = new MockPkcs11SecurityProvider(); + + @BeforeAll + static void beforeAllTests() { + /* + * Add the mock Java security provider for PKCS#11-related unit tests. + * + */ + Security.addProvider(PKCS11_PROVIDER); + } + + @AfterAll + static void afterAllTests() { + // Remove the provider previously added in setup() + Security.removeProvider(PKCS11_PROVIDER.getName()); + } + @Test void getKeyManagersWhenAliasIsNullShouldNotDecorate() throws Exception { Ssl ssl = new Ssl(); @@ -76,8 +100,11 @@ void trustStoreProviderIsUsedWhenCreatingTrustStore() throws Exception { .withMessageContaining("com.example.TrustStoreProvider"); } + /** + * Null/undefined keystore is invalid unless keystore type is PKCS11. + */ @Test - void getKeyManagersWhenSslIsEnabledWithNoKeyStoreThrowsWebServerException() throws Exception { + void getKeyManagersWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsWebServerException() throws Exception { Ssl ssl = new Ssl(); SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); assertThatIllegalStateException() @@ -85,4 +112,33 @@ void getKeyManagersWhenSslIsEnabledWithNoKeyStoreThrowsWebServerException() thro .withCauseInstanceOf(WebServerException.class).withMessageContaining("Could not load key store 'null'"); } + /** + * No keystore path should be defined if keystore type is PKCS#11. + */ + @Test + void configureSslWhenSslIsEnabledWithPkcs11AndKeyStoreThrowsIllegalArgumentException() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStoreType("PKCS11"); + ssl.setKeyStoreProvider(PKCS11_PROVIDER.getName()); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyPassword("password"); + SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); + assertThatIllegalStateException() + .isThrownBy(() -> ReflectionTestUtils.invokeMethod(customizer, "getKeyManagers", ssl, null)) + .withCauseInstanceOf(IllegalArgumentException.class) + .withMessageContaining("Input keystore location is not valid for keystore type 'PKCS11'"); + } + + @Test + void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStoreType("PKCS11"); + ssl.setKeyStoreProvider(PKCS11_PROVIDER.getName()); + ssl.setKeyStorePassword("1234"); + SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); + // Loading the KeyManagerFactory should be successful + assertThatNoException() + .isThrownBy(() -> ReflectionTestUtils.invokeMethod(customizer, "getKeyManagers", ssl, null)); + } + } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/build.gradle new file mode 100644 index 000000000000..91c7ceda3d29 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/build.gradle @@ -0,0 +1,52 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" + id "org.springframework.boot.integration-test" +} + +description = "Spring Boot WebFlux SSL smoke test" + + +configurations { + app +} + +dependencies { + app project(path: ":spring-boot-project:spring-boot-dependencies", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-parent", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-webflux", configuration: "mavenRepository") + + testImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-parent"))) + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) + testImplementation("org.testcontainers:junit-jupiter") + // For the WebClient in tests + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-webflux")) +} + +task syncMavenRepository(type: Sync) { + from configurations.app + into "${buildDir}/int-test-maven-repository" +} + +task syncAppGradleFiles(type: org.springframework.boot.build.SyncAppSource) { + sourceDirectory = file("spring-boot-starter-webflux-tests-app") + destinationDirectory = file("${buildDir}/spring-boot-starter-webflux-tests-app") +} + +task syncAppSource(type: org.springframework.boot.build.SyncAppSource) { + sourceDirectory = file("../spring-boot-smoke-test-webflux/src/main") + destinationDirectory = file("${buildDir}/spring-boot-starter-webflux-tests-app/src/main") +} + +task buildApp(type: GradleBuild) { + dependsOn syncAppGradleFiles, syncAppSource, syncMavenRepository + dir = "${buildDir}/spring-boot-starter-webflux-tests-app" + startParameter.buildCacheEnabled = false + tasks = ["build"] +} + +test { + dependsOn buildApp +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/spring-boot-starter-webflux-tests-app/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/spring-boot-starter-webflux-tests-app/build.gradle new file mode 100644 index 000000000000..335f53c5737a --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/spring-boot-starter-webflux-tests-app/build.gradle @@ -0,0 +1,31 @@ +plugins { + id "java" + id "org.springframework.boot" +} + +apply plugin: "io.spring.dependency-management" + +repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { + url "https://repo.spring.io/milestone" + content { + excludeGroup "org.springframework.boot" + } + } + maven { + url "https://repo.spring.io/snapshot" + content { + excludeGroup "org.springframework.boot" + } + } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-webflux") +} + +bootJar { + launchScript() +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/spring-boot-starter-webflux-tests-app/settings.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/spring-boot-starter-webflux-tests-app/settings.gradle new file mode 100644 index 000000000000..06d9554ad0d6 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/spring-boot-starter-webflux-tests-app/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.springframework.boot") { + useModule "org.springframework.boot:spring-boot-gradle-plugin:${requested.version}" + } + } + } +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/java/smoketest.webflux.ssl/EmbeddedNettySslServerWithPkcs11KeystoreTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/java/smoketest.webflux.ssl/EmbeddedNettySslServerWithPkcs11KeystoreTests.java new file mode 100644 index 000000000000..5a11e8cdbd8b --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/java/smoketest.webflux.ssl/EmbeddedNettySslServerWithPkcs11KeystoreTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.webflux.ssl; + +import java.io.File; +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; + +import javax.net.ssl.TrustManagerFactory; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.ToStringConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; +import reactor.netty.http.client.HttpClient; + +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.fail; + +/** + * Integration tests of Spring Boot's SSL server configured to use a PKCS#11 keystore + * (HSM). + * + * @author Cyril Dangerville + */ +@Testcontainers(disabledWithoutDocker = true) +class EmbeddedNettySslServerWithPkcs11KeystoreTests { + + @Test + void launchWithPkcs11KeystoreProvider() { + /* + * We are going to use the server certificate of the keypair generated in the + * PKCS#11 HSM inside the container, as trusted certificate for the SSL + * connection, to make sure that the Netty SSL server is actually using this + * certificate and the associated keypair in the HSM. The certificate is extracted + * to /server-cert.pem by the keytool command run inside the container at startup + * (see src/test/resources/docker-entrypoint.sh). + */ + final File serverCertDestinationFile = new File("build/tmp/test/server-cert.pem"); + final ToStringConsumer consumer = new ToStringConsumer().withRemoveAnsiCodes(false); + try (SpringBootJarTestContainer container = new SpringBootJarTestContainer()) { + container.withLogConsumer(consumer); + container.start(); + assertThat(consumer.toUtf8String().contains("Netty started")); + + // HTTPS connection test + container.copyFileFromContainer("/server-cert.pem", serverCertDestinationFile.getAbsolutePath()); + final KeyStore truststore = KeyStore.getInstance(KeyStore.getDefaultType()); + truststore.load(null, null); + final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + final Certificate cert; + try (FileInputStream input = new FileInputStream(serverCertDestinationFile)) { + cert = certFactory.generateCertificate(input); + } + truststore.setCertificateEntry("server", cert); + TrustManagerFactory trustManagerFactory = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(truststore); + final SslContext sslContext = SslContextBuilder.forClient().trustManager(trustManagerFactory).build(); + final HttpClient httpClient = HttpClient.create().secure((sslSpec) -> sslSpec.sslContext(sslContext)); + final WebClient httpsClient = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)).build(); + assertThatNoException() + .isThrownBy(() -> httpsClient.get().uri("https://localhost:" + container.getFirstMappedPort() + "/") + .retrieve().toEntity(String.class).block()); + return; + } + catch (Throwable ex) { + ex.printStackTrace(); + } + + fail("Container failed to start or SSL test failed. Startup logs: " + consumer.toUtf8String()); + } + + private static final class SpringBootJarTestContainer extends GenericContainer { + + private SpringBootJarTestContainer() { + super(new ImageFromDockerfile("spring-boot-smoke-test-webflux-ssl/ssl-server-with-pkcs11-keystore") + .withFileFromFile("Dockerfile", + new File("src/test/resources/ssl-server-with-pkcs11-keystore/Dockerfile"))); + withCopyFileToContainer(MountableFile.forHostPath(new File( + "build/spring-boot-starter-webflux-tests-app/build/libs/spring-boot-starter-webflux-tests-app.jar") + .getAbsolutePath()), + "/app.jar"); + final String startupScript = "docker-entrypoint.sh"; + withCopyFileToContainer( + MountableFile.forHostPath("src/test/resources/ssl-server-with-pkcs11-keystore/" + startupScript), + "/" + startupScript); + withCommand("/bin/bash", "-c", "chown root:root *.sh && chown root:root *.jar && chmod +x " + startupScript + + " && ./" + startupScript); + withExposedPorts(8443); + waitingFor(Wait.forListeningPort()); + } + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/resources/logback.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/resources/logback.xml new file mode 100644 index 000000000000..132ca69d3cf5 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/resources/logback.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/resources/ssl-server-with-pkcs11-keystore/Dockerfile b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/resources/ssl-server-with-pkcs11-keystore/Dockerfile new file mode 100644 index 000000000000..07c740de0130 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/resources/ssl-server-with-pkcs11-keystore/Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu:jammy +RUN apt-get update && \ + apt-get install -y software-properties-common curl softhsm2 && \ + mkdir -p /opt/openjdk && \ + cd /opt/openjdk && \ + curl -L https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.1%2B12/OpenJDK17U-jdk_x64_linux_hotspot_17.0.1_12.tar.gz | tar zx --strip-components=1 && \ + # this mkdir fixes old SoftHSMv2 install issue in older Ubuntus: https://github.com/opendnssec/SoftHSMv2/issues/283 + # mkdir -p /var/lib/softhsm/tokens && \ + echo "name = SoftHSM\nlibrary = /usr/lib/softhsm/libsofthsm2.so\nslotListIndex = 0" > /pkcs11.cfg && \ + echo "security.provider.12=SunPKCS11 /pkcs11.cfg" > /java.security.override + +ENV JAVA_HOME /opt/openjdk +ENV PATH $JAVA_HOME/bin:$PATH +ENV JAVA_OPTS "-Djava.security.properties=/java.security.override -Djava.security.debug=sunpkcs11 -Djava.security.debug=pkcs11keystore" \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/resources/ssl-server-with-pkcs11-keystore/docker-entrypoint.sh b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/resources/ssl-server-with-pkcs11-keystore/docker-entrypoint.sh new file mode 100644 index 000000000000..da686384be62 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-webflux-ssl/src/test/resources/ssl-server-with-pkcs11-keystore/docker-entrypoint.sh @@ -0,0 +1,3 @@ +# Initialize a SoftHSM token only if not done already, e.g. at first start +softhsm2-util --show-slots | grep "token-0" || { softhsm2-util --init-token --free --label "token-0" --pin 1234 --so-pin 0000; keytool -genkeypair -alias server -dname CN=localhost -ext san=dns:localhost -keyalg RSA -keysize 2048 -keystore NONE -storetype PKCS11 -providerclass sun.security.pkcs11.SunPKCS11 -providerarg /pkcs11.cfg -storepass 1234; keytool -exportcert -rfc -alias server -keystore NONE -storetype PKCS11 -providerclass sun.security.pkcs11.SunPKCS11 -providerarg /pkcs11.cfg -storepass 1234 > /server-cert.pem; } +java ${JAVA_OPTS} -jar /app.jar --server.port=8443 --server.ssl.enabled=true --server.ssl.key-alias=server --server.ssl.key-store-provider=SunPKCS11-SoftHSM --server.ssl.key-store-type=PKCS11 --server.ssl.key-store-password=1234 From 16569099ba0daa390b23c7cfab3c83f0809d34fc Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 1 Dec 2022 12:17:34 +0100 Subject: [PATCH 2/2] Polish "Fix bug in webserver start when loading PKCS#11 KeyStore" See gh-32179 --- .../boot/web/embedded/jetty/SslServerCustomizer.java | 8 +++----- .../boot/web/embedded/netty/SslServerCustomizer.java | 4 +--- .../boot/web/embedded/tomcat/SslConnectorCustomizer.java | 8 +++----- .../boot/web/embedded/undertow/SslBuilderCustomizer.java | 3 +-- .../web/embedded/netty/MockPkcs11SecurityProvider.java | 2 +- .../web/embedded/tomcat/SslConnectorCustomizerTests.java | 2 +- 6 files changed, 10 insertions(+), 17 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java index f2fb7458fb16..c60cb4d983f2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.URL; -import java.util.Objects; import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; import org.eclipse.jetty.http.HttpVersion; @@ -222,10 +221,10 @@ private void configureSslPasswords(SslContextFactory.Server factory, Ssl ssl) { } private void configureSslKeyStore(SslContextFactory.Server factory, Ssl ssl) { - final String keystoreType = Objects.requireNonNullElse(ssl.getKeyStoreType(), "JKS"); - final String keystoreLocation = ssl.getKeyStore(); + String keystoreType = (ssl.getKeyStoreType() != null) ? ssl.getKeyStoreType() : "JKS"; + String keystoreLocation = ssl.getKeyStore(); if (keystoreType.equalsIgnoreCase("PKCS11")) { - if (keystoreLocation != null && !keystoreLocation.isBlank()) { + if (keystoreLocation != null && !keystoreLocation.isEmpty()) { throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '" + keystoreLocation + "'. Must be undefined / null."); } @@ -239,7 +238,6 @@ private void configureSslKeyStore(SslContextFactory.Server factory, Ssl ssl) { throw new WebServerException("Could not load key store '" + keystoreLocation + "'", ex); } } - factory.setKeyStoreType(keystoreType); if (ssl.getKeyStoreProvider() != null) { factory.setKeyStoreProvider(ssl.getKeyStoreProvider()); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java index cfa809ab9586..d00ff005ed6b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java @@ -173,11 +173,10 @@ private KeyStore loadStore(String type, String provider, String resource, String type = (type != null) ? type : "JKS"; KeyStore store = (provider != null) ? KeyStore.getInstance(type, provider) : KeyStore.getInstance(type); if (type.equalsIgnoreCase("PKCS11")) { - if (resource != null && !resource.isBlank()) { + if (resource != null && !resource.isEmpty()) { throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '" + resource + "'. Must be undefined / null."); } - store.load(null, (password != null) ? password.toCharArray() : null); } else { @@ -191,7 +190,6 @@ private KeyStore loadStore(String type, String provider, String resource, String throw new WebServerException("Could not load key store '" + resource + "'", ex); } } - return store; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java index 134b2e8883f1..c243d88ef4b0 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java @@ -17,7 +17,6 @@ package org.springframework.boot.web.embedded.tomcat; import java.io.FileNotFoundException; -import java.util.Objects; import org.apache.catalina.connector.Connector; import org.apache.coyote.ProtocolHandler; @@ -141,10 +140,10 @@ protected void configureSslStoreProvider(AbstractHttp11JsseProtocol protocol, } private void configureSslKeyStore(SSLHostConfigCertificate certificate, Ssl ssl) { - final String keystoreType = Objects.requireNonNullElse(ssl.getKeyStoreType(), "JKS"); - final String keystoreLocation = ssl.getKeyStore(); + String keystoreType = (ssl.getKeyStoreType() != null) ? ssl.getKeyStoreType() : "JKS"; + String keystoreLocation = ssl.getKeyStore(); if (keystoreType.equalsIgnoreCase("PKCS11")) { - if (keystoreLocation != null && !keystoreLocation.isBlank()) { + if (keystoreLocation != null && !keystoreLocation.isEmpty()) { throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '" + keystoreLocation + "'. Must be undefined / null."); } @@ -157,7 +156,6 @@ private void configureSslKeyStore(SSLHostConfigCertificate certificate, Ssl ssl) throw new WebServerException("Could not load key store '" + keystoreLocation + "'", ex); } } - certificate.setCertificateKeystoreType(keystoreType); if (ssl.getKeyStoreProvider() != null) { certificate.setCertificateKeystoreProvider(ssl.getKeyStoreProvider()); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java index 3cfd1b817b6e..d8615085b409 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java @@ -182,11 +182,10 @@ private KeyStore loadStore(String type, String provider, String resource, String type = (type != null) ? type : "JKS"; KeyStore store = (provider != null) ? KeyStore.getInstance(type, provider) : KeyStore.getInstance(type); if (type.equalsIgnoreCase("PKCS11")) { - if (resource != null && !resource.isBlank()) { + if (resource != null && !resource.isEmpty()) { throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '" + resource + "'. Must be undefined / null."); } - store.load(null, (password != null) ? password.toCharArray() : null); } else { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/MockPkcs11SecurityProvider.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/MockPkcs11SecurityProvider.java index 31bc824e6294..5bc49afbd73a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/MockPkcs11SecurityProvider.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/MockPkcs11SecurityProvider.java @@ -29,7 +29,7 @@ public class MockPkcs11SecurityProvider extends Provider { private static final String DEFAULT_PROVIDER_NAME = "Mock-PKCS11"; - private static final String VERSION = "0.1"; + private static final double VERSION = 0.1; private static final String DESCRIPTION = "Mock PKCS11 Provider"; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java index be18d9ee4e23..3ab6d5a84368 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java @@ -40,8 +40,8 @@ import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; -import org.springframework.boot.web.embedded.netty.MockPkcs11SecurityProvider; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; +import org.springframework.boot.web.embedded.netty.MockPkcs11SecurityProvider; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.SslStoreProvider; import org.springframework.boot.web.server.WebServerException;