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                     if (logger.isWarnEnabled()) {
179                         logger.warn("Failed to close a file: " + certificate, e);
180                     }
181                 }
182             }
183         }
184     }
185 
186     /**
187      * Returns the generated X.509 certificate file in PEM format.
188      */
189     public File certificate() {
190         return certificate;
191     }
192 
193     /**
194      * Returns the generated RSA private key file in PEM format.
195      */
196     public File privateKey() {
197         return privateKey;
198     }
199 
200     /**
201      *  Returns the generated X.509 certificate.
202      */
203     public X509Certificate cert() {
204         return cert;
205     }
206 
207     /**
208      * Returns the generated RSA private key.
209      */
210     public PrivateKey key() {
211         return key;
212     }
213 
214     /**
215      * Deletes the generated X.509 certificate file and RSA private key file.
216      */
217     public void delete() {
218         safeDelete(certificate);
219         safeDelete(privateKey);
220     }
221 
222     static String[] newSelfSignedCertificate(
223             String fqdn, PrivateKey key, X509Certificate cert) throws IOException, CertificateEncodingException {
224         // Encode the private key into a file.
225         ByteBuf wrappedBuf = Unpooled.wrappedBuffer(key.getEncoded());
226         ByteBuf encodedBuf;
227         final String keyText;
228         try {
229             encodedBuf = Base64.encode(wrappedBuf, true);
230             try {
231                 keyText = "-----BEGIN PRIVATE KEY-----\n" +
232                           encodedBuf.toString(CharsetUtil.US_ASCII) +
233                           "\n-----END PRIVATE KEY-----\n";
234             } finally {
235                 encodedBuf.release();
236             }
237         } finally {
238             wrappedBuf.release();
239         }
240 
241         File keyFile = File.createTempFile("keyutil_" + fqdn + '_', ".key");
242         keyFile.deleteOnExit();
243 
244         OutputStream keyOut = new FileOutputStream(keyFile);
245         try {
246             keyOut.write(keyText.getBytes(CharsetUtil.US_ASCII));
247             keyOut.close();
248             keyOut = null;
249         } finally {
250             if (keyOut != null) {
251                 safeClose(keyFile, keyOut);
252                 safeDelete(keyFile);
253             }
254         }
255 
256         wrappedBuf = Unpooled.wrappedBuffer(cert.getEncoded());
257         final String certText;
258         try {
259             encodedBuf = Base64.encode(wrappedBuf, true);
260             try {
261                 // Encode the certificate into a CRT file.
262                 certText = "-----BEGIN CERTIFICATE-----\n" +
263                            encodedBuf.toString(CharsetUtil.US_ASCII) +
264                            "\n-----END CERTIFICATE-----\n";
265             } finally {
266                 encodedBuf.release();
267             }
268         } finally {
269             wrappedBuf.release();
270         }
271 
272         File certFile = File.createTempFile("keyutil_" + fqdn + '_', ".crt");
273         certFile.deleteOnExit();
274 
275         OutputStream certOut = new FileOutputStream(certFile);
276         try {
277             certOut.write(certText.getBytes(CharsetUtil.US_ASCII));
278             certOut.close();
279             certOut = null;
280         } finally {
281             if (certOut != null) {
282                 safeClose(certFile, certOut);
283                 safeDelete(certFile);
284                 safeDelete(keyFile);
285             }
286         }
287 
288         return new String[] { certFile.getPath(), keyFile.getPath() };
289     }
290 
291     private static void safeDelete(File certFile) {
292         if (!certFile.delete()) {
293             if (logger.isWarnEnabled()) {
294                 logger.warn("Failed to delete a file: " + certFile);
295             }
296         }
297     }
298 
299     private static void safeClose(File keyFile, OutputStream keyOut) {
300         try {
301             keyOut.close();
302         } catch (IOException e) {
303             if (logger.isWarnEnabled()) {
304                 logger.warn("Failed to close a file: " + keyFile, e);
305             }
306         }
307     }
308 }