diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java index 327dcc94a1f..58cf1ac4447 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java @@ -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 parse(String text) { + if (text == null) { return null; } CertificateFactory factory = getCertificateFactory(); List 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() { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java index 31782857506..ec0a0905c5e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java @@ -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; + } + + List getCertificates() { + return PemCertificateParser.parse(this.text); + } + + List getPrivateKeys() { + return PemPrivateKeyParser.parse(this.text); + } + + List 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 String load(String content) { - if (content == null || isPemContent(content)) { - return content; + 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) { 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 0df0fbdf4d5..1d9c06b2443 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 @@ -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 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 parse(String text, String password) { + if (text == null) { return null; } List 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); } /** 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 7221305656c..14077cdbb20 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 @@ -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 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 certificates = pemContent.getCertificates(); + Assert.state(!CollectionUtils.isEmpty(certificates), "Loaded certificates are empty"); + return certificates.toArray(X509Certificate[]::new); } private static KeyStore createKeyStore(PemSslStoreDetails details) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemCertificateParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemCertificateParserTests.java index 20ceee1b9a4..db8f71f6b74 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemCertificateParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemCertificateParserTests.java @@ -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 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 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 { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java index 649d66f699b..6a8ddedb5d5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java @@ -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); } 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 0d5587fa568..c61396a63cd 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 @@ -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 is aes128 or aes256 String content = read("org/springframework/boot/web/server/pkcs8/" + file); - PrivateKey[] privateKeys = PemPrivateKeyParser.parse(content, "test"); + List 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 keys = PemPrivateKeyParser.parse(key); + return (!ObjectUtils.isEmpty(keys)) ? keys.get(0) : null; } private String read(String path) throws IOException {