<description>Legacy NETCONF keystore</description>
<dependencies>
+ <dependency>
+ <groupId>com.guicedee.services</groupId>
+ <artifactId>javax.inject</artifactId>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>jakarta.annotation</groupId>
+ <artifactId>jakarta.annotation-api</artifactId>
+ <scope>provided</scope>
+ <optional>true</optional>
+ </dependency>
<dependency>
<groupId>org.opendaylight.aaa</groupId>
<artifactId>aaa-encrypt-service</artifactId>
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>concepts</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.component.annotations</artifactId>
+ </dependency>
<dependency>
<groupId>org.opendaylight.netconf</groupId>
<scope>test</scope>
</dependency>
</dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Automatic-Module-Name>org.opendaylight.netconf.keystore.legacy</Automatic-Module-Name>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
</project>
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.keystore.legacy;
+
+import static java.util.Objects.requireNonNull;
+
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.concepts.Immutable;
+
+@NonNullByDefault
+public record CertifiedPrivateKey(
+ PrivateKey key,
+ List<X509Certificate> certificateChain) implements Immutable {
+ public CertifiedPrivateKey {
+ requireNonNull(key);
+ certificateChain = List.copyOf(certificateChain);
+ if (certificateChain.isEmpty()) {
+ throw new IllegalArgumentException("Certificate chain must not be empty");
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.keystore.legacy;
+
+import java.security.cert.X509Certificate;
+import java.util.Map;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.concepts.Immutable;
+
+@NonNullByDefault
+public record NetconfKeystore(
+ Map<String, CertifiedPrivateKey> privateKeys,
+ Map<String, X509Certificate> trustedCertificates) implements Immutable {
+ public static final NetconfKeystore EMPTY = new NetconfKeystore(Map.of(), Map.of());
+
+ public NetconfKeystore {
+ privateKeys = Map.copyOf(privateKeys);
+ trustedCertificates = Map.copyOf(trustedCertificates);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.keystore.legacy;
+
+import java.util.function.Consumer;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.concepts.Registration;
+
+/**
+ * Asynchronous access to {@link NetconfKeystore}.
+ */
+@NonNullByDefault
+public interface NetconfKeystoreService {
+
+ Registration registerKeystoreConsumer(Consumer<NetconfKeystore> consumer);
+}
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.keystore.legacy;
+package org.opendaylight.netconf.keystore.legacy.impl;
import static java.util.Objects.requireNonNull;
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.keystore.legacy;
+package org.opendaylight.netconf.keystore.legacy.impl;
import static java.util.Objects.requireNonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
import org.opendaylight.mdsal.binding.api.DataTreeModification;
-import org.opendaylight.netconf.keystore.legacy.AbstractNetconfKeystore.ConfigStateBuilder;
+import org.opendaylight.netconf.keystore.legacy.impl.DefaultNetconfKeystoreService.ConfigStateBuilder;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKey;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
import org.slf4j.LoggerFactory;
@NonNullByDefault
-record ConfigListener(AbstractNetconfKeystore keystore) implements DataTreeChangeListener<Keystore> {
+record ConfigListener(DefaultNetconfKeystoreService keystore) implements DataTreeChangeListener<Keystore> {
private static final Logger LOG = LoggerFactory.getLogger(ConfigListener.class);
ConfigListener {
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.keystore.legacy;
+package org.opendaylight.netconf.keystore.legacy.impl;
import static java.util.Objects.requireNonNull;
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.keystore.legacy;
+package org.opendaylight.netconf.keystore.legacy.impl;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.keystore.legacy;
+package org.opendaylight.netconf.keystore.legacy.impl;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.keystore.legacy;
+package org.opendaylight.netconf.keystore.legacy.impl;
import static java.util.Objects.requireNonNull;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.inject.Singleton;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.mdsal.binding.api.RpcProviderService;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.mdsal.singleton.api.ClusterSingletonServiceProvider;
+import org.opendaylight.netconf.keystore.legacy.CertifiedPrivateKey;
+import org.opendaylight.netconf.keystore.legacy.NetconfKeystore;
+import org.opendaylight.netconf.keystore.legacy.NetconfKeystoreService;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKey;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
+import org.opendaylight.yangtools.concepts.AbstractObjectRegistration;
import org.opendaylight.yangtools.concepts.Immutable;
import org.opendaylight.yangtools.concepts.Mutable;
+import org.opendaylight.yangtools.concepts.ObjectRegistration;
import org.opendaylight.yangtools.concepts.Registration;
import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract substrate for implementing security services based on the contents of {@link Keystore}.
*/
-public abstract class AbstractNetconfKeystore {
- @NonNullByDefault
- protected record CertifiedPrivateKey(
- java.security.PrivateKey key,
- List<X509Certificate> certificateChain) implements Immutable {
- public CertifiedPrivateKey {
- requireNonNull(key);
- certificateChain = List.copyOf(certificateChain);
- if (certificateChain.isEmpty()) {
- throw new IllegalArgumentException("Certificate chain must not be empty");
- }
- }
- }
-
- @NonNullByDefault
- protected record State(
- Map<String, CertifiedPrivateKey> privateKeys,
- Map<String, X509Certificate> trustedCertificates) implements Immutable {
- public static final State EMPTY = new State(Map.of(), Map.of());
-
- public State {
- privateKeys = Map.copyOf(privateKeys);
- trustedCertificates = Map.copyOf(trustedCertificates);
- }
- }
-
+@Singleton
+@Component(service = NetconfKeystoreService.class)
+public final class DefaultNetconfKeystoreService implements NetconfKeystoreService, AutoCloseable {
@NonNullByDefault
private record ConfigState(
Map<String, PrivateKey> privateKeys,
}
}
- private static final Logger LOG = LoggerFactory.getLogger(AbstractNetconfKeystore.class);
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultNetconfKeystoreService.class);
- private final AtomicReference<@NonNull ConfigState> state = new AtomicReference<>(ConfigState.EMPTY);
+ private final Set<ObjectRegistration<Consumer<NetconfKeystore>>> consumers = ConcurrentHashMap.newKeySet();
+ private final AtomicReference<NetconfKeystore> keystore = new AtomicReference<>(null);
+ private final AtomicReference<ConfigState> config = new AtomicReference<>(ConfigState.EMPTY);
private final SecurityHelper securityHelper = new SecurityHelper();
+ private final Registration configListener;
+ private final Registration rpcSingleton;
+
+ @Inject
+ @Activate
+ public DefaultNetconfKeystoreService(@Reference final DataBroker dataBroker,
+ @Reference final RpcProviderService rpcProvider,
+ @Reference final ClusterSingletonServiceProvider cssProvider,
+ @Reference final AAAEncryptionService encryptionService) {
+ configListener = dataBroker.registerTreeChangeListener(
+ DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
+ new ConfigListener(this));
+ rpcSingleton = cssProvider.registerClusterSingletonService(
+ new RpcSingleton(dataBroker, rpcProvider, encryptionService));
+
+ // FIXME: create an operation datastore updater and register it as a consumer
- private @Nullable Registration configListener;
- private @Nullable Registration rpcSingleton;
-
- protected final void start(final DataBroker dataBroker, final RpcProviderService rpcProvider,
- final ClusterSingletonServiceProvider cssProvider, final AAAEncryptionService encryptionService) {
- if (configListener == null) {
- configListener = dataBroker.registerTreeChangeListener(
- DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
- new ConfigListener(this));
- LOG.debug("NETCONF keystore configuration listener started");
- }
- if (rpcSingleton == null) {
- rpcSingleton = cssProvider.registerClusterSingletonService(
- new RpcSingleton(dataBroker, rpcProvider, encryptionService));
- LOG.debug("NETCONF keystore configuration singleton registered");
- }
LOG.info("NETCONF keystore service started");
}
- protected final void stop() {
- final var singleton = rpcSingleton;
- if (singleton != null) {
- rpcSingleton = null;
- singleton.close();
- }
- final var listener = configListener;
- if (listener != null) {
- configListener = null;
- listener.close();
- state.set(ConfigState.EMPTY);
- }
+ @PreDestroy
+ @Deactivate
+ @Override
+ public void close() {
+ rpcSingleton.close();
+ configListener.close();
+ LOG.info("NETCONF keystore service stopped");
}
- protected abstract void onStateUpdated(@NonNull State newState);
+ @Override
+ public Registration registerKeystoreConsumer(final Consumer<NetconfKeystore> consumer) {
+ final var reg = new AbstractObjectRegistration<>(consumer) {
+ @Override
+ protected void removeRegistration() {
+ consumers.remove(this);
+ }
+ };
- final void runUpdate(final Consumer<@NonNull ConfigStateBuilder> task) {
- final var prevState = state.getAcquire();
+ consumers.add(reg);
+ final var ks = keystore.getAcquire();
+ if (ks != null) {
+ consumer.accept(ks);
+ }
+ return reg;
+ }
+
+ void runUpdate(final Consumer<@NonNull ConfigStateBuilder> task) {
+ final var prevState = config.getAcquire();
final var builder = new ConfigStateBuilder(new HashMap<>(prevState.privateKeys),
new HashMap<>(prevState.trustedCertificates));
final var newState = new ConfigState(builder.privateKeys, builder.trustedCertificates);
// Careful application -- check if listener is still up and whether the state was not updated.
- if (configListener == null || state.compareAndExchangeRelease(prevState, newState) != prevState) {
+ if (configListener == null || config.compareAndExchangeRelease(prevState, newState) != prevState) {
return;
}
return;
}
- onStateUpdated(new State(keys, certs));
-
- // FIXME: tickle operational updater (which does not exist yet)
+ final var newKeystore = new NetconfKeystore(keys, certs);
+ keystore.setRelease(newKeystore);
+ consumers.forEach(consumer -> consumer.getInstance().accept(newKeystore));
}
private static byte[] base64Decode(final String base64) {
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.keystore.legacy;
+package org.opendaylight.netconf.keystore.legacy.impl;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.keystore.legacy;
+package org.opendaylight.netconf.keystore.legacy.impl;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.keystore.legacy;
+package org.opendaylight.netconf.keystore.legacy.impl;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.keystore.legacy;
+package org.opendaylight.netconf.keystore.legacy.impl;
import static java.util.Objects.requireNonNull;
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.keystore.legacy;
+package org.opendaylight.netconf.keystore.legacy.impl;
import java.io.ByteArrayInputStream;
import java.security.GeneralSecurityException;
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+/**
+ * Implementation details.
+ */
+package org.opendaylight.netconf.keystore.legacy.impl;
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+/**
+ * Access to key and trust material stored in
+ * {@link org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.NetconfKeystoreData}.
+ * The primary access interface is {@link NetconfKeystoreService}, which allows subscription to receive updates about
+ * {@link NetconfKeystore}.
+ */
+package org.opendaylight.netconf.keystore.legacy;
\ No newline at end of file
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
-package org.opendaylight.netconf.keystore.legacy;
+package org.opendaylight.netconf.keystore.legacy.impl;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.eclipse.jdt.annotation.NonNull;
-import org.opendaylight.aaa.encrypt.AAAEncryptionService;
-import org.opendaylight.mdsal.binding.api.DataBroker;
-import org.opendaylight.mdsal.binding.api.RpcProviderService;
-import org.opendaylight.mdsal.singleton.api.ClusterSingletonServiceProvider;
import org.opendaylight.netconf.client.SslHandlerFactory;
import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider;
-import org.opendaylight.netconf.keystore.legacy.AbstractNetconfKeystore;
+import org.opendaylight.netconf.keystore.legacy.NetconfKeystore;
+import org.opendaylight.netconf.keystore.legacy.NetconfKeystoreService;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.protocol.Specification;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.parameters.protocol.specification.TlsCase;
+import org.opendaylight.yangtools.concepts.Registration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
@Singleton
@Component(service = SslHandlerFactoryProvider.class)
-public final class DefaultSslHandlerFactoryProvider extends AbstractNetconfKeystore
- implements SslHandlerFactoryProvider, AutoCloseable {
+public final class DefaultSslHandlerFactoryProvider implements SslHandlerFactoryProvider, AutoCloseable {
private static final X509Certificate[] EMPTY_CERTS = { };
private static final char[] EMPTY_CHARS = { };
private final @NonNull SslHandlerFactory nospecFactory = new SslHandlerFactoryImpl(this, Set.of());
+ private final Registration reg;
- private volatile @NonNull State state = State.EMPTY;
+ private volatile @NonNull NetconfKeystore keystore = NetconfKeystore.EMPTY;
@Inject
@Activate
- public DefaultSslHandlerFactoryProvider(@Reference final DataBroker dataBroker,
- @Reference final RpcProviderService rpcProvider,
- @Reference final ClusterSingletonServiceProvider cssProvider,
- @Reference final AAAEncryptionService encryptionService) {
- start(dataBroker, rpcProvider, cssProvider, encryptionService);
+ public DefaultSslHandlerFactoryProvider(@Reference final NetconfKeystoreService keystoreService) {
+ reg = keystoreService.registerKeystoreConsumer(this::onKeystoreUpdated);
}
- @Deactivate
- @PreDestroy
@Override
+ @PreDestroy
+ @Deactivate
public void close() {
- stop();
+ reg.close();
}
- @Override
- protected void onStateUpdated(final State newState) {
- state = newState;
+ private void onKeystoreUpdated(final @NonNull NetconfKeystore newKeystore) {
+ keystore = newKeystore;
}
@Override
*/
KeyStore getJavaKeyStore(final Set<String> allowedKeys) throws GeneralSecurityException, IOException {
requireNonNull(allowedKeys);
- final var current = state;
+ final var current = keystore;
if (current.privateKeys().isEmpty()) {
throw new KeyStoreException("No keystore private key found");
}
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.opendaylight.mdsal.binding.api.RpcProviderService;
import org.opendaylight.mdsal.singleton.api.ClusterSingletonServiceProvider;
import org.opendaylight.netconf.api.xml.XmlUtil;
+import org.opendaylight.netconf.keystore.legacy.impl.DefaultNetconfKeystoreService;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKey;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017._private.keys.PrivateKeyBuilder;
@Mock
private AAAEncryptionService encryptionService;
@Mock
- private Registration listenerRegistration;
+ private Registration registration;
@Mock
private DataTreeModification<Keystore> dataTreeModification1;
@Mock
private DataObjectModification<TrustedCertificate> trustedCertificateModification;
private DataTreeChangeListener<Keystore> listener;
+ private DefaultNetconfKeystoreService keystore;
@BeforeEach
void beforeEach() {
doAnswer(inv -> {
listener = inv.getArgument(1);
- return listenerRegistration;
+ return registration;
}).when(dataBroker).registerTreeChangeListener(any(), any());
+ doReturn(registration).when(cssProvider).registerClusterSingletonService(any());
+ keystore = new DefaultNetconfKeystoreService(dataBroker, rpcProvider, cssProvider, encryptionService);
+ }
+
+ @AfterEach
+ void afterEach() {
+ keystore.close();
}
private DefaultSslHandlerFactoryProvider newProvider() {
- return new DefaultSslHandlerFactoryProvider(dataBroker, rpcProvider, cssProvider, encryptionService);
+ return new DefaultSslHandlerFactoryProvider(keystore);
}
@Test