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

View File

@ -21,6 +21,10 @@ import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; 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 java.util.regex.Pattern;
import org.springframework.util.FileCopyUtils; 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 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) { List<X509Certificate> getCertificates() {
if (content == null || isPemContent(content)) { return PemCertificateParser.parse(this.text);
return content; }
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 { try {
URL url = ResourceUtils.getURL(content); URL url = ResourceUtils.getURL(content);
try (Reader reader = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)) { try (Reader reader = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)) {
return FileCopyUtils.copyToString(reader); return new PemContent(FileCopyUtils.copyToString(reader));
} }
} }
catch (IOException ex) { catch (IOException ex) {

View File

@ -176,28 +176,28 @@ final class PemPrivateKeyParser {
/** /**
* Parse a private key from the specified string. * 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 * @return the parsed private key
*/ */
static PrivateKey[] parse(String key) { static List<PrivateKey> parse(String text) {
return parse(key, null); return parse(text, null);
} }
/** /**
* Parse a private key from the specified string, using the provided password for * Parse a private key from the specified string, using the provided password for
* decryption if necessary. * 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 * @param password the password used to decrypt an encrypted private key
* @return the parsed private key * @return the parsed private key
*/ */
static PrivateKey[] parse(String key, String password) { static List<PrivateKey> parse(String text, String password) {
if (key == null) { if (text == null) {
return null; return null;
} }
List<PrivateKey> keys = new ArrayList<>(); List<PrivateKey> keys = new ArrayList<>();
try { try {
for (PemParser pemParser : PEM_PARSERS) { for (PemParser pemParser : PEM_PARSERS) {
PrivateKey privateKey = pemParser.parse(key, password); PrivateKey privateKey = pemParser.parse(text, password);
if (privateKey != null) { if (privateKey != null) {
keys.add(privateKey); keys.add(privateKey);
} }
@ -206,7 +206,7 @@ final class PemPrivateKeyParser {
catch (Exception ex) { catch (Exception ex) {
throw new IllegalStateException("Error loading private key file: " + ex.getMessage(), 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.PrivateKey;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.List;
import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.boot.ssl.pem.KeyVerifier.Result; import org.springframework.boot.ssl.pem.KeyVerifier.Result;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@ -150,20 +151,20 @@ public class PemSslStoreBundle implements SslStoreBundle {
} }
private static PrivateKey loadPrivateKey(PemSslStoreDetails details) { private static PrivateKey loadPrivateKey(PemSslStoreDetails details) {
String privateKeyContent = PemContent.load(details.privateKey()); PemContent pemContent = PemContent.load(details.privateKey());
if (privateKeyContent == null) { if (pemContent == null) {
return null; return null;
} }
PrivateKey[] privateKeys = PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword()); List<PrivateKey> privateKeys = pemContent.getPrivateKeys(details.privateKeyPassword());
Assert.state(!ObjectUtils.isEmpty(privateKeys), "Loaded private keys are empty"); Assert.state(!CollectionUtils.isEmpty(privateKeys), "Loaded private keys are empty");
return privateKeys[0]; return privateKeys.get(0);
} }
private static X509Certificate[] loadCertificates(PemSslStoreDetails details) { private static X509Certificate[] loadCertificates(PemSslStoreDetails details) {
String certificateContent = PemContent.load(details.certificate()); PemContent pemContent = PemContent.load(details.certificate());
X509Certificate[] certificates = PemCertificateParser.parse(certificateContent); List<X509Certificate> certificates = pemContent.getCertificates();
Assert.state(!ObjectUtils.isEmpty(certificates), "Loaded certificates are empty"); Assert.state(!CollectionUtils.isEmpty(certificates), "Loaded certificates are empty");
return certificates; return certificates.toArray(X509Certificate[]::new);
} }
private static KeyStore createKeyStore(PemSslStoreDetails details) private static KeyStore createKeyStore(PemSslStoreDetails details)

View File

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

View File

@ -57,19 +57,19 @@ class PemContentTests {
+lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO +lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO
32C9XWHwRA4= 32C9XWHwRA4=
-----END CERTIFICATE-----"""; -----END CERTIFICATE-----""";
assertThat(PemContent.load(content)).isEqualTo(content); assertThat(PemContent.load(content)).hasToString(content);
} }
@Test @Test
void loadWhenClasspathLocationReturnsContent() throws IOException { 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); String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
} }
@Test @Test
void loadWhenFileLocationReturnsContent() throws IOException { 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); String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
} }

View File

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