Skip to content

Commit 32e6ce2

Browse files
committed
Allow PemPrivateKeyParser to parse multiple keys
Update `PemPrivateKeyParser` so that it can parse multiple keys in a single PEM file. Closes gh-37970
1 parent deb7942 commit 32e6ce2

File tree

3 files changed

+47
-29
lines changed

3 files changed

+47
-29
lines changed

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ final class PemPrivateKeyParser {
6969

7070
private static final String SEC1_EC_FOOTER = "-+END\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+";
7171

72+
private static final String PKCS1_DSA_HEADER = "-+BEGIN\\s+DSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
73+
74+
private static final String PKCS1_DSA_FOOTER = "-+END\\s+DSA\\s+PRIVATE\\s+KEY[^-]*-+";
75+
7276
private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)";
7377

7478
public static final int BASE64_TEXT_GROUP = 1;
@@ -83,6 +87,9 @@ final class PemPrivateKeyParser {
8387
"RSASSA-PSS", "EC", "DSA", "EdDSA", "XDH"));
8488
parsers.add(new PemParser(PKCS8_ENCRYPTED_HEADER, PKCS8_ENCRYPTED_FOOTER,
8589
PemPrivateKeyParser::createKeySpecForPkcs8Encrypted, "RSA", "RSASSA-PSS", "EC", "DSA", "EdDSA", "XDH"));
90+
parsers.add(new PemParser(PKCS1_DSA_HEADER, PKCS1_DSA_FOOTER, (bytes, password) -> {
91+
throw new IllegalStateException("Unsupported private key format");
92+
}));
8693
PEM_PARSERS = Collections.unmodifiableList(parsers);
8794
}
8895

