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