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