mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-07-15 01:07:30 +08:00
Merge branch '2.7.x'
Closes gh-33433
This commit is contained in:
commit
a4779b7859
@ -51,6 +51,7 @@ import org.springframework.util.ResourceUtils;
|
|||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @author Olivier Lamy
|
* @author Olivier Lamy
|
||||||
* @author Chris Bono
|
* @author Chris Bono
|
||||||
|
* @author Cyril Dangerville
|
||||||
*/
|
*/
|
||||||
class SslServerCustomizer implements JettyServerCustomizer {
|
class SslServerCustomizer implements JettyServerCustomizer {
|
||||||
|
|
||||||
@ -220,16 +221,24 @@ class SslServerCustomizer implements JettyServerCustomizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void configureSslKeyStore(SslContextFactory.Server factory, Ssl ssl) {
|
private void configureSslKeyStore(SslContextFactory.Server factory, Ssl ssl) {
|
||||||
try {
|
String keystoreType = (ssl.getKeyStoreType() != null) ? ssl.getKeyStoreType() : "JKS";
|
||||||
URL url = ResourceUtils.getURL(ssl.getKeyStore());
|
String keystoreLocation = ssl.getKeyStore();
|
||||||
factory.setKeyStoreResource(Resource.newResource(url));
|
if (keystoreType.equalsIgnoreCase("PKCS11")) {
|
||||||
|
if (keystoreLocation != null && !keystoreLocation.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '"
|
||||||
|
+ keystoreLocation + "'. Must be undefined / null.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
else {
|
||||||
throw new WebServerException("Could not load key store '" + ssl.getKeyStore() + "'", ex);
|
try {
|
||||||
}
|
URL url = ResourceUtils.getURL(keystoreLocation);
|
||||||
if (ssl.getKeyStoreType() != null) {
|
factory.setKeyStoreResource(Resource.newResource(url));
|
||||||
factory.setKeyStoreType(ssl.getKeyStoreType());
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new WebServerException("Could not load key store '" + keystoreLocation + "'", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
factory.setKeyStoreType(keystoreType);
|
||||||
if (ssl.getKeyStoreProvider() != null) {
|
if (ssl.getKeyStoreProvider() != null) {
|
||||||
factory.setKeyStoreProvider(ssl.getKeyStoreProvider());
|
factory.setKeyStoreProvider(ssl.getKeyStoreProvider());
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@ import org.springframework.util.ResourceUtils;
|
|||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @author Raheela Aslam
|
* @author Raheela Aslam
|
||||||
* @author Chris Bono
|
* @author Chris Bono
|
||||||
|
* @author Cyril Dangerville
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
* @deprecated this class is meant for Spring Boot internal use only.
|
* @deprecated this class is meant for Spring Boot internal use only.
|
||||||
*/
|
*/
|
||||||
@ -171,17 +172,25 @@ public class SslServerCustomizer implements NettyServerCustomizer {
|
|||||||
private KeyStore loadStore(String type, String provider, String resource, String password) throws Exception {
|
private KeyStore loadStore(String type, String provider, String resource, String password) throws Exception {
|
||||||
type = (type != null) ? type : "JKS";
|
type = (type != null) ? type : "JKS";
|
||||||
KeyStore store = (provider != null) ? KeyStore.getInstance(type, provider) : KeyStore.getInstance(type);
|
KeyStore store = (provider != null) ? KeyStore.getInstance(type, provider) : KeyStore.getInstance(type);
|
||||||
try {
|
if (type.equalsIgnoreCase("PKCS11")) {
|
||||||
URL url = ResourceUtils.getURL(resource);
|
if (resource != null && !resource.isEmpty()) {
|
||||||
try (InputStream stream = url.openStream()) {
|
throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '"
|
||||||
store.load(stream, (password != null) ? password.toCharArray() : null);
|
+ resource + "'. Must be undefined / null.");
|
||||||
}
|
}
|
||||||
return store;
|
store.load(null, (password != null) ? password.toCharArray() : null);
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
else {
|
||||||
throw new WebServerException("Could not load key store '" + resource + "'", ex);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,6 +39,7 @@ import org.springframework.util.StringUtils;
|
|||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
* @author Scott Frederick
|
* @author Scott Frederick
|
||||||
|
* @author Cyril Dangerville
|
||||||
*/
|
*/
|
||||||
class SslConnectorCustomizer implements TomcatConnectorCustomizer {
|
class SslConnectorCustomizer implements TomcatConnectorCustomizer {
|
||||||
|
|
||||||
@ -139,15 +140,23 @@ class SslConnectorCustomizer implements TomcatConnectorCustomizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void configureSslKeyStore(SSLHostConfigCertificate certificate, Ssl ssl) {
|
private void configureSslKeyStore(SSLHostConfigCertificate certificate, Ssl ssl) {
|
||||||
try {
|
String keystoreType = (ssl.getKeyStoreType() != null) ? ssl.getKeyStoreType() : "JKS";
|
||||||
certificate.setCertificateKeystoreFile(ResourceUtils.getURL(ssl.getKeyStore()).toString());
|
String keystoreLocation = ssl.getKeyStore();
|
||||||
|
if (keystoreType.equalsIgnoreCase("PKCS11")) {
|
||||||
|
if (keystoreLocation != null && !keystoreLocation.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '"
|
||||||
|
+ keystoreLocation + "'. Must be undefined / null.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
else {
|
||||||
throw new WebServerException("Could not load key store '" + ssl.getKeyStore() + "'", ex);
|
try {
|
||||||
}
|
certificate.setCertificateKeystoreFile(ResourceUtils.getURL(keystoreLocation).toString());
|
||||||
if (ssl.getKeyStoreType() != null) {
|
}
|
||||||
certificate.setCertificateKeystoreType(ssl.getKeyStoreType());
|
catch (Exception ex) {
|
||||||
|
throw new WebServerException("Could not load key store '" + keystoreLocation + "'", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
certificate.setCertificateKeystoreType(keystoreType);
|
||||||
if (ssl.getKeyStoreProvider() != null) {
|
if (ssl.getKeyStoreProvider() != null) {
|
||||||
certificate.setCertificateKeystoreProvider(ssl.getKeyStoreProvider());
|
certificate.setCertificateKeystoreProvider(ssl.getKeyStoreProvider());
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ import org.springframework.util.ResourceUtils;
|
|||||||
*
|
*
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @author Raheela Aslam
|
* @author Raheela Aslam
|
||||||
|
* @author Cyril Dangerville
|
||||||
*/
|
*/
|
||||||
class SslBuilderCustomizer implements UndertowBuilderCustomizer {
|
class SslBuilderCustomizer implements UndertowBuilderCustomizer {
|
||||||
|
|
||||||
@ -180,16 +181,26 @@ class SslBuilderCustomizer implements UndertowBuilderCustomizer {
|
|||||||
private KeyStore loadStore(String type, String provider, String resource, String password) throws Exception {
|
private KeyStore loadStore(String type, String provider, String resource, String password) throws Exception {
|
||||||
type = (type != null) ? type : "JKS";
|
type = (type != null) ? type : "JKS";
|
||||||
KeyStore store = (provider != null) ? KeyStore.getInstance(type, provider) : KeyStore.getInstance(type);
|
KeyStore store = (provider != null) ? KeyStore.getInstance(type, provider) : KeyStore.getInstance(type);
|
||||||
try {
|
if (type.equalsIgnoreCase("PKCS11")) {
|
||||||
URL url = ResourceUtils.getURL(resource);
|
if (resource != null && !resource.isEmpty()) {
|
||||||
try (InputStream stream = url.openStream()) {
|
throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '"
|
||||||
store.load(stream, (password != null) ? password.toCharArray() : null);
|
+ resource + "'. Must be undefined / null.");
|
||||||
}
|
}
|
||||||
return store;
|
store.load(null, (password != null) ? password.toCharArray() : null);
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
else {
|
||||||
throw new WebServerException("Could not load key store '" + resource + "'", ex);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
package org.springframework.boot.web.embedded.jetty;
|
package org.springframework.boot.web.embedded.jetty;
|
||||||
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
import java.security.Provider;
|
||||||
|
import java.security.Security;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -27,24 +29,47 @@ import org.eclipse.jetty.server.HttpConnectionFactory;
|
|||||||
import org.eclipse.jetty.server.Server;
|
import org.eclipse.jetty.server.Server;
|
||||||
import org.eclipse.jetty.server.SslConnectionFactory;
|
import org.eclipse.jetty.server.SslConnectionFactory;
|
||||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
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.Test;
|
||||||
import org.junit.jupiter.api.condition.OS;
|
import org.junit.jupiter.api.condition.OS;
|
||||||
|
|
||||||
import org.springframework.boot.testsupport.junit.DisabledOnOs;
|
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.Http2;
|
||||||
import org.springframework.boot.web.server.Ssl;
|
import org.springframework.boot.web.server.Ssl;
|
||||||
import org.springframework.boot.web.server.WebServerException;
|
import org.springframework.boot.web.server.WebServerException;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
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}.
|
* Tests for {@link SslServerCustomizer}.
|
||||||
*
|
*
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
|
* @author Cyril Dangerville
|
||||||
*/
|
*/
|
||||||
class SslServerCustomizerTests {
|
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
|
@Test
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
void whenHttp2IsNotEnabledServerConnectorHasSslAndHttpConnectionFactories() {
|
void whenHttp2IsNotEnabledServerConnectorHasSslAndHttpConnectionFactories() {
|
||||||
@ -82,8 +107,11 @@ class SslServerCustomizerTests {
|
|||||||
assertThat(((ALPNServerConnectionFactory) factories.get(1)).getDefaultProtocol()).isNull();
|
assertThat(((ALPNServerConnectionFactory) factories.get(1)).getDefaultProtocol()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Null/undefined keystore is invalid unless keystore type is PKCS11.
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
void configureSslWhenSslIsEnabledWithNoKeyStoreThrowsWebServerException() {
|
void configureSslWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsWebServerException() {
|
||||||
Ssl ssl = new Ssl();
|
Ssl ssl = new Ssl();
|
||||||
SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null);
|
SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null);
|
||||||
assertThatExceptionOfType(Exception.class)
|
assertThatExceptionOfType(Exception.class)
|
||||||
@ -94,6 +122,33 @@ class SslServerCustomizerTests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
private Server createCustomizedServer() {
|
||||||
return createCustomizedServer(new Http2());
|
return createCustomizedServer(new Http2());
|
||||||
}
|
}
|
||||||
|
@ -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<String, KeyPair> 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<String> 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.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -17,23 +17,50 @@
|
|||||||
package org.springframework.boot.web.embedded.netty;
|
package org.springframework.boot.web.embedded.netty;
|
||||||
|
|
||||||
import java.security.NoSuchProviderException;
|
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.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.boot.web.server.Ssl;
|
import org.springframework.boot.web.server.Ssl;
|
||||||
import org.springframework.boot.web.server.WebServerException;
|
import org.springframework.boot.web.server.WebServerException;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link SslServerCustomizer}.
|
* Tests for {@link SslServerCustomizer}.
|
||||||
*
|
*
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
* @author Raheela Aslam
|
* @author Raheela Aslam
|
||||||
|
* @author Cyril Dangerville
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
class SslServerCustomizerTests {
|
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
|
@Test
|
||||||
void keyStoreProviderIsUsedWhenCreatingKeyStore() {
|
void keyStoreProviderIsUsedWhenCreatingKeyStore() {
|
||||||
Ssl ssl = new Ssl();
|
Ssl ssl = new Ssl();
|
||||||
@ -58,12 +85,42 @@ class SslServerCustomizerTests {
|
|||||||
.withMessageContaining("com.example.TrustStoreProvider");
|
.withMessageContaining("com.example.TrustStoreProvider");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Null/undefined keystore is not valid unless keystore type is PKCS11.
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
void getKeyManagerFactoryWhenSslIsEnabledWithNoKeyStoreThrowsWebServerException() {
|
void getKeyManagerFactoryWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsWebServerException() {
|
||||||
Ssl ssl = new Ssl();
|
Ssl ssl = new Ssl();
|
||||||
SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null);
|
SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null);
|
||||||
assertThatIllegalStateException().isThrownBy(() -> customizer.getKeyManagerFactory(ssl, null))
|
assertThatIllegalStateException().isThrownBy(() -> customizer.getKeyManagerFactory(ssl, null))
|
||||||
.withCauseInstanceOf(WebServerException.class).withMessageContaining("Could not load key store '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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ import java.io.InputStream;
|
|||||||
import java.security.KeyStore;
|
import java.security.KeyStore;
|
||||||
import java.security.KeyStoreException;
|
import java.security.KeyStoreException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.Provider;
|
||||||
|
import java.security.Security;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@ -29,7 +31,9 @@ import org.apache.catalina.connector.Connector;
|
|||||||
import org.apache.catalina.startup.Tomcat;
|
import org.apache.catalina.startup.Tomcat;
|
||||||
import org.apache.tomcat.util.net.SSLHostConfig;
|
import org.apache.tomcat.util.net.SSLHostConfig;
|
||||||
import org.apache.tomcat.util.net.SSLHostConfigCertificate;
|
import org.apache.tomcat.util.net.SSLHostConfigCertificate;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@ -37,6 +41,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||||||
import org.springframework.boot.testsupport.system.CapturedOutput;
|
import org.springframework.boot.testsupport.system.CapturedOutput;
|
||||||
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
|
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
|
||||||
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
|
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.Ssl;
|
||||||
import org.springframework.boot.web.server.SslStoreProvider;
|
import org.springframework.boot.web.server.SslStoreProvider;
|
||||||
import org.springframework.boot.web.server.WebServerException;
|
import org.springframework.boot.web.server.WebServerException;
|
||||||
@ -45,6 +50,8 @@ import org.springframework.core.io.Resource;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
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.BDDMockito.given;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
@ -54,15 +61,33 @@ import static org.mockito.Mockito.mock;
|
|||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
* @author Scott Frederick
|
* @author Scott Frederick
|
||||||
|
* @author Cyril Dangerville
|
||||||
*/
|
*/
|
||||||
@ExtendWith(OutputCaptureExtension.class)
|
@ExtendWith(OutputCaptureExtension.class)
|
||||||
@DirtiesUrlFactories
|
@DirtiesUrlFactories
|
||||||
class SslConnectorCustomizerTests {
|
class SslConnectorCustomizerTests {
|
||||||
|
|
||||||
|
private static final Provider PKCS11_PROVIDER = new MockPkcs11SecurityProvider();
|
||||||
|
|
||||||
private Tomcat tomcat;
|
private Tomcat tomcat;
|
||||||
|
|
||||||
private Connector connector;
|
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
|
@BeforeEach
|
||||||
void setup() {
|
void setup() {
|
||||||
this.tomcat = new Tomcat();
|
this.tomcat = new Tomcat();
|
||||||
@ -176,13 +201,42 @@ class SslConnectorCustomizerTests {
|
|||||||
assertThat(output).doesNotContain("Password verification failed");
|
assertThat(output).doesNotContain("Password verification failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Null/undefined keystore is invalid unless keystore type is PKCS11.
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
void customizeWhenSslIsEnabledWithNoKeyStoreThrowsWebServerException() {
|
void customizeWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsWebServerException() {
|
||||||
assertThatExceptionOfType(WebServerException.class)
|
assertThatExceptionOfType(WebServerException.class)
|
||||||
.isThrownBy(() -> new SslConnectorCustomizer(new Ssl(), null).customize(this.tomcat.getConnector()))
|
.isThrownBy(() -> new SslConnectorCustomizer(new Ssl(), null).customize(this.tomcat.getConnector()))
|
||||||
.withMessageContaining("Could not load key store 'null'");
|
.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 {
|
private KeyStore loadStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
|
||||||
KeyStore keyStore = KeyStore.getInstance("JKS");
|
KeyStore keyStore = KeyStore.getInstance("JKS");
|
||||||
Resource resource = new ClassPathResource("test.jks");
|
Resource resource = new ClassPathResource("test.jks");
|
||||||
|
@ -18,26 +18,50 @@ package org.springframework.boot.web.embedded.undertow;
|
|||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.security.NoSuchProviderException;
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.Provider;
|
||||||
|
import java.security.Security;
|
||||||
|
|
||||||
import javax.net.ssl.KeyManager;
|
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.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.Ssl;
|
||||||
import org.springframework.boot.web.server.WebServerException;
|
import org.springframework.boot.web.server.WebServerException;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link SslBuilderCustomizer}
|
* Tests for {@link SslBuilderCustomizer}
|
||||||
*
|
*
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @author Raheela Aslam
|
* @author Raheela Aslam
|
||||||
|
* @author Cyril Dangerville
|
||||||
*/
|
*/
|
||||||
class SslBuilderCustomizerTests {
|
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
|
@Test
|
||||||
void getKeyManagersWhenAliasIsNullShouldNotDecorate() throws Exception {
|
void getKeyManagersWhenAliasIsNullShouldNotDecorate() throws Exception {
|
||||||
Ssl ssl = new Ssl();
|
Ssl ssl = new Ssl();
|
||||||
@ -76,8 +100,11 @@ class SslBuilderCustomizerTests {
|
|||||||
.withMessageContaining("com.example.TrustStoreProvider");
|
.withMessageContaining("com.example.TrustStoreProvider");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Null/undefined keystore is invalid unless keystore type is PKCS11.
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
void getKeyManagersWhenSslIsEnabledWithNoKeyStoreThrowsWebServerException() throws Exception {
|
void getKeyManagersWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsWebServerException() throws Exception {
|
||||||
Ssl ssl = new Ssl();
|
Ssl ssl = new Ssl();
|
||||||
SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null);
|
SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null);
|
||||||
assertThatIllegalStateException()
|
assertThatIllegalStateException()
|
||||||
@ -85,4 +112,33 @@ class SslBuilderCustomizerTests {
|
|||||||
.withCauseInstanceOf(WebServerException.class).withMessageContaining("Could not load key store '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 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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
@ -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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<SpringBootJarTestContainer> {
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
<configuration>
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<logger name="org.testcontainers" level="INFO"/>
|
||||||
|
<logger name="com.github.dockerjava" level="WARN"/>
|
||||||
|
</configuration>
|
@ -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"
|
@ -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
|
Loading…
Reference in New Issue
Block a user