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    *   https://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.ObjectUtil;
24  import io.netty.util.internal.PlatformDependent;
25  import io.netty.util.internal.SystemPropertyUtil;
26  import io.netty.util.internal.logging.InternalLogger;
27  import io.netty.util.internal.logging.InternalLoggerFactory;
28  
29  import java.io.File;
30  import java.io.FileInputStream;
31  import java.io.FileOutputStream;
32  import java.io.IOException;
33  import java.io.OutputStream;
34  import java.security.KeyPair;
35  import java.security.KeyPairGenerator;
36  import java.security.NoSuchAlgorithmException;
37  import java.security.PrivateKey;
38  import java.security.SecureRandom;
39  import java.security.cert.CertificateEncodingException;
40  import java.security.cert.CertificateException;
41  import java.security.cert.CertificateFactory;
42  import java.security.cert.X509Certificate;
43  import java.util.Date;
44  
45  /**
46   * Generates a temporary self-signed certificate for testing purposes.
47   * <p>
48   * <strong>NOTE:</strong>
49   * Never use the certificate and private key generated by this class in production.
50   * It is purely for testing purposes, and thus it is very insecure.
51   * It even uses an insecure pseudo-random generator for faster generation internally.
52   * </p><p>
53   * An X.509 certificate file and a EC/RSA private key file are generated in a system's temporary directory using
54   * {@link java.io.File#createTempFile(String, String)}, and they are deleted when the JVM exits using
55   * {@link java.io.File#deleteOnExit()}.
56   * </p><p>
57   * At first, this method tries to use OpenJDK's X.509 implementation (the {@code sun.security.x509} package).
58   * If it fails, it tries to use <a href="https://www.bouncycastle.org/">Bouncy Castle</a> as a fallback.
59   * </p>
60   */
61  public final class SelfSignedCertificate {
62  
63      private static final InternalLogger logger = InternalLoggerFactory.getInstance(SelfSignedCertificate.class);
64  
65      /** Current time minus 1 year, just in case software clock goes back due to time synchronization */
66      private static final Date DEFAULT_NOT_BEFORE = new Date(SystemPropertyUtil.getLong(
67              "io.netty.selfSignedCertificate.defaultNotBefore", System.currentTimeMillis() - 86400000L * 365));
68      /** The maximum possible value in X.509 specification: 9999-12-31 23:59:59 */
69      private static final Date DEFAULT_NOT_AFTER = new Date(SystemPropertyUtil.getLong(
70              "io.netty.selfSignedCertificate.defaultNotAfter", 253402300799000L));
71  
72      /**
73       * FIPS 140-2 encryption requires the RSA key length to be 2048 bits or greater.
74       * Let's use that as a sane default but allow the default to be set dynamically
75       * for those that need more stringent security requirements.
76       */
77      private static final int DEFAULT_KEY_LENGTH_BITS =
78              SystemPropertyUtil.getInt("io.netty.handler.ssl.util.selfSignedKeyStrength", 2048);
79  
80      private final File certificate;
81      private final File privateKey;
82      private final X509Certificate cert;
83      private final PrivateKey key;
84  
85      /**
86       * Creates a new instance.
87       * <p> Algorithm: RSA </p>
88       */
89      public SelfSignedCertificate() throws CertificateException {
90          this(new Builder());
91      }
92  
93      /**
94       * Creates a new instance.
95       * <p> Algorithm: RSA </p>
96       *
97       * @param notBefore Certificate is not valid before this time
98       * @param notAfter  Certificate is not valid after this time
99       */
100     public SelfSignedCertificate(Date notBefore, Date notAfter)
101             throws CertificateException {
102         this(new Builder().notBefore(notBefore).notAfter(notAfter));
103     }
104 
105     /**
106      * Creates a new instance.
107      *
108      * @param notBefore Certificate is not valid before this time
109      * @param notAfter  Certificate is not valid after this time
110      * @param algorithm Key pair algorithm
111      * @param bits      the number of bits of the generated private key
112      */
113     public SelfSignedCertificate(Date notBefore, Date notAfter, String algorithm, int bits)
114             throws CertificateException {
115         this(new Builder().notBefore(notBefore).notAfter(notAfter).algorithm(algorithm).bits(bits));
116     }
117 
118     /**
119      * Creates a new instance.
120      * <p> Algorithm: RSA </p>
121      *
122      * @param fqdn a fully qualified domain name
123      */
124     public SelfSignedCertificate(String fqdn) throws CertificateException {
125         this(new Builder().fqdn(fqdn));
126     }
127 
128     /**
129      * Creates a new instance.
130      *
131      * @param fqdn      a fully qualified domain name
132      * @param algorithm Key pair algorithm
133      * @param bits      the number of bits of the generated private key
134      */
135     public SelfSignedCertificate(String fqdn, String algorithm, int bits) throws CertificateException {
136         this(new Builder().fqdn(fqdn).algorithm(algorithm).bits(bits));
137     }
138 
139     /**
140      * Creates a new instance.
141      * <p> Algorithm: RSA </p>
142      *
143      * @param fqdn      a fully qualified domain name
144      * @param notBefore Certificate is not valid before this time
145      * @param notAfter  Certificate is not valid after this time
146      */
147     public SelfSignedCertificate(String fqdn, Date notBefore, Date notAfter) throws CertificateException {
148         this(new Builder().fqdn(fqdn).notBefore(notBefore).notAfter(notAfter));
149     }
150 
151     /**
152      * Creates a new instance.
153      *
154      * @param fqdn      a fully qualified domain name
155      * @param notBefore Certificate is not valid before this time
156      * @param notAfter  Certificate is not valid after this time
157      * @param algorithm Key pair algorithm
158      * @param bits      the number of bits of the generated private key
159      */
160     public SelfSignedCertificate(String fqdn, Date notBefore, Date notAfter, String algorithm, int bits)
161             throws CertificateException {
162         this(new Builder().fqdn(fqdn).notBefore(notBefore).notAfter(notAfter).algorithm(algorithm).bits(bits));
163     }
164 
165     /**
166      * Creates a new instance.
167      * <p> Algorithm: RSA </p>
168      *
169      * @param fqdn      a fully qualified domain name
170      * @param random    the {@link SecureRandom} to use
171      * @param bits      the number of bits of the generated private key
172      */
173     public SelfSignedCertificate(String fqdn, SecureRandom random, int bits)
174             throws CertificateException {
175         this(new Builder().fqdn(fqdn).random(random).bits(bits));
176     }
177 
178     /**
179      * Creates a new instance.
180      *
181      * @param fqdn      a fully qualified domain name
182      * @param random    the {@link SecureRandom} to use
183      * @param algorithm Key pair algorithm
184      * @param bits      the number of bits of the generated private key
185      */
186     public SelfSignedCertificate(String fqdn, SecureRandom random, String algorithm, int bits)
187             throws CertificateException {
188         this(new Builder().fqdn(fqdn).random(random).algorithm(algorithm).bits(bits));
189     }
190 
191     /**
192      * Creates a new instance.
193      * <p> Algorithm: RSA </p>
194      *
195      * @param fqdn      a fully qualified domain name
196      * @param random    the {@link SecureRandom} to use
197      * @param bits      the number of bits of the generated private key
198      * @param notBefore Certificate is not valid before this time
199      * @param notAfter  Certificate is not valid after this time
200      */
201     public SelfSignedCertificate(String fqdn, SecureRandom random, int bits, Date notBefore, Date notAfter)
202             throws CertificateException {
203         this(new Builder().fqdn(fqdn).notBefore(notBefore).notAfter(notAfter).random(random).bits(bits));
204     }
205 
206     /**
207      * Creates a new instance.
208      *
209      * @param fqdn      a fully qualified domain name
210      * @param random    the {@link SecureRandom} to use
211      * @param bits      the number of bits of the generated private key
212      * @param notBefore Certificate is not valid before this time
213      * @param notAfter  Certificate is not valid after this time
214      * @param algorithm Key pair algorithm
215      */
216     public SelfSignedCertificate(String fqdn, SecureRandom random, int bits, Date notBefore, Date notAfter,
217                                  String algorithm) throws CertificateException {
218         this(new Builder().fqdn(fqdn).random(random).algorithm(algorithm).bits(bits)
219                 .notBefore(notBefore).notAfter(notAfter));
220     }
221 
222     private SelfSignedCertificate(Builder builder) throws CertificateException {
223         if (!builder.generateBc()) {
224             if (!builder.generateKeytool()) {
225                 if (!builder.generateSunMiscSecurity()) {
226                     // last exception is always from generateSunMiscSecurity, so we can cast here
227                     throw (CertificateException) builder.failure;
228                 }
229             }
230         }
231 
232         certificate = new File(builder.paths[0]);
233         privateKey = new File(builder.paths[1]);
234         key = builder.privateKey;
235         try (FileInputStream certificateInput = new FileInputStream(certificate)) {
236             cert = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(certificateInput);
237         } catch (Exception e) {
238             throw new CertificateEncodingException(e);
239         }
240     }
241 
242     public static Builder builder() {
243         return new Builder();
244     }
245 
246     /**
247      * Returns the generated X.509 certificate file in PEM format.
248      */
249     public File certificate() {
250         return certificate;
251     }
252 
253     /**
254      * Returns the generated EC/RSA private key file in PEM format.
255      */
256     public File privateKey() {
257         return privateKey;
258     }
259 
260     /**
261      *  Returns the generated X.509 certificate.
262      */
263     public X509Certificate cert() {
264         return cert;
265     }
266 
267     /**
268      * Returns the generated EC/RSA private key.
269      */
270     public PrivateKey key() {
271         return key;
272     }
273 
274     /**
275      * Deletes the generated X.509 certificate file and EC/RSA private key file.
276      */
277     public void delete() {
278         safeDelete(certificate);
279         safeDelete(privateKey);
280     }
281 
282     static String[] newSelfSignedCertificate(
283             String fqdn, PrivateKey key, X509Certificate cert) throws IOException, CertificateEncodingException {
284         // Encode the private key into a file.
285         ByteBuf wrappedBuf = Unpooled.wrappedBuffer(key.getEncoded());
286         ByteBuf encodedBuf;
287         final String keyText;
288         try {
289             encodedBuf = Base64.encode(wrappedBuf, true);
290             try {
291                 keyText = "-----BEGIN PRIVATE KEY-----\n" +
292                         encodedBuf.toString(CharsetUtil.US_ASCII) +
293                         "\n-----END PRIVATE KEY-----\n";
294             } finally {
295                 encodedBuf.release();
296             }
297         } finally {
298             wrappedBuf.release();
299         }
300 
301         // Change all asterisk to 'x' for file name safety.
302         fqdn = fqdn.replaceAll("[^\\w.-]", "x");
303 
304         File keyFile = PlatformDependent.createTempFile("keyutil_" + fqdn + '_', ".key", null);
305         keyFile.deleteOnExit();
306 
307         OutputStream keyOut = new FileOutputStream(keyFile);
308         try {
309             keyOut.write(keyText.getBytes(CharsetUtil.US_ASCII));
310             keyOut.close();
311             keyOut = null;
312         } finally {
313             if (keyOut != null) {
314                 safeClose(keyFile, keyOut);
315                 safeDelete(keyFile);
316             }
317         }
318 
319         wrappedBuf = Unpooled.wrappedBuffer(cert.getEncoded());
320         final String certText;
321         try {
322             encodedBuf = Base64.encode(wrappedBuf, true);
323             try {
324                 // Encode the certificate into a CRT file.
325                 certText = "-----BEGIN CERTIFICATE-----\n" +
326                         encodedBuf.toString(CharsetUtil.US_ASCII) +
327                         "\n-----END CERTIFICATE-----\n";
328             } finally {
329                 encodedBuf.release();
330             }
331         } finally {
332             wrappedBuf.release();
333         }
334 
335         File certFile = PlatformDependent.createTempFile("keyutil_" + fqdn + '_', ".crt", null);
336         certFile.deleteOnExit();
337 
338         OutputStream certOut = new FileOutputStream(certFile);
339         try {
340             certOut.write(certText.getBytes(CharsetUtil.US_ASCII));
341             certOut.close();
342             certOut = null;
343         } finally {
344             if (certOut != null) {
345                 safeClose(certFile, certOut);
346                 safeDelete(certFile);
347                 safeDelete(keyFile);
348             }
349         }
350 
351         return new String[] { certFile.getPath(), keyFile.getPath() };
352     }
353 
354     private static void safeDelete(File certFile) {
355         if (!certFile.delete()) {
356             if (logger.isWarnEnabled()) {
357                 logger.warn("Failed to delete a file: " + certFile);
358             }
359         }
360     }
361 
362     private static void safeClose(File keyFile, OutputStream keyOut) {
363         try {
364             keyOut.close();
365         } catch (IOException e) {
366             if (logger.isWarnEnabled()) {
367                 logger.warn("Failed to close a file: " + keyFile, e);
368             }
369         }
370     }
371 
372     private static boolean isBouncyCastleAvailable() {
373         try {
374             // this class is in bcpkix, both fips and non-fips
375             Class.forName("org.bouncycastle.cert.X509v3CertificateBuilder");
376             return true;
377         } catch (ClassNotFoundException e) {
378             return false;
379         }
380     }
381 
382     public static final class Builder {
383         // user fields
384         String fqdn = "localhost";
385         SecureRandom random;
386         int bits = DEFAULT_KEY_LENGTH_BITS;
387         Date notBefore = DEFAULT_NOT_BEFORE;
388         Date notAfter = DEFAULT_NOT_AFTER;
389         String algorithm = "RSA";
390 
391         // fields that are populated on demand
392         Throwable failure;
393         KeyPair keypair;
394         PrivateKey privateKey;
395         String[] paths;
396 
397         private Builder() {
398         }
399 
400         /**
401          * Set the fully-qualified domain name of the certificate that should be generated.
402          *
403          * @param fqdn The FQDN
404          * @return This builder
405          */
406         public Builder fqdn(String fqdn) {
407             this.fqdn = ObjectUtil.checkNotNullWithIAE(fqdn, "fqdn");
408             return this;
409         }
410 
411         /**
412          * Set the RNG to use for key generation. This setting is not supported by the keytool-based generator.
413          *
414          * @param random The CSPRNG
415          * @return This builder
416          */
417         public Builder random(SecureRandom random) {
418             this.random = random;
419             return this;
420         }
421 
422         /**
423          * Set the key size.
424          *
425          * @param bits The key size
426          * @return This builder
427          */
428         public Builder bits(int bits) {
429             this.bits = bits;
430             return this;
431         }
432 
433         /**
434          * Set the start of the certificate validity period.
435          *
436          * @param notBefore The start date
437          * @return This builder
438          */
439         public Builder notBefore(Date notBefore) {
440             this.notBefore = ObjectUtil.checkNotNullWithIAE(notBefore, "notBefore");
441             return this;
442         }
443 
444         /**
445          * Set the end of the certificate validity period.
446          *
447          * @param notAfter The start date
448          * @return This builder
449          */
450         public Builder notAfter(Date notAfter) {
451             this.notAfter = ObjectUtil.checkNotNullWithIAE(notAfter, "notAfter");
452             return this;
453         }
454 
455         /**
456          * Set the key algorithm. Only RSA and EC are supported.
457          *
458          * @param algorithm The key algorithm
459          * @return This builder
460          */
461         public Builder algorithm(String algorithm) {
462             if ("EC".equalsIgnoreCase(algorithm)) {
463                 this.algorithm = "EC";
464             } else if ("RSA".equalsIgnoreCase(algorithm)) {
465                 this.algorithm = "RSA";
466             } else {
467                 throw new IllegalArgumentException("Algorithm not valid: " + algorithm);
468             }
469             return this;
470         }
471 
472         private SecureRandom randomOrDefault() {
473             // Bypass entropy collection by using insecure random generator.
474             // We just want to generate it without any delay because it's for testing purposes only.
475             return random == null ? ThreadLocalInsecureRandom.current() : random;
476         }
477 
478         private void generateKeyPairLocally() {
479             if (keypair != null) {
480                 return;
481             }
482 
483             try {
484                 KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm);
485                 keyGen.initialize(bits, randomOrDefault());
486                 keypair = keyGen.generateKeyPair();
487             } catch (NoSuchAlgorithmException e) {
488                 // Should not reach here because every Java implementation must have RSA and EC key pair generator.
489                 throw new IllegalStateException(e);
490             }
491             privateKey = keypair.getPrivate();
492         }
493 
494         private void addFailure(Throwable t) {
495             if (failure != null) {
496                 t.addSuppressed(failure);
497             }
498             failure = t;
499         }
500 
501         boolean generateBc() {
502             if (!isBouncyCastleAvailable()) {
503                 // no need to even try. We can avoid generating the key pair with this check.
504                 logger.debug("Failed to generate a self-signed X.509 certificate because " +
505                         "BouncyCastle PKIX is not available in classpath");
506                 return false;
507             }
508             generateKeyPairLocally();
509             try {
510                 // Try Bouncy Castle first as otherwise we will see an IllegalAccessError on more recent JDKs.
511                 paths = BouncyCastleSelfSignedCertGenerator.generate(
512                         fqdn, keypair, randomOrDefault(), notBefore, notAfter, algorithm);
513                 return true;
514             } catch (Throwable t) {
515                 logger.debug("Failed to generate a self-signed X.509 certificate using Bouncy Castle:", t);
516                 addFailure(t);
517                 return false;
518             }
519         }
520 
521         boolean generateKeytool() {
522             if (!KeytoolSelfSignedCertGenerator.isAvailable()) {
523                 logger.debug("Not attempting to generate certificate with keytool because keytool is missing");
524                 return false;
525             }
526             if (random != null) {
527                 logger.debug("Not attempting to generate certificate with keytool because of explicitly set " +
528                         "SecureRandom");
529                 return false;
530             }
531             try {
532                 KeytoolSelfSignedCertGenerator.generate(this);
533                 return true;
534             } catch (Throwable t) {
535                 logger.debug("Failed to generate a self-signed X.509 certificate using keytool:", t);
536                 addFailure(t);
537                 return false;
538             }
539         }
540 
541         boolean generateSunMiscSecurity() {
542             generateKeyPairLocally();
543             try {
544                 // Try the OpenJDK's proprietary implementation.
545                 paths = OpenJdkSelfSignedCertGenerator.generate(
546                         fqdn, keypair, randomOrDefault(), notBefore, notAfter, algorithm);
547                 return true;
548             } catch (Throwable t2) {
549                 logger.debug("Failed to generate a self-signed X.509 certificate using sun.security.x509:", t2);
550                 final CertificateException certificateException = new CertificateException(
551                         "No provider succeeded to generate a self-signed certificate. " +
552                                 "See debug log for the root cause.", t2);
553                 addFailure(certificateException);
554                 return false;
555             }
556         }
557 
558         /**
559          * Build the certificate. This builder must not be used again after this method is called.
560          *
561          * @return The certificate
562          * @throws CertificateException If generation fails
563          */
564         public SelfSignedCertificate build() throws CertificateException {
565             return new SelfSignedCertificate(this);
566         }
567     }
568 }