2 * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.netconf.keystore.legacy.impl;
10 import static java.util.Objects.requireNonNull;
11 import static java.util.Objects.requireNonNullElse;
13 import com.google.common.collect.Maps;
14 import java.io.IOException;
15 import java.nio.charset.StandardCharsets;
16 import java.security.GeneralSecurityException;
17 import java.security.KeyPair;
18 import java.security.cert.X509Certificate;
19 import java.util.ArrayList;
20 import java.util.Base64;
21 import java.util.HashMap;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.atomic.AtomicReference;
26 import java.util.function.Consumer;
27 import javax.annotation.PreDestroy;
28 import javax.inject.Inject;
29 import javax.inject.Singleton;
30 import org.eclipse.jdt.annotation.NonNull;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.opendaylight.aaa.encrypt.AAAEncryptionService;
34 import org.opendaylight.mdsal.binding.api.DataBroker;
35 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
36 import org.opendaylight.mdsal.binding.api.RpcProviderService;
37 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
38 import org.opendaylight.mdsal.singleton.api.ClusterSingletonServiceProvider;
39 import org.opendaylight.netconf.keystore.legacy.CertifiedPrivateKey;
40 import org.opendaylight.netconf.keystore.legacy.NetconfKeystore;
41 import org.opendaylight.netconf.keystore.legacy.NetconfKeystoreService;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev231109.Keystore;
43 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev231109._private.keys.PrivateKey;
44 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev231109.keystore.entry.KeyCredential;
45 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev231109.trusted.certificates.TrustedCertificate;
46 import org.opendaylight.yangtools.concepts.AbstractObjectRegistration;
47 import org.opendaylight.yangtools.concepts.Immutable;
48 import org.opendaylight.yangtools.concepts.Mutable;
49 import org.opendaylight.yangtools.concepts.ObjectRegistration;
50 import org.opendaylight.yangtools.concepts.Registration;
51 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
52 import org.osgi.service.component.annotations.Activate;
53 import org.osgi.service.component.annotations.Component;
54 import org.osgi.service.component.annotations.Deactivate;
55 import org.osgi.service.component.annotations.Reference;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
60 * Abstract substrate for implementing security services based on the contents of {@link Keystore}.
63 @Component(service = NetconfKeystoreService.class)
64 public final class DefaultNetconfKeystoreService implements NetconfKeystoreService, AutoCloseable {
66 private record ConfigState(
67 Map<String, PrivateKey> privateKeys,
68 Map<String, TrustedCertificate> trustedCertificates,
69 Map<String, KeyCredential> credentials) implements Immutable {
70 static final ConfigState EMPTY = new ConfigState(Map.of(), Map.of(), Map.of());
73 privateKeys = Map.copyOf(privateKeys);
74 trustedCertificates = Map.copyOf(trustedCertificates);
75 credentials = Map.copyOf(credentials);
80 record ConfigStateBuilder(
81 HashMap<String, PrivateKey> privateKeys,
82 HashMap<String, TrustedCertificate> trustedCertificates,
83 HashMap<String, KeyCredential> credentials) implements Mutable {
85 requireNonNull(privateKeys);
86 requireNonNull(trustedCertificates);
87 requireNonNull(credentials);
91 private static final Logger LOG = LoggerFactory.getLogger(DefaultNetconfKeystoreService.class);
93 private final Set<ObjectRegistration<Consumer<NetconfKeystore>>> consumers = ConcurrentHashMap.newKeySet();
94 private final AtomicReference<NetconfKeystore> keystore = new AtomicReference<>(null);
95 private final AtomicReference<ConfigState> config = new AtomicReference<>(ConfigState.EMPTY);
96 private final SecurityHelper securityHelper = new SecurityHelper();
97 private final AAAEncryptionService encryptionService;
98 private final Registration configListener;
99 private final Registration rpcSingleton;
103 public DefaultNetconfKeystoreService(@Reference final DataBroker dataBroker,
104 @Reference final RpcProviderService rpcProvider,
105 @Reference final ClusterSingletonServiceProvider cssProvider,
106 @Reference final AAAEncryptionService encryptionService) {
107 this.encryptionService = requireNonNull(encryptionService);
108 configListener = dataBroker.registerTreeChangeListener(
109 DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
110 new ConfigListener(this));
111 rpcSingleton = cssProvider.registerClusterSingletonService(
112 new RpcSingleton(dataBroker, rpcProvider, encryptionService));
114 // FIXME: create an operation datastore updater and register it as a consumer
116 LOG.info("NETCONF keystore service started");
122 public void close() {
123 rpcSingleton.close();
124 configListener.close();
125 LOG.info("NETCONF keystore service stopped");
129 public Registration registerKeystoreConsumer(final Consumer<NetconfKeystore> consumer) {
130 final var reg = new AbstractObjectRegistration<>(consumer) {
132 protected void removeRegistration() {
133 consumers.remove(this);
138 final var ks = keystore.getAcquire();
145 void runUpdate(final Consumer<@NonNull ConfigStateBuilder> task) {
146 final var prevState = config.getAcquire();
148 final var builder = new ConfigStateBuilder(new HashMap<>(prevState.privateKeys),
149 new HashMap<>(prevState.trustedCertificates), new HashMap<>(prevState.credentials));
150 task.accept(builder);
151 final var newState = new ConfigState(builder.privateKeys, builder.trustedCertificates, builder.credentials);
153 // Careful application -- check if listener is still up and whether the state was not updated.
154 if (configListener == null || config.compareAndExchangeRelease(prevState, newState) != prevState) {
158 Throwable failure = null;
160 final var keys = Maps.<String, CertifiedPrivateKey>newHashMapWithExpectedSize(newState.privateKeys.size());
161 for (var key : newState.privateKeys.values()) {
162 final var keyName = key.requireName();
164 final byte[] keyBytes;
166 keyBytes = base64Decode(new String(key.requireData(), StandardCharsets.UTF_8));
167 } catch (IllegalArgumentException e) {
168 LOG.debug("Failed to decode private key {}", keyName, e);
169 failure = updateFailure(failure, e);
173 final java.security.PrivateKey privateKey;
175 privateKey = securityHelper.generatePrivateKey(keyBytes);
176 } catch (GeneralSecurityException e) {
177 LOG.debug("Failed to generate key for {}", keyName, e);
178 failure = updateFailure(failure, e);
182 final var certChain = key.requireCertificateChain();
183 if (certChain.isEmpty()) {
184 LOG.debug("Key {} has an empty certificate chain", keyName);
185 failure = updateFailure(failure,
186 new IllegalArgumentException("Empty certificate chain for private key " + keyName));
190 final var certs = new ArrayList<X509Certificate>(certChain.size());
191 for (int i = 0, size = certChain.size(); i < size; i++) {
194 bytes = base64Decode(new String(certChain.get(i), StandardCharsets.UTF_8));
195 } catch (IllegalArgumentException e) {
196 LOG.debug("Failed to decode certificate chain item {} for private key {}", i, keyName, e);
197 failure = updateFailure(failure, e);
201 final X509Certificate x509cert;
203 x509cert = securityHelper.generateCertificate(bytes);
204 } catch (GeneralSecurityException e) {
205 LOG.debug("Failed to generate certificate chain item {} for private key {}", i, keyName, e);
206 failure = updateFailure(failure, e);
213 keys.put(keyName, new CertifiedPrivateKey(privateKey, certs));
216 final var certs = Maps.<String, X509Certificate>newHashMapWithExpectedSize(newState.trustedCertificates.size());
217 for (var cert : newState.trustedCertificates.values()) {
218 final var certName = cert.requireName();
222 bytes = base64Decode(new String(cert.requireCertificate(), StandardCharsets.UTF_8));
223 } catch (IllegalArgumentException e) {
224 LOG.debug("Failed to decode trusted certificate {}", certName, e);
225 failure = updateFailure(failure, e);
229 final X509Certificate x509cert;
231 x509cert = securityHelper.generateCertificate(bytes);
232 } catch (GeneralSecurityException e) {
233 LOG.debug("Failed to generate certificate for {}", certName, e);
234 failure = updateFailure(failure, e);
238 certs.put(certName, x509cert);
241 final var creds = Maps.<String, KeyPair>newHashMapWithExpectedSize(newState.credentials.size());
242 for (var cred : newState.credentials.values()) {
243 final var keyId = cred.requireKeyId();
244 final String passPhrase;
246 passPhrase = decryptString(requireNonNullElse(new String(cred.getPassphrase(), StandardCharsets.UTF_8),
248 } catch (GeneralSecurityException e) {
249 LOG.debug("Failed to decrypt pass phrase for {}", keyId, e);
250 failure = updateFailure(failure, e);
254 final String privateKey;
256 privateKey = decryptString(new String(cred.getPrivateKey(), StandardCharsets.UTF_8));
257 } catch (GeneralSecurityException e) {
258 LOG.debug("Failed to decrypt private key for {}", keyId, e);
259 failure = updateFailure(failure, e);
263 final KeyPair keyPair;
265 keyPair = securityHelper.decodePrivateKey(privateKey, passPhrase);
266 } catch (IOException e) {
267 LOG.debug("Failed to decode key pair for {}", keyId, e);
268 failure = updateFailure(failure, e);
272 creds.put(keyId, keyPair);
275 if (failure != null) {
276 LOG.warn("New configuration is invalid, not applying it", failure);
280 final var newKeystore = new NetconfKeystore(keys, certs, creds);
281 keystore.setRelease(newKeystore);
282 consumers.forEach(consumer -> consumer.getInstance().accept(newKeystore));
285 private static byte[] base64Decode(final String base64) {
286 return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.UTF_8));
289 private String decryptString(final String encrypted) throws GeneralSecurityException {
290 return new String(encryptionService.decrypt(Base64.getDecoder().decode(encrypted)), StandardCharsets.UTF_8);
293 private static @NonNull Throwable updateFailure(final @Nullable Throwable failure, final @NonNull Exception ex) {
294 if (failure != null) {
295 failure.addSuppressed(ex);