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;
10 import static java.util.Objects.requireNonNull;
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.List;
21 import java.util.concurrent.atomic.AtomicReference;
22 import java.util.function.Consumer;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.opendaylight.aaa.encrypt.AAAEncryptionService;
27 import org.opendaylight.mdsal.binding.api.DataBroker;
28 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
29 import org.opendaylight.mdsal.binding.api.RpcProviderService;
30 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
31 import org.opendaylight.mdsal.singleton.api.ClusterSingletonServiceProvider;
32 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
33 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKey;
34 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
35 import org.opendaylight.yangtools.concepts.Immutable;
36 import org.opendaylight.yangtools.concepts.Mutable;
37 import org.opendaylight.yangtools.concepts.Registration;
38 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * Abstract substrate for implementing security services based on the contents of {@link Keystore}.
45 public abstract class AbstractNetconfKeystore {
47 protected record CertifiedPrivateKey(
48 java.security.PrivateKey key,
49 List<X509Certificate> certificateChain) implements Immutable {
50 public CertifiedPrivateKey {
52 certificateChain = List.copyOf(certificateChain);
53 if (certificateChain.isEmpty()) {
54 throw new IllegalArgumentException("Certificate chain must not be empty");
60 protected record State(
61 Map<String, CertifiedPrivateKey> privateKeys,
62 Map<String, X509Certificate> trustedCertificates) implements Immutable {
63 public static final State EMPTY = new State(Map.of(), Map.of());
66 privateKeys = Map.copyOf(privateKeys);
67 trustedCertificates = Map.copyOf(trustedCertificates);
72 private record ConfigState(
73 Map<String, PrivateKey> privateKeys,
74 Map<String, TrustedCertificate> trustedCertificates) implements Immutable {
75 static final ConfigState EMPTY = new ConfigState(Map.of(), Map.of());
78 privateKeys = Map.copyOf(privateKeys);
79 trustedCertificates = Map.copyOf(trustedCertificates);
84 record ConfigStateBuilder(
85 HashMap<String, PrivateKey> privateKeys,
86 HashMap<String, TrustedCertificate> trustedCertificates) implements Mutable {
88 requireNonNull(privateKeys);
89 requireNonNull(trustedCertificates);
93 private static final Logger LOG = LoggerFactory.getLogger(AbstractNetconfKeystore.class);
95 private final AtomicReference<@NonNull ConfigState> state = new AtomicReference<>(ConfigState.EMPTY);
96 private final SecurityHelper securityHelper = new SecurityHelper();
98 private @Nullable Registration configListener;
99 private @Nullable Registration rpcSingleton;
101 protected final void start(final DataBroker dataBroker, final RpcProviderService rpcProvider,
102 final ClusterSingletonServiceProvider cssProvider, final AAAEncryptionService encryptionService) {
103 if (configListener == null) {
104 configListener = dataBroker.registerTreeChangeListener(
105 DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
106 new ConfigListener(this));
107 LOG.debug("NETCONF keystore configuration listener started");
109 if (rpcSingleton == null) {
110 rpcSingleton = cssProvider.registerClusterSingletonService(
111 new RpcSingleton(dataBroker, rpcProvider, encryptionService));
112 LOG.debug("NETCONF keystore configuration singleton registered");
114 LOG.info("NETCONF keystore service started");
117 protected final void stop() {
118 final var singleton = rpcSingleton;
119 if (singleton != null) {
123 final var listener = configListener;
124 if (listener != null) {
125 configListener = null;
127 state.set(ConfigState.EMPTY);
131 protected abstract void onStateUpdated(@NonNull State newState);
133 final void runUpdate(final Consumer<@NonNull ConfigStateBuilder> task) {
134 final var prevState = state.getAcquire();
136 final var builder = new ConfigStateBuilder(new HashMap<>(prevState.privateKeys),
137 new HashMap<>(prevState.trustedCertificates));
138 task.accept(builder);
139 final var newState = new ConfigState(builder.privateKeys, builder.trustedCertificates);
141 // Careful application -- check if listener is still up and whether the state was not updated.
142 if (configListener == null || state.compareAndExchangeRelease(prevState, newState) != prevState) {
146 Throwable failure = null;
148 final var keys = Maps.<String, CertifiedPrivateKey>newHashMapWithExpectedSize(newState.privateKeys.size());
149 for (var key : newState.privateKeys.values()) {
150 final var keyName = key.requireName();
152 final byte[] keyBytes;
154 keyBytes = base64Decode(key.requireData());
155 } catch (IllegalArgumentException e) {
156 LOG.debug("Failed to decode private key {}", keyName, e);
157 failure = updateFailure(failure, e);
161 final java.security.PrivateKey privateKey;
163 privateKey = securityHelper.generatePrivateKey(keyBytes);
164 } catch (GeneralSecurityException e) {
165 LOG.debug("Failed to generate key for {}", keyName, e);
166 failure = updateFailure(failure, e);
170 final var certChain = key.requireCertificateChain();
171 if (certChain.isEmpty()) {
172 LOG.debug("Key {} has an empty certificate chain", keyName);
173 failure = updateFailure(failure,
174 new IllegalArgumentException("Empty certificate chain for private key " + keyName));
178 final var certs = new ArrayList<X509Certificate>(certChain.size());
179 for (int i = 0, size = certChain.size(); i < size; i++) {
182 bytes = base64Decode(certChain.get(i));
183 } catch (IllegalArgumentException e) {
184 LOG.debug("Failed to decode certificate chain item {} for private key {}", i, keyName, e);
185 failure = updateFailure(failure, e);
189 final X509Certificate x509cert;
191 x509cert = securityHelper.generateCertificate(bytes);
192 } catch (GeneralSecurityException e) {
193 LOG.debug("Failed to generate certificate chain item {} for private key {}", i, keyName, e);
194 failure = updateFailure(failure, e);
201 keys.put(keyName, new CertifiedPrivateKey(privateKey, certs));
204 final var certs = Maps.<String, X509Certificate>newHashMapWithExpectedSize(newState.trustedCertificates.size());
205 for (var cert : newState.trustedCertificates.values()) {
206 final var certName = cert.requireName();
210 bytes = base64Decode(cert.requireCertificate());
211 } catch (IllegalArgumentException e) {
212 LOG.debug("Failed to decode trusted certificate {}", certName, e);
213 failure = updateFailure(failure, e);
217 final X509Certificate x509cert;
219 x509cert = securityHelper.generateCertificate(bytes);
220 } catch (GeneralSecurityException e) {
221 LOG.debug("Failed to generate certificate for {}", certName, e);
222 failure = updateFailure(failure, e);
226 certs.put(certName, x509cert);
229 if (failure != null) {
230 LOG.warn("New configuration is invalid, not applying it", failure);
234 onStateUpdated(new State(keys, certs));
236 // FIXME: tickle operational updater (which does not exist yet)
239 private static byte[] base64Decode(final String base64) {
240 return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.US_ASCII));
243 private static @NonNull Throwable updateFailure(final @Nullable Throwable failure, final @NonNull Exception ex) {
244 if (failure != null) {
245 failure.addSuppressed(ex);