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