View Javadoc
1   /*
2    * Copyright 2024 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.pkitesting;
17  
18  import io.netty.util.internal.EmptyArrays;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.nio.charset.StandardCharsets;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.nio.file.StandardOpenOption;
27  import java.security.KeyPair;
28  import java.security.KeyStore;
29  import java.security.KeyStoreException;
30  import java.security.NoSuchAlgorithmException;
31  import java.security.PrivateKey;
32  import java.security.UnrecoverableKeyException;
33  import java.security.cert.CertificateEncodingException;
34  import java.security.cert.CertificateException;
35  import java.security.cert.TrustAnchor;
36  import java.security.cert.X509Certificate;
37  import java.util.Arrays;
38  import java.util.Base64;
39  import java.util.LinkedHashSet;
40  import java.util.List;
41  import javax.net.ssl.KeyManagerFactory;
42  import javax.net.ssl.TrustManager;
43  import javax.net.ssl.TrustManagerFactory;
44  
45  import static java.util.Objects.requireNonNull;
46  
47  /**
48   * A certificate bundle is a private key and a full certificate path, all the way to the root certificate.
49   * The bundle offers ways of accessing these, and converting them into various representations.
50   */
51  public final class X509Bundle {
52      private final X509Certificate[] certPath;
53      private final X509Certificate root;
54      private final KeyPair keyPair;
55  
56      /**
57       * Construct a bundle from a given certificate path, root certificate, and {@link KeyPair}.
58       * @param certPath The certificate path, starting with the leaf certificate.The path can end either with the
59       * root certificate, or the intermediate certificate signed by the root certificate.
60       * @param root The self-signed root certificate.
61       * @param keyPair The key pair.
62       */
63      private X509Bundle(X509Certificate[] certPath, X509Certificate root, KeyPair keyPair) {
64          requireNonNull(root, "root");
65          requireNonNull(keyPair, "keyPair");
66          if (certPath.length > 1 && certPath[certPath.length - 1].equals(root)) {
67              this.certPath = Arrays.copyOf(certPath, certPath.length - 1);
68          } else {
69              this.certPath = certPath.clone();
70          }
71          this.root = root;
72          this.keyPair = keyPair;
73      }
74  
75      /**
76       * Construct a bundle for a certificate authority.
77       * @param root The self-signed root certificate.
78       * @param keyPair The key pair.
79       * @return The new bundle.
80       */
81      public static X509Bundle fromRootCertificateAuthority(X509Certificate root, KeyPair keyPair) {
82          requireNonNull(root, "root");
83          requireNonNull(keyPair, "keyPair");
84          X509Bundle bundle = new X509Bundle(new X509Certificate[]{root}, root, keyPair);
85          if (!bundle.isCertificateAuthority() || !bundle.isSelfSigned()) {
86              throw new IllegalArgumentException("Given certificate is not a root CA certificate: " +
87                      root.getSubjectX500Principal() + ", issued by " + root.getIssuerX500Principal());
88          }
89          return bundle;
90      }
91  
92      /**
93       * Construct a bundle from a given certificate path, root certificate, and {@link KeyPair}.
94       * @param certPath The certificate path, starting with the leaf certificate.The path can end either with the
95       * root certificate, or the intermediate certificate signed by the root certificate.
96       * @param root The self-signed root certificate.
97       * @param keyPair The key pair.
98       */
99      public static X509Bundle fromCertificatePath(
100             X509Certificate[] certPath, X509Certificate root, KeyPair keyPair) {
101         return new X509Bundle(certPath, root, keyPair);
102     }
103 
104     /**
105      * Get the leaf certificate of the bundle.
106      * If this bundle is for a certificate authority, then this return the same as {@link #getRootCertificate()}.
107      * @return The leaf certificate.
108      */
109     public X509Certificate getCertificate() {
110         return certPath[0];
111     }
112 
113     /**
114      * Get the PEM encoded string of the {@linkplain #getCertificate() leaf certificate}.
115      * @return The certificate PEM string.
116      */
117     public String getCertificatePEM() {
118         return toCertPem(certPath[0]);
119     }
120 
121     /**
122      * Get the certificate path, starting with the leaf certificate up to but excluding the root certificate.
123      * @return The certificate path.
124      */
125     public X509Certificate[] getCertificatePath() {
126         return certPath.clone();
127     }
128 
129     /**
130      * Get the certificate path, starting with the leaf certificate up to and including the root certificate.
131      * @return The certificate path, including the root certificate.
132      */
133     public X509Certificate[] getCertificatePathWithRoot() {
134         X509Certificate[] path = Arrays.copyOf(certPath, certPath.length + 1);
135         path[path.length - 1] = root;
136         return path;
137     }
138 
139     /**
140      * Get the certificate path as a list, starting with the leaf certificate up to but excluding the root certificate.
141      * @return The certificate path list.
142      */
143     public List<X509Certificate> getCertificatePathList() {
144         return Arrays.asList(certPath);
145     }
146 
147     /**
148      * Get the {@linkplain #getCertificatePath() certificate path} as a PEM encoded string.
149      * @return The PEM encoded certificate path.
150      */
151     public String getCertificatePathPEM() {
152         return toCertPem(certPath);
153     }
154 
155     /**
156      * Get the key pair.
157      * @return The key pair.
158      */
159     public KeyPair getKeyPair() {
160         return keyPair;
161     }
162 
163     /**
164      * Get the root certificate that anchors the certificate path.
165      * @return The root certificate.
166      */
167     public X509Certificate getRootCertificate() {
168         return root;
169     }
170 
171     /**
172      * Get the {@linkplain #getRootCertificate() root certificate} as a PEM encoded string.
173      * @return The PEM encoded root certificate.
174      */
175     public String getRootCertificatePEM() {
176         return toCertPem(root);
177     }
178 
179     private static String toCertPem(X509Certificate... certs) {
180         Base64.Encoder encoder = getMimeEncoder();
181         StringBuilder sb = new StringBuilder();
182         for (X509Certificate cert : certs) {
183             sb.append("-----BEGIN CERTIFICATE-----\r\n");
184             try {
185                 sb.append(encoder.encodeToString(cert.getEncoded()));
186             } catch (CertificateEncodingException e) {
187                 throw new IllegalStateException(e);
188             }
189             sb.append("\r\n-----END CERTIFICATE-----\r\n");
190         }
191         return sb.toString();
192     }
193 
194     /**
195      * Get the private key as a PEM encoded PKCS#8 string.
196      * @return The private key in PKCS#8 and PEM encoded string.
197      */
198     public String getPrivateKeyPEM() {
199         Base64.Encoder encoder = getMimeEncoder();
200         StringBuilder sb = new StringBuilder();
201         sb.append("-----BEGIN PRIVATE KEY-----\r\n");
202         PrivateKey privateKey = keyPair.getPrivate();
203         sb.append(encoder.encodeToString(privateKey.getEncoded()));
204         sb.append("\r\n-----END PRIVATE KEY-----\r\n");
205         return sb.toString();
206     }
207 
208     private static Base64.Encoder getMimeEncoder() {
209         return Base64.getMimeEncoder(64, new byte[]{ '\r', '\n' });
210     }
211 
212     /**
213      * Get the root certificate as a new {@link TrustAnchor} object.
214      * Note that {@link TrustAnchor} instance have object identity, so if this method is called twice,
215      * the two trust anchors will not be equal to each other.
216      * @return A new {@link TrustAnchor} instance containing the root certificate.
217      */
218     public TrustAnchor getTrustAnchor() {
219         return new TrustAnchor(root, root.getExtensionValue(CertificateBuilder.OID_X509_NAME_CONSTRAINTS));
220     }
221 
222     /**
223      * Query if this bundle is for a certificate authority root certificate.
224      * @return {@code true} if the {@linkplain #getCertificate() leaf certificate} is a certificate authority,
225      * otherwise {@code false}.
226      */
227     public boolean isCertificateAuthority() {
228         return certPath[0].getBasicConstraints() != -1;
229     }
230 
231     /**
232      * Query if this bundle is for a self-signed certificate.
233      * @return {@code true} if the {@linkplain #getCertificate() leaf certificate} is self-signed.
234      */
235     public boolean isSelfSigned() {
236         X509Certificate leaf = certPath[0];
237         return certPath.length == 1 &&
238                 leaf.getSubjectX500Principal().equals(leaf.getIssuerX500Principal()) &&
239                 Arrays.equals(leaf.getSubjectUniqueID(), leaf.getIssuerUniqueID());
240     }
241 
242     /**
243      * Create a {@link TrustManager} instance that trusts the root certificate in this bundle.
244      * @return The new {@link TrustManager}.
245      */
246     public TrustManager toTrustManager() {
247         TrustManagerFactory tmf = toTrustManagerFactory();
248         return tmf.getTrustManagers()[0];
249     }
250 
251     /**
252      * Create {@link TrustManagerFactory} instance that trusts the root certificate in this bundle.
253      * <p>
254      * The trust manager factory will use the {@linkplain TrustManagerFactory#getDefaultAlgorithm() default algorithm}.
255      *
256      * @return The new {@link TrustManagerFactory}.
257      */
258     public TrustManagerFactory toTrustManagerFactory() {
259         return toTrustManagerFactory(TrustManagerFactory.getDefaultAlgorithm());
260     }
261 
262     /**
263      * Create {@link TrustManagerFactory} instance that trusts the root certificate in this bundle,
264      * with the given algorithm.
265      *
266      * @return The new {@link TrustManagerFactory}.
267      */
268     public TrustManagerFactory toTrustManagerFactory(String algorithm) {
269         try {
270             TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm);
271             tmf.init(toKeyStore(EmptyArrays.EMPTY_CHARS));
272             return tmf;
273         } catch (NoSuchAlgorithmException e) {
274             throw new AssertionError("Default TrustManagerFactory algorithm was not available.", e);
275         } catch (KeyStoreException e) {
276             throw new IllegalStateException("Failed to initialize TrustManagerFactory with KeyStore.", e);
277         }
278     }
279 
280     /**
281      * Create a {@link KeyStore} with the contents of this bundle.
282      * The root certificate will be a trusted root in the key store.
283      * If this bundle has a {@linkplain #getPrivateKeyPEM() private key},
284      * then the private key and certificate path will also be added to the key store.
285      * <p>
286      * The key store will use the PKCS#12 format.
287      *
288      * @param keyEntryPassword The password used to encrypt the private key entry in the key store.
289      * @return The key store.
290      * @throws KeyStoreException If an error occurred when adding entries to the key store.
291      */
292     public KeyStore toKeyStore(char[] keyEntryPassword) throws KeyStoreException {
293         return toKeyStore("PKCS12", keyEntryPassword);
294     }
295 
296     /**
297      * Create a {@link KeyStore} with the contents of this bundle.
298      * The root certificate will be a trusted root in the key store.
299      * If this bundle has a {@linkplain #getPrivateKeyPEM() private key},
300      * then the private key and certificate path will also be added to the key store.
301      * <p>
302      * The key store will use the format defined by the given algorithm.
303      *
304      * @param keyEntryPassword The password used to encrypt the private key entry in the key store.
305      * @return The key store.
306      * @throws KeyStoreException If an error occurred when adding entries to the key store.
307      */
308     public KeyStore toKeyStore(String algorithm, char[] keyEntryPassword) throws KeyStoreException {
309         KeyStore keyStore;
310         try {
311             keyStore = KeyStore.getInstance(algorithm);
312             keyStore.load(null, null);
313         } catch (IOException | NoSuchAlgorithmException | CertificateException e) {
314             throw new KeyStoreException("Failed to initialize '" + algorithm + "' KeyStore.", e);
315         }
316         keyStore.setCertificateEntry("1", root);
317         if (keyPair.getPrivate() != null) {
318             keyStore.setKeyEntry("2", keyPair.getPrivate(), keyEntryPassword, certPath);
319         }
320         return keyStore;
321     }
322 
323     /**
324      * Create a temporary PKCS#12 file with the {@linkplain #toKeyStore(char[]) key store} of this bundle.
325      * The temporary file is automatically deleted when the JVM terminates normally.
326      * @param password The password used both to encrypt the private key in the key store,
327      * and to protect the key store itself.
328      * @return The {@link File} object with the path to the PKCS#12 key store.
329      * @throws Exception If something went wrong with creating the key store file.
330      */
331     public File toTempKeyStoreFile(char[] password) throws Exception {
332         return toTempKeyStoreFile(password, password);
333     }
334 
335     /**
336      * Create a temporary PKCS#12 file with the {@linkplain #toKeyStore(char[]) key store} of this bundle.
337      * The temporary file is automatically deleted when the JVM terminates normally.
338      * @param pkcs12Password The password used to encrypt the PKCS#12 file.
339      * @param keyEntryPassword The password used to encrypt the private key entry in the PKCS#12 file.
340      * @return The {@link File} object with the path to the PKCS#12 key store.
341      * @throws Exception If something went wrong with creating the key store file.
342      */
343     public File toTempKeyStoreFile(char[] pkcs12Password, char[] keyEntryPassword) throws Exception {
344         KeyStore keyStore = toKeyStore(keyEntryPassword);
345         Path tempFile = Files.createTempFile("ks", ".p12");
346         try (OutputStream out = Files.newOutputStream(tempFile, StandardOpenOption.WRITE)) {
347             keyStore.store(out, pkcs12Password);
348         }
349         File file = tempFile.toFile();
350         file.deleteOnExit();
351         return file;
352     }
353 
354     /**
355      * Create a temporary PEM file with the {@linkplain #getRootCertificate() root certificate} of this bundle.
356      * The temporary file is automatically deleted when the JVM terminates normally.
357      * @return The {@link File} object with the path to the trust root PEM file.
358      * @throws IOException If an IO error occurred when creating the trust root file.
359      */
360     public File toTempRootCertPem() throws IOException {
361         return createTempPemFile(getRootCertificatePEM(), "ca");
362     }
363 
364     /**
365      * Create a temporary PEM file with the {@linkplain #getCertificatePath() certificate chain} of this bundle.
366      * The temporary file is automatically deleted when the JVM terminates normally.
367      * @return The {@link File} object with the path to the certificate chain PEM file.
368      * @throws IOException If an IO error occurred when creating the certificate chain file.
369      */
370     public File toTempCertChainPem() throws IOException {
371         return createTempPemFile(getCertificatePathPEM(), "chain");
372     }
373 
374     /**
375      * Create a temporary PEM file with the {@linkplain #getPrivateKeyPEM() private key} of this bundle.
376      * The temporary file is automatically deleted when the JVM terminates normally.
377      * @return The {@link File} object with the path to the private key PEM file.
378      * @throws IOException If an IO error occurred when creating the private key file.
379      */
380     public File toTempPrivateKeyPem() throws IOException {
381         return createTempPemFile(getPrivateKeyPEM(), "key");
382     }
383 
384     private static File createTempPemFile(String pem, String filePrefix) throws IOException {
385         Path tempFile = Files.createTempFile(filePrefix, ".pem");
386         try (OutputStream out = Files.newOutputStream(tempFile, StandardOpenOption.WRITE)) {
387             out.write(pem.getBytes(StandardCharsets.ISO_8859_1));
388         }
389         File file = tempFile.toFile();
390         file.deleteOnExit();
391         return file;
392     }
393 
394     /**
395      * Create a {@link KeyManagerFactory} from this bundle.
396      * <p>
397      * The {@link KeyManagerFactory} will use the
398      * {@linkplain KeyManagerFactory#getDefaultAlgorithm() default algorithm}.
399      *
400      * @return The new {@link KeyManagerFactory}.
401      * @throws KeyStoreException If there was a problem creating or initializing the key store.
402      * @throws UnrecoverableKeyException If the private key could not be recovered,
403      * for instance if this bundle is a {@linkplain #isCertificateAuthority() certificate authority}.
404      * @throws NoSuchAlgorithmException If the key manager factory algorithm is not supported by the current
405      * security provider.
406      */
407     public KeyManagerFactory toKeyManagerFactory()
408             throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
409         return toKeyManagerFactory(KeyManagerFactory.getDefaultAlgorithm());
410     }
411 
412     /**
413      * Create a {@link KeyManagerFactory} from this bundle, using the given algorithm.
414      *
415      * @return The new {@link KeyManagerFactory}.
416      * @throws KeyStoreException If there was a problem creating or initializing the key store.
417      * @throws UnrecoverableKeyException If the private key could not be recovered,
418      * for instance if this bundle is a {@linkplain #isCertificateAuthority() certificate authority}.
419      * @throws NoSuchAlgorithmException If the key manager factory algorithm is not supported by the current
420      * security provider.
421      */
422     public KeyManagerFactory toKeyManagerFactory(String algorithm)
423             throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
424         KeyManagerFactory kmf;
425         try {
426             kmf = KeyManagerFactory.getInstance(algorithm);
427         } catch (NoSuchAlgorithmException e) {
428             throw new AssertionError("Default KeyManagerFactory algorithm was not available.", e);
429         }
430         kmf.init(toKeyStore(EmptyArrays.EMPTY_CHARS), EmptyArrays.EMPTY_CHARS);
431         return kmf;
432     }
433 
434     /**
435      * Create a new {@link X509Bundle} that has the leaf and root certificates of <em>this</em> bundle,
436      * but a certificate path that is the combination all the intermediate certificates of both this and the given
437      * bundle.
438      * <p>
439      * This is useful when building a bundle with a cross-signed certificate,
440      * or when you just want to have additional unrelated intermediate certificates in the path.
441      *
442      * @param other The other bundle.
443      * @return A new {@link X509Bundle} that has the leaf and root of this bundle,
444      * but the combined intermediates of both bundles.
445      */
446     public X509Bundle mergeIntermediates(X509Bundle other) {
447         LinkedHashSet<X509Certificate> certs = new LinkedHashSet<>(Arrays.asList(certPath));
448         X509Certificate[] otherPath = other.certPath;
449         for (int i = 1; i < otherPath.length; i++) {
450             X509Certificate intermediate = otherPath[i];
451             certs.add(intermediate);
452         }
453         return fromCertificatePath(certs.toArray(EmptyArrays.EMPTY_X509_CERTIFICATES), root, keyPair);
454     }
455 }