From 32e6ce210e1315b35e814344e986c44e78c9ed95 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 19 Oct 2023 22:01:23 -0700 Subject: [PATCH] Allow PemPrivateKeyParser to parse multiple keys Update `PemPrivateKeyParser` so that it can parse multiple keys in a single PEM file. Closes gh-37970 --- .../boot/ssl/pem/PemPrivateKeyParser.java | 18 +++++-- .../boot/ssl/pem/PemSslStoreBundle.java | 10 +++- .../ssl/pem/PemPrivateKeyParserTests.java | 48 ++++++++++--------- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java index 2aa432afb8..0df0fbdf4d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java @@ -69,6 +69,10 @@ final class PemPrivateKeyParser { private static final String SEC1_EC_FOOTER = "-+END\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+"; + private static final String PKCS1_DSA_HEADER = "-+BEGIN\\s+DSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; + + private static final String PKCS1_DSA_FOOTER = "-+END\\s+DSA\\s+PRIVATE\\s+KEY[^-]*-+"; + private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)"; public static final int BASE64_TEXT_GROUP = 1; @@ -83,6 +87,9 @@ final class PemPrivateKeyParser { "RSASSA-PSS", "EC", "DSA", "EdDSA", "XDH")); parsers.add(new PemParser(PKCS8_ENCRYPTED_HEADER, PKCS8_ENCRYPTED_FOOTER, PemPrivateKeyParser::createKeySpecForPkcs8Encrypted, "RSA", "RSASSA-PSS", "EC", "DSA", "EdDSA", "XDH")); + parsers.add(new PemParser(PKCS1_DSA_HEADER, PKCS1_DSA_FOOTER, (bytes, password) -> { + throw new IllegalStateException("Unsupported private key format"); + })); PEM_PARSERS = Collections.unmodifiableList(parsers); } @@ -172,7 +179,7 @@ final class PemPrivateKeyParser { * @param key the private key to parse * @return the parsed private key */ - static PrivateKey parse(String key) { + static PrivateKey[] parse(String key) { return parse(key, null); } @@ -183,22 +190,23 @@ final class PemPrivateKeyParser { * @param password the password used to decrypt an encrypted private key * @return the parsed private key */ - static PrivateKey parse(String key, String password) { + static PrivateKey[] parse(String key, String password) { if (key == null) { return null; } + List keys = new ArrayList<>(); try { for (PemParser pemParser : PEM_PARSERS) { PrivateKey privateKey = pemParser.parse(key, password); if (privateKey != null) { - return privateKey; + keys.add(privateKey); } } - throw new IllegalStateException("Unrecognized private key format"); } catch (Exception ex) { throw new IllegalStateException("Error loading private key file: " + ex.getMessage(), ex); } + return keys.toArray(PrivateKey[]::new); } /** @@ -239,7 +247,7 @@ final class PemPrivateKeyParser { catch (InvalidKeySpecException | NoSuchAlgorithmException ex) { } } - return null; + throw new IllegalStateException("Unrecognized private key format"); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index b71750f361..7221305656 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -27,6 +27,7 @@ import java.security.cert.X509Certificate; import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.boot.ssl.pem.KeyVerifier.Result; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -150,13 +151,18 @@ public class PemSslStoreBundle implements SslStoreBundle { private static PrivateKey loadPrivateKey(PemSslStoreDetails details) { String privateKeyContent = PemContent.load(details.privateKey()); - return PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword()); + if (privateKeyContent == null) { + return null; + } + PrivateKey[] privateKeys = PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword()); + Assert.state(!ObjectUtils.isEmpty(privateKeys), "Loaded private keys are empty"); + return privateKeys[0]; } private static X509Certificate[] loadCertificates(PemSslStoreDetails details) { String certificateContent = PemContent.load(details.certificate()); X509Certificate[] certificates = PemCertificateParser.parse(certificateContent); - Assert.state(certificates != null && certificates.length > 0, "Loaded certificates are empty"); + Assert.state(!ObjectUtils.isEmpty(certificates), "Loaded certificates are empty"); return certificates; } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java index 3f5796103a..0d5587fa56 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java @@ -27,6 +27,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.core.io.ClassPathResource; +import org.springframework.util.ObjectUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -49,7 +50,7 @@ class PemPrivateKeyParserTests { }) // @formatter:on void shouldParseTraditionalPkcs8(String file, String algorithm) throws IOException { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file)); + PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm); @@ -62,7 +63,7 @@ class PemPrivateKeyParserTests { }) // @formatter:on void shouldParseTraditionalPkcs1(String file, String algorithm) throws IOException { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/" + file)); + PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs1/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm); @@ -76,11 +77,11 @@ class PemPrivateKeyParserTests { // @formatter:on void shouldNotParseUnsupportedTraditionalPkcs1(String file) { assertThatIllegalStateException() - .isThrownBy(() -> PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/" + file))) + .isThrownBy(() -> parse(read("org/springframework/boot/web/server/pkcs1/" + file))) .withMessageContaining("Error loading private key file") .withCauseInstanceOf(IllegalStateException.class) .havingCause() - .withMessageContaining("Unrecognized private key format"); + .withMessageContaining("Unsupported private key format"); } @ParameterizedTest @@ -99,7 +100,7 @@ class PemPrivateKeyParserTests { }) // @formatter:on void shouldParseEcPkcs8(String file, String curveName, String oid) throws IOException { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file)); + PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("EC"); @@ -134,7 +135,7 @@ class PemPrivateKeyParserTests { }) // @formatter:on void shouldParseEdDsaPkcs8(String file) throws IOException { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file)); + PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("EdDSA"); @@ -148,7 +149,7 @@ class PemPrivateKeyParserTests { }) // @formatter:on void shouldParseXdhPkcs8(String file) throws IOException { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file)); + PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("XDH"); @@ -170,7 +171,7 @@ class PemPrivateKeyParserTests { }) // @formatter:on void shouldParseEcSec1(String file, String curveName, String oid) throws IOException { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/sec1/" + file)); + PrivateKey privateKey = parse(read("org/springframework/boot/web/server/sec1/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("EC"); @@ -198,8 +199,8 @@ class PemPrivateKeyParserTests { } @Test - void parseWithNonKeyTextWillThrowException() { - assertThatIllegalStateException().isThrownBy(() -> PemPrivateKeyParser.parse(read("test-banner.txt"))); + void parseWithNonKeyTextWillReturnEmptyArray() throws Exception { + assertThat(PemPrivateKeyParser.parse(read("test-banner.txt"))).isEmpty(); } @ParameterizedTest @@ -217,9 +218,10 @@ class PemPrivateKeyParserTests { // openssl pkcs8 -topk8 -in -out -v2 // -passout pass:test // where is aes128 or aes256 - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file), - "test"); - assertThat(privateKey).isNotNull(); + String content = read("org/springframework/boot/web/server/pkcs8/" + file); + PrivateKey[] privateKeys = PemPrivateKeyParser.parse(content, "test"); + assertThat(privateKeys).isNotEmpty(); + PrivateKey privateKey = privateKeys[0]; assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm); } @@ -248,24 +250,26 @@ class PemPrivateKeyParserTests { } @Test - void shouldNotParseEncryptedSec1() { + void shouldNotParseEncryptedSec1() throws Exception { // created with: // openssl ecparam -genkey -name prime256v1 | openssl ec -aes-128-cbc -out // prime256v1-aes-128-cbc.key - assertThatIllegalStateException() - .isThrownBy(() -> PemPrivateKeyParser - .parse(read("org/springframework/boot/web/server/sec1/prime256v1-aes-128-cbc.key"), "test")) - .withMessageContaining("Unrecognized private key format"); + assertThat(PemPrivateKeyParser + .parse(read("org/springframework/boot/web/server/sec1/prime256v1-aes-128-cbc.key"), "test")).isEmpty(); } @Test void shouldNotParseEncryptedPkcs1() throws Exception { // created with: // openssl genrsa -aes-256-cbc -out rsa-aes-256-cbc.key - assertThatIllegalStateException() - .isThrownBy(() -> PemPrivateKeyParser - .parse(read("org/springframework/boot/web/server/pkcs1/rsa-aes-256-cbc.key"), "test")) - .withMessageContaining("Unrecognized private key format"); + assertThat(PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/rsa-aes-256-cbc.key"), + "test")) + .isEmpty(); + } + + private PrivateKey parse(String key) { + PrivateKey[] keys = PemPrivateKeyParser.parse(key); + return (!ObjectUtils.isEmpty(keys)) ? keys[0] : null; } private String read(String path) throws IOException {