Skip to content

Commit

Permalink
Allow PemPrivateKeyParser to parse multiple keys
Browse files Browse the repository at this point in the history
Update `PemPrivateKeyParser` so that it can parse multiple keys in a
single PEM file.

Closes gh-37970
  • Loading branch information
philwebb committed Oct 20, 2023
1 parent deb7942 commit 32e6ce2
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

Expand Down Expand Up @@ -172,7 +179,7 @@ private static PKCS8EncodedKeySpec createKeySpecForPkcs8Encrypted(byte[] bytes,
* @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);
}

Expand All @@ -183,22 +190,23 @@ static PrivateKey parse(String key) {
* @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<PrivateKey> 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);
}

/**
Expand Down Expand Up @@ -239,7 +247,7 @@ private PrivateKey parse(byte[] bytes, String password) {
catch (InvalidKeySpecException | NoSuchAlgorithmException ex) {
}
}
return null;
throw new IllegalStateException("Unrecognized private key format");
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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;

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

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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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;
Expand All @@ -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);
Expand All @@ -62,7 +63,7 @@ void shouldParseTraditionalPkcs8(String file, String algorithm) throws IOExcepti
})
// @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);
Expand All @@ -76,11 +77,11 @@ void shouldParseTraditionalPkcs1(String file, String algorithm) throws IOExcepti
// @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
Expand All @@ -99,7 +100,7 @@ void shouldNotParseUnsupportedTraditionalPkcs1(String file) {
})
// @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");
Expand Down Expand Up @@ -134,7 +135,7 @@ void shouldNotParseUnsupportedEcPkcs8(String file) {
})
// @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");
Expand All @@ -148,7 +149,7 @@ void shouldParseEdDsaPkcs8(String file) throws IOException {
})
// @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");
Expand All @@ -170,7 +171,7 @@ void shouldParseXdhPkcs8(String file) throws IOException {
})
// @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");
Expand Down Expand Up @@ -198,8 +199,8 @@ void shouldNotParseUnsupportedEcSec1(String file) {
}

@Test
void parseWithNonKeyTextWillThrowException() {
assertThatIllegalStateException().isThrownBy(() -> PemPrivateKeyParser.parse(read("test-banner.txt")));
void parseWithNonKeyTextWillReturnEmptyArray() throws Exception {
assertThat(PemPrivateKeyParser.parse(read("test-banner.txt"))).isEmpty();
}

@ParameterizedTest
Expand All @@ -217,9 +218,10 @@ void shouldParseEncryptedPkcs8(String file, String algorithm) throws IOException
// openssl pkcs8 -topk8 -in <input file> -out <output file> -v2 <algorithm>
// -passout pass:test
// where <algorithm> 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);
}
Expand Down Expand Up @@ -248,24 +250,26 @@ void shouldNotParseEncryptedPkcs8NotUsingPbkdf2() {
}

@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 {
Expand Down

0 comments on commit 32e6ce2

Please sign in to comment.