Browse Source
This commit adds the ability to configure SSL in embedded web containers using PEM-encoded certificate and private key files, as an alternative to configuring SSL with Java KeyStore files. Closes gh-29273pull/29812/head
23 changed files with 910 additions and 20 deletions
@ -0,0 +1,118 @@ |
|||||||
|
/* |
||||||
|
* 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.server; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.security.GeneralSecurityException; |
||||||
|
import java.security.KeyStore; |
||||||
|
import java.security.KeyStoreException; |
||||||
|
import java.security.PrivateKey; |
||||||
|
import java.security.cert.X509Certificate; |
||||||
|
|
||||||
|
/** |
||||||
|
* An {@link SslStoreProvider} that creates key and trust stores from certificate and |
||||||
|
* private key PEM files. |
||||||
|
* |
||||||
|
* @author Scott Frederick |
||||||
|
* @since 2.7.0 |
||||||
|
*/ |
||||||
|
public final class CertificateFileSslStoreProvider implements SslStoreProvider { |
||||||
|
|
||||||
|
private static final char[] NO_PASSWORD = {}; |
||||||
|
|
||||||
|
private static final String DEFAULT_KEY_ALIAS = "spring-boot-web"; |
||||||
|
|
||||||
|
private final Ssl ssl; |
||||||
|
|
||||||
|
private CertificateFileSslStoreProvider(Ssl ssl) { |
||||||
|
this.ssl = ssl; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public KeyStore getKeyStore() throws Exception { |
||||||
|
return createKeyStore(this.ssl.getCertificate(), this.ssl.getCertificatePrivateKey(), |
||||||
|
this.ssl.getKeyStorePassword(), this.ssl.getKeyStoreType(), this.ssl.getKeyAlias()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public KeyStore getTrustStore() throws Exception { |
||||||
|
if (this.ssl.getTrustCertificate() == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return createKeyStore(this.ssl.getTrustCertificate(), this.ssl.getTrustCertificatePrivateKey(), |
||||||
|
this.ssl.getTrustStorePassword(), this.ssl.getTrustStoreType(), this.ssl.getKeyAlias()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link KeyStore} populated with the certificate stored at the |
||||||
|
* specified file path and an optional private key. |
||||||
|
* @param certPath the path to the certificate authority file |
||||||
|
* @param keyPath the path to the private file |
||||||
|
* @param password the key store password |
||||||
|
* @param storeType the {@code KeyStore} type to create |
||||||
|
* @param keyAlias the alias to use when adding keys to the {@code KeyStore} |
||||||
|
* @return the {@code KeyStore} |
||||||
|
*/ |
||||||
|
private KeyStore createKeyStore(String certPath, String keyPath, String password, String storeType, |
||||||
|
String keyAlias) { |
||||||
|
try { |
||||||
|
KeyStore keyStore = KeyStore.getInstance((storeType != null) ? storeType : KeyStore.getDefaultType()); |
||||||
|
keyStore.load(null); |
||||||
|
X509Certificate[] certificates = CertificateParser.parse(certPath); |
||||||
|
PrivateKey privateKey = (keyPath != null) ? PrivateKeyParser.parse(keyPath) : null; |
||||||
|
try { |
||||||
|
addCertificates(keyStore, certificates, privateKey, password, keyAlias); |
||||||
|
} |
||||||
|
catch (KeyStoreException ex) { |
||||||
|
throw new IllegalStateException("Error adding certificates to KeyStore: " + ex.getMessage(), ex); |
||||||
|
} |
||||||
|
return keyStore; |
||||||
|
} |
||||||
|
catch (GeneralSecurityException | IOException ex) { |
||||||
|
throw new IllegalStateException("Error creating KeyStore: " + ex.getMessage(), ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey, |
||||||
|
String password, String keyAlias) throws KeyStoreException { |
||||||
|
String alias = (keyAlias != null) ? keyAlias : DEFAULT_KEY_ALIAS; |
||||||
|
if (privateKey != null) { |
||||||
|
keyStore.setKeyEntry(alias, privateKey, ((password != null) ? password.toCharArray() : NO_PASSWORD), |
||||||
|
certificates); |
||||||
|
} |
||||||
|
else { |
||||||
|
for (int index = 0; index < certificates.length; index++) { |
||||||
|
keyStore.setCertificateEntry(alias + "-" + index, certificates[index]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a {@link SslStoreProvider} if the appropriate SSL properties are configured. |
||||||
|
* @param ssl the SSL properties |
||||||
|
* @return a {@code SslStoreProvider} or {@code null} |
||||||
|
*/ |
||||||
|
public static SslStoreProvider from(Ssl ssl) { |
||||||
|
if (ssl != null && ssl.isEnabled()) { |
||||||
|
if (ssl.getCertificate() != null && ssl.getCertificatePrivateKey() != null) { |
||||||
|
return new CertificateFileSslStoreProvider(ssl); |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,109 @@ |
|||||||
|
/* |
||||||
|
* 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.server; |
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStreamReader; |
||||||
|
import java.io.Reader; |
||||||
|
import java.net.URL; |
||||||
|
import java.security.cert.CertificateException; |
||||||
|
import java.security.cert.CertificateFactory; |
||||||
|
import java.security.cert.X509Certificate; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.function.Consumer; |
||||||
|
import java.util.regex.Matcher; |
||||||
|
import java.util.regex.Pattern; |
||||||
|
|
||||||
|
import org.springframework.util.Base64Utils; |
||||||
|
import org.springframework.util.FileCopyUtils; |
||||||
|
import org.springframework.util.ResourceUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* Parser for X.509 certificates in PEM format. |
||||||
|
* |
||||||
|
* @author Scott Frederick |
||||||
|
* @author Phillip Webb |
||||||
|
*/ |
||||||
|
final class CertificateParser { |
||||||
|
|
||||||
|
private static final String HEADER = "-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+"; |
||||||
|
|
||||||
|
private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)"; |
||||||
|
|
||||||
|
private static final String FOOTER = "-+END\\s+.*CERTIFICATE[^-]*-+"; |
||||||
|
|
||||||
|
private static final Pattern PATTERN = Pattern.compile(HEADER + BASE64_TEXT + FOOTER, Pattern.CASE_INSENSITIVE); |
||||||
|
|
||||||
|
private CertificateParser() { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Load certificates from the specified resource. |
||||||
|
* @param path the certificate to parse |
||||||
|
* @return the parsed certificates |
||||||
|
*/ |
||||||
|
static X509Certificate[] parse(String path) { |
||||||
|
CertificateFactory factory = getCertificateFactory(); |
||||||
|
List<X509Certificate> certificates = new ArrayList<>(); |
||||||
|
readCertificates(path, factory, certificates::add); |
||||||
|
return certificates.toArray(new X509Certificate[0]); |
||||||
|
} |
||||||
|
|
||||||
|
private static CertificateFactory getCertificateFactory() { |
||||||
|
try { |
||||||
|
return CertificateFactory.getInstance("X.509"); |
||||||
|
} |
||||||
|
catch (CertificateException ex) { |
||||||
|
throw new IllegalStateException("Unable to get X.509 certificate factory", ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static void readCertificates(String resource, CertificateFactory factory, |
||||||
|
Consumer<X509Certificate> consumer) { |
||||||
|
try { |
||||||
|
String text = readText(resource); |
||||||
|
Matcher matcher = PATTERN.matcher(text); |
||||||
|
while (matcher.find()) { |
||||||
|
String encodedText = matcher.group(1); |
||||||
|
byte[] decodedBytes = decodeBase64(encodedText); |
||||||
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(decodedBytes); |
||||||
|
while (inputStream.available() > 0) { |
||||||
|
consumer.accept((X509Certificate) factory.generateCertificate(inputStream)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
catch (CertificateException | IOException ex) { |
||||||
|
throw new IllegalStateException("Error reading certificate from '" + resource + "' : " + ex.getMessage(), |
||||||
|
ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static String readText(String resource) throws IOException { |
||||||
|
URL url = ResourceUtils.getURL(resource); |
||||||
|
try (Reader reader = new InputStreamReader(url.openStream())) { |
||||||
|
return FileCopyUtils.copyToString(reader); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static byte[] decodeBase64(String content) { |
||||||
|
byte[] bytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes(); |
||||||
|
return Base64Utils.decode(bytes); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,149 @@ |
|||||||
|
/* |
||||||
|
* 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.server; |
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStreamReader; |
||||||
|
import java.io.Reader; |
||||||
|
import java.net.URL; |
||||||
|
import java.security.GeneralSecurityException; |
||||||
|
import java.security.KeyFactory; |
||||||
|
import java.security.PrivateKey; |
||||||
|
import java.security.spec.InvalidKeySpecException; |
||||||
|
import java.security.spec.PKCS8EncodedKeySpec; |
||||||
|
import java.util.regex.Matcher; |
||||||
|
import java.util.regex.Pattern; |
||||||
|
|
||||||
|
import org.springframework.util.Base64Utils; |
||||||
|
import org.springframework.util.FileCopyUtils; |
||||||
|
import org.springframework.util.ResourceUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* Parser for PKCS private key files in PEM format. |
||||||
|
* |
||||||
|
* @author Scott Frederick |
||||||
|
* @author Phillip Webb |
||||||
|
*/ |
||||||
|
final class PrivateKeyParser { |
||||||
|
|
||||||
|
private static final String PKCS1_HEADER = "-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; |
||||||
|
|
||||||
|
private static final String PKCS1_FOOTER = "-+END\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+"; |
||||||
|
|
||||||
|
private static final String PKCS8_FOOTER = "-+END\\s+PRIVATE\\s+KEY[^-]*-+"; |
||||||
|
|
||||||
|
private static final String PKCS8_HEADER = "-+BEGIN\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; |
||||||
|
|
||||||
|
private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)"; |
||||||
|
|
||||||
|
private static final Pattern PKCS1_PATTERN = Pattern.compile(PKCS1_HEADER + BASE64_TEXT + PKCS1_FOOTER, |
||||||
|
Pattern.CASE_INSENSITIVE); |
||||||
|
|
||||||
|
private static final Pattern PKCS8_KEY_PATTERN = Pattern.compile(PKCS8_HEADER + BASE64_TEXT + PKCS8_FOOTER, |
||||||
|
Pattern.CASE_INSENSITIVE); |
||||||
|
|
||||||
|
private PrivateKeyParser() { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Load a private key from the specified resource. |
||||||
|
* @param resource the private key to parse |
||||||
|
* @return the parsed private key |
||||||
|
*/ |
||||||
|
static PrivateKey parse(String resource) { |
||||||
|
try { |
||||||
|
String text = readText(resource); |
||||||
|
Matcher matcher = PKCS1_PATTERN.matcher(text); |
||||||
|
if (matcher.find()) { |
||||||
|
return parsePkcs1(decodeBase64(matcher.group(1))); |
||||||
|
} |
||||||
|
matcher = PKCS8_KEY_PATTERN.matcher(text); |
||||||
|
if (matcher.find()) { |
||||||
|
return parsePkcs8(decodeBase64(matcher.group(1))); |
||||||
|
} |
||||||
|
throw new IllegalStateException("Unrecognized private key format in " + resource); |
||||||
|
} |
||||||
|
catch (GeneralSecurityException | IOException ex) { |
||||||
|
throw new IllegalStateException("Error loading private key file " + resource, ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static PrivateKey parsePkcs1(byte[] privateKeyBytes) throws GeneralSecurityException { |
||||||
|
byte[] pkcs8Bytes = convertPkcs1ToPkcs8(privateKeyBytes); |
||||||
|
return parsePkcs8(pkcs8Bytes); |
||||||
|
} |
||||||
|
|
||||||
|
private static byte[] convertPkcs1ToPkcs8(byte[] pkcs1) { |
||||||
|
try { |
||||||
|
ByteArrayOutputStream result = new ByteArrayOutputStream(); |
||||||
|
int pkcs1Length = pkcs1.length; |
||||||
|
int totalLength = pkcs1Length + 22; |
||||||
|
// Sequence + total length
|
||||||
|
result.write(bytes(0x30, 0x82)); |
||||||
|
result.write((totalLength >> 8) & 0xff); |
||||||
|
result.write(totalLength & 0xff); |
||||||
|
// Integer (0)
|
||||||
|
result.write(bytes(0x02, 0x01, 0x00)); |
||||||
|
// Sequence: 1.2.840.113549.1.1.1, NULL
|
||||||
|
result.write( |
||||||
|
bytes(0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00)); |
||||||
|
// Octet string + length
|
||||||
|
result.write(bytes(0x04, 0x82)); |
||||||
|
result.write((pkcs1Length >> 8) & 0xff); |
||||||
|
result.write(pkcs1Length & 0xff); |
||||||
|
// PKCS1
|
||||||
|
result.write(pkcs1); |
||||||
|
return result.toByteArray(); |
||||||
|
} |
||||||
|
catch (IOException ex) { |
||||||
|
throw new IllegalStateException(ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static byte[] bytes(int... elements) { |
||||||
|
byte[] result = new byte[elements.length]; |
||||||
|
for (int i = 0; i < elements.length; i++) { |
||||||
|
result[i] = (byte) elements[i]; |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private static PrivateKey parsePkcs8(byte[] privateKeyBytes) throws GeneralSecurityException { |
||||||
|
try { |
||||||
|
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); |
||||||
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA"); |
||||||
|
return keyFactory.generatePrivate(keySpec); |
||||||
|
} |
||||||
|
catch (InvalidKeySpecException ex) { |
||||||
|
throw new IllegalArgumentException("Unexpected key format", ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static String readText(String resource) throws IOException { |
||||||
|
URL url = ResourceUtils.getURL(resource); |
||||||
|
try (Reader reader = new InputStreamReader(url.openStream())) { |
||||||
|
return FileCopyUtils.copyToString(reader); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static byte[] decodeBase64(String content) { |
||||||
|
byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes(); |
||||||
|
return Base64Utils.decode(contentBytes); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,133 @@ |
|||||||
|
/* |
||||||
|
* 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.server; |
||||||
|
|
||||||
|
import java.security.KeyStore; |
||||||
|
import java.security.KeyStoreException; |
||||||
|
import java.security.NoSuchAlgorithmException; |
||||||
|
import java.security.UnrecoverableKeyException; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link CertificateFileSslStoreProvider}. |
||||||
|
* |
||||||
|
* @author Scott Frederick |
||||||
|
*/ |
||||||
|
class CertificateFileSslStoreProviderTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
void fromSslWhenNullReturnsNull() { |
||||||
|
assertThat(CertificateFileSslStoreProvider.from(null)).isNull(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void fromSslWhenDisabledReturnsNull() { |
||||||
|
assertThat(CertificateFileSslStoreProvider.from(new Ssl())).isNull(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void fromSslWithCertAndKeyReturnsStoreProvider() throws Exception { |
||||||
|
Ssl ssl = new Ssl(); |
||||||
|
ssl.setEnabled(true); |
||||||
|
ssl.setCertificate("classpath:test-cert.pem"); |
||||||
|
ssl.setCertificatePrivateKey("classpath:test-key.pem"); |
||||||
|
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl); |
||||||
|
assertThat(storeProvider).isNotNull(); |
||||||
|
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "spring-boot-web"); |
||||||
|
assertThat(storeProvider.getTrustStore()).isNull(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void fromSslWithCertAndKeyAndTrustCertReturnsStoreProvider() throws Exception { |
||||||
|
Ssl ssl = new Ssl(); |
||||||
|
ssl.setEnabled(true); |
||||||
|
ssl.setCertificate("classpath:test-cert.pem"); |
||||||
|
ssl.setCertificatePrivateKey("classpath:test-key.pem"); |
||||||
|
ssl.setTrustCertificate("classpath:test-cert.pem"); |
||||||
|
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl); |
||||||
|
assertThat(storeProvider).isNotNull(); |
||||||
|
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "spring-boot-web"); |
||||||
|
assertStoreContainsCert(storeProvider.getTrustStore(), KeyStore.getDefaultType(), "spring-boot-web-0"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void fromSslWithCertAndKeyAndTrustCertAndTrustKeyReturnsStoreProvider() throws Exception { |
||||||
|
Ssl ssl = new Ssl(); |
||||||
|
ssl.setEnabled(true); |
||||||
|
ssl.setCertificate("classpath:test-cert.pem"); |
||||||
|
ssl.setCertificatePrivateKey("classpath:test-key.pem"); |
||||||
|
ssl.setTrustCertificate("classpath:test-cert.pem"); |
||||||
|
ssl.setTrustCertificatePrivateKey("classpath:test-key.pem"); |
||||||
|
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl); |
||||||
|
assertThat(storeProvider).isNotNull(); |
||||||
|
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "spring-boot-web"); |
||||||
|
assertStoreContainsCertAndKey(storeProvider.getTrustStore(), KeyStore.getDefaultType(), "spring-boot-web"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void fromSslWithKeyAliasReturnsStoreProvider() throws Exception { |
||||||
|
Ssl ssl = new Ssl(); |
||||||
|
ssl.setEnabled(true); |
||||||
|
ssl.setKeyAlias("test-alias"); |
||||||
|
ssl.setCertificate("classpath:test-cert.pem"); |
||||||
|
ssl.setCertificatePrivateKey("classpath:test-key.pem"); |
||||||
|
ssl.setTrustCertificate("classpath:test-cert.pem"); |
||||||
|
ssl.setTrustCertificatePrivateKey("classpath:test-key.pem"); |
||||||
|
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl); |
||||||
|
assertThat(storeProvider).isNotNull(); |
||||||
|
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "test-alias"); |
||||||
|
assertStoreContainsCertAndKey(storeProvider.getTrustStore(), KeyStore.getDefaultType(), "test-alias"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void fromSslWithStoreTypeReturnsStoreProvider() throws Exception { |
||||||
|
Ssl ssl = new Ssl(); |
||||||
|
ssl.setEnabled(true); |
||||||
|
ssl.setKeyStoreType("PKCS12"); |
||||||
|
ssl.setTrustStoreType("PKCS12"); |
||||||
|
ssl.setCertificate("classpath:test-cert.pem"); |
||||||
|
ssl.setCertificatePrivateKey("classpath:test-key.pem"); |
||||||
|
ssl.setTrustCertificate("classpath:test-cert.pem"); |
||||||
|
ssl.setTrustCertificatePrivateKey("classpath:test-key.pem"); |
||||||
|
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl); |
||||||
|
assertThat(storeProvider).isNotNull(); |
||||||
|
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), "PKCS12", "spring-boot-web"); |
||||||
|
assertStoreContainsCertAndKey(storeProvider.getTrustStore(), "PKCS12", "spring-boot-web"); |
||||||
|
} |
||||||
|
|
||||||
|
private void assertStoreContainsCertAndKey(KeyStore keyStore, String keyStoreType, String keyAlias) |
||||||
|
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { |
||||||
|
assertThat(keyStore).isNotNull(); |
||||||
|
assertThat(keyStore.getType()).isEqualTo(keyStoreType); |
||||||
|
assertThat(keyStore.containsAlias(keyAlias)).isTrue(); |
||||||
|
assertThat(keyStore.getCertificate(keyAlias)).isNotNull(); |
||||||
|
assertThat(keyStore.getKey(keyAlias, new char[] {})).isNotNull(); |
||||||
|
} |
||||||
|
|
||||||
|
private void assertStoreContainsCert(KeyStore keyStore, String keyStoreType, String keyAlias) |
||||||
|
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { |
||||||
|
assertThat(keyStore).isNotNull(); |
||||||
|
assertThat(keyStore.getType()).isEqualTo(keyStoreType); |
||||||
|
assertThat(keyStore.containsAlias(keyAlias)).isTrue(); |
||||||
|
assertThat(keyStore.getCertificate(keyAlias)).isNotNull(); |
||||||
|
assertThat(keyStore.getKey(keyAlias, new char[] {})).isNull(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,57 @@ |
|||||||
|
/* |
||||||
|
* 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.server; |
||||||
|
|
||||||
|
import java.security.cert.X509Certificate; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link CertificateParser}. |
||||||
|
* |
||||||
|
* @author Scott Frederick |
||||||
|
*/ |
||||||
|
class CertificateParserTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
void parseCertificate() { |
||||||
|
X509Certificate[] certificates = CertificateParser.parse("classpath:test-cert.pem"); |
||||||
|
assertThat(certificates).isNotNull(); |
||||||
|
assertThat(certificates.length).isEqualTo(1); |
||||||
|
assertThat(certificates[0].getType()).isEqualTo("X.509"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void parseCertificateChain() { |
||||||
|
X509Certificate[] certificates = CertificateParser.parse("classpath:test-cert-chain.pem"); |
||||||
|
assertThat(certificates).isNotNull(); |
||||||
|
assertThat(certificates.length).isEqualTo(2); |
||||||
|
assertThat(certificates[0].getType()).isEqualTo("X.509"); |
||||||
|
assertThat(certificates[1].getType()).isEqualTo("X.509"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void parseWithInvalidPathWillThrowException() { |
||||||
|
String path = "file:///bad/path/cert.pem"; |
||||||
|
assertThatIllegalStateException().isThrownBy(() -> CertificateParser.parse("file:///bad/path/cert.pem")) |
||||||
|
.withMessageContaining(path); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
/* |
||||||
|
* 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.server; |
||||||
|
|
||||||
|
import java.security.PrivateKey; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link PrivateKeyParser}. |
||||||
|
* |
||||||
|
* @author Scott Frederick |
||||||
|
*/ |
||||||
|
class PrivateKeyParserTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
void parsePkcs8KeyFile() { |
||||||
|
PrivateKey privateKey = PrivateKeyParser.parse("classpath:test-key.pem"); |
||||||
|
assertThat(privateKey).isNotNull(); |
||||||
|
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void parseWithNonKeyFileWillThrowException() { |
||||||
|
String path = "classpath:test-banner.txt"; |
||||||
|
assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse("file://" + path)) |
||||||
|
.withMessageContaining(path); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void parseWithInvalidPathWillThrowException() { |
||||||
|
String path = "file:///bad/path/key.pem"; |
||||||
|
assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(path)).withMessageContaining(path); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
-----BEGIN TRUSTED CERTIFICATE----- |
||||||
|
MIIClzCCAgACCQCPbjkRoMVEQDANBgkqhkiG9w0BAQUFADCBjzELMAkGA1UEBhMC |
||||||
|
VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28x |
||||||
|
DTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3QxFDASBgNVBAMMC2V4YW1wbGUu |
||||||
|
Y29tMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMB4XDTIwMDMyNzIx |
||||||
|
NTgwNFoXDTIxMDMyNzIxNTgwNFowgY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD |
||||||
|
YWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKDARUZXN0 |
||||||
|
MQ0wCwYDVQQLDARUZXN0MRQwEgYDVQQDDAtleGFtcGxlLmNvbTEfMB0GCSqGSIb3 |
||||||
|
DQEJARYQdGVzdEBleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC |
||||||
|
gYEA1YzixWEoyzrd20C2R1gjyPCoPfFLlG6UYTyT0tueNy6yjv6qbJ8lcZg7616O |
||||||
|
3I9LuOHhZh9U+fCDCgPfiDdyJfDEW/P+dsOMFyMUXPrJPze2yPpOnvV8iJ5DM93u |
||||||
|
fEVhCCyzLdYu0P2P3hU2W+T3/Im9DA7FOPA2vF1SrIJ2qtUCAwEAATANBgkqhkiG |
||||||
|
9w0BAQUFAAOBgQBdShkwUv78vkn1jAdtfbB+7mpV9tufVdo29j7pmotTCz3ny5fc |
||||||
|
zLEfeu6JPugAR71JYbc2CqGrMneSk1zT91EH6ohIz8OR5VNvzB7N7q65Ci7OFMPl |
||||||
|
ly6k3rHpMCBtHoyNFhNVfPLxGJ9VlWFKLgIAbCmL4OIQm1l6Fr1MSM38Zw== |
||||||
|
-----END TRUSTED CERTIFICATE----- |
||||||
|
-----BEGIN CERTIFICATE----- |
||||||
|
MIICjzCCAfgCAQEwDQYJKoZIhvcNAQEFBQAwgY8xCzAJBgNVBAYTAlVTMRMwEQYD |
||||||
|
VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQK |
||||||
|
DARUZXN0MQ0wCwYDVQQLDARUZXN0MRQwEgYDVQQDDAtleGFtcGxlLmNvbTEfMB0G |
||||||
|
CSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTAeFw0yMDAzMjcyMjAxNDZaFw0y |
||||||
|
MTAzMjcyMjAxNDZaMIGPMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5p |
||||||
|
YTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwEVGVzdDENMAsGA1UE |
||||||
|
CwwEVGVzdDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xHzAdBgkqhkiG9w0BCQEWEHRl |
||||||
|
c3RAZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM7kd2cj |
||||||
|
F49wm1+OQ7Q5GE96cXueWNPr/Nwei71tf6G4BmE0B+suXHEvnLpHTj9pdX/ZzBIK |
||||||
|
8jIZ/x8RnSduK/Ky+zm1QMYUWZtWCAgCW8WzgB69Cn/hQG8KSX3S9bqODuQAvP54 |
||||||
|
GQJD7+4kVuNBGjFb4DaD4nvMmPtALSZf8ZCZAgMBAAEwDQYJKoZIhvcNAQEFBQAD |
||||||
|
gYEAOn6X8+0VVlDjF+TvTgI0KIasA6nDm+KXe7LVtfvqWqQZH4qyd2uiwcDM3Aux |
||||||
|
a/OsPdOw0j+NqFDBd3mSMhSVgfvXdK6j9WaxY1VGXyaidLARgvn63wfzgr857sQW |
||||||
|
c8eSxbwEQxwlMvVxW6Os4VhCfUQr8VrBrvPa2zs+6IlK+Ug= |
||||||
|
-----END CERTIFICATE----- |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
-----BEGIN CERTIFICATE----- |
||||||
|
MIICpDCCAYwCCQCDOqHKPjAhCTANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDDAls |
||||||
|
b2NhbGhvc3QwHhcNMTQwOTEwMjE0MzA1WhcNMTQxMDEwMjE0MzA1WjAUMRIwEAYD |
||||||
|
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDR |
||||||
|
0KfxUw7MF/8RB5/YXOM7yLnoHYb/M/6dyoulMbtEdKKhQhU28o5FiDkHcEG9PJQL |
||||||
|
gqrRgAjl3VmCC9omtfZJQ2EpfkTttkJjnKOOroXhYE51/CYSckapBYCVh8GkjUEJ |
||||||
|
uEfnp07cTfYZFqViIgIWPZyjkzl3w4girS7kCuzNdDntVJVx5F/EsFwMA8n3C0Qa |
||||||
|
zHQoM5s00Fer6aTwd6AW0JD5QkADavpfzZ554e4HrVGwHlM28WKQQkFzzGu44FFX |
||||||
|
yVuEF3HeyVPug8GRHAc8UU7ijVgJB5TmbvRGYowIErD5i4VvGLuOv9mgR3aVyN0S |
||||||
|
dJ1N7aJnXpeSQjAgf03jAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAE4yvwhbPldg |
||||||
|
Bpl7sBw/m2B3bfiNeSqa4tII1PQ7ysgWVb9HbFNKkriScwDWlqo6ljZfJ+SDFCoj |
||||||
|
bQz4fOFdMAOzRnpTrG2NAKMoJLY0/g/p7XO00PiC8T3h3BOJ5SHuW3gUyfGXmAYs |
||||||
|
DnJxJOrwPzj57xvNXjNSbDOJ3DRfCbB0CWBexOeGDiUokoEq3Gnz04Q4ZfHyAcpZ |
||||||
|
3deMw8Od5p9WAoCh3oClpFyOSzXYKZd+3ppMMtfc4wnbfocnfSFxj0UCpOEJw4Ez |
||||||
|
+lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO |
||||||
|
32C9XWHwRA4= |
||||||
|
-----END CERTIFICATE----- |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
-----BEGIN PRIVATE KEY----- |
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDR0KfxUw7MF/8R |
||||||
|
B5/YXOM7yLnoHYb/M/6dyoulMbtEdKKhQhU28o5FiDkHcEG9PJQLgqrRgAjl3VmC |
||||||
|
C9omtfZJQ2EpfkTttkJjnKOOroXhYE51/CYSckapBYCVh8GkjUEJuEfnp07cTfYZ |
||||||
|
FqViIgIWPZyjkzl3w4girS7kCuzNdDntVJVx5F/EsFwMA8n3C0QazHQoM5s00Fer |
||||||
|
6aTwd6AW0JD5QkADavpfzZ554e4HrVGwHlM28WKQQkFzzGu44FFXyVuEF3HeyVPu |
||||||
|
g8GRHAc8UU7ijVgJB5TmbvRGYowIErD5i4VvGLuOv9mgR3aVyN0SdJ1N7aJnXpeS |
||||||
|
QjAgf03jAgMBAAECggEBAIhQyzwj3WJGWOZkkLqOpufJotcmj/Wwf0VfOdkq9WMl |
||||||
|
cB/bAlN/xWVxerPVgDCFch4EWBzi1WUaqbOvJZ2u7QNubmr56aiTmJCFTVI/GyZx |
||||||
|
XqiTGN01N6lKtN7xo6LYTyAUhUsBTWAemrx0FSErvTVb9C/mUBj6hbEZ2XQ5kN5t |
||||||
|
7qYX4Lu0zyn7s1kX5SLtm5I+YRq7HSwB6wLy+DSroO71izZ/VPwME3SwT5SN+c87 |
||||||
|
3dkklR7fumNd9dOpSWKrLPnq4aMko00rvIGc63xD1HrEpXUkB5v24YEn7HwCLEH7 |
||||||
|
b8jrp79j2nCvvR47inpf+BR8FIWAHEOUUqCEzjQkdiECgYEA6ifjMM0f02KPeIs7 |
||||||
|
zXd1lI7CUmJmzkcklCIpEbKWf/t/PHv3QgqIkJzERzRaJ8b+GhQ4zrSwAhrGUmI8 |
||||||
|
kDkXIqe2/2ONgIOX2UOHYHyTDQZHnlXyDecvHUTqs2JQZCGBZkXyZ9i0j3BnTymC |
||||||
|
iZ8DvEa0nxsbP+U3rgzPQmXiQVMCgYEA5WN2Y/RndbriNsNrsHYRldbPO5nfV9rp |
||||||
|
cDzcQU66HRdK5VIdbXT9tlMYCJIZsSqE0tkOwTgEB/sFvF/tIHSCY5iO6hpIyk6g |
||||||
|
kkUzPcld4eM0dEPAge7SYUbakB9CMvA7MkDQSXQNFyZ0mH83+UikwT6uYHFh7+ox |
||||||
|
N1P+psDhXzECgYEA1gXLVQnIcy/9LxMkgDMWV8j8uMyUZysDthpbK3/uq+A2dhRg |
||||||
|
9g4msPd5OBQT65OpIjElk1n4HpRWfWqpLLHiAZ0GWPynk7W0D7P3gyuaRSdeQs0P |
||||||
|
x8FtgPVDCN9t13gAjHiWjnC26Py2kNbCKAQeJ/MAmQTvrUFX2VCACJKTcV0CgYAj |
||||||
|
xJWSUmrLfb+GQISLOG3Xim434e9keJsLyEGj4U29+YLRLTOvfJ2PD3fg5j8hU/rw |
||||||
|
Ea5uTHi8cdTcIa0M8X3fX8txD3YoLYh2JlouGTcNYOst8d6TpBSj3HN6I5Wj8beZ |
||||||
|
R2fy/CiKYpGtsbCdq0kdZNO18BgQW9kewncjs1GxEQKBgQCf8q34h6KuHpHSDh9h |
||||||
|
YkDTypk0FReWBAVJCzDNDUMhVLFivjcwtaMd2LiC3FMKZYodr52iKg60cj43vbYI |
||||||
|
frmFFxoL37rTmUocCTBKc0LhWj6MicI+rcvQYe1uwTrpWdFf1aZJMYRLRczeKtev |
||||||
|
OWaE/9hVZ5+9pild1NukGpOydw== |
||||||
|
-----END PRIVATE KEY----- |
||||||
Loading…
Reference in new issue