a079b8b4691c4b0e472a5cdae89ed4d3837d3846
[netconf.git] / transport / transport-tls / src / main / java / org / opendaylight / netconf / transport / tls / SslHandlerFactory.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.transport.tls;
9
10 import static org.opendaylight.netconf.transport.tls.ConfigUtils.setAsymmetricKey;
11 import static org.opendaylight.netconf.transport.tls.ConfigUtils.setEndEntityCertificateWithKey;
12 import static org.opendaylight.netconf.transport.tls.ConfigUtils.setX509Certificates;
13 import static org.opendaylight.netconf.transport.tls.KeyStoreUtils.buildKeyManagerFactory;
14 import static org.opendaylight.netconf.transport.tls.KeyStoreUtils.buildTrustManagerFactory;
15 import static org.opendaylight.netconf.transport.tls.KeyStoreUtils.newKeyStore;
16
17 import com.google.common.collect.ImmutableList;
18 import com.google.common.collect.ImmutableMap;
19 import io.netty.channel.Channel;
20 import io.netty.handler.ssl.ClientAuth;
21 import io.netty.handler.ssl.SslContext;
22 import io.netty.handler.ssl.SslContextBuilder;
23 import io.netty.handler.ssl.SslHandler;
24 import java.net.SocketAddress;
25 import java.security.KeyStore;
26 import java.util.List;
27 import javax.net.ssl.KeyManagerFactory;
28 import javax.net.ssl.SSLException;
29 import javax.net.ssl.TrustManagerFactory;
30 import org.eclipse.jdt.annotation.NonNull;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
33 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.CipherSuiteAlgBase;
34 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsAes128CcmSha256;
35 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsAes128GcmSha256;
36 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsAes256GcmSha384;
37 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsChacha20Poly1305Sha256;
38 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithAes128Ccm;
39 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithAes128GcmSha256;
40 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithAes256Ccm;
41 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithAes256GcmSha384;
42 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDhePskWithChacha20Poly1305Sha256;
43 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithAes128Ccm;
44 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithAes128GcmSha256;
45 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithAes256Ccm;
46 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithAes256GcmSha384;
47 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsDheRsaWithChacha20Poly1305Sha256;
48 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheEcdsaWithAes128GcmSha256;
49 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheEcdsaWithAes256GcmSha384;
50 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheEcdsaWithChacha20Poly1305Sha256;
51 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdhePskWithAes128CcmSha256;
52 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdhePskWithAes128GcmSha256;
53 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdhePskWithAes256GcmSha384;
54 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdhePskWithChacha20Poly1305Sha256;
55 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheRsaWithAes128GcmSha256;
56 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheRsaWithAes256GcmSha384;
57 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.tls.cipher.suite.algs.rev220616.TlsEcdheRsaWithChacha20Poly1305Sha256;
58 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev231228.InlineOrKeystoreAsymmetricKeyGrouping;
59 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev231228.InlineOrKeystoreEndEntityCertWithKeyGrouping;
60 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.TlsClientGrouping;
61 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.tls.client.grouping.client.identity.auth.type.Certificate;
62 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.rev231228.tls.client.grouping.client.identity.auth.type.RawPublicKey;
63 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.common.rev231228.HelloParamsGrouping;
64 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.common.rev231228.TlsVersionBase;
65 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228.TlsServerGrouping;
66 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228.tls.server.grouping.server.identity.auth.type.RawPrivateKey;
67 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.truststore.rev231228.InlineOrTruststoreCertsGrouping;
68 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.truststore.rev231228.InlineOrTruststorePublicKeysGrouping;
69
70 /**
71  * Extension interface for external service integration with TLS transport. Used to build {@link TLSClient} and
72  * {@link TLSServer} instances.
73  */
74 public abstract class SslHandlerFactory {
75     private static final ImmutableMap<CipherSuiteAlgBase, String> CIPHER_SUITES =
76         ImmutableMap.<CipherSuiteAlgBase, String>builder()
77             .put(TlsAes128CcmSha256.VALUE, "TLS_AES_128_CCM_SHA256")
78             .put(TlsAes128GcmSha256.VALUE, "TLS_AES_128_GCM_SHA256")
79             .put(TlsAes256GcmSha384.VALUE, "TLS_AES_256_GCM_SHA384")
80             .put(TlsChacha20Poly1305Sha256.VALUE, "TLS_CHACHA20_POLY1305_SHA256")
81             .put(TlsDhePskWithAes128Ccm.VALUE, "TLS_DHE_PSK_WITH_AES_128_CCM")
82             .put(TlsDhePskWithAes128GcmSha256.VALUE, "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256")
83             .put(TlsDhePskWithAes256Ccm.VALUE, "TLS_DHE_PSK_WITH_AES_256_CCM")
84             .put(TlsDhePskWithAes256GcmSha384.VALUE, "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384")
85             .put(TlsDhePskWithChacha20Poly1305Sha256.VALUE, "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256")
86             .put(TlsDheRsaWithAes128Ccm.VALUE, "TLS_DHE_RSA_WITH_AES_128_CCM")
87             .put(TlsDheRsaWithAes128GcmSha256.VALUE, "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256")
88             .put(TlsDheRsaWithAes256Ccm.VALUE, "TLS_DHE_RSA_WITH_AES_256_CCM")
89             .put(TlsDheRsaWithAes256GcmSha384.VALUE, "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384")
90             .put(TlsDheRsaWithChacha20Poly1305Sha256.VALUE, "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256")
91             .put(TlsEcdheEcdsaWithAes128GcmSha256.VALUE, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256")
92             .put(TlsEcdheEcdsaWithAes256GcmSha384.VALUE, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384")
93             .put(TlsEcdheEcdsaWithChacha20Poly1305Sha256.VALUE, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256")
94             .put(TlsEcdhePskWithAes128CcmSha256.VALUE, "TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256")
95             .put(TlsEcdhePskWithAes128GcmSha256.VALUE, "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256")
96             .put(TlsEcdhePskWithAes256GcmSha384.VALUE, "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384")
97             .put(TlsEcdhePskWithChacha20Poly1305Sha256.VALUE, "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256")
98             .put(TlsEcdheRsaWithAes128GcmSha256.VALUE, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256")
99             .put(TlsEcdheRsaWithAes256GcmSha384.VALUE, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")
100             .put(TlsEcdheRsaWithChacha20Poly1305Sha256.VALUE, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256")
101             .build();
102
103     /**
104      * Builds {@link SslHandler} instance for given {@link Channel}.
105      *
106      * @param channel channel
107      * @return A {@link SslHandler}, or {@code null} if the connection should be rejected
108      */
109     public final @Nullable SslHandler createSslHandler(final @NonNull Channel channel) {
110         final var sslContext = getSslContext(channel.remoteAddress());
111         return sslContext == null ? null : sslContext.newHandler(channel.alloc());
112     }
113
114     protected abstract @Nullable SslContext getSslContext(SocketAddress remoteAddress);
115
116     protected static final @NonNull SslContext createSslContext(final @NonNull TlsClientGrouping clientParams)
117             throws UnsupportedConfigurationException {
118         final var builder = SslContextBuilder.forClient();
119
120         final var clientIdentity = clientParams.getClientIdentity();
121         if (clientIdentity != null) {
122             final var authType = clientIdentity.getAuthType();
123             if (authType instanceof Certificate cert) {
124                 // if-feature "client-ident-x509-cert"
125                 final var certificate = cert.getCertificate();
126                 if (certificate == null) {
127                     throw new UnsupportedConfigurationException("Missing certificate in " + cert);
128                 }
129                 builder.keyManager(newKeyManager(certificate));
130             } else if (authType instanceof RawPublicKey rawKey) {
131                 // if-feature "client-ident-raw-public-key"
132                 final var rawPrivateKey = rawKey.getRawPrivateKey();
133                 if (rawPrivateKey == null) {
134                     throw new UnsupportedConfigurationException("Missing key in " + rawKey);
135                 }
136                 builder.keyManager(newKeyManager(rawPrivateKey));
137             } else if (authType != null) {
138                 throw new UnsupportedConfigurationException("Unsupported client authentication type " + authType);
139             }
140         }
141
142         final var serverAuth = clientParams.getServerAuthentication();
143         if (serverAuth != null) {
144             // CA && EE X509 certificates : if-feature "server-ident-x509-cert"
145             // Raw public key : if-feature "server-ident-raw-public-key"
146             final var trustManager = newTrustManager(serverAuth.getCaCerts(), serverAuth.getEeCerts(),
147                     serverAuth.getRawPublicKeys());
148             if (trustManager == null) {
149                 throw new UnsupportedOperationException("No server authentication methods in " + serverAuth);
150             }
151             builder.trustManager(trustManager);
152         }
153
154         return buildSslContext(builder, clientParams.getHelloParams());
155     }
156
157     protected static final @NonNull SslContext createSslContext(final @NonNull TlsServerGrouping serverParams)
158             throws UnsupportedConfigurationException {
159         final var serverIdentity = serverParams.getServerIdentity();
160         if (serverIdentity == null) {
161             throw new UnsupportedConfigurationException("Missing server identity");
162         }
163         final SslContextBuilder builder;
164         final var authType = serverIdentity.getAuthType();
165         if (authType
166                 instanceof org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev231228
167                            .tls.server.grouping.server.identity.auth.type.Certificate cert) {
168             // if-feature "server-ident-x509-cert"
169             final var certificate = cert.getCertificate();
170             if (certificate == null) {
171                 throw new UnsupportedConfigurationException("Missing certificate in " + cert);
172             }
173             builder = SslContextBuilder.forServer(newKeyManager(certificate));
174         } else if (authType instanceof RawPrivateKey rawKey) {
175             // if-feature "server-ident-raw-public-key"
176             final var rawPrivateKey = rawKey.getRawPrivateKey();
177             if (rawPrivateKey == null) {
178                 throw new UnsupportedConfigurationException("Missing key in " + rawKey);
179             }
180             builder = SslContextBuilder.forServer(newKeyManager(rawPrivateKey));
181         } else if (authType != null) {
182             throw new UnsupportedConfigurationException("Unsupported server authentication type " + authType);
183         } else {
184             throw new UnsupportedConfigurationException("Missing server authentication type");
185         }
186
187         final var clientAuth = serverParams.getClientAuthentication();
188         if (clientAuth != null) {
189             // CA && EE Certs : if-feature "client-ident-x509-cert"
190             // Raw public keys : if-feature "client-ident-raw-public-key"
191             final var trustManager = newTrustManager(clientAuth.getCaCerts(), clientAuth.getEeCerts(),
192                     clientAuth.getRawPublicKeys());
193             if (trustManager == null) {
194                 throw new UnsupportedOperationException("No client authentication methods in " + clientAuth);
195             }
196             builder.clientAuth(ClientAuth.REQUIRE).trustManager(trustManager);
197         } else {
198             builder.clientAuth(ClientAuth.NONE);
199         }
200
201         return buildSslContext(builder, serverParams.getHelloParams());
202     }
203
204     // FIXME: should be TrustManagerBuilder
205     private static @Nullable TrustManagerFactory newTrustManager(
206             final @Nullable InlineOrTruststoreCertsGrouping caCerts,
207             final @Nullable InlineOrTruststoreCertsGrouping eeCerts,
208             final @Nullable InlineOrTruststorePublicKeysGrouping publicKeys) throws UnsupportedConfigurationException {
209
210         if (publicKeys != null) {
211             // FIXME: implement this and advertize server-auth-raw-public-key from IetfTlsClientFeatureProvider
212             throw new UnsupportedConfigurationException("Public key authentication not implemented");
213         }
214         if (caCerts != null || eeCerts != null) {
215             // X.509 certificates
216             final KeyStore keyStore = newKeyStore();
217             setX509Certificates(keyStore, caCerts, eeCerts);
218             return buildTrustManagerFactory(keyStore);
219         }
220         return null;
221     }
222
223     private static KeyManagerFactory newKeyManager(
224             final @NonNull InlineOrKeystoreEndEntityCertWithKeyGrouping endEntityCert)
225             throws UnsupportedConfigurationException {
226         final var keyStore = newKeyStore();
227         setEndEntityCertificateWithKey(keyStore, endEntityCert);
228         return buildKeyManagerFactory(keyStore);
229     }
230
231     private static KeyManagerFactory newKeyManager(final @NonNull InlineOrKeystoreAsymmetricKeyGrouping rawPrivateKey)
232             throws UnsupportedConfigurationException {
233         final var keyStore = newKeyStore();
234         setAsymmetricKey(keyStore, rawPrivateKey);
235         return buildKeyManagerFactory(keyStore);
236     }
237
238     private static @NonNull SslContext buildSslContext(final SslContextBuilder builder,
239             final HelloParamsGrouping helloParams) throws UnsupportedConfigurationException {
240         if (helloParams != null) {
241             final var tlsVersions = helloParams.getTlsVersions();
242             if (tlsVersions != null) {
243                 final var versions = tlsVersions.getTlsVersion();
244                 if (versions != null && !versions.isEmpty()) {
245                     builder.protocols(createTlsStrings(versions));
246                 }
247             }
248             final var cipherSuites = helloParams.getCipherSuites();
249             if (cipherSuites != null) {
250                 final var ciphers = cipherSuites.getCipherSuite();
251                 if (ciphers != null && !ciphers.isEmpty()) {
252                     builder.ciphers(createCipherStrings(ciphers));
253                 }
254             }
255         }
256         try {
257             return builder.build();
258         } catch (SSLException e) {
259             throw new UnsupportedConfigurationException("Cannot instantiate TLS context", e);
260         }
261     }
262
263     private static String[] createTlsStrings(final List<TlsVersionBase> versions)
264             throws UnsupportedConfigurationException {
265         // FIXME: cache these
266         final var ret = new String[versions.size()];
267         int idx = 0;
268         for (var version : versions) {
269             final var str = IetfTlsCommonFeatureProvider.algorithmNameOf(version);
270             if (str == null) {
271                 throw new UnsupportedConfigurationException("Unhandled TLS version " + version);
272             }
273             ret[idx++] = str;
274         }
275         return ret;
276     }
277
278     private static ImmutableList<String> createCipherStrings(final List<CipherSuiteAlgBase> ciphers)
279             throws UnsupportedConfigurationException {
280         // FIXME: cache these
281         final var builder = ImmutableList.<String>builderWithExpectedSize(ciphers.size());
282         for (var cipher : ciphers) {
283             final var str = CIPHER_SUITES.get(cipher);
284             if (str == null) {
285                 throw new UnsupportedConfigurationException("Unhandled cipher suite " + cipher);
286             }
287             builder.add(str);
288         }
289         return builder.build();
290     }
291 }