Allow PemPrivateKeyParser to parse multiple keys
Update `PemPrivateKeyParser` so that it can parse multiple keys in a single PEM file. Closes gh-37970
This commit is contained in:
parent
deb79425ee
commit
32e6ce210e
|
@ -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<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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -239,7 +247,7 @@ final class PemPrivateKeyParser {
|
|||
catch (InvalidKeySpecException | NoSuchAlgorithmException ex) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
throw new IllegalStateException("Unrecognized private key format");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue