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   * An 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      /**
71       * FIPS 140-2 encryption requires the key length to be 2048 bits or greater.
72       * Let's use that as a sane default but allow the default to be set dynamically
73       * for those that need more stringent security requirements.
74       */
75      private static final int DEFAULT_KEY_LENGTH_BITS =
76              SystemPropertyUtil.getInt("io.netty.handler.ssl.util.selfSignedKeyStrength", 2048);
77  
78      private final File certificate;
79      private final File privateKey;
80      private final X509Certificate cert;
81      private final PrivateKey key;
82  
83      /**
84       * Creates a new instance.
85       */
86      public SelfSignedCertificate() throws CertificateException {
87          this(DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
88      }
89  
90      /**
91       * Creates a new instance.
92       * @param notBefore Certificate is not valid before this time
93       * @param notAfter Certificate is not valid after this time
94       */
95      public SelfSignedCertificate(Date notBefore, Date notAfter) throws CertificateException {
96          this("example.com", notBefore, notAfter);
97      }
98  
99      /**
100      * Creates a new instance.
101      *
102      * @param fqdn a fully qualified domain name
103      */
104     public SelfSignedCertificate(String fqdn) throws CertificateException {
105         this(fqdn, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
106     }
107 
108     /**
109      * Creates a new instance.
110      *
111      * @param fqdn a fully qualified domain name
112      * @param notBefore Certificate is not valid before this time
113      * @param notAfter Certificate is not valid after this time
114      */
115     public SelfSignedCertificate(String fqdn, Date notBefore, Date notAfter) throws CertificateException {
116         // Bypass entropy collection by using insecure random generator.
117         // We just want to generate it without any delay because it's for testing purposes only.
118         this(fqdn, ThreadLocalInsecureRandom.current(), DEFAULT_KEY_LENGTH_BITS, notBefore, notAfter);
119     }
120 
121     /**
122      * Creates a new instance.
123      *
124      * @param fqdn a fully qualified domain name
125      * @param random the {@link java.security.SecureRandom} to use
126      * @param bits the number of bits of the generated private key
127      */
128     public SelfSignedCertificate(String fqdn, SecureRandom random, int bits) throws CertificateException {
129         this(fqdn, random, bits, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
130     }
131 
132     /**
133      * Creates a new instance.
134      *
135      * @param fqdn a fully qualified domain name
136      * @param random the {@link java.security.SecureRandom} to use
137      * @param bits the number of bits of the generated private key
138      * @param notBefore Certificate is not valid before this time
139      * @param notAfter Certificate is not valid after this time
140      */
141     public SelfSignedCertificate(String fqdn, SecureRandom random, int bits, Date notBefore, Date notAfter)
142             throws CertificateException {
143         // Generate an RSA key pair.
144         final KeyPair keypair;
145         try {
146             KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
147             keyGen.initialize(bits, random);
148             keypair = keyGen.generateKeyPair();
149         } catch (NoSuchAlgorithmException e) {
150             // Should not reach here because every Java implementation must have RSA key pair generator.
151             throw new Error(e);
152         }
153 
154         String[] paths;
155         try {
156             // Try the OpenJDK's proprietary implementation.
157             paths = OpenJdkSelfSignedCertGenerator.generate(fqdn, keypair, random, notBefore, notAfter);
158         } catch (Throwable t) {
159             logger.debug("Failed to generate a self-signed X.509 certificate using sun.security.x509:", t);
160             try {
161                 // Try Bouncy Castle if the current JVM didn't have sun.security.x509.
162                 paths = BouncyCastleSelfSignedCertGenerator.generate(fqdn, keypair, random, notBefore, notAfter);
163             } catch (Throwable t2) {
164                 logger.debug("Failed to generate a self-signed X.509 certificate using Bouncy Castle:", t2);
165                 throw new CertificateException(
166                         "No provider succeeded to generate a self-signed certificate. " +
167                                 "See debug log for the root cause.", t2);
168                 // TODO: consider using Java 7 addSuppressed to append t
169             }
170         }
171 
172         certificate = new File(paths[0]);
173         privateKey = new File(paths[1]);
174         key = keypair.getPrivate();
175         FileInputStream certificateInput = null;
176         try {
177             certificateInput = new FileInputStream(certificate);
178             cert = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(certificateInput);
179         } catch (Exception e) {
180             throw new CertificateEncodingException(e);
181         } finally {
182             if (certificateInput != null) {
183                 try {
184                     certificateInput.close();
185                 } catch (IOException e) {
186                     if (logger.isWarnEnabled()) {
187                         logger.warn("Failed to close a file: " + certificate, e);
188                     }
189                 }
190             }
191         }
192     }
193 
194     /**
195      * Returns the generated X.509 certificate file in PEM format.
196      */
197     public File certificate() {
198         return certificate;
199     }
200 
201     /**
202      * Returns the generated RSA private key file in PEM format.
203      */
204     public File privateKey() {
205         return privateKey;
206     }
207 
208     /**
209      *  Returns the generated X.509 certificate.
210      */
211     public X509Certificate cert() {
212         return cert;
213     }
214 
215     /**
216      * Returns the generated RSA private key.
217      */
218     public PrivateKey key() {
219         return key;
220     }
221 
222     /**
223      * Deletes the generated X.509 certificate file and RSA private key file.
224      */
225     public void delete() {
226         safeDelete(certificate);
227         safeDelete(privateKey);
228     }
229 
230     static String[] newSelfSignedCertificate(
231             String fqdn, PrivateKey key, X509Certificate cert) throws IOException, CertificateEncodingException {
232         // Encode the private key into a file.
233         ByteBuf wrappedBuf = Unpooled.wrappedBuffer(key.getEncoded());
234         ByteBuf encodedBuf;
235         final String keyText;
236         try {
237             encodedBuf = Base64.encode(wrappedBuf, true);
238             try {
239                 keyText = "-----BEGIN PRIVATE KEY-----\n" +
240                           encodedBuf.toString(CharsetUtil.US_ASCII) +
241                           "\n-----END PRIVATE KEY-----\n";
242             } finally {
243                 encodedBuf.release();
244             }
245         } finally {
246             wrappedBuf.release();
247         }
248 
249         File keyFile = File.createTempFile("keyutil_" + fqdn + '_', ".key");
250         keyFile.deleteOnExit();
251 
252         OutputStream keyOut = new FileOutputStream(keyFile);
253         try {
254             keyOut.write(keyText.getBytes(CharsetUtil.US_ASCII));
255             keyOut.close();
256             keyOut = null;
257         } finally {
258             if (keyOut != null) {
259                 safeClose(keyFile, keyOut);
260                 safeDelete(keyFile);
261             }
262         }
263 
264         wrappedBuf = Unpooled.wrappedBuffer(cert.getEncoded());
265         final String certText;
266         try {
267             encodedBuf = Base64.encode(wrappedBuf, true);
268             try {
269                 // Encode the certificate into a CRT file.
270                 certText = "-----BEGIN CERTIFICATE-----\n" +
271                            encodedBuf.toString(CharsetUtil.US_ASCII) +
272                            "\n-----END CERTIFICATE-----\n";
273             } finally {
274                 encodedBuf.release();
275             }
276         } finally {
277             wrappedBuf.release();
278         }
279 
280         File certFile = File.createTempFile("keyutil_" + fqdn + '_', ".crt");
281         certFile.deleteOnExit();
282 
283         OutputStream certOut = new FileOutputStream(certFile);
284         try {
285             certOut.write(certText.getBytes(CharsetUtil.US_ASCII));
286             certOut.close();
287             certOut = null;
288         } finally {
289             if (certOut != null) {
290                 safeClose(certFile, certOut);
291                 safeDelete(certFile);
292                 safeDelete(keyFile);
293             }
294         }
295 
296         return new String[] { certFile.getPath(), keyFile.getPath() };
297     }
298 
299     private static void safeDelete(File certFile) {
300         if (!certFile.delete()) {
301             if (logger.isWarnEnabled()) {
302                 logger.warn("Failed to delete a file: " + certFile);
303             }
304         }
305     }
306 
307     private static void safeClose(File keyFile, OutputStream keyOut) {
308         try {
309             keyOut.close();
310         } catch (IOException e) {
311             if (logger.isWarnEnabled()) {
312                 logger.warn("Failed to close a file: " + keyFile, e);
313             }
314         }
315     }
316 }