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", 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 }