View Javadoc
1   /*
2    * Copyright 2014 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.netty5.handler.ssl.util;
17  
18  import io.netty5.buffer.api.Buffer;
19  import io.netty5.handler.codec.base64.Base64;
20  import io.netty5.util.CharsetUtil;
21  import io.netty5.util.internal.PlatformDependent;
22  import io.netty5.util.internal.SystemPropertyUtil;
23  import io.netty5.util.internal.logging.InternalLogger;
24  import io.netty5.util.internal.logging.InternalLoggerFactory;
25  
26  import java.io.File;
27  import java.io.FileInputStream;
28  import java.io.FileOutputStream;
29  import java.io.IOException;
30  import java.io.OutputStream;
31  import java.security.KeyPair;
32  import java.security.KeyPairGenerator;
33  import java.security.NoSuchAlgorithmException;
34  import java.security.PrivateKey;
35  import java.security.SecureRandom;
36  import java.security.cert.CertificateEncodingException;
37  import java.security.cert.CertificateException;
38  import java.security.cert.CertificateFactory;
39  import java.security.cert.X509Certificate;
40  import java.util.Date;
41  
42  import static io.netty5.buffer.api.DefaultBufferAllocators.onHeapAllocator;
43  
44  /**
45   * Generates a temporary self-signed certificate for testing purposes.
46   * <p>
47   * <strong>NOTE:</strong>
48   * Never use the certificate and private key generated by this class in production.
49   * It is purely for testing purposes, and thus it is very insecure.
50   * It even uses an insecure pseudo-random generator for faster generation internally.
51   * </p><p>
52   * An X.509 certificate file and a EC/RSA private key file are generated in a system's temporary directory using
53   * {@link File#createTempFile(String, String)}, and they are deleted when the JVM exits using
54   * {@link File#deleteOnExit()}.
55   * </p><p>
56   * The certificate is generated using <a href="https://www.bouncycastle.org/">Bouncy Castle</a>, which is an
57   * <em>optional</em> dependency of Netty.
58   * </p>
59   */
60  public final class SelfSignedCertificate {
61  
62      private static final InternalLogger logger = InternalLoggerFactory.getInstance(SelfSignedCertificate.class);
63  
64      /** Current time minus 1 year, just in case software clock goes back due to time synchronization */
65      private static final Date DEFAULT_NOT_BEFORE = new Date(SystemPropertyUtil.getLong(
66              "io.netty5.selfSignedCertificate.defaultNotBefore", System.currentTimeMillis() - 86400000L * 365));
67      /** The maximum possible value in X.509 specification: 9999-12-31 23:59:59 */
68      private static final Date DEFAULT_NOT_AFTER = new Date(SystemPropertyUtil.getLong(
69              "io.netty5.selfSignedCertificate.defaultNotAfter", 253402300799000L));
70  
71      /**
72       * FIPS 140-2 encryption requires the RSA key length to be 2048 bits or greater.
73       * Let's use that as a sane default but allow the default to be set dynamically
74       * for those that need more stringent security requirements.
75       */
76      private static final int DEFAULT_KEY_LENGTH_BITS =
77              SystemPropertyUtil.getInt("io.netty5.handler.ssl.util.selfSignedKeyStrength", 2048);
78  
79      private final File certificate;
80      private final File privateKey;
81      private final X509Certificate cert;
82      private final PrivateKey key;
83  
84      /**
85       * Creates a new instance.
86       * <p> Algorithm: RSA </p>
87       */
88      public SelfSignedCertificate() throws CertificateException {
89          this(DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER, "RSA", DEFAULT_KEY_LENGTH_BITS);
90      }
91  
92      /**
93       * Creates a new instance.
94       * <p> Algorithm: RSA </p>
95       *
96       * @param notBefore Certificate is not valid before this time
97       * @param notAfter  Certificate is not valid after this time
98       */
99      public SelfSignedCertificate(Date notBefore, Date notAfter)
100             throws CertificateException {
101         this("localhost", notBefore, notAfter, "RSA", DEFAULT_KEY_LENGTH_BITS);
102     }
103 
104     /**
105      * Creates a new instance.
106      *
107      * @param notBefore Certificate is not valid before this time
108      * @param notAfter  Certificate is not valid after this time
109      * @param algorithm Key pair algorithm
110      * @param bits      the number of bits of the generated private key
111      */
112     public SelfSignedCertificate(Date notBefore, Date notAfter, String algorithm, int bits)
113             throws CertificateException {
114         this("localhost", notBefore, notAfter, algorithm, bits);
115     }
116 
117     /**
118      * Creates a new instance.
119      * <p> Algorithm: RSA </p>
120      *
121      * @param fqdn a fully qualified domain name
122      */
123     public SelfSignedCertificate(String fqdn) throws CertificateException {
124         this(fqdn, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER, "RSA", DEFAULT_KEY_LENGTH_BITS);
125     }
126 
127     /**
128      * Creates a new instance.
129      *
130      * @param fqdn      a fully qualified domain name
131      * @param algorithm Key pair algorithm
132      * @param bits      the number of bits of the generated private key
133      */
134     public SelfSignedCertificate(String fqdn, String algorithm, int bits) throws CertificateException {
135         this(fqdn, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER, algorithm, bits);
136     }
137 
138     /**
139      * Creates a new instance.
140      * <p> Algorithm: RSA </p>
141      *
142      * @param fqdn      a fully qualified domain name
143      * @param notBefore Certificate is not valid before this time
144      * @param notAfter  Certificate is not valid after this time
145      */
146     public SelfSignedCertificate(String fqdn, Date notBefore, Date notAfter) throws CertificateException {
147         // Bypass entropy collection by using insecure random generator.
148         // We just want to generate it without any delay because it's for testing purposes only.
149         this(fqdn, ThreadLocalInsecureRandom.current(), DEFAULT_KEY_LENGTH_BITS, notBefore, notAfter, "RSA");
150     }
151 
152     /**
153      * Creates a new instance.
154      *
155      * @param fqdn      a fully qualified domain name
156      * @param notBefore Certificate is not valid before this time
157      * @param notAfter  Certificate is not valid after this time
158      * @param algorithm Key pair algorithm
159      * @param bits      the number of bits of the generated private key
160      */
161     public SelfSignedCertificate(String fqdn, Date notBefore, Date notAfter, String algorithm, int bits)
162             throws CertificateException {
163         // Bypass entropy collection by using insecure random generator.
164         // We just want to generate it without any delay because it's for testing purposes only.
165         this(fqdn, ThreadLocalInsecureRandom.current(), bits, notBefore, notAfter, algorithm);
166     }
167 
168     /**
169      * Creates a new instance.
170      * <p> Algorithm: RSA </p>
171      *
172      * @param fqdn      a fully qualified domain name
173      * @param random    the {@link SecureRandom} to use
174      * @param bits      the number of bits of the generated private key
175      */
176     public SelfSignedCertificate(String fqdn, SecureRandom random, int bits)
177             throws CertificateException {
178         this(fqdn, random, bits, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER, "RSA");
179     }
180 
181     /**
182      * Creates a new instance.
183      *
184      * @param fqdn      a fully qualified domain name
185      * @param random    the {@link SecureRandom} to use
186      * @param algorithm Key pair algorithm
187      * @param bits      the number of bits of the generated private key
188      */
189     public SelfSignedCertificate(String fqdn, SecureRandom random, String algorithm, int bits)
190             throws CertificateException {
191         this(fqdn, random, bits, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER, algorithm);
192     }
193 
194     /**
195      * Creates a new instance.
196      * <p> Algorithm: RSA </p>
197      *
198      * @param fqdn      a fully qualified domain name
199      * @param random    the {@link SecureRandom} to use
200      * @param bits      the number of bits of the generated private key
201      * @param notBefore Certificate is not valid before this time
202      * @param notAfter  Certificate is not valid after this time
203      */
204     public SelfSignedCertificate(String fqdn, SecureRandom random, int bits, Date notBefore, Date notAfter)
205             throws CertificateException {
206         this(fqdn, random, bits, notBefore, notAfter, "RSA");
207     }
208 
209     /**
210      * Creates a new instance.
211      *
212      * @param fqdn      a fully qualified domain name
213      * @param random    the {@link SecureRandom} to use
214      * @param bits      the number of bits of the generated private key
215      * @param notBefore Certificate is not valid before this time
216      * @param notAfter  Certificate is not valid after this time
217      * @param algorithm Key pair algorithm
218      */
219     public SelfSignedCertificate(String fqdn, SecureRandom random, int bits, Date notBefore, Date notAfter,
220                                  String algorithm) throws CertificateException {
221 
222         if (!"EC".equalsIgnoreCase(algorithm) && !"RSA".equalsIgnoreCase(algorithm)) {
223             throw new IllegalArgumentException("Algorithm not valid: " + algorithm);
224         }
225 
226         final KeyPair keypair;
227         try {
228             KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm);
229             keyGen.initialize(bits, random);
230             keypair = keyGen.generateKeyPair();
231         } catch (NoSuchAlgorithmException e) {
232             // Should not reach here because every Java implementation must have RSA and EC key pair generator.
233             throw new Error(e);
234         }
235 
236         String[] paths;
237         try {
238             paths = BouncyCastleSelfSignedCertGenerator.generate(
239                     fqdn, keypair, random, notBefore, notAfter, algorithm);
240         } catch (Throwable throwable) {
241             logger.debug("Failed to generate a self-signed X.509 certificate using Bouncy Castle:", throwable);
242             throw new CertificateException(
243                     "No provider succeeded to generate a self-signed certificate. " +
244                     "See debug log for the root cause.", throwable);
245         }
246 
247         certificate = new File(paths[0]);
248         privateKey = new File(paths[1]);
249         key = keypair.getPrivate();
250         FileInputStream certificateInput = null;
251         try {
252             certificateInput = new FileInputStream(certificate);
253             cert = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(certificateInput);
254         } catch (Exception e) {
255             throw new CertificateEncodingException(e);
256         } finally {
257             if (certificateInput != null) {
258                 try {
259                     certificateInput.close();
260                 } catch (IOException e) {
261                     if (logger.isWarnEnabled()) {
262                         logger.warn("Failed to close a file: " + certificate, e);
263                     }
264                 }
265             }
266         }
267     }
268 
269     /**
270      * Returns the generated X.509 certificate file in PEM format.
271      */
272     public File certificate() {
273         return certificate;
274     }
275 
276     /**
277      * Returns the generated EC/RSA private key file in PEM format.
278      */
279     public File privateKey() {
280         return privateKey;
281     }
282 
283     /**
284      *  Returns the generated X.509 certificate.
285      */
286     public X509Certificate cert() {
287         return cert;
288     }
289 
290     /**
291      * Returns the generated EC/RSA private key.
292      */
293     public PrivateKey key() {
294         return key;
295     }
296 
297     /**
298      * Deletes the generated X.509 certificate file and EC/RSA private key file.
299      */
300     public void delete() {
301         safeDelete(certificate);
302         safeDelete(privateKey);
303     }
304 
305     static String[] newSelfSignedCertificate(
306             String fqdn, PrivateKey key, X509Certificate cert) throws IOException, CertificateEncodingException {
307         // Encode the private key into a file.
308         final String keyText;
309         try (Buffer wrappedBuf = onHeapAllocator().copyOf(key.getEncoded());
310              Buffer encodedBuf = Base64.encode(wrappedBuf, true)) {
311             keyText = "-----BEGIN PRIVATE KEY-----\n" +
312                       encodedBuf.toString(CharsetUtil.US_ASCII) +
313                       "\n-----END PRIVATE KEY-----\n";
314         }
315 
316         // Change all asterisk to 'x' for file name safety.
317         fqdn = fqdn.replaceAll("[^\\w.-]", "x");
318 
319         File keyFile = PlatformDependent.createTempFile("keyutil_" + fqdn + '_', ".key", null);
320         keyFile.deleteOnExit();
321 
322         OutputStream keyOut = new FileOutputStream(keyFile);
323         try {
324             keyOut.write(keyText.getBytes(CharsetUtil.US_ASCII));
325             keyOut.close();
326             keyOut = null;
327         } finally {
328             if (keyOut != null) {
329                 safeClose(keyFile, keyOut);
330                 safeDelete(keyFile);
331             }
332         }
333 
334         final String certText;
335         try (Buffer wrappedBuf = onHeapAllocator().copyOf(cert.getEncoded());
336              Buffer encodedBuf = Base64.encode(wrappedBuf, true)) {
337             // Encode the certificate into a CRT file.
338             certText = "-----BEGIN CERTIFICATE-----\n" +
339                        encodedBuf.toString(CharsetUtil.US_ASCII) +
340                        "\n-----END CERTIFICATE-----\n";
341         }
342 
343         File certFile = PlatformDependent.createTempFile("keyutil_" + fqdn + '_', ".crt", null);
344         certFile.deleteOnExit();
345 
346         OutputStream certOut = new FileOutputStream(certFile);
347         try {
348             certOut.write(certText.getBytes(CharsetUtil.US_ASCII));
349             certOut.close();
350             certOut = null;
351         } finally {
352             if (certOut != null) {
353                 safeClose(certFile, certOut);
354                 safeDelete(certFile);
355                 safeDelete(keyFile);
356             }
357         }
358 
359         return new String[] { certFile.getPath(), keyFile.getPath() };
360     }
361 
362     private static void safeDelete(File certFile) {
363         if (!certFile.delete()) {
364             if (logger.isWarnEnabled()) {
365                 logger.warn("Failed to delete a file: " + certFile);
366             }
367         }
368     }
369 
370     private static void safeClose(File keyFile, OutputStream keyOut) {
371         try {
372             keyOut.close();
373         } catch (IOException e) {
374             if (logger.isWarnEnabled()) {
375                 logger.warn("Failed to close a file: " + keyFile, e);
376             }
377         }
378     }
379 }