Refine `PemContent` and PEM parsers

This commit is contained in:
Phillip Webb 2023-10-19 23:19:45 -07:00
parent 41724fc5f1
commit 25ce0ef3fc
7 changed files with 86 additions and 40 deletions

View File

@ -48,17 +48,17 @@ final class PemCertificateParser {
/**
* Parse certificates from the specified string.
* @param certificates the certificates to parse
* @param text the text to parse
* @return the parsed certificates
*/
static X509Certificate[] parse(String certificates) {
if (certificates == null) {
static List<X509Certificate> parse(String text) {
if (text == null) {
return null;
}
CertificateFactory factory = getCertificateFactory();
List<X509Certificate> certs = new ArrayList<>();
readCertificates(certificates, factory, certs::add);
return (!certs.isEmpty()) ? certs.toArray(X509Certificate[]::new) : null;
readCertificates(text, factory, certs::add);
return List.copyOf(certs);
}
private static CertificateFactory getCertificateFactory() {

View File

@ -21,6 +21,10 @@ import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import org.springframework.util.FileCopyUtils;
@ -38,17 +42,56 @@ final class PemContent {
private static final Pattern PEM_FOOTER = Pattern.compile("-+END\\s+[^-]*-+", Pattern.CASE_INSENSITIVE);
private PemContent() {
private String text;
private PemContent(String text) {
this.text = text;
}
static String load(String content) {
if (content == null || isPemContent(content)) {
return content;
List<X509Certificate> getCertificates() {
return PemCertificateParser.parse(this.text);
}
List<PrivateKey> getPrivateKeys() {
return PemPrivateKeyParser.parse(this.text);
}
List<PrivateKey> getPrivateKeys(String password) {
return PemPrivateKeyParser.parse(this.text, password);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return Objects.equals(this.text, ((PemContent) obj).text);
}
@Override
public int hashCode() {
return Objects.hash(this.text);
}
@Override
public String toString() {
return this.text;
}
static PemContent load(String content) {
if (content == null) {
return null;
}
if (isPemContent(content)) {
return new PemContent(content);
}
try {
URL url = ResourceUtils.getURL(content);
try (Reader reader = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)) {
return FileCopyUtils.copyToString(reader);
return new PemContent(FileCopyUtils.copyToString(reader));
}
}
catch (IOException ex) {

View File

@ -176,28 +176,28 @@ final class PemPrivateKeyParser {
/**
* Parse a private key from the specified string.
* @param key the private key to parse
* @param text the text to parse
* @return the parsed private key
*/
static PrivateKey[] parse(String key) {
return parse(key, null);
static List<PrivateKey> parse(String text) {
return parse(text, null);
}
/**
* Parse a private key from the specified string, using the provided password for
* decryption if necessary.
* @param key the private key to parse
* @param text the text to parse
* @param password the password used to decrypt an encrypted private key
* @return the parsed private key
*/
static PrivateKey[] parse(String key, String password) {
if (key == null) {
static List<PrivateKey> parse(String text, String password) {
if (text == null) {
return null;
}
List<PrivateKey> keys = new ArrayList<>();
try {
for (PemParser pemParser : PEM_PARSERS) {
PrivateKey privateKey = pemParser.parse(key, password);
PrivateKey privateKey = pemParser.parse(text, password);
if (privateKey != null) {
keys.add(privateKey);
}
@ -206,7 +206,7 @@ final class PemPrivateKeyParser {
catch (Exception ex) {
throw new IllegalStateException("Error loading private key file: " + ex.getMessage(), ex);
}
return keys.toArray(PrivateKey[]::new);
return List.copyOf(keys);
}
/**

View File

@ -23,11 +23,12 @@ import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
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.CollectionUtils;
import org.springframework.util.StringUtils;
/**
@ -150,20 +151,20 @@ public class PemSslStoreBundle implements SslStoreBundle {
}
private static PrivateKey loadPrivateKey(PemSslStoreDetails details) {
String privateKeyContent = PemContent.load(details.privateKey());
if (privateKeyContent == null) {
PemContent pemContent = PemContent.load(details.privateKey());
if (pemContent == null) {
return null;
}
PrivateKey[] privateKeys = PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword());
Assert.state(!ObjectUtils.isEmpty(privateKeys), "Loaded private keys are empty");
return privateKeys[0];
List<PrivateKey> privateKeys = pemContent.getPrivateKeys(details.privateKeyPassword());
Assert.state(!CollectionUtils.isEmpty(privateKeys), "Loaded private keys are empty");
return privateKeys.get(0);
}
private static X509Certificate[] loadCertificates(PemSslStoreDetails details) {
String certificateContent = PemContent.load(details.certificate());
X509Certificate[] certificates = PemCertificateParser.parse(certificateContent);
Assert.state(!ObjectUtils.isEmpty(certificates), "Loaded certificates are empty");
return certificates;
PemContent pemContent = PemContent.load(details.certificate());
List<X509Certificate> certificates = pemContent.getCertificates();
Assert.state(!CollectionUtils.isEmpty(certificates), "Loaded certificates are empty");
return certificates.toArray(X509Certificate[]::new);
}
private static KeyStore createKeyStore(PemSslStoreDetails details)

View File

@ -19,6 +19,7 @@ package org.springframework.boot.ssl.pem;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.List;
import org.junit.jupiter.api.Test;
@ -35,19 +36,19 @@ class PemCertificateParserTests {
@Test
void parseCertificate() throws Exception {
X509Certificate[] certificates = PemCertificateParser.parse(read("test-cert.pem"));
List<X509Certificate> certificates = PemCertificateParser.parse(read("test-cert.pem"));
assertThat(certificates).isNotNull();
assertThat(certificates).hasSize(1);
assertThat(certificates[0].getType()).isEqualTo("X.509");
assertThat(certificates.get(0).getType()).isEqualTo("X.509");
}
@Test
void parseCertificateChain() throws Exception {
X509Certificate[] certificates = PemCertificateParser.parse(read("test-cert-chain.pem"));
List<X509Certificate> certificates = PemCertificateParser.parse(read("test-cert-chain.pem"));
assertThat(certificates).isNotNull();
assertThat(certificates).hasSize(2);
assertThat(certificates[0].getType()).isEqualTo("X.509");
assertThat(certificates[1].getType()).isEqualTo("X.509");
assertThat(certificates.get(0).getType()).isEqualTo("X.509");
assertThat(certificates.get(1).getType()).isEqualTo("X.509");
}
private String read(String path) throws IOException {

View File

@ -57,19 +57,19 @@ class PemContentTests {
+lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO
32C9XWHwRA4=
-----END CERTIFICATE-----""";
assertThat(PemContent.load(content)).isEqualTo(content);
assertThat(PemContent.load(content)).hasToString(content);
}
@Test
void loadWhenClasspathLocationReturnsContent() throws IOException {
String actual = PemContent.load("classpath:test-cert.pem");
String actual = PemContent.load("classpath:test-cert.pem").toString();
String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8);
assertThat(actual).isEqualTo(expected);
}
@Test
void loadWhenFileLocationReturnsContent() throws IOException {
String actual = PemContent.load("src/test/resources/test-cert.pem");
String actual = PemContent.load("src/test/resources/test-cert.pem").toString();
String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8);
assertThat(actual).isEqualTo(expected);
}

View File

@ -20,6 +20,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.interfaces.ECPrivateKey;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@ -219,9 +220,9 @@ class PemPrivateKeyParserTests {
// -passout pass:test
// where <algorithm> is aes128 or aes256
String content = read("org/springframework/boot/web/server/pkcs8/" + file);
PrivateKey[] privateKeys = PemPrivateKeyParser.parse(content, "test");
List<PrivateKey> privateKeys = PemPrivateKeyParser.parse(content, "test");
assertThat(privateKeys).isNotEmpty();
PrivateKey privateKey = privateKeys[0];
PrivateKey privateKey = privateKeys.get(0);
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm);
}
@ -268,8 +269,8 @@ class PemPrivateKeyParserTests {
}
private PrivateKey parse(String key) {
PrivateKey[] keys = PemPrivateKeyParser.parse(key);
return (!ObjectUtils.isEmpty(keys)) ? keys[0] : null;
List<PrivateKey> keys = PemPrivateKeyParser.parse(key);
return (!ObjectUtils.isEmpty(keys)) ? keys.get(0) : null;
}
private String read(String path) throws IOException {