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.logging.InternalLogger;
25  import io.netty.util.internal.logging.InternalLoggerFactory;
26  
27  import java.io.File;
28  import java.io.FileInputStream;
29  import java.io.FileOutputStream;
30  import java.io.IOException;
31  import java.io.OutputStream;
32  import java.security.KeyPair;
33  import java.security.KeyPairGenerator;
34  import java.security.NoSuchAlgorithmException;
35  import java.security.PrivateKey;
36  import java.security.SecureRandom;
37  import java.security.cert.CertificateEncodingException;
38  import java.security.cert.CertificateException;
39  import java.security.cert.CertificateFactory;
40  import java.security.cert.X509Certificate;
41  import java.util.Date;
42  
43  /**
44   * Generates a temporary self-signed certificate for testing purposes.
45   * <p>
46   * <strong>NOTE:</strong>
47   * Never use the certificate and private key generated by this class in production.
48   * It is purely for testing purposes, and thus it is very insecure.
49   * It even uses an insecure pseudo-random generator for faster generation internally.
50   * </p><p>
51   * A X.509 certificate file and a RSA private key file are generated in a system's temporary directory using
52   * {@link java.io.File#createTempFile(String, String)}, and they are deleted when the JVM exits using
53   * {@link java.io.File#deleteOnExit()}.
54   * </p><p>
55   * At first, this method tries to use OpenJDK's X.509 implementation (the {@code sun.security.x509} package).
56   * If it fails, it tries to use <a href="http://www.bouncycastle.org/">Bouncy Castle</a> as a fallback.
57   * </p>
58   */
59  public final class SelfSignedCertificate {
60  
61      private static final InternalLogger logger = InternalLoggerFactory.getInstance(SelfSignedCertificate.class);
62  
63      /** Current time minus 1 year, just in case software clock goes back due to time synchronization */
64      private static final Date DEFAULT_NOT_BEFORE = new Date(SystemPropertyUtil.getLong(
65              "io.netty.selfSignedCertificate.defaultNotBefore", System.currentTimeMillis() - 86400000L * 365));
66      /** The maximum possible value in X.509 specification: 9999-12-31 23:59:59 */
67      private static final Date DEFAULT_NOT_AFTER = new Date(SystemPropertyUtil.getLong(
68              "io.netty.selfSignedCertificate.defaultNotAfter", 253402300799000L));
69  
70      private final File certificate;
71      private final File privateKey;
72      private final X509Certificate cert;
73      private final PrivateKey key;
74  
75      /**
76       * Creates a new instance.
77       */
78      public SelfSignedCertificate() throws CertificateException {
79          this(DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
80      }
81  
82      /**
83       * Creates a new instance.
84       * @param notBefore Certificate is not valid before this time
85       * @param notAfter Certificate is not valid after this time
86       */
87      public SelfSignedCertificate(Date notBefore, Date notAfter) throws CertificateException {
88          this("example.com", notBefore, notAfter);
89      }
90  
91      /**
92       * Creates a new instance.
93       *
94       * @param fqdn a fully qualified domain name
95       */
96      public SelfSignedCertificate(String fqdn) throws CertificateException {
97          this(fqdn, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
98      }
99  
100     /**
101      * Creates a new instance.
102      *
103      * @param fqdn a fully qualified domain name
104      * @param notBefore Certificate is not valid before this time
105      * @param notAfter Certificate is not valid after this time
106      */
107     public SelfSignedCertificate(String fqdn, Date notBefore, Date notAfter) throws CertificateException {
108         // Bypass entropy collection by using insecure random generator.
109         // We just want to generate it without any delay because it's for testing purposes only.
110         this(fqdn, ThreadLocalInsecureRandom.current(), 1024, notBefore, notAfter);
111     }
112 
113     /**
114      * Creates a new instance.
115      *
116      * @param fqdn a fully qualified domain name
117      * @param random the {@link java.security.SecureRandom} to use
118      * @param bits the number of bits of the generated private key
119      */
120     public SelfSignedCertificate(String fqdn, SecureRandom random, int bits) throws CertificateException {
121         this(fqdn, random, bits, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
122     }
123 
124     /**
125      * Creates a new instance.
126      *
127      * @param fqdn a fully qualified domain name
128      * @param random the {@link java.security.SecureRandom} to use
129      * @param bits the number of bits of the generated private key
130      * @param notBefore Certificate is not valid before this time
131      * @param notAfter Certificate is not valid after this time
132      */
133     public SelfSignedCertificate(String fqdn, SecureRandom random, int bits, Date notBefore, Date notAfter)
134             throws CertificateException {
135         // Generate an RSA key pair.
136         final KeyPair keypair;
137         try {
138             KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
139             keyGen.initialize(bits, random);
140             keypair = keyGen.generateKeyPair();
141         } catch (NoSuchAlgorithmException e) {
142             // Should not reach here because every Java implementation must have RSA key pair generator.
143             throw new Error(e);
144         }
145 
146         String[] paths;
147         try {
148             // Try the OpenJDK's proprietary implementation.
149             paths = OpenJdkSelfSignedCertGenerator.generate(fqdn, keypair, random, notBefore, notAfter);
150         } catch (Throwable t) {
151             logger.debug("Failed to generate a self-signed X.509 certificate using sun.security.x509:", t);
152             try {
153                 // Try Bouncy Castle if the current JVM didn't have sun.security.x509.
154                 paths = BouncyCastleSelfSignedCertGenerator.generate(fqdn, keypair, random, notBefore, notAfter);
155             } catch (Throwable t2) {
156                 logger.debug("Failed to generate a self-signed X.509 certificate using Bouncy Castle:", t2);
157                 throw new CertificateException(
158                         "No provider succeeded to generate a self-signed certificate. " +
159                                 "See debug log for the root cause.", t2);
160                 // TODO: consider using Java 7 addSuppressed to append t
161             }
162         }
163 
164         certificate = new File(paths[0]);
165         privateKey = new File(paths[1]);
166         key = keypair.getPrivate();
167         FileInputStream certificateInput = null;
168         try {
169             certificateInput = new FileInputStream(certificate);
170             cert = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(certificateInput);
171         } catch (Exception e) {
172             throw new CertificateEncodingException(e);
173         } finally {
174             if (certificateInput != null) {
175                 try {
176                     certificateInput.close();
177                 } catch (IOException e) {
178                     logger.warn("Failed to close a file: " + certificate, e);
179                 }
180             }
181         }
182     }
183 
184     /**
185      * Returns the generated X.509 certificate file in PEM format.
186      */
187     public File certificate() {
188         return certificate;
189     }
190 
191     /**
192      * Returns the generated RSA private key file in PEM format.
193      */
194     public File privateKey() {
195         return privateKey;
196     }
197 
198     /**
199      *  Returns the generated X.509 certificate.
200      */
201     public X509Certificate cert() {
202         return cert;
203     }
204 
205     /**
206      * Returns the generated RSA private key.
207      */
208     public PrivateKey key() {
209         return key;
210     }
211 
212     /**
213      * Deletes the generated X.509 certificate file and RSA private key file.
214      */
215     public void delete() {
216         safeDelete(certificate);
217         safeDelete(privateKey);
218     }
219 
220     static String[] newSelfSignedCertificate(
221             String fqdn, PrivateKey key, X509Certificate cert) throws IOException, CertificateEncodingException {
222         // Encode the private key into a file.
223         ByteBuf wrappedBuf = Unpooled.wrappedBuffer(key.getEncoded());
224         ByteBuf encodedBuf;
225         final String keyText;
226         try {
227             encodedBuf = Base64.encode(wrappedBuf, true);
228             try {
229                 keyText = "-----BEGIN PRIVATE KEY-----\n" +
230                           encodedBuf.toString(CharsetUtil.US_ASCII) +
231                           "\n-----END PRIVATE KEY-----\n";
232             } finally {
233                 encodedBuf.release();
234             }
235         } finally {
236             wrappedBuf.release();
237         }
238 
239         File keyFile = File.createTempFile("keyutil_" + fqdn + '_', ".key");
240         keyFile.deleteOnExit();
241 
242         OutputStream keyOut = new FileOutputStream(keyFile);
243         try {
244             keyOut.write(keyText.getBytes(CharsetUtil.US_ASCII));
245             keyOut.close();
246             keyOut = null;
247         } finally {
248             if (keyOut != null) {
249                 safeClose(keyFile, keyOut);
250                 safeDelete(keyFile);
251             }
252         }
253 
254         wrappedBuf = Unpooled.wrappedBuffer(cert.getEncoded());
255         final String certText;
256         try {
257             encodedBuf = Base64.encode(wrappedBuf, true);
258             try {
259                 // Encode the certificate into a CRT file.
260                 certText = "-----BEGIN CERTIFICATE-----\n" +
261                            encodedBuf.toString(CharsetUtil.US_ASCII) +
262                            "\n-----END CERTIFICATE-----\n";
263             } finally {
264                 encodedBuf.release();
265             }
266         } finally {
267             wrappedBuf.release();
268         }
269 
270         File certFile = File.createTempFile("keyutil_" + fqdn + '_', ".crt");
271         certFile.deleteOnExit();
272 
273         OutputStream certOut = new FileOutputStream(certFile);
274         try {
275             certOut.write(certText.getBytes(CharsetUtil.US_ASCII));
276             certOut.close();
277             certOut = null;
278         } finally {
279             if (certOut != null) {
280                 safeClose(certFile, certOut);
281                 safeDelete(certFile);
282                 safeDelete(keyFile);
283             }
284         }
285 
286         return new String[] { certFile.getPath(), keyFile.getPath() };
287     }
288 
289     private static void safeDelete(File certFile) {
290         if (!certFile.delete()) {
291             logger.warn("Failed to delete a file: " + certFile);
292         }
293     }
294 
295     private static void safeClose(File keyFile, OutputStream keyOut) {
296         try {
297             keyOut.close();
298         } catch (IOException e) {
299             logger.warn("Failed to close a file: " + keyFile, e);
300         }
301     }
302 }