Skip to content

Commit 07c5757

Browse files
Merge branch '3.1.x'
2 parents d0d7107 + 767ec4e commit 07c5757

File tree

12 files changed

+355
-17
lines changed

12 files changed

+355
-17
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ public static class Store {
6666
*/
6767
String privateKey;
6868

69+
/**
70+
* Password used to decrypt an encrypted private key.
71+
*/
72+
String privateKeyPassword;
73+
6974
public String getType() {
7075
return this.type;
7176
}
@@ -90,6 +95,14 @@ public void setPrivateKey(String privateKey) {
9095
this.privateKey = privateKey;
9196
}
9297

98+
public String getPrivateKeyPassword() {
99+
return this.privateKeyPassword;
100+
}
101+
102+
public void setPrivateKeyPassword(String privateKeyPassword) {
103+
this.privateKeyPassword = privateKeyPassword;
104+
}
105+
93106
}
94107

95108
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties
113113
}
114114

115115
private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) {
116-
return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey());
116+
return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey(),
117+
properties.getPrivateKeyPassword());
117118
}
118119

119120
private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.ssl;
18+
19+
import java.util.Set;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.boot.ssl.SslBundle;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
/**
28+
* Tests for {@link PropertiesSslBundle}.
29+
*
30+
* @author Scott Frederick
31+
*/
32+
class PropertiesSslBundleTests {
33+
34+
@Test
35+
void pemPropertiesAreMappedToSslBundle() {
36+
PemSslBundleProperties properties = new PemSslBundleProperties();
37+
properties.getKey().setAlias("alias");
38+
properties.getKey().setPassword("secret");
39+
properties.getOptions().setCiphers(Set.of("cipher1", "cipher2", "cipher3"));
40+
properties.getOptions().setEnabledProtocols(Set.of("protocol1", "protocol2"));
41+
properties.getKeystore().setCertificate("cert1.pem");
42+
properties.getKeystore().setPrivateKey("key1.pem");
43+
properties.getKeystore().setPrivateKeyPassword("keysecret1");
44+
properties.getKeystore().setType("PKCS12");
45+
properties.getTruststore().setCertificate("cert2.pem");
46+
properties.getTruststore().setPrivateKey("key2.pem");
47+
properties.getTruststore().setPrivateKeyPassword("keysecret2");
48+
properties.getTruststore().setType("JKS");
49+
SslBundle sslBundle = PropertiesSslBundle.get(properties);
50+
assertThat(sslBundle.getKey().getAlias()).isEqualTo("alias");
51+
assertThat(sslBundle.getKey().getPassword()).isEqualTo("secret");
52+
assertThat(sslBundle.getOptions().getCiphers()).containsExactlyInAnyOrder("cipher1", "cipher2", "cipher3");
53+
assertThat(sslBundle.getOptions().getEnabledProtocols()).containsExactlyInAnyOrder("protocol1", "protocol2");
54+
assertThat(sslBundle.getStores()).isNotNull();
55+
assertThat(sslBundle.getStores()).extracting("keyStoreDetails")
56+
.extracting("certificate", "privateKey", "privateKeyPassword", "type")
57+
.containsExactly("cert1.pem", "key1.pem", "keysecret1", "PKCS12");
58+
assertThat(sslBundle.getStores()).extracting("trustStoreDetails")
59+
.extracting("certificate", "privateKey", "privateKeyPassword", "type")
60+
.containsExactly("cert2.pem", "key2.pem", "keysecret2", "JKS");
61+
}
62+
63+
@Test
64+
void jksPropertiesAreMappedToSslBundle() {
65+
JksSslBundleProperties properties = new JksSslBundleProperties();
66+
properties.getKey().setAlias("alias");
67+
properties.getKey().setPassword("secret");
68+
properties.getOptions().setCiphers(Set.of("cipher1", "cipher2", "cipher3"));
69+
properties.getOptions().setEnabledProtocols(Set.of("protocol1", "protocol2"));
70+
properties.getKeystore().setLocation("cert1.p12");
71+
properties.getKeystore().setPassword("secret1");
72+
properties.getKeystore().setProvider("provider1");
73+
properties.getKeystore().setType("JKS");
74+
properties.getTruststore().setLocation("cert2.jks");
75+
properties.getTruststore().setPassword("secret2");
76+
properties.getTruststore().setProvider("provider2");
77+
properties.getTruststore().setType("PKCS12");
78+
SslBundle sslBundle = PropertiesSslBundle.get(properties);
79+
assertThat(sslBundle.getKey().getAlias()).isEqualTo("alias");
80+
assertThat(sslBundle.getKey().getPassword()).isEqualTo("secret");
81+
assertThat(sslBundle.getOptions().getCiphers()).containsExactlyInAnyOrder("cipher1", "cipher2", "cipher3");
82+
assertThat(sslBundle.getOptions().getEnabledProtocols()).containsExactlyInAnyOrder("protocol1", "protocol2");
83+
assertThat(sslBundle.getStores()).isNotNull();
84+
assertThat(sslBundle.getStores()).extracting("keyStoreDetails")
85+
.extracting("location", "password", "provider", "type")
86+
.containsExactly("cert1.p12", "secret1", "provider1", "JKS");
87+
assertThat(sslBundle.getStores()).extracting("trustStoreDetails")
88+
.extracting("location", "password", "provider", "type")
89+
.containsExactly("cert2.jks", "secret2", "provider2", "PKCS12");
90+
}
91+
92+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.ByteArrayOutputStream;
2020
import java.io.IOException;
21+
import java.security.AlgorithmParameters;
2122
import java.security.GeneralSecurityException;
2223
import java.security.KeyFactory;
2324
import java.security.PrivateKey;
@@ -27,10 +28,18 @@
2728
import java.util.Base64;
2829
import java.util.Collections;
2930
import java.util.List;
30-
import java.util.function.Function;
31+
import java.util.function.BiFunction;
3132
import java.util.regex.Matcher;
3233
import java.util.regex.Pattern;
3334

35+
import javax.crypto.Cipher;
36+
import javax.crypto.EncryptedPrivateKeyInfo;
37+
import javax.crypto.SecretKey;
38+
import javax.crypto.SecretKeyFactory;
39+
import javax.crypto.spec.PBEKeySpec;
40+
41+
import org.springframework.util.Assert;
42+
3443
/**
3544
* Parser for PKCS private key files in PEM format.
3645
*
@@ -48,18 +57,27 @@ final class PemPrivateKeyParser {
4857

4958
private static final String PKCS8_FOOTER = "-+END\\s+PRIVATE\\s+KEY[^-]*-+";
5059

60+
private static final String PKCS8_ENCRYPTED_HEADER = "-+BEGIN\\s+ENCRYPTED\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
61+
62+
private static final String PKCS8_ENCRYPTED_FOOTER = "-+END\\s+ENCRYPTED\\s+PRIVATE\\s+KEY[^-]*-+";
63+
5164
private static final String EC_HEADER = "-+BEGIN\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
5265

5366
private static final String EC_FOOTER = "-+END\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+";
5467

5568
private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)";
5669

70+
public static final int BASE64_TEXT_GROUP = 1;
71+
5772
private static final List<PemParser> PEM_PARSERS;
5873
static {
5974
List<PemParser> parsers = new ArrayList<>();
6075
parsers.add(new PemParser(PKCS1_HEADER, PKCS1_FOOTER, PemPrivateKeyParser::createKeySpecForPkcs1, "RSA"));
6176
parsers.add(new PemParser(EC_HEADER, EC_FOOTER, PemPrivateKeyParser::createKeySpecForEc, "EC"));
62-
parsers.add(new PemParser(PKCS8_HEADER, PKCS8_FOOTER, PKCS8EncodedKeySpec::new, "RSA", "EC", "DSA", "Ed25519"));
77+
parsers.add(new PemParser(PKCS8_HEADER, PKCS8_FOOTER, PemPrivateKeyParser::createKeySpecForPkcs8, "RSA", "EC",
78+
"DSA", "Ed25519"));
79+
parsers.add(new PemParser(PKCS8_ENCRYPTED_HEADER, PKCS8_ENCRYPTED_FOOTER,
80+
PemPrivateKeyParser::createKeySpecForPkcs8Encrypted, "RSA", "EC", "DSA", "Ed25519"));
6381
PEM_PARSERS = Collections.unmodifiableList(parsers);
6482
}
6583

@@ -81,11 +99,11 @@ final class PemPrivateKeyParser {
8199
private PemPrivateKeyParser() {
82100
}
83101

84-
private static PKCS8EncodedKeySpec createKeySpecForPkcs1(byte[] bytes) {
102+
private static PKCS8EncodedKeySpec createKeySpecForPkcs1(byte[] bytes, String password) {
85103
return createKeySpecForAlgorithm(bytes, RSA_ALGORITHM, null);
86104
}
87105

88-
private static PKCS8EncodedKeySpec createKeySpecForEc(byte[] bytes) {
106+
private static PKCS8EncodedKeySpec createKeySpecForEc(byte[] bytes, String password) {
89107
return createKeySpecForAlgorithm(bytes, EC_ALGORITHM, EC_PARAMETERS);
90108
}
91109

@@ -106,18 +124,37 @@ private static PKCS8EncodedKeySpec createKeySpecForAlgorithm(byte[] bytes, int[]
106124
}
107125
}
108126

127+
private static PKCS8EncodedKeySpec createKeySpecForPkcs8(byte[] bytes, String password) {
128+
return new PKCS8EncodedKeySpec(bytes);
129+
}
130+
131+
private static PKCS8EncodedKeySpec createKeySpecForPkcs8Encrypted(byte[] bytes, String password) {
132+
return Pkcs8PrivateKeyDecryptor.decrypt(bytes, password);
133+
}
134+
109135
/**
110136
* Parse a private key from the specified string.
111137
* @param key the private key to parse
112138
* @return the parsed private key
113139
*/
114140
static PrivateKey parse(String key) {
141+
return parse(key, null);
142+
}
143+
144+
/**
145+
* Parse a private key from the specified string, using the provided password for
146+
* decryption if necessary.
147+
* @param key the private key to parse
148+
* @param password the password used to decrypt an encrypted private key
149+
* @return the parsed private key
150+
*/
151+
static PrivateKey parse(String key, String password) {
115152
if (key == null) {
116153
return null;
117154
}
118155
try {
119156
for (PemParser pemParser : PEM_PARSERS) {
120-
PrivateKey privateKey = pemParser.parse(key);
157+
PrivateKey privateKey = pemParser.parse(key, password);
121158
if (privateKey != null) {
122159
return privateKey;
123160
}
@@ -136,30 +173,30 @@ private static class PemParser {
136173

137174
private final Pattern pattern;
138175

139-
private final Function<byte[], PKCS8EncodedKeySpec> keySpecFactory;
176+
private final BiFunction<byte[], String, PKCS8EncodedKeySpec> keySpecFactory;
140177

141178
private final String[] algorithms;
142179

143-
PemParser(String header, String footer, Function<byte[], PKCS8EncodedKeySpec> keySpecFactory,
180+
PemParser(String header, String footer, BiFunction<byte[], String, PKCS8EncodedKeySpec> keySpecFactory,
144181
String... algorithms) {
145182
this.pattern = Pattern.compile(header + BASE64_TEXT + footer, Pattern.CASE_INSENSITIVE);
146-
this.algorithms = algorithms;
147183
this.keySpecFactory = keySpecFactory;
184+
this.algorithms = algorithms;
148185
}
149186

150-
PrivateKey parse(String text) {
187+
PrivateKey parse(String text, String password) {
151188
Matcher matcher = this.pattern.matcher(text);
152-
return (!matcher.find()) ? null : parse(decodeBase64(matcher.group(1)));
189+
return (!matcher.find()) ? null : parse(decodeBase64(matcher.group(BASE64_TEXT_GROUP)), password);
153190
}
154191

155192
private static byte[] decodeBase64(String content) {
156193
byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes();
157194
return Base64.getDecoder().decode(contentBytes);
158195
}
159196

160-
private PrivateKey parse(byte[] bytes) {
197+
private PrivateKey parse(byte[] bytes, String password) {
161198
try {
162-
PKCS8EncodedKeySpec keySpec = this.keySpecFactory.apply(bytes);
199+
PKCS8EncodedKeySpec keySpec = this.keySpecFactory.apply(bytes, password);
163200
for (String algorithm : this.algorithms) {
164201
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
165202
try {
@@ -251,4 +288,34 @@ byte[] toByteArray() {
251288

252289
}
253290

291+
static class Pkcs8PrivateKeyDecryptor {
292+
293+
public static final String PBES2_ALGORITHM = "PBES2";
294+
295+
static PKCS8EncodedKeySpec decrypt(byte[] bytes, String password) {
296+
Assert.notNull(password, "Password is required for an encrypted private key");
297+
try {
298+
EncryptedPrivateKeyInfo keyInfo = new EncryptedPrivateKeyInfo(bytes);
299+
AlgorithmParameters algorithmParameters = keyInfo.getAlgParameters();
300+
String encryptionAlgorithm = getEncryptionAlgorithm(algorithmParameters, keyInfo.getAlgName());
301+
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptionAlgorithm);
302+
SecretKey key = keyFactory.generateSecret(new PBEKeySpec(password.toCharArray()));
303+
Cipher cipher = Cipher.getInstance(encryptionAlgorithm);
304+
cipher.init(Cipher.DECRYPT_MODE, key, algorithmParameters);
305+
return keyInfo.getKeySpec(cipher);
306+
}
307+
catch (IOException | GeneralSecurityException ex) {
308+
throw new IllegalArgumentException("Error decrypting private key", ex);
309+
}
310+
}
311+
312+
private static String getEncryptionAlgorithm(AlgorithmParameters algParameters, String algName) {
313+
if (algParameters != null && PBES2_ALGORITHM.equals(algName)) {
314+
return algParameters.toString();
315+
}
316+
return algName;
317+
}
318+
319+
}
320+
254321
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,14 @@ private KeyStore createKeyStore(String name, PemSslStoreDetails details) {
8484
return null;
8585
}
8686
try {
87-
Assert.notNull(details.certificate(), "CertificateContent must not be null");
87+
Assert.notNull(details.certificate(), "Certificate content must not be null");
8888
String type = (!StringUtils.hasText(details.type())) ? KeyStore.getDefaultType() : details.type();
8989
KeyStore store = KeyStore.getInstance(type);
9090
store.load(null);
9191
String certificateContent = PemContent.load(details.certificate());
9292
String privateKeyContent = PemContent.load(details.privateKey());
9393
X509Certificate[] certificates = PemCertificateParser.parse(certificateContent);
94-
PrivateKey privateKey = PemPrivateKeyParser.parse(privateKeyContent);
94+
PrivateKey privateKey = PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword());
9595
addCertificates(store, certificates, privateKey);
9696
return store;
9797
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,33 @@
3030
* that can be loaded by {@link ResourceUtils#getURL})
3131
* @param privateKey the private key content (either the PEM content itself or something
3232
* that can be loaded by {@link ResourceUtils#getURL})
33+
* @param privateKeyPassword a password used to decrypt an encrypted private key
3334
* @author Scott Frederick
3435
* @author Phillip Webb
3536
* @since 3.1.0
3637
*/
37-
public record PemSslStoreDetails(String type, String certificate, String privateKey) {
38+
public record PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword) {
39+
40+
public PemSslStoreDetails(String type, String certificate, String privateKey) {
41+
this(type, certificate, privateKey, null);
42+
}
3843

3944
/**
4045
* Return a new {@link PemSslStoreDetails} instance with a new private key.
4146
* @param privateKey the new private key
4247
* @return a new {@link PemSslStoreDetails} instance
4348
*/
4449
public PemSslStoreDetails withPrivateKey(String privateKey) {
45-
return new PemSslStoreDetails(this.type, this.certificate, privateKey);
50+
return new PemSslStoreDetails(this.type, this.certificate, privateKey, this.privateKeyPassword);
51+
}
52+
53+
/**
54+
* Return a new {@link PemSslStoreDetails} instance with a new private key password.
55+
* @param password the new private key password
56+
* @return a new {@link PemSslStoreDetails} instance
57+
*/
58+
public PemSslStoreDetails withPrivateKeyPassword(String password) {
59+
return new PemSslStoreDetails(this.type, this.certificate, this.privateKey, password);
4660
}
4761

4862
boolean isEmpty() {

0 commit comments

Comments
 (0)