Remove obsoleted way of configuring password
[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.Collection;
27 import java.util.HashMap;
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.ClusteredDataTreeChangeListener;
35 import org.opendaylight.mdsal.binding.api.DataBroker;
36 import org.opendaylight.mdsal.binding.api.DataObjectModification;
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.rev231025.connection.parameters.protocol.Specification;
43 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev231025.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, ClusteredDataTreeChangeListener<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.registerDataTreeChangeListener(
140             DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
141             this);
142     }
143
144     @Deactivate
145     @PreDestroy
146     @Override
147     public void close() {
148         reg.close();
149     }
150
151     @Override
152     public SslHandlerFactory getSslHandlerFactory(final Specification specification) {
153         if (specification == null) {
154             return nospecFactory;
155         }
156         if (specification instanceof TlsCase tlsSpecification) {
157             final var excludedVersions = tlsSpecification.nonnullTls().getExcludedVersions();
158             return excludedVersions == null || excludedVersions.isEmpty() ? nospecFactory
159                 : new SslHandlerFactoryImpl(this, excludedVersions);
160         }
161         throw new IllegalArgumentException("Cannot get TLS specification from: " + specification);
162     }
163
164     /**
165      * Using private keys and trusted certificates to create a new JDK <code>KeyStore</code> which
166      * will be used by TLS clients to create <code>SSLEngine</code>. The private keys are essential
167      * to create JDK <code>KeyStore</code> while the trusted certificates are optional.
168      *
169      * @return A JDK KeyStore object
170      * @throws GeneralSecurityException If any security exception occurred
171      * @throws IOException If there is an I/O problem with the keystore data
172      */
173     KeyStore getJavaKeyStore() throws GeneralSecurityException, IOException {
174         return getJavaKeyStore(Set.of());
175     }
176
177     /**
178      * Using private keys and trusted certificates to create a new JDK <code>KeyStore</code> which
179      * will be used by TLS clients to create <code>SSLEngine</code>. The private keys are essential
180      * to create JDK <code>KeyStore</code> while the trusted certificates are optional.
181      *
182      * @param allowedKeys Set of keys to include during KeyStore generation, empty set will create
183      *                   a KeyStore with all possible keys.
184      * @return A JDK KeyStore object
185      * @throws GeneralSecurityException If any security exception occurred
186      * @throws IOException If there is an I/O problem with the keystore data
187      */
188     KeyStore getJavaKeyStore(final Set<String> allowedKeys) throws GeneralSecurityException, IOException {
189         requireNonNull(allowedKeys);
190         final var current = state;
191         if (current.privateKeys.isEmpty()) {
192             throw new KeyStoreException("No keystore private key found");
193         }
194
195         final var keyStore = KeyStore.getInstance("JKS");
196         keyStore.load(null, null);
197
198         final var helper = new SecurityHelper();
199
200         // Private keys first
201         for (var entry : current.privateKeys.entrySet()) {
202             final var alias = entry.getKey();
203             if (!allowedKeys.isEmpty() && !allowedKeys.contains(alias)) {
204                 continue;
205             }
206
207             final var privateKey = entry.getValue();
208             final var key = helper.getJavaPrivateKey(privateKey.getData());
209             // TODO: requireCertificateChain() here and filter in update path
210             final var certChain = privateKey.getCertificateChain();
211             if (certChain == null || certChain.isEmpty()) {
212                 throw new CertificateException("No certificate chain associated with private key " + alias + " found");
213             }
214
215             final var chain = new Certificate[certChain.size()];
216             int idx = 0;
217             for (var cert : certChain) {
218                 chain[idx++] = helper.getCertificate(cert);
219             }
220             keyStore.setKeyEntry(alias, key, EMPTY_CHARS, chain);
221         }
222
223         for (var entry : current.trustedCertificates.entrySet()) {
224             keyStore.setCertificateEntry(entry.getKey(), helper.getCertificate(entry.getValue().getCertificate()));
225         }
226
227         return keyStore;
228     }
229
230     private static byte[] base64Decode(final String base64) {
231         return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.US_ASCII));
232     }
233
234     @Override
235     public void onDataTreeChanged(final Collection<DataTreeModification<Keystore>> changes) {
236         LOG.debug("Starting update with {} changes", changes.size());
237         final var builder = state.newBuilder();
238         onDataTreeChanged(builder, changes);
239         state = builder.build();
240         LOG.debug("Update finished");
241     }
242
243     private static void onDataTreeChanged(final StateBuilder builder,
244             final Collection<DataTreeModification<Keystore>> changes) {
245         for (var change : changes) {
246             LOG.debug("Processing change {}", change);
247             final var rootNode = change.getRootNode();
248
249             for (var changedChild : rootNode.getModifiedChildren()) {
250                 if (changedChild.getDataType().equals(PrivateKey.class)) {
251                     onPrivateKeyChanged(builder.privateKeys, (DataObjectModification<PrivateKey>)changedChild);
252                 } else if (changedChild.getDataType().equals(TrustedCertificate.class)) {
253                     onTrustedCertificateChanged(builder.trustedCertificates,
254                         (DataObjectModification<TrustedCertificate>)changedChild);
255                 }
256             }
257         }
258     }
259
260     private static void onPrivateKeyChanged(final HashMap<String, PrivateKey> privateKeys,
261             final DataObjectModification<PrivateKey> objectModification) {
262         switch (objectModification.getModificationType()) {
263             case SUBTREE_MODIFIED:
264             case WRITE:
265                 final var privateKey = objectModification.getDataAfter();
266                 privateKeys.put(privateKey.getName(), privateKey);
267                 break;
268             case DELETE:
269                 privateKeys.remove(objectModification.getDataBefore().getName());
270                 break;
271             default:
272                 break;
273         }
274     }
275
276     private static void onTrustedCertificateChanged(final HashMap<String, TrustedCertificate> trustedCertificates,
277             final DataObjectModification<TrustedCertificate> objectModification) {
278         switch (objectModification.getModificationType()) {
279             case SUBTREE_MODIFIED:
280             case WRITE:
281                 final var trustedCertificate = objectModification.getDataAfter();
282                 trustedCertificates.put(trustedCertificate.getName(), trustedCertificate);
283                 break;
284             case DELETE:
285                 trustedCertificates.remove(objectModification.getDataBefore().getName());
286                 break;
287             default:
288                 break;
289         }
290     }
291 }