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