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