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