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 }