Expose NetconfKeystoreService
[netconf.git] / keystore / keystore-legacy / src / main / java / org / opendaylight / netconf / keystore / legacy / impl / DefaultNetconfKeystoreService.java
1 /*
2  * Copyright (c) 2024 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.keystore.legacy.impl;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.collect.Maps;
13 import java.nio.charset.StandardCharsets;
14 import java.security.GeneralSecurityException;
15 import java.security.cert.X509Certificate;
16 import java.util.ArrayList;
17 import java.util.Base64;
18 import java.util.HashMap;
19 import java.util.Map;
20 import java.util.Set;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.atomic.AtomicReference;
23 import java.util.function.Consumer;
24 import javax.annotation.PreDestroy;
25 import javax.inject.Inject;
26 import javax.inject.Singleton;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.opendaylight.aaa.encrypt.AAAEncryptionService;
31 import org.opendaylight.mdsal.binding.api.DataBroker;
32 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
33 import org.opendaylight.mdsal.binding.api.RpcProviderService;
34 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
35 import org.opendaylight.mdsal.singleton.api.ClusterSingletonServiceProvider;
36 import org.opendaylight.netconf.keystore.legacy.CertifiedPrivateKey;
37 import org.opendaylight.netconf.keystore.legacy.NetconfKeystore;
38 import org.opendaylight.netconf.keystore.legacy.NetconfKeystoreService;
39 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
40 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKey;
41 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
42 import org.opendaylight.yangtools.concepts.AbstractObjectRegistration;
43 import org.opendaylight.yangtools.concepts.Immutable;
44 import org.opendaylight.yangtools.concepts.Mutable;
45 import org.opendaylight.yangtools.concepts.ObjectRegistration;
46 import org.opendaylight.yangtools.concepts.Registration;
47 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
48 import org.osgi.service.component.annotations.Activate;
49 import org.osgi.service.component.annotations.Component;
50 import org.osgi.service.component.annotations.Deactivate;
51 import org.osgi.service.component.annotations.Reference;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 /**
56  * Abstract substrate for implementing security services based on the contents of {@link Keystore}.
57  */
58 @Singleton
59 @Component(service = NetconfKeystoreService.class)
60 public final class DefaultNetconfKeystoreService implements NetconfKeystoreService, AutoCloseable {
61     @NonNullByDefault
62     private record ConfigState(
63             Map<String, PrivateKey> privateKeys,
64             Map<String, TrustedCertificate> trustedCertificates) implements Immutable {
65         static final ConfigState EMPTY = new ConfigState(Map.of(), Map.of());
66
67         ConfigState {
68             privateKeys = Map.copyOf(privateKeys);
69             trustedCertificates = Map.copyOf(trustedCertificates);
70         }
71     }
72
73     @NonNullByDefault
74     record ConfigStateBuilder(
75             HashMap<String, PrivateKey> privateKeys,
76             HashMap<String, TrustedCertificate> trustedCertificates) implements Mutable {
77         ConfigStateBuilder {
78             requireNonNull(privateKeys);
79             requireNonNull(trustedCertificates);
80         }
81     }
82
83     private static final Logger LOG = LoggerFactory.getLogger(DefaultNetconfKeystoreService.class);
84
85     private final Set<ObjectRegistration<Consumer<NetconfKeystore>>> consumers = ConcurrentHashMap.newKeySet();
86     private final AtomicReference<NetconfKeystore> keystore = new AtomicReference<>(null);
87     private final AtomicReference<ConfigState> config = new AtomicReference<>(ConfigState.EMPTY);
88     private final SecurityHelper securityHelper = new SecurityHelper();
89     private final Registration configListener;
90     private final Registration rpcSingleton;
91
92     @Inject
93     @Activate
94     public DefaultNetconfKeystoreService(@Reference final DataBroker dataBroker,
95             @Reference final RpcProviderService rpcProvider,
96             @Reference final ClusterSingletonServiceProvider cssProvider,
97             @Reference final AAAEncryptionService encryptionService) {
98         configListener = dataBroker.registerTreeChangeListener(
99             DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
100             new ConfigListener(this));
101         rpcSingleton = cssProvider.registerClusterSingletonService(
102             new RpcSingleton(dataBroker, rpcProvider, encryptionService));
103
104         // FIXME: create an operation datastore updater and register it as a consumer
105
106         LOG.info("NETCONF keystore service started");
107     }
108
109     @PreDestroy
110     @Deactivate
111     @Override
112     public void close() {
113         rpcSingleton.close();
114         configListener.close();
115         LOG.info("NETCONF keystore service stopped");
116     }
117
118     @Override
119     public Registration registerKeystoreConsumer(final Consumer<NetconfKeystore> consumer) {
120         final var reg = new AbstractObjectRegistration<>(consumer) {
121             @Override
122             protected void removeRegistration() {
123                 consumers.remove(this);
124             }
125         };
126
127         consumers.add(reg);
128         final var ks = keystore.getAcquire();
129         if (ks != null) {
130             consumer.accept(ks);
131         }
132         return reg;
133     }
134
135     void runUpdate(final Consumer<@NonNull ConfigStateBuilder> task) {
136         final var prevState = config.getAcquire();
137
138         final var builder = new ConfigStateBuilder(new HashMap<>(prevState.privateKeys),
139             new HashMap<>(prevState.trustedCertificates));
140         task.accept(builder);
141         final var newState = new ConfigState(builder.privateKeys, builder.trustedCertificates);
142
143         // Careful application -- check if listener is still up and whether the state was not updated.
144         if (configListener == null || config.compareAndExchangeRelease(prevState, newState) != prevState) {
145             return;
146         }
147
148         Throwable failure = null;
149
150         final var keys = Maps.<String, CertifiedPrivateKey>newHashMapWithExpectedSize(newState.privateKeys.size());
151         for (var key : newState.privateKeys.values()) {
152             final var keyName = key.requireName();
153
154             final byte[] keyBytes;
155             try {
156                 keyBytes = base64Decode(key.requireData());
157             } catch (IllegalArgumentException e) {
158                 LOG.debug("Failed to decode private key {}", keyName, e);
159                 failure = updateFailure(failure, e);
160                 continue;
161             }
162
163             final java.security.PrivateKey privateKey;
164             try {
165                 privateKey = securityHelper.generatePrivateKey(keyBytes);
166             } catch (GeneralSecurityException e) {
167                 LOG.debug("Failed to generate key for {}", keyName, e);
168                 failure = updateFailure(failure, e);
169                 continue;
170             }
171
172             final var certChain = key.requireCertificateChain();
173             if (certChain.isEmpty()) {
174                 LOG.debug("Key {} has an empty certificate chain", keyName);
175                 failure = updateFailure(failure,
176                     new IllegalArgumentException("Empty certificate chain for private key " + keyName));
177                 continue;
178             }
179
180             final var certs = new ArrayList<X509Certificate>(certChain.size());
181             for (int i = 0, size = certChain.size(); i < size; i++) {
182                 final byte[] bytes;
183                 try {
184                     bytes = base64Decode(certChain.get(i));
185                 } catch (IllegalArgumentException e) {
186                     LOG.debug("Failed to decode certificate chain item {} for private key {}", i, keyName, e);
187                     failure = updateFailure(failure, e);
188                     continue;
189                 }
190
191                 final X509Certificate x509cert;
192                 try {
193                     x509cert = securityHelper.generateCertificate(bytes);
194                 } catch (GeneralSecurityException e) {
195                     LOG.debug("Failed to generate certificate chain item {} for private key {}", i, keyName, e);
196                     failure = updateFailure(failure, e);
197                     continue;
198                 }
199
200                 certs.add(x509cert);
201             }
202
203             keys.put(keyName, new CertifiedPrivateKey(privateKey, certs));
204         }
205
206         final var certs = Maps.<String, X509Certificate>newHashMapWithExpectedSize(newState.trustedCertificates.size());
207         for (var cert : newState.trustedCertificates.values()) {
208             final var certName = cert.requireName();
209
210             final byte[] bytes;
211             try {
212                 bytes = base64Decode(cert.requireCertificate());
213             } catch (IllegalArgumentException e) {
214                 LOG.debug("Failed to decode trusted certificate {}", certName, e);
215                 failure = updateFailure(failure, e);
216                 continue;
217             }
218
219             final X509Certificate x509cert;
220             try {
221                 x509cert = securityHelper.generateCertificate(bytes);
222             } catch (GeneralSecurityException e) {
223                 LOG.debug("Failed to generate certificate for {}", certName, e);
224                 failure = updateFailure(failure, e);
225                 continue;
226             }
227
228             certs.put(certName, x509cert);
229         }
230
231         if (failure != null) {
232             LOG.warn("New configuration is invalid, not applying it", failure);
233             return;
234         }
235
236         final var newKeystore = new NetconfKeystore(keys, certs);
237         keystore.setRelease(newKeystore);
238         consumers.forEach(consumer -> consumer.getInstance().accept(newKeystore));
239     }
240
241     private static byte[] base64Decode(final String base64) {
242         return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.US_ASCII));
243     }
244
245     private static @NonNull Throwable updateFailure(final @Nullable Throwable failure, final @NonNull Exception ex) {
246         if (failure != null) {
247             failure.addSuppressed(ex);
248             return failure;
249         }
250         return ex;
251     }
252 }