Bump upstreams
[netconf.git] / plugins / netconf-client-mdsal / src / main / java / org / opendaylight / netconf / client / mdsal / impl / DefaultSslHandlerFactoryProvider.java
1 /*
2  * Copyright (c) 2017 Cisco Systems, Inc. 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.client.mdsal.impl;
9
10 import static java.util.Objects.requireNonNull;
11
12 import java.io.ByteArrayInputStream;
13 import java.io.IOException;
14 import java.nio.charset.StandardCharsets;
15 import java.security.GeneralSecurityException;
16 import java.security.KeyFactory;
17 import java.security.KeyStore;
18 import java.security.KeyStoreException;
19 import java.security.cert.Certificate;
20 import java.security.cert.CertificateException;
21 import java.security.cert.CertificateFactory;
22 import java.security.cert.X509Certificate;
23 import java.security.spec.InvalidKeySpecException;
24 import java.security.spec.PKCS8EncodedKeySpec;
25 import java.util.Base64;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30 import javax.annotation.PreDestroy;
31 import javax.inject.Inject;
32 import javax.inject.Singleton;
33 import org.eclipse.jdt.annotation.NonNull;
34 import org.opendaylight.mdsal.binding.api.DataBroker;
35 import org.opendaylight.mdsal.binding.api.DataObjectModification;
36 import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
37 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
38 import org.opendaylight.mdsal.binding.api.DataTreeModification;
39 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
40 import org.opendaylight.netconf.client.SslHandlerFactory;
41 import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.protocol.Specification;
43 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.protocol.specification.TlsCase;
44 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
45 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKey;
46 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
47 import org.opendaylight.yangtools.concepts.Registration;
48 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
49 import org.osgi.service.component.annotations.Activate;
50 import org.osgi.service.component.annotations.Component;
51 import org.osgi.service.component.annotations.Deactivate;
52 import org.osgi.service.component.annotations.Reference;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 @Singleton
57 @Component(service = SslHandlerFactoryProvider.class)
58 public final class DefaultSslHandlerFactoryProvider
59         implements SslHandlerFactoryProvider, DataTreeChangeListener<Keystore>, AutoCloseable {
60     /**
61      * Internal state, updated atomically.
62      */
63     private record State(
64         @NonNull Map<String, PrivateKey> privateKeys,
65         @NonNull Map<String, TrustedCertificate> trustedCertificates) {
66
67         State {
68             requireNonNull(privateKeys);
69             requireNonNull(trustedCertificates);
70         }
71
72         @NonNull StateBuilder newBuilder() {
73             return new StateBuilder(new HashMap<>(privateKeys), new HashMap<>(trustedCertificates));
74         }
75     }
76
77     /**
78      * Intermediate builder for State.
79      */
80     private record StateBuilder(
81         @NonNull HashMap<String, PrivateKey> privateKeys,
82         @NonNull HashMap<String, TrustedCertificate> trustedCertificates) {
83
84         StateBuilder {
85             requireNonNull(privateKeys);
86             requireNonNull(trustedCertificates);
87         }
88
89         @NonNull State build() {
90             return new State(Map.copyOf(privateKeys), Map.copyOf(trustedCertificates));
91         }
92     }
93
94     private static final class SecurityHelper {
95         private CertificateFactory certFactory;
96         private KeyFactory dsaFactory;
97         private KeyFactory rsaFactory;
98
99         java.security.PrivateKey getJavaPrivateKey(final String base64PrivateKey) throws GeneralSecurityException {
100             final var keySpec = new PKCS8EncodedKeySpec(base64Decode(base64PrivateKey));
101
102             if (rsaFactory == null) {
103                 rsaFactory = KeyFactory.getInstance("RSA");
104             }
105             try {
106                 return rsaFactory.generatePrivate(keySpec);
107             } catch (InvalidKeySpecException ignore) {
108                 // Ignored
109             }
110
111             if (dsaFactory == null) {
112                 dsaFactory = KeyFactory.getInstance("DSA");
113             }
114             return dsaFactory.generatePrivate(keySpec);
115         }
116
117         private X509Certificate getCertificate(final String base64Certificate) throws GeneralSecurityException {
118             // TODO: https://stackoverflow.com/questions/43809909/is-certificatefactory-getinstancex-509-thread-safe
119             //        indicates this is thread-safe in most cases, but can we get a better assurance?
120             if (certFactory == null) {
121                 certFactory = CertificateFactory.getInstance("X.509");
122             }
123             return (X509Certificate) certFactory.generateCertificate(
124                 new ByteArrayInputStream(base64Decode(base64Certificate)));
125         }
126     }
127
128     private static final Logger LOG = LoggerFactory.getLogger(DefaultSslHandlerFactoryProvider.class);
129     private static final char[] EMPTY_CHARS = { };
130
131     private final @NonNull SslHandlerFactory nospecFactory = new SslHandlerFactoryImpl(this, Set.of());
132     private final @NonNull Registration reg;
133
134     private volatile @NonNull State state = new State(Map.of(), Map.of());
135
136     @Inject
137     @Activate
138     public DefaultSslHandlerFactoryProvider(@Reference final DataBroker dataBroker) {
139         reg = dataBroker.registerTreeChangeListener(
140             DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)), this);
141     }
142
143     @Deactivate
144     @PreDestroy
145     @Override
146     public void close() {
147         reg.close();
148     }
149
150     @Override
151     public SslHandlerFactory getSslHandlerFactory(final Specification specification) {
152         if (specification == null) {
153             return nospecFactory;
154         }
155         if (specification instanceof TlsCase tlsSpecification) {
156             final var excludedVersions = tlsSpecification.nonnullTls().getExcludedVersions();
157             return excludedVersions == null || excludedVersions.isEmpty() ? nospecFactory
158                 : new SslHandlerFactoryImpl(this, excludedVersions);
159         }
160         throw new IllegalArgumentException("Cannot get TLS specification from: " + specification);
161     }
162
163     /**
164      * Using private keys and trusted certificates to create a new JDK <code>KeyStore</code> which
165      * will be used by TLS clients to create <code>SSLEngine</code>. The private keys are essential
166      * to create JDK <code>KeyStore</code> while the trusted certificates are optional.
167      *
168      * @return A JDK KeyStore object
169      * @throws GeneralSecurityException If any security exception occurred
170      * @throws IOException If there is an I/O problem with the keystore data
171      */
172     KeyStore getJavaKeyStore() throws GeneralSecurityException, IOException {
173         return getJavaKeyStore(Set.of());
174     }
175
176     /**
177      * Using private keys and trusted certificates to create a new JDK <code>KeyStore</code> which
178      * will be used by TLS clients to create <code>SSLEngine</code>. The private keys are essential
179      * to create JDK <code>KeyStore</code> while the trusted certificates are optional.
180      *
181      * @param allowedKeys Set of keys to include during KeyStore generation, empty set will create
182      *                   a KeyStore with all possible keys.
183      * @return A JDK KeyStore object
184      * @throws GeneralSecurityException If any security exception occurred
185      * @throws IOException If there is an I/O problem with the keystore data
186      */
187     KeyStore getJavaKeyStore(final Set<String> allowedKeys) throws GeneralSecurityException, IOException {
188         requireNonNull(allowedKeys);
189         final var current = state;
190         if (current.privateKeys.isEmpty()) {
191             throw new KeyStoreException("No keystore private key found");
192         }
193
194         final var keyStore = KeyStore.getInstance("JKS");
195         keyStore.load(null, null);
196
197         final var helper = new SecurityHelper();
198
199         // Private keys first
200         for (var entry : current.privateKeys.entrySet()) {
201             final var alias = entry.getKey();
202             if (!allowedKeys.isEmpty() && !allowedKeys.contains(alias)) {
203                 continue;
204             }
205
206             final var privateKey = entry.getValue();
207             final var key = helper.getJavaPrivateKey(privateKey.getData());
208             // TODO: requireCertificateChain() here and filter in update path
209             final var certChain = privateKey.getCertificateChain();
210             if (certChain == null || certChain.isEmpty()) {
211                 throw new CertificateException("No certificate chain associated with private key " + alias + " found");
212             }
213
214             final var chain = new Certificate[certChain.size()];
215             int idx = 0;
216             for (var cert : certChain) {
217                 chain[idx++] = helper.getCertificate(cert);
218             }
219             keyStore.setKeyEntry(alias, key, EMPTY_CHARS, chain);
220         }
221
222         for (var entry : current.trustedCertificates.entrySet()) {
223             keyStore.setCertificateEntry(entry.getKey(), helper.getCertificate(entry.getValue().getCertificate()));
224         }
225
226         return keyStore;
227     }
228
229     private static byte[] base64Decode(final String base64) {
230         return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.US_ASCII));
231     }
232
233     @Override
234     public void onDataTreeChanged(final List<DataTreeModification<Keystore>> changes) {
235         LOG.debug("Starting update with {} changes", changes.size());
236         final var builder = state.newBuilder();
237         onDataTreeChanged(builder, changes);
238         state = builder.build();
239         LOG.debug("Update finished");
240     }
241
242     private static void onDataTreeChanged(final StateBuilder builder,
243             final List<DataTreeModification<Keystore>> changes) {
244         for (var change : changes) {
245             LOG.debug("Processing change {}", change);
246             final var rootNode = change.getRootNode();
247
248             for (var changedChild : rootNode.modifiedChildren()) {
249                 if (changedChild.dataType().equals(PrivateKey.class)) {
250                     onPrivateKeyChanged(builder.privateKeys, (DataObjectModification<PrivateKey>)changedChild);
251                 } else if (changedChild.dataType().equals(TrustedCertificate.class)) {
252                     onTrustedCertificateChanged(builder.trustedCertificates,
253                         (DataObjectModification<TrustedCertificate>)changedChild);
254                 }
255             }
256         }
257     }
258
259     private static void onPrivateKeyChanged(final HashMap<String, PrivateKey> privateKeys,
260             final DataObjectModification<PrivateKey> objectModification) {
261         switch (objectModification.modificationType()) {
262             case SUBTREE_MODIFIED:
263             case WRITE:
264                 final var privateKey = objectModification.dataAfter();
265                 privateKeys.put(privateKey.getName(), privateKey);
266                 break;
267             case DELETE:
268                 privateKeys.remove(objectModification.dataBefore().getName());
269                 break;
270             default:
271                 break;
272         }
273     }
274
275     private static void onTrustedCertificateChanged(final HashMap<String, TrustedCertificate> trustedCertificates,
276             final DataObjectModification<TrustedCertificate> objectModification) {
277         switch (objectModification.modificationType()) {
278             case SUBTREE_MODIFIED:
279             case WRITE:
280                 final var trustedCertificate = objectModification.dataAfter();
281                 trustedCertificates.put(trustedCertificate.getName(), trustedCertificate);
282                 break;
283             case DELETE:
284                 trustedCertificates.remove(objectModification.dataBefore().getName());
285                 break;
286             default:
287                 break;
288         }
289     }
290 }