View Javadoc
1   /*
2    * Copyright 2021 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.codec.quic;
18  
19  import io.netty.handler.ssl.ClientAuth;
20  import io.netty.handler.ssl.SslContextOption;
21  import io.netty.handler.ssl.util.KeyManagerFactoryWrapper;
22  import io.netty.handler.ssl.util.TrustManagerFactoryWrapper;
23  import io.netty.util.Mapping;
24  import org.jetbrains.annotations.Nullable;
25  
26  import javax.net.ssl.KeyManager;
27  import javax.net.ssl.KeyManagerFactory;
28  import javax.net.ssl.SSLEngine;
29  import javax.net.ssl.TrustManager;
30  import javax.net.ssl.TrustManagerFactory;
31  import javax.net.ssl.X509ExtendedKeyManager;
32  import java.io.File;
33  import java.net.Socket;
34  import java.security.KeyStore;
35  import java.security.Principal;
36  import java.security.PrivateKey;
37  import java.security.cert.X509Certificate;
38  import java.util.ArrayList;
39  import java.util.HashMap;
40  import java.util.List;
41  import java.util.Map;
42  
43  import static io.netty.util.internal.ObjectUtil.checkNotNull;
44  
45  /**
46   * Builder for configuring a new SslContext for creation.
47   */
48  public final class QuicSslContextBuilder {
49  
50      /**
51       * Special {@link X509ExtendedKeyManager} implementation which will just fail the certificate selection.
52       * This is used as a "dummy" implementation when SNI is used as we should always select an other
53       * {@link QuicSslContext} based on the provided hostname.
54       */
55      private static final X509ExtendedKeyManager SNI_KEYMANAGER = new X509ExtendedKeyManager() {
56          private final X509Certificate[] emptyCerts = new X509Certificate[0];
57          private final String[] emptyStrings = new String[0];
58  
59          @Override
60          public String[] getClientAliases(String keyType, Principal[] issuers) {
61              return emptyStrings;
62          }
63  
64          @Override
65          @Nullable
66          public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
67              return null;
68          }
69  
70          @Override
71          public String[] getServerAliases(String keyType, Principal[] issuers) {
72              return emptyStrings;
73          }
74  
75          @Override
76          @Nullable
77          public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
78              return null;
79          }
80  
81          @Override
82          public X509Certificate[] getCertificateChain(String alias) {
83              return emptyCerts;
84          }
85  
86          @Override
87          @Nullable
88          public PrivateKey getPrivateKey(String alias) {
89              return null;
90          }
91      };
92  
93      /**
94       * Creates a builder for new client-side {@link QuicSslContext} that can be used for {@code QUIC}.
95       */
96      public static QuicSslContextBuilder forClient() {
97          return new QuicSslContextBuilder(false);
98      }
99  
100     /**
101      * Creates a builder for new server-side {@link QuicSslContext} that can be used for {@code QUIC}.
102      *
103      * @param keyFile a PKCS#8 private key file in PEM format
104      * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not
105      *     password-protected
106      * @param certChainFile an X.509 certificate chain file in PEM format
107      * @see #keyManager(File, String, File)
108      */
109     public static QuicSslContextBuilder forServer(
110             File keyFile, @Nullable String keyPassword, File certChainFile) {
111         return new QuicSslContextBuilder(true).keyManager(keyFile, keyPassword, certChainFile);
112     }
113 
114     /**
115      * Creates a builder for new server-side {@link QuicSslContext} that can be used for {@code QUIC}.
116      *
117      * @param key a PKCS#8 private key
118      * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not
119      *     password-protected
120      * @param certChain the X.509 certificate chain
121      * @see #keyManager(File, String, File)
122      */
123     public static QuicSslContextBuilder forServer(
124             PrivateKey key, @Nullable String keyPassword, X509Certificate... certChain) {
125         return new QuicSslContextBuilder(true).keyManager(key, keyPassword, certChain);
126     }
127 
128     /**
129      * Creates a builder for new server-side {@link QuicSslContext} that can be used for {@code QUIC}.
130      *
131      * @param keyManagerFactory non-{@code null} factory for server's private key
132      * @see #keyManager(KeyManagerFactory, String)
133      */
134     public static QuicSslContextBuilder forServer(KeyManagerFactory keyManagerFactory, @Nullable String password) {
135         return new QuicSslContextBuilder(true).keyManager(keyManagerFactory, password);
136     }
137 
138     /**
139      * Creates a builder for new server-side {@link QuicSslContext} with {@link KeyManager} that can be used for
140      * {@code QUIC}.
141      *
142      * @param keyManager non-{@code null} KeyManager for server's private key
143      * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not
144      *     password-protected
145      */
146     public static QuicSslContextBuilder forServer(KeyManager keyManager, @Nullable String keyPassword) {
147         return new QuicSslContextBuilder(true).keyManager(keyManager, keyPassword);
148     }
149 
150     /**
151      * Enables support for
152      * <a href="https://quicwg.org/ops-drafts/draft-ietf-quic-manageability.html#name-server-name-indication-sni">
153      *     SNI</a> on the server side.
154      *
155      * @param mapping   the {@link Mapping} that is used to map names to the {@link QuicSslContext} to use.
156      *                  Usually using {@link io.netty.util.DomainWildcardMappingBuilder} should be used
157      *                  to create the {@link Mapping}.
158      */
159     public static QuicSslContext buildForServerWithSni(Mapping<? super String, ? extends QuicSslContext> mapping) {
160         return forServer(SNI_KEYMANAGER, null).sni(mapping).build();
161     }
162 
163     private static final Map.Entry[] EMPTY_ENTRIES = new Map.Entry[0];
164 
165     private final boolean forServer;
166     private final Map<SslContextOption<?>, Object> options = new HashMap<>();
167     private TrustManagerFactory trustManagerFactory;
168     private String keyPassword;
169     private KeyManagerFactory keyManagerFactory;
170     private long sessionCacheSize = 20480;
171     private long sessionTimeout = 300;
172     private ClientAuth clientAuth = ClientAuth.NONE;
173     private String[] applicationProtocols;
174     private Boolean earlyData;
175     private BoringSSLKeylog keylog;
176     private Mapping<? super String, ? extends QuicSslContext> mapping;
177 
178     private QuicSslContextBuilder(boolean forServer) {
179         this.forServer = forServer;
180     }
181 
182     private QuicSslContextBuilder sni(Mapping<? super String, ? extends QuicSslContext> mapping) {
183         this.mapping = checkNotNull(mapping, "mapping");
184         return this;
185     }
186 
187     /**
188      * Configure a {@link SslContextOption}.
189      */
190     public <T> QuicSslContextBuilder option(SslContextOption<T> option, T value) {
191         if (value == null) {
192             options.remove(option);
193         } else {
194             options.put(option, value);
195         }
196         return this;
197     }
198 
199     /**
200      * Enable / disable the usage of early data.
201      */
202     public QuicSslContextBuilder earlyData(boolean enabled) {
203         this.earlyData = enabled;
204         return this;
205     }
206 
207     /**
208      * Enable / disable keylog. When enabled, TLS keys are logged to an internal logger named
209      * "io.netty.handler.codec.quic.BoringSSLLogginKeylog" with DEBUG level, see
210      * {@link BoringSSLKeylog} for detail, logging keys are following
211      * <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format">
212      *     NSS Key Log Format</a>. This is intended for debugging use with tools like Wireshark.
213      */
214     public QuicSslContextBuilder keylog(boolean enabled) {
215         keylog(enabled ? BoringSSLLoggingKeylog.INSTANCE : null);
216         return this;
217     }
218 
219     /**
220      * Enable / disable keylog. When enabled, TLS keys are logged to {@link BoringSSLKeylog#logKey(SSLEngine, String)}
221      * logging keys are following
222      * <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format">
223      *     NSS Key Log Format</a>. This is intended for debugging use with tools like Wireshark.
224      */
225     public QuicSslContextBuilder keylog(@Nullable BoringSSLKeylog keylog) {
226         this.keylog = keylog;
227         return this;
228     }
229 
230     /**
231      * Trusted certificates for verifying the remote endpoint's certificate. The file should
232      * contain an X.509 certificate collection in PEM format. {@code null} uses the system default
233      * which only works with Java 8u261 and later as these versions support TLS1.3,
234      * see <a href="https://www.oracle.com/java/technologies/javase/8u261-relnotes.html">
235      *     JDK 8u261 Update Release Notes</a>
236      */
237     public QuicSslContextBuilder trustManager(@Nullable File trustCertCollectionFile) {
238         try {
239             return trustManager(QuicheQuicSslContext.toX509Certificates0(trustCertCollectionFile));
240         } catch (Exception e) {
241             throw new IllegalArgumentException("File does not contain valid certificates: "
242                     + trustCertCollectionFile, e);
243         }
244     }
245 
246     /**
247      * Trusted certificates for verifying the remote endpoint's certificate. {@code null} uses the system default
248      * which only works with Java 8u261 and later as these versions support TLS1.3,
249      * see <a href="https://www.oracle.com/java/technologies/javase/8u261-relnotes.html">
250      *     JDK 8u261 Update Release Notes</a>
251      */
252     public QuicSslContextBuilder trustManager(X509Certificate @Nullable ... trustCertCollection) {
253         try {
254             return trustManager(QuicheQuicSslContext.buildTrustManagerFactory0(trustCertCollection));
255         } catch (Exception e) {
256             throw new IllegalArgumentException(e);
257         }
258     }
259 
260     /**
261      * Trusted manager for verifying the remote endpoint's certificate. {@code null} uses the system default
262      * which only works with Java 8u261 and later as these versions support TLS1.3,
263      * see <a href="https://www.oracle.com/java/technologies/javase/8u261-relnotes.html">
264      *     JDK 8u261 Update Release Notes</a>
265      */
266     public QuicSslContextBuilder trustManager(@Nullable TrustManagerFactory trustManagerFactory) {
267         this.trustManagerFactory = trustManagerFactory;
268         return this;
269     }
270 
271     /**
272      * A single trusted manager for verifying the remote endpoint's certificate.
273      * This is helpful when custom implementation of {@link TrustManager} is needed.
274      * Internally, a simple wrapper of {@link TrustManagerFactory} that only produces this
275      * specified {@link TrustManager} will be created, thus all the requirements specified in
276      * {@link #trustManager(TrustManagerFactory trustManagerFactory)} also apply here.
277      */
278     public QuicSslContextBuilder trustManager(TrustManager trustManager) {
279         return trustManager(new TrustManagerFactoryWrapper(trustManager));
280     }
281 
282     /**
283      * Identifying certificate for this host. {@code keyCertChainFile} and {@code keyFile} may
284      * be {@code null} for client contexts, which disables mutual authentication.
285      *
286      * @param keyFile a PKCS#8 private key file in PEM format
287      * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not
288      *     password-protected
289      * @param keyCertChainFile an X.509 certificate chain file in PEM format
290      */
291     public QuicSslContextBuilder keyManager(
292             @Nullable File keyFile, @Nullable String keyPassword, @Nullable File keyCertChainFile) {
293         X509Certificate[] keyCertChain;
294         PrivateKey key;
295         try {
296             keyCertChain = QuicheQuicSslContext.toX509Certificates0(keyCertChainFile);
297         } catch (Exception e) {
298             throw new IllegalArgumentException("File does not contain valid certificates: " + keyCertChainFile, e);
299         }
300         try {
301             key = QuicheQuicSslContext.toPrivateKey0(keyFile, keyPassword);
302         } catch (Exception e) {
303             throw new IllegalArgumentException("File does not contain valid private key: " + keyFile, e);
304         }
305         return keyManager(key, keyPassword, keyCertChain);
306     }
307 
308     /**
309      * Identifying certificate for this host. {@code keyCertChain} and {@code key} may
310      * be {@code null} for client contexts, which disables mutual authentication.
311      *
312      * @param key a PKCS#8 private key file
313      * @param keyPassword the password of the {@code key}, or {@code null} if it's not
314      *     password-protected
315      * @param certChain an X.509 certificate chain
316      */
317     public QuicSslContextBuilder keyManager(
318             @Nullable PrivateKey key, @Nullable String keyPassword, X509Certificate @Nullable ... certChain) {
319         try {
320             java.security.KeyStore ks = java.security.KeyStore.getInstance(KeyStore.getDefaultType());
321             ks.load(null);
322             char[] pass = keyPassword == null ? new char[0]: keyPassword.toCharArray();
323             ks.setKeyEntry("alias", key, pass, certChain);
324             KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
325                     KeyManagerFactory.getDefaultAlgorithm());
326             keyManagerFactory.init(ks, pass);
327             return keyManager(keyManagerFactory, keyPassword);
328         } catch (Exception e) {
329             throw new IllegalArgumentException(e);
330         }
331     }
332 
333     /**
334      * Identifying manager for this host. {@code keyManagerFactory} may be {@code null} for
335      * client contexts, which disables mutual authentication.
336      */
337     public QuicSslContextBuilder keyManager(
338             @Nullable KeyManagerFactory keyManagerFactory, @Nullable String keyPassword) {
339         this.keyPassword = keyPassword;
340         this.keyManagerFactory = keyManagerFactory;
341         return this;
342     }
343 
344     /**
345      * A single key manager managing the identity information of this host.
346      * This is helpful when custom implementation of {@link KeyManager} is needed.
347      * Internally, a wrapper of {@link KeyManagerFactory} that only produces this specified
348      * {@link KeyManager} will be created, thus all the requirements specified in
349      * {@link #keyManager(KeyManagerFactory, String)} also apply here.
350      */
351     public QuicSslContextBuilder keyManager(KeyManager keyManager, @Nullable String password) {
352         return keyManager(new KeyManagerFactoryWrapper(keyManager), password);
353     }
354 
355     /**
356      * Application protocol negotiation configuration. {@code null} disables support.
357      */
358     public QuicSslContextBuilder applicationProtocols(String @Nullable ... applicationProtocols) {
359         this.applicationProtocols = applicationProtocols;
360         return this;
361     }
362 
363     /**
364      * Set the size of the cache used for storing SSL session objects. {@code 0} to use the
365      * default value.
366      */
367     public QuicSslContextBuilder sessionCacheSize(long sessionCacheSize) {
368         this.sessionCacheSize = sessionCacheSize;
369         return this;
370     }
371 
372     /**
373      * Set the timeout for the cached SSL session objects, in seconds. {@code 0} to use the
374      * default value.
375      */
376     public QuicSslContextBuilder sessionTimeout(long sessionTimeout) {
377         this.sessionTimeout = sessionTimeout;
378         return this;
379     }
380 
381     /**
382      * Sets the client authentication mode.
383      */
384     public QuicSslContextBuilder clientAuth(ClientAuth clientAuth) {
385         if (!forServer) {
386             throw new UnsupportedOperationException("Only supported for server");
387         }
388         this.clientAuth = checkNotNull(clientAuth, "clientAuth");
389         return this;
390     }
391 
392     /**
393      * Create new {@link QuicSslContext} instance with configured settings that can be used for {@code QUIC}.
394      *
395      */
396     public QuicSslContext build() {
397         if (forServer) {
398             return new QuicheQuicSslContext(true, sessionTimeout, sessionCacheSize, clientAuth, trustManagerFactory,
399                     keyManagerFactory, keyPassword, mapping, earlyData, keylog,
400                     applicationProtocols, toArray(options.entrySet(), EMPTY_ENTRIES));
401         } else {
402             return new QuicheQuicSslContext(false, sessionTimeout, sessionCacheSize, clientAuth, trustManagerFactory,
403                     keyManagerFactory, keyPassword, mapping, earlyData, keylog,
404                     applicationProtocols, toArray(options.entrySet(), EMPTY_ENTRIES));
405         }
406     }
407 
408     private static <T> T[] toArray(Iterable<? extends T> iterable, T[] prototype) {
409         if (iterable == null) {
410             return null;
411         }
412         final List<T> list = new ArrayList<>();
413         for (T element : iterable) {
414             list.add(element);
415         }
416         return list.toArray(prototype);
417     }
418 }