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 }