View Javadoc
1   /*
2    * Copyright 2024 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.util.internal.PlatformDependent;
22  
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.InterruptedIOException;
26  import java.nio.charset.StandardCharsets;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.nio.file.Paths;
30  import java.security.GeneralSecurityException;
31  import java.security.KeyStore;
32  import java.security.cert.X509Certificate;
33  import java.time.ZoneId;
34  import java.time.format.DateTimeFormatter;
35  import java.time.temporal.ChronoUnit;
36  import java.util.Locale;
37  import java.util.concurrent.TimeUnit;
38  
39  /**
40   * Self-signed certificate generator based on the keytool CLI.
41   */
42  final class KeytoolSelfSignedCertGenerator {
43      private static final DateTimeFormatter DATE_FORMAT =
44              DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss", Locale.ROOT);
45      private static final String ALIAS = "alias";
46      private static final String PASSWORD = "insecurepassword";
47      private static final Path KEYTOOL;
48      private static final String KEY_STORE_TYPE;
49  
50      static {
51          String home = System.getProperty("java.home");
52          if (home == null) {
53              KEYTOOL = null;
54          } else {
55              Path likely = Paths.get(home).resolve("bin").resolve("keytool");
56              if (Files.exists(likely)) {
57                  KEYTOOL = likely;
58              } else {
59                  KEYTOOL = null;
60              }
61          }
62          // Java < 11 does not support encryption for PKCS#12: JDK-8220734
63          // For 11+, we prefer PKCS#12 for FIPS compliance
64          KEY_STORE_TYPE = PlatformDependent.javaVersion() >= 11 ? "PKCS12" : "JKS";
65      }
66  
67      private KeytoolSelfSignedCertGenerator() {
68      }
69  
70      static boolean isAvailable() {
71          return KEYTOOL != null;
72      }
73  
74      static void generate(SelfSignedCertificate.Builder builder) throws IOException, GeneralSecurityException {
75          // Change all asterisk to 'x' for file name safety.
76          String dirFqdn = builder.fqdn.replaceAll("[^\\w.-]", "x");
77  
78          Path directory = Files.createTempDirectory("keytool_" + dirFqdn);
79          Path keyStore = directory.resolve("keystore.jks");
80          try {
81              Process process = new ProcessBuilder()
82                      .command(
83                              "keytool",
84                              "-genkeypair",
85                              "-keyalg", builder.algorithm,
86                              "-keysize", String.valueOf(builder.bits),
87                              "-startdate", DATE_FORMAT.format(
88                                      builder.notBefore.toInstant().atZone(ZoneId.systemDefault())),
89                              "-validity", String.valueOf(builder.notBefore.toInstant().until(
90                                      builder.notAfter.toInstant(), ChronoUnit.DAYS)),
91                              "-keystore", keyStore.toString(),
92                              "-alias", ALIAS,
93                              "-keypass", PASSWORD,
94                              "-storepass", PASSWORD,
95                              "-dname", "CN=" + builder.fqdn,
96                              "-storetype", KEY_STORE_TYPE
97                      )
98                      .redirectErrorStream(true)
99                      .start();
100             try {
101                 if (!process.waitFor(60, TimeUnit.SECONDS)) {
102                     process.destroyForcibly();
103                     throw new IOException("keytool timeout");
104                 }
105             } catch (InterruptedException e) {
106                 process.destroyForcibly();
107                 Thread.currentThread().interrupt();
108                 throw new InterruptedIOException();
109             }
110 
111             if (process.exitValue() != 0) {
112                 ByteBuf buffer = Unpooled.buffer();
113                 try {
114                     try (InputStream stream = process.getInputStream()) {
115                         while (true) {
116                             if (buffer.writeBytes(stream, 4096) == -1) {
117                                 break;
118                             }
119                         }
120                     }
121                     String log = buffer.toString(StandardCharsets.UTF_8);
122                     throw new IOException("Keytool exited with status " + process.exitValue() + ": " + log);
123                 } finally {
124                     buffer.release();
125                 }
126             }
127 
128             KeyStore ks = KeyStore.getInstance(KEY_STORE_TYPE);
129             try (InputStream is = Files.newInputStream(keyStore)) {
130                 ks.load(is, PASSWORD.toCharArray());
131             }
132             KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) ks.getEntry(
133                     ALIAS, new KeyStore.PasswordProtection(PASSWORD.toCharArray()));
134             builder.paths = SelfSignedCertificate.newSelfSignedCertificate(
135                     builder.fqdn, entry.getPrivateKey(), (X509Certificate) entry.getCertificate());
136             builder.privateKey = entry.getPrivateKey();
137         } finally {
138             Files.deleteIfExists(keyStore);
139             Files.delete(directory);
140         }
141     }
142 }