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.Unpooled;
20  import io.netty.handler.codec.base64.Base64;
21  import io.netty.util.CharsetUtil;
22  import io.netty.util.internal.logging.InternalLogger;
23  import io.netty.util.internal.logging.InternalLoggerFactory;
24  
25  import java.io.File;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.OutputStream;
29  import java.security.KeyPair;
30  import java.security.KeyPairGenerator;
31  import java.security.NoSuchAlgorithmException;
32  import java.security.PrivateKey;
33  import java.security.SecureRandom;
34  import java.security.cert.CertificateEncodingException;
35  import java.security.cert.CertificateException;
36  import java.security.cert.X509Certificate;
37  import java.util.Date;
38  
39  /**
40   * Generates a temporary self-signed certificate for testing purposes.
41   * <p>
42   * <strong>NOTE:</strong>
43   * Never use the certificate and private key generated by this class in production.
44   * It is purely for testing purposes, and thus it is very insecure.
45   * It even uses an insecure pseudo-random generator for faster generation internally.
46   * </p><p>
47   * A X.509 certificate file and a RSA private key file are generated in a system's temporary directory using
48   * {@link java.io.File#createTempFile(String, String)}, and they are deleted when the JVM exits using
49   * {@link java.io.File#deleteOnExit()}.
50   * </p><p>
51   * At first, this method tries to use OpenJDK's X.509 implementation (the {@code sun.security.x509} package).
52   * If it fails, it tries to use <a href="http://www.bouncycastle.org/">Bouncy Castle</a> as a fallback.
53   * </p>
54   */
55  public final class SelfSignedCertificate {
56  
57      private static final InternalLogger logger = InternalLoggerFactory.getInstance(SelfSignedCertificate.class);
58  
59      /** Current time minus 1 year, just in case software clock goes back due to time synchronization */
60      static final Date NOT_BEFORE = new Date(System.currentTimeMillis() - 86400000L * 365);
61      /** The maximum possible value in X.509 specification: 9999-12-31 23:59:59 */
62      static final Date NOT_AFTER = new Date(253402300799000L);
63  
64      private final File certificate;
65      private final File privateKey;
66  
67      /**
68       * Creates a new instance.
69       */
70      public SelfSignedCertificate() throws CertificateException {
71          this("example.com");
72      }
73  
74      /**
75       * Creates a new instance.
76       *
77       * @param fqdn a fully qualified domain name
78       */
79      public SelfSignedCertificate(String fqdn) throws CertificateException {
80          // Bypass entrophy collection by using insecure random generator.
81          // We just want to generate it without any delay because it's for testing purposes only.
82          this(fqdn, ThreadLocalInsecureRandom.current(), 1024);
83      }
84  
85      /**
86       * Creates a new instance.
87       *
88       * @param fqdn a fully qualified domain name
89       * @param random the {@link java.security.SecureRandom} to use
90       * @param bits the number of bits of the generated private key
91       */
92      public SelfSignedCertificate(String fqdn, SecureRandom random, int bits) throws CertificateException {
93          // Generate an RSA key pair.
94          final KeyPair keypair;
95          try {
96              KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
97              keyGen.initialize(bits, random);
98              keypair = keyGen.generateKeyPair();
99          } catch (NoSuchAlgorithmException e) {
100             // Should not reach here because every Java implementation must have RSA key pair generator.
101             throw new Error(e);
102         }
103 
104         String[] paths;
105         try {
106             // Try the OpenJDK's proprietary implementation.
107             paths = OpenJdkSelfSignedCertGenerator.generate(fqdn, keypair, random);
108         } catch (Throwable t) {
109             logger.debug("Failed to generate a self-signed X.509 certificate using sun.security.x509:", t);
110             try {
111                 // Try Bouncy Castle if the current JVM didn't have sun.security.x509.
112                 paths = BouncyCastleSelfSignedCertGenerator.generate(fqdn, keypair, random);
113             } catch (Throwable t2) {
114                 logger.debug("Failed to generate a self-signed X.509 certificate using Bouncy Castle:", t2);
115                 throw new CertificateException(
116                         "No provider succeeded to generate a self-signed certificate. " +
117                                 "See debug log for the root cause.");
118             }
119         }
120 
121         certificate = new File(paths[0]);
122         privateKey = new File(paths[1]);
123     }
124 
125     /**
126      * Returns the generated X.509 certificate file in PEM format.
127      */
128     public File certificate() {
129         return certificate;
130     }
131 
132     /**
133      * Returns the generated RSA private key file in PEM format.
134      */
135     public File privateKey() {
136         return privateKey;
137     }
138 
139     /**
140      * Deletes the generated X.509 certificate file and RSA private key file.
141      */
142     public void delete() {
143         safeDelete(certificate);
144         safeDelete(privateKey);
145     }
146 
147     static String[] newSelfSignedCertificate(
148             String fqdn, PrivateKey key, X509Certificate cert) throws IOException, CertificateEncodingException {
149 
150         // Encode the private key into a file.
151         String keyText = "-----BEGIN PRIVATE KEY-----\n" +
152                 Base64.encode(Unpooled.wrappedBuffer(key.getEncoded()), true).toString(CharsetUtil.US_ASCII) +
153                 "\n-----END PRIVATE KEY-----\n";
154 
155         File keyFile = File.createTempFile("keyutil_" + fqdn + '_', ".key");
156         keyFile.deleteOnExit();
157 
158         OutputStream keyOut = new FileOutputStream(keyFile);
159         try {
160             keyOut.write(keyText.getBytes(CharsetUtil.US_ASCII));
161             keyOut.close();
162             keyOut = null;
163         } finally {
164             if (keyOut != null) {
165                 safeClose(keyFile, keyOut);
166                 safeDelete(keyFile);
167             }
168         }
169 
170         // Encode the certificate into a CRT file.
171         String certText = "-----BEGIN CERTIFICATE-----\n" +
172                 Base64.encode(Unpooled.wrappedBuffer(cert.getEncoded()), true).toString(CharsetUtil.US_ASCII) +
173                 "\n-----END CERTIFICATE-----\n";
174 
175         File certFile = File.createTempFile("keyutil_" + fqdn + '_', ".crt");
176         certFile.deleteOnExit();
177 
178         OutputStream certOut = new FileOutputStream(certFile);
179         try {
180             certOut.write(certText.getBytes(CharsetUtil.US_ASCII));
181             certOut.close();
182             certOut = null;
183         } finally {
184             if (certOut != null) {
185                 safeClose(certFile, certOut);
186                 safeDelete(certFile);
187                 safeDelete(keyFile);
188             }
189         }
190 
191         return new String[] { certFile.getPath(), keyFile.getPath() };
192     }
193 
194     private static void safeDelete(File certFile) {
195         if (!certFile.delete()) {
196             logger.warn("Failed to delete a file: " + certFile);
197         }
198     }
199 
200     private static void safeClose(File keyFile, OutputStream keyOut) {
201         try {
202             keyOut.close();
203         } catch (IOException e) {
204             logger.warn("Failed to close a file: " + keyFile, e);
205         }
206     }
207 }