1717package org .springframework .boot .ssl .pem ;
1818
1919import java .io .IOException ;
20- import java .io .InputStreamReader ;
21- import java .io .Reader ;
20+ import java .io .InputStream ;
21+ import java .io .UncheckedIOException ;
2222import java .net .URL ;
2323import java .nio .charset .StandardCharsets ;
24+ import java .nio .file .Files ;
25+ import java .nio .file .Path ;
26+ import java .nio .file .StandardOpenOption ;
2427import java .security .PrivateKey ;
2528import java .security .cert .X509Certificate ;
2629import java .util .List ;
2730import java .util .Objects ;
2831import java .util .regex .Pattern ;
2932
30- import org .springframework .util .FileCopyUtils ;
33+ import org .springframework .util .Assert ;
3134import org .springframework .util .ResourceUtils ;
35+ import org .springframework .util .StreamUtils ;
3236
3337/**
34- * Utility to load PEM content.
38+ * PEM encoded content that can provide {@link X509Certificate certificates} and
39+ * {@link PrivateKey private keys}.
3540 *
3641 * @author Scott Frederick
3742 * @author Phillip Webb
43+ * @since 3.2.0
3844 */
39- final class PemContent {
45+ public final class PemContent {
4046
4147 private static final Pattern PEM_HEADER = Pattern .compile ("-+BEGIN\\ s+[^-]*-+" , Pattern .CASE_INSENSITIVE );
4248
@@ -48,11 +54,32 @@ private PemContent(String text) {
4854 this .text = text ;
4955 }
5056
51- List <X509Certificate > getCertificates () {
57+ /**
58+ * Parse and return all {@link X509Certificate certificates} from the PEM content.
59+ * Most PEM files either contain a single certificate or a certificate chain.
60+ * @return the certificates
61+ * @throws IllegalStateException if no certificates could be loaded
62+ */
63+ public List <X509Certificate > getCertificates () {
5264 return PemCertificateParser .parse (this .text );
5365 }
5466
55- PrivateKey getPrivateKeys (String password ) {
67+ /**
68+ * Parse and return the {@link PrivateKey private keys} from the PEM content.
69+ * @return the private keys
70+ * @throws IllegalStateException if no private key could be loaded
71+ */
72+ public PrivateKey getPrivateKey () {
73+ return getPrivateKey (null );
74+ }
75+
76+ /**
77+ * Parse and return the {@link PrivateKey private keys} from the PEM content or
78+ * {@code null} if there is no private key.
79+ * @param password the password to decrypt the private keys or {@code null}
80+ * @return the private keys
81+ */
82+ public PrivateKey getPrivateKey (String password ) {
5683 return PemPrivateKeyParser .parse (this .text , password );
5784 }
5885
@@ -77,27 +104,74 @@ public String toString() {
77104 return this .text ;
78105 }
79106
80- static PemContent load (String content ) {
107+ /**
108+ * Load {@link PemContent} from the given content (either the PEM content itself or
109+ * something that can be loaded by {@link ResourceUtils#getURL}).
110+ * @param content the content to load
111+ * @return a new {@link PemContent} instance
112+ * @throws IOException on IO error
113+ */
114+ static PemContent load (String content ) throws IOException {
81115 if (content == null ) {
82116 return null ;
83117 }
84- if (isPemContent (content )) {
118+ if (isPresentInText (content )) {
85119 return new PemContent (content );
86120 }
87121 try {
88- URL url = ResourceUtils .getURL (content );
89- try (Reader reader = new InputStreamReader (url .openStream (), StandardCharsets .UTF_8 )) {
90- return new PemContent (FileCopyUtils .copyToString (reader ));
91- }
122+ return load (ResourceUtils .getURL (content ));
123+ }
124+ catch (IOException | UncheckedIOException ex ) {
125+ throw new IOException ("Error reading certificate or key from file '%s'" .formatted (content ), ex );
126+ }
127+ }
128+
129+ /**
130+ * Load {@link PemContent} from the given {@link URL}.
131+ * @param url the URL to load content from
132+ * @return the loaded PEM content
133+ * @throws IOException on IO error
134+ */
135+ public static PemContent load (URL url ) throws IOException {
136+ Assert .notNull (url , "Url must not be null" );
137+ try (InputStream in = url .openStream ()) {
138+ return load (in );
92139 }
93- catch (IOException ex ) {
94- throw new IllegalStateException (
95- "Error reading certificate or key from file '" + content + "':" + ex .getMessage (), ex );
140+ }
141+
142+ /**
143+ * Load {@link PemContent} from the given {@link Path}.
144+ * @param path a path to load the content from
145+ * @return the loaded PEM content
146+ * @throws IOException on IO error
147+ */
148+ public static PemContent load (Path path ) throws IOException {
149+ Assert .notNull (path , "Path must not be null" );
150+ try (InputStream in = Files .newInputStream (path , StandardOpenOption .READ )) {
151+ return load (in );
96152 }
97153 }
98154
99- private static boolean isPemContent (String content ) {
100- return content != null && PEM_HEADER .matcher (content ).find () && PEM_FOOTER .matcher (content ).find ();
155+ private static PemContent load (InputStream in ) throws IOException {
156+ return of (StreamUtils .copyToString (in , StandardCharsets .UTF_8 ));
157+ }
158+
159+ /**
160+ * Return a new {@link PemContent} instance containing the given text.
161+ * @param text the text containing PEM encoded content
162+ * @return a new {@link PemContent} instance
163+ */
164+ public static PemContent of (String text ) {
165+ return (text != null ) ? new PemContent (text ) : null ;
166+ }
167+
168+ /**
169+ * Return if PEM content is present in the given text.
170+ * @param text the text to check
171+ * @return if the text includes PEM encoded content.
172+ */
173+ public static boolean isPresentInText (String text ) {
174+ return text != null && PEM_HEADER .matcher (text ).find () && PEM_FOOTER .matcher (text ).find ();
101175 }
102176
103177}
0 commit comments