@@ -172,7 +179,7 @@ private static PKCS8EncodedKeySpec createKeySpecForPkcs8Encrypted(byte[] bytes,
172179
* @param key the private key to parse
173180
* @return the parsed private key
174181
*/
175-
static PrivateKey parse(String key) {
182+
static PrivateKey[] parse(String key) {
176183
return parse(key, null);
177184
}
178185

@@ -183,22 +190,23 @@ static PrivateKey parse(String key) {
183190
* @param password the password used to decrypt an encrypted private key
184191
* @return the parsed private key
185192
*/
186-
static PrivateKey parse(String key, String password) {
193+
static PrivateKey[] parse(String key, String password) {
187194
if (key == null) {
188195
return null;
189196
}
197+
List<PrivateKey> keys = new ArrayList<>();
190198
try {
191199
for (PemParser pemParser : PEM_PARSERS) {
192200
PrivateKey privateKey = pemParser.parse(key, password);
193201
if (privateKey != null) {
194-
return privateKey;
202+
keys.add(privateKey);
195203
}
196204
}
197-
throw new IllegalStateException("Unrecognized private key format");
198205
}
199206
catch (Exception ex) {
200207
throw new IllegalStateException("Error loading private key file: " + ex.getMessage(), ex);
201208
}
209+
return keys.toArray(PrivateKey[]::new);
202210
}
203211

204212
/**
@@ -239,7 +247,7 @@ private PrivateKey parse(byte[] bytes, String password) {
239247
catch (InvalidKeySpecException | NoSuchAlgorithmException ex) {
240248
}
241249
}
242-
return null;
250+
throw new IllegalStateException("Unrecognized private key format");
243251
}
244252

245253
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.boot.ssl.SslStoreBundle;
2828
import org.springframework.boot.ssl.pem.KeyVerifier.Result;
2929
import org.springframework.util.Assert;
30+
import org.springframework.util.ObjectUtils;
3031
import org.springframework.util.StringUtils;
3132

3233
/**
@@ -150,13 +151,18 @@ private static void verifyKeys(PrivateKey privateKey, X509Certificate[] certific
150151

151152
private static PrivateKey loadPrivateKey(PemSslStoreDetails details) {
152153
String privateKeyContent = PemContent.load(details.privateKey());
153-
return PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword());
154+
if (privateKeyContent == null) {
155+
return null;
156+
}
157+
PrivateKey[] privateKeys = PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword());
158+
Assert.state(!ObjectUtils.isEmpty(privateKeys), "Loaded private keys are empty");
159+
return privateKeys[0];
154160
}
155161

156162
private static X509Certificate[] loadCertificates(PemSslStoreDetails details) {
157163
String certificateContent = PemContent.load(details.certificate());
158164
X509Certificate[] certificates = PemCertificateParser.parse(certificateContent);
159-
Assert.state(certificates != null && certificates.length > 0, "Loaded certificates are empty");
165+
Assert.state(!ObjectUtils.isEmpty(certificates), "Loaded certificates are empty");
160166
return certificates;
161167
}
162168

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.junit.jupiter.params.provider.ValueSource;
2828

2929
import org.springframework.core.io.ClassPathResource;
30+
import org.springframework.util.ObjectUtils;
3031

3132
import static org.assertj.core.api.Assertions.assertThat;
3233
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@@ -49,7 +50,7 @@ class PemPrivateKeyParserTests {
4950
})
5051
// @formatter:on
5152
void shouldParseTraditionalPkcs8(String file, String algorithm) throws IOException {
52-
PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file));
53+
PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file));
5354
assertThat(privateKey).isNotNull();
5455
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
5556
assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm);
@@ -62,7 +63,7 @@ void shouldParseTraditionalPkcs8(String file, String algorithm) throws IOExcepti
6263
})
6364
// @formatter:on
6465
void shouldParseTraditionalPkcs1(String file, String algorithm) throws IOException {
65-
PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/" + file));
66+
PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs1/" + file));
6667
assertThat(privateKey).isNotNull();
6768
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
6869
assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm);
@@ -76,11 +77,11 @@ void shouldParseTraditionalPkcs1(String file, String algorithm) throws IOExcepti
7677
// @formatter:on
7778
void shouldNotParseUnsupportedTraditionalPkcs1(String file) {
7879
assertThatIllegalStateException()
79-
.isThrownBy(() -> PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/" + file)))
80+
.isThrownBy(() -> parse(read("org/springframework/boot/web/server/pkcs1/" + file)))
8081
.withMessageContaining("Error loading private key file")
8182
.withCauseInstanceOf(IllegalStateException.class)
8283
.havingCause()
83-
.withMessageContaining("Unrecognized private key format");
84+
.withMessageContaining("Unsupported private key format");
8485
}
8586

8687
@ParameterizedTest
@@ -99,7 +100,7 @@ void shouldNotParseUnsupportedTraditionalPkcs1(String file) {
99100
})
100101
// @formatter:on
101102
void shouldParseEcPkcs8(String file, String curveName, String oid) throws IOException {
102-
PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file));
103+
PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file));
103104
assertThat(privateKey).isNotNull();
104105
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
105106
assertThat(privateKey.getAlgorithm()).isEqualTo("EC");
@@ -134,7 +135,7 @@ void shouldNotParseUnsupportedEcPkcs8(String file) {
134135
})
135136
// @formatter:on
136137
void shouldParseEdDsaPkcs8(String file) throws IOException {
137-
PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file));
138+
PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file));
138139
assertThat(privateKey).isNotNull();
139140
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
140141
assertThat(privateKey.getAlgorithm()).isEqualTo("EdDSA");
@@ -148,7 +149,7 @@ void shouldParseEdDsaPkcs8(String file) throws IOException {
148149
})
149150
// @formatter:on
150151
void shouldParseXdhPkcs8(String file) throws IOException {
151-
PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file));
152+
PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file));
152153
assertThat(privateKey).isNotNull();
153154
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
154155
assertThat(privateKey.getAlgorithm()).isEqualTo("XDH");
@@ -170,7 +171,7 @@ void shouldParseXdhPkcs8(String file) throws IOException {
170171
})
171172
// @formatter:on
172173
void shouldParseEcSec1(String file, String curveName, String oid) throws IOException {
173-
PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/sec1/" + file));
174+
PrivateKey privateKey = parse(read("org/springframework/boot/web/server/sec1/" + file));
174175
assertThat(privateKey).isNotNull();
175176
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
176177
assertThat(privateKey.getAlgorithm()).isEqualTo("EC");
@@ -198,8 +199,8 @@ void shouldNotParseUnsupportedEcSec1(String file) {
198199
}
199200

200201
@Test
201-
void parseWithNonKeyTextWillThrowException() {
202-
assertThatIllegalStateException().isThrownBy(() -> PemPrivateKeyParser.parse(read("test-banner.txt")));
202+
void parseWithNonKeyTextWillReturnEmptyArray() throws Exception {
203+
assertThat(PemPrivateKeyParser.parse(read("test-banner.txt"))).isEmpty();
203204
}
204205

205206
@ParameterizedTest
@@ -217,9 +218,10 @@ void shouldParseEncryptedPkcs8(String file, String algorithm) throws IOException
217218
// openssl pkcs8 -topk8 -in <input file> -out <output file> -v2 <algorithm>
218219
// -passout pass:test
219220
// where <algorithm> is aes128 or aes256
220-
PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file),
221-
"test");
222-
assertThat(privateKey).isNotNull();
221+
String content = read("org/springframework/boot/web/server/pkcs8/" + file);
222+
PrivateKey[] privateKeys = PemPrivateKeyParser.parse(content, "test");
223+
assertThat(privateKeys).isNotEmpty();
224+
PrivateKey privateKey = privateKeys[0];
223225
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
224226
assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm);
225227
}
@@ -248,24 +250,26 @@ void shouldNotParseEncryptedPkcs8NotUsingPbkdf2() {
248250
}
249251

250252
@Test
251-
void shouldNotParseEncryptedSec1() {
253+
void shouldNotParseEncryptedSec1() throws Exception {
252254
// created with:
253255
// openssl ecparam -genkey -name prime256v1 | openssl ec -aes-128-cbc -out
254256
// prime256v1-aes-128-cbc.key
255-
assertThatIllegalStateException()
256-
.isThrownBy(() -> PemPrivateKeyParser
257-
.parse(read("org/springframework/boot/web/server/sec1/prime256v1-aes-128-cbc.key"), "test"))
258-
.withMessageContaining("Unrecognized private key format");
257+
assertThat(PemPrivateKeyParser
258+
.parse(read("org/springframework/boot/web/server/sec1/prime256v1-aes-128-cbc.key"), "test")).isEmpty();
259259
}
260260

261261
@Test
262262
void shouldNotParseEncryptedPkcs1() throws Exception {
263263
// created with:
264264
// openssl genrsa -aes-256-cbc -out rsa-aes-256-cbc.key
265-
assertThatIllegalStateException()
266-
.isThrownBy(() -> PemPrivateKeyParser
267-
.parse(read("org/springframework/boot/web/server/pkcs1/rsa-aes-256-cbc.key"), "test"))
268-
.withMessageContaining("Unrecognized private key format");
265+
assertThat(PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/rsa-aes-256-cbc.key"),
266+
"test"))
267+
.isEmpty();
268+
}
269+
270+
private PrivateKey parse(String key) {
271+
PrivateKey[] keys = PemPrivateKeyParser.parse(key);
272+
return (!ObjectUtils.isEmpty(keys)) ? keys[0] : null;
269273
}
270274

271275
private String read(String path) throws IOException {

0 commit comments

Comments
 (0)