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    *   http://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.SystemPropertyUtil;
24  import io.netty.util.internal.ThrowableUtil;
25  import io.netty.util.internal.logging.InternalLogger;
26  import io.netty.util.internal.logging.InternalLoggerFactory;
27  
28  import java.io.File;
29  import java.io.FileInputStream;
30  import java.io.FileOutputStream;
31  import java.io.IOException;
32  import java.io.OutputStream;
33  import java.security.KeyPair;
34  import java.security.KeyPairGenerator;
35  import java.security.NoSuchAlgorithmException;
36  import java.security.PrivateKey;
37  import java.security.SecureRandom;
38  import java.security.cert.CertificateEncodingException;
39  import java.security.cert.CertificateException;
40  import java.security.cert.CertificateFactory;
41  import java.security.cert.X509Certificate;
42  import java.util.Date;
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 java.io.File#createTempFile(String, String)}, and they are deleted when the JVM exits using
54   * {@link java.io.File#deleteOnExit()}.
55   * </p><p>
56   * At first, this method tries to use OpenJDK's X.509 implementation (the {@code sun.security.x509} package).
57   * If it fails, it tries to use <a href="http://www.bouncycastle.org/">Bouncy Castle</a> as a fallback.
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.netty.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.netty.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.netty.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 (!algorithm.equalsIgnoreCase("EC") && !algorithm.equalsIgnoreCase("RSA")) {
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             // Try the OpenJDK's proprietary implementation.
239             paths = OpenJdkSelfSignedCertGenerator.generate(fqdn, keypair, random, notBefore, notAfter, algorithm);
240         } catch (Throwable t) {
241             logger.debug("Failed to generate a self-signed X.509 certificate using sun.security.x509:", t);
242             try {
243                 // Try Bouncy Castle if the current JVM didn't have sun.security.x509.
244                 paths = BouncyCastleSelfSignedCertGenerator.generate(
245                         fqdn, keypair, random, notBefore, notAfter, algorithm);
246             } catch (Throwable t2) {
247                 logger.debug("Failed to generate a self-signed X.509 certificate using Bouncy Castle:", t2);
248                 final CertificateException certificateException = new CertificateException(
249                         "No provider succeeded to generate a self-signed certificate. " +
250                                 "See debug log for the root cause.", t2);
251                 ThrowableUtil.addSuppressed(certificateException, t);
252                 throw certificateException;
253             }
254         }
255 
256         certificate = new File(paths[0]);
257         privateKey = new File(paths[1]);
258         key = keypair.getPrivate();
259         FileInputStream certificateInput = null;
260         try {
261             certificateInput = new FileInputStream(certificate);
262             cert = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(certificateInput);
263         } catch (Exception e) {
264             throw new CertificateEncodingException(e);
265         } finally {
266             if (certificateInput != null) {
267                 try {
268                     certificateInput.close();
269                 } catch (IOException e) {
270                     if (logger.isWarnEnabled()) {
271                         logger.warn("Failed to close a file: " + certificate, e);
272                     }
273                 }
274             }
275         }
276     }
277 
278     /**
279      * Returns the generated X.509 certificate file in PEM format.
280      */
281     public File certificate() {
282         return certificate;
283     }
284 
285     /**
286      * Returns the generated RSA private key file in PEM format.
287      */
288     public File privateKey() {
289         return privateKey;
290     }
291 
292     /**
293      *  Returns the generated X.509 certificate.
294      */
295     public X509Certificate cert() {
296         return cert;
297     }
298 
299     /**
300      * Returns the generated RSA private key.
301      */
302     public PrivateKey key() {
303         return key;
304     }
305 
306     /**
307      * Deletes the generated X.509 certificate file and RSA private key file.
308      */
309     public void delete() {
310         safeDelete(certificate);
311         safeDelete(privateKey);
312     }
313 
314     static String[] newSelfSignedCertificate(
315             String fqdn, PrivateKey key, X509Certificate cert) throws IOException, CertificateEncodingException {
316         // Encode the private key into a file.
317         ByteBuf wrappedBuf = Unpooled.wrappedBuffer(key.getEncoded());
318         ByteBuf encodedBuf;
319         final String keyText;
320         try {
321             encodedBuf = Base64.encode(wrappedBuf, true);
322             try {
323                 keyText = "-----BEGIN PRIVATE KEY-----\n" +
324                         encodedBuf.toString(CharsetUtil.US_ASCII) +
325                         "\n-----END PRIVATE KEY-----\n";
326             } finally {
327                 encodedBuf.release();
328             }
329         } finally {
330             wrappedBuf.release();
331         }
332 
333         File keyFile = File.createTempFile("keyutil_" + fqdn + '_', ".key");
334         keyFile.deleteOnExit();
335 
336         OutputStream keyOut = new FileOutputStream(keyFile);
337         try {
338             keyOut.write(keyText.getBytes(CharsetUtil.US_ASCII));
339             keyOut.close();
340             keyOut = null;
341         } finally {
342             if (keyOut != null) {
343                 safeClose(keyFile, keyOut);
344                 safeDelete(keyFile);
345             }
346         }
347 
348         wrappedBuf = Unpooled.wrappedBuffer(cert.getEncoded());
349         final String certText;
350         try {
351             encodedBuf = Base64.encode(wrappedBuf, true);
352             try {
353                 // Encode the certificate into a CRT file.
354                 certText = "-----BEGIN CERTIFICATE-----\n" +
355                         encodedBuf.toString(CharsetUtil.US_ASCII) +
356                         "\n-----END CERTIFICATE-----\n";
357             } finally {
358                 encodedBuf.release();
359             }
360         } finally {
361             wrappedBuf.release();
362         }
363 
364         File certFile = File.createTempFile("keyutil_" + fqdn + '_', ".crt");
365         certFile.deleteOnExit();
366 
367         OutputStream certOut = new FileOutputStream(certFile);
368         try {
369             certOut.write(certText.getBytes(CharsetUtil.US_ASCII));
370             certOut.close();
371             certOut = null;
372         } finally {
373             if (certOut != null) {
374                 safeClose(certFile, certOut);
375                 safeDelete(certFile);
376                 safeDelete(keyFile);
377             }
378         }
379 
380         return new String[] { certFile.getPath(), keyFile.getPath() };
381     }
382 
383     private static void safeDelete(File certFile) {
384         if (!certFile.delete()) {
385             if (logger.isWarnEnabled()) {
386                 logger.warn("Failed to delete a file: " + certFile);
387             }
388         }
389     }
390 
391     private static void safeClose(File keyFile, OutputStream keyOut) {
392         try {
393             keyOut.close();
394         } catch (IOException e) {
395             if (logger.isWarnEnabled()) {
396                 logger.warn("Failed to close a file: " + keyFile, e);
397             }
398         }
399     }
400 }