<artifactId>keystore-api</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>keystore-legacy</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.opendaylight.netconf</groupId>
<artifactId>keystore-none</artifactId>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-parent</artifactId>
+ <version>7.0.0-SNAPSHOT</version>
+ <relativePath>../../parent</relativePath>
+ </parent>
+
+ <artifactId>keystore-legacy</artifactId>
+ <packaging>bundle</packaging>
+ <name>${project.artifactId}</name>
+ <description>Legacy NETCONF keystore</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.opendaylight.mdsal</groupId>
+ <artifactId>mdsal-binding-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.mdsal</groupId>
+ <artifactId>mdsal-common-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>concepts</artifactId>
+ </dependency>
+ </dependencies>
+</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.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.mdsal.binding.api.DataBroker;
+import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+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.Immutable;
+import org.opendaylight.yangtools.concepts.Registration;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+
+/**
+ * Abstract substrate for implementing security services based on the contents of {@link Keystore}.
+ */
+public abstract class AbstractNetconfKeystore {
+ @NonNullByDefault
+ protected record State(
+ Map<String, PrivateKey> privateKeys,
+ Map<String, TrustedCertificate> trustedCertificates) implements Immutable {
+ public static final State EMPTY = new State(Map.of(), Map.of());
+
+ public State {
+ privateKeys = Map.copyOf(privateKeys);
+ trustedCertificates = Map.copyOf(trustedCertificates);
+ }
+ }
+
+ @NonNullByDefault
+ private record ConfigState(
+ Map<String, PrivateKey> privateKeys,
+ Map<String, TrustedCertificate> trustedCertificates) implements Immutable {
+ static final ConfigState EMPTY = new ConfigState(Map.of(), Map.of());
+
+ ConfigState {
+ privateKeys = Map.copyOf(privateKeys);
+ trustedCertificates = Map.copyOf(trustedCertificates);
+ }
+ }
+
+ @NonNullByDefault
+ record ConfigStateBuilder(
+ HashMap<String, PrivateKey> privateKeys,
+ HashMap<String, TrustedCertificate> trustedCertificates) {
+ ConfigStateBuilder {
+ requireNonNull(privateKeys);
+ requireNonNull(trustedCertificates);
+ }
+ }
+
+ private final AtomicReference<@NonNull ConfigState> state = new AtomicReference<>(ConfigState.EMPTY);
+
+ private @Nullable Registration configListener;
+
+ protected final void start(final DataBroker dataBroker) {
+ if (configListener == null) {
+ configListener = dataBroker.registerTreeChangeListener(
+ DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
+ new ConfigListener(this));
+ }
+ }
+
+ protected final void stop() {
+ final var listener = configListener;
+ if (listener != null) {
+ configListener = null;
+ listener.close();
+ state.set(ConfigState.EMPTY);
+ }
+ }
+
+ protected abstract void onStateUpdated(@NonNull State newState);
+
+ final void runUpdate(final Consumer<@NonNull ConfigStateBuilder> task) {
+ final var prevState = state.getAcquire();
+
+ final var builder = new ConfigStateBuilder(new HashMap<>(prevState.privateKeys),
+ new HashMap<>(prevState.trustedCertificates));
+ task.accept(builder);
+ 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) {
+ return;
+ }
+
+ // FIXME: compile to crypto
+
+ onStateUpdated(new State(newState.privateKeys, newState.trustedCertificates));
+
+ // FIXME: tickle operational updater (which does not exist yet)
+ }
+}
--- /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 com.google.common.base.Stopwatch;
+import java.util.List;
+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.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.Logger;
+import org.slf4j.LoggerFactory;
+
+@NonNullByDefault
+record ConfigListener(AbstractNetconfKeystore keystore) implements DataTreeChangeListener<Keystore> {
+ private static final Logger LOG = LoggerFactory.getLogger(ConfigListener.class);
+
+ ConfigListener {
+ requireNonNull(keystore);
+ }
+
+ @Override
+ public void onInitialData() {
+ keystore.runUpdate(builder -> {
+ builder.privateKeys().clear();
+ builder.trustedCertificates().clear();
+ });
+ }
+
+ @Override
+ public void onDataTreeChanged(final List<DataTreeModification<Keystore>> changes) {
+ LOG.debug("Starting update with {} changes", changes.size());
+ final var sw = Stopwatch.createStarted();
+ keystore.runUpdate(builder -> onDataTreeChanged(builder, changes));
+ LOG.debug("Update finished in {}", sw);
+ }
+
+ private static void onDataTreeChanged(final ConfigStateBuilder builder,
+ final List<DataTreeModification<Keystore>> changes) {
+ for (var change : changes) {
+ LOG.debug("Processing change {}", change);
+ final var rootNode = change.getRootNode();
+
+ for (var mod : rootNode.getModifiedChildren(PrivateKey.class)) {
+ switch (mod.modificationType()) {
+ case SUBTREE_MODIFIED, WRITE -> {
+ final var privateKey = mod.dataAfter();
+ builder.privateKeys().put(privateKey.requireName(), privateKey);
+ }
+ case DELETE -> builder.privateKeys().remove(mod.dataBefore().requireName());
+ default -> {
+ // no-op
+ }
+ }
+ }
+ for (var mod : rootNode.getModifiedChildren(TrustedCertificate.class)) {
+ switch (mod.modificationType()) {
+ case SUBTREE_MODIFIED, WRITE -> {
+ final var trustedCertificate = mod.dataAfter();
+ builder.trustedCertificates().put(trustedCertificate.requireName(), trustedCertificate);
+ }
+ case DELETE -> builder.trustedCertificates().remove(mod.dataBefore().requireName());
+ default -> {
+ // no-op
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2023 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.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+import org.eclipse.jdt.annotation.NonNull;
+
+public final class SecurityHelper {
+ private CertificateFactory certFactory;
+ private KeyFactory dsaFactory;
+ private KeyFactory rsaFactory;
+
+ public @NonNull PrivateKey getJavaPrivateKey(final String base64PrivateKey) throws GeneralSecurityException {
+ final var keySpec = new PKCS8EncodedKeySpec(base64Decode(base64PrivateKey));
+
+ if (rsaFactory == null) {
+ rsaFactory = KeyFactory.getInstance("RSA");
+ }
+ try {
+ return rsaFactory.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException ignore) {
+ // Ignored
+ }
+
+ if (dsaFactory == null) {
+ dsaFactory = KeyFactory.getInstance("DSA");
+ }
+ return dsaFactory.generatePrivate(keySpec);
+ }
+
+ public @NonNull X509Certificate getCertificate(final String base64Certificate) throws GeneralSecurityException {
+ // TODO: https://stackoverflow.com/questions/43809909/is-certificatefactory-getinstancex-509-thread-safe
+ // indicates this is thread-safe in most cases, but can we get a better assurance?
+ if (certFactory == null) {
+ certFactory = CertificateFactory.getInstance("X.509");
+ }
+ return (X509Certificate) certFactory.generateCertificate(
+ new ByteArrayInputStream(base64Decode(base64Certificate)));
+ }
+
+ private static byte[] base64Decode(final String base64) {
+ return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.US_ASCII));
+ }
+
+}
\ No newline at end of file
<modules>
<module>keystore-api</module>
+ <module>keystore-legacy</module>
<module>keystore-none</module>
</modules>
</project>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.guicedee.services</groupId>
+ <artifactId>javax.inject</artifactId>
+ <optional>true</optional>
+ </dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-common</artifactId>
<artifactId>netty-transport</artifactId>
</dependency>
<dependency>
- <groupId>org.checkerframework</groupId>
- <artifactId>checker-qual</artifactId>
+ <groupId>jakarta.annotation</groupId>
+ <artifactId>jakarta.annotation-api</artifactId>
+ <scope>provided</scope>
+ <optional>true</optional>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>concepts</artifactId>
+ <groupId>org.checkerframework</groupId>
+ <artifactId>checker-qual</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>rfc8528-model-api</artifactId>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-encrypt-service</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>util</artifactId>
+ <groupId>org.opendaylight.mdsal</groupId>
+ <artifactId>mdsal-binding-api</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-common</artifactId>
+ <groupId>org.opendaylight.mdsal</groupId>
+ <artifactId>mdsal-binding-runtime-spi</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-data-api</artifactId>
+ <groupId>org.opendaylight.mdsal</groupId>
+ <artifactId>mdsal-common-api</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-data-codec-gson</artifactId>
+ <groupId>org.opendaylight.mdsal</groupId>
+ <artifactId>mdsal-dom-api</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-data-codec-xml</artifactId>
+ <groupId>org.opendaylight.mdsal</groupId>
+ <artifactId>mdsal-dom-spi</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-data-impl</artifactId>
+ <groupId>org.opendaylight.mdsal.binding.model.ietf</groupId>
+ <artifactId>rfc6991-ietf-inet-types</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-data-spi</artifactId>
+ <groupId>org.opendaylight.mdsal.binding.model.ietf</groupId>
+ <artifactId>rfc6991-ietf-yang-types</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-data-util</artifactId>
+ <groupId>org.opendaylight.mdsal.binding.model.ietf</groupId>
+ <artifactId>rfc8525</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-model-api</artifactId>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-api</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-model-spi</artifactId>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>keystore-legacy</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-model-util</artifactId>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-client</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-parser-api</artifactId>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-common</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-parser-impl</artifactId>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-common-mdsal</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-parser-rfc7950</artifactId>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-dom-api</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-repo-api</artifactId>
+ <groupId>org.opendaylight.netconf.model</groupId>
+ <artifactId>rfc5277</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-repo-fs</artifactId>
+ <groupId>org.opendaylight.netconf.model</groupId>
+ <artifactId>rfc6022</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-repo-spi</artifactId>
+ <groupId>org.opendaylight.netconf.model</groupId>
+ <artifactId>rfc6241</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.mdsal</groupId>
- <artifactId>mdsal-binding-api</artifactId>
+ <groupId>org.opendaylight.netconf.model</groupId>
+ <artifactId>rfc6470</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.mdsal</groupId>
- <artifactId>mdsal-binding-runtime-spi</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>concepts</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.mdsal</groupId>
- <artifactId>mdsal-common-api</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>rfc8528-model-api</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.mdsal</groupId>
- <artifactId>mdsal-dom-api</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>util</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.mdsal</groupId>
- <artifactId>mdsal-dom-spi</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-common</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.mdsal.binding.model.ietf</groupId>
- <artifactId>rfc6991-ietf-inet-types</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-data-api</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.mdsal.binding.model.ietf</groupId>
- <artifactId>rfc6991-ietf-yang-types</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-data-codec-gson</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.mdsal.binding.model.ietf</groupId>
- <artifactId>rfc8525</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-data-codec-xml</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.aaa</groupId>
- <artifactId>aaa-encrypt-service</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-data-impl</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.netconf</groupId>
- <artifactId>netconf-api</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-data-spi</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.netconf</groupId>
- <artifactId>netconf-client</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-data-util</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.netconf</groupId>
- <artifactId>netconf-common</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-model-api</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.netconf</groupId>
- <artifactId>netconf-common-mdsal</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-model-spi</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.netconf</groupId>
- <artifactId>netconf-dom-api</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-model-util</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.netconf.model</groupId>
- <artifactId>rfc5277</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-parser-api</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.netconf.model</groupId>
- <artifactId>rfc6022</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-parser-impl</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.netconf.model</groupId>
- <artifactId>rfc6241</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-parser-rfc7950</artifactId>
</dependency>
<dependency>
- <groupId>org.opendaylight.netconf.model</groupId>
- <artifactId>rfc6470</artifactId>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-repo-api</artifactId>
</dependency>
<dependency>
- <groupId>com.guicedee.services</groupId>
- <artifactId>javax.inject</artifactId>
- <optional>true</optional>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-repo-fs</artifactId>
</dependency>
<dependency>
- <groupId>jakarta.annotation</groupId>
- <artifactId>jakarta.annotation-api</artifactId>
- <scope>provided</scope>
- <optional>true</optional>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-repo-spi</artifactId>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
import static java.util.Objects.requireNonNull;
-import java.io.ByteArrayInputStream;
import java.io.IOException;
-import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
-import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.PKCS8EncodedKeySpec;
-import java.util.Base64;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
import java.util.Set;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.mdsal.binding.api.DataBroker;
-import org.opendaylight.mdsal.binding.api.DataObjectModification;
-import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
-import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
-import org.opendaylight.mdsal.binding.api.DataTreeModification;
-import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
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.SecurityHelper;
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.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.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;
@Singleton
@Component(service = SslHandlerFactoryProvider.class)
-public final class DefaultSslHandlerFactoryProvider
- implements SslHandlerFactoryProvider, DataTreeChangeListener<Keystore>, AutoCloseable {
- /**
- * Internal state, updated atomically.
- */
- private record State(
- @NonNull Map<String, PrivateKey> privateKeys,
- @NonNull Map<String, TrustedCertificate> trustedCertificates) {
-
- State {
- requireNonNull(privateKeys);
- requireNonNull(trustedCertificates);
- }
-
- @NonNull StateBuilder newBuilder() {
- return new StateBuilder(new HashMap<>(privateKeys), new HashMap<>(trustedCertificates));
- }
- }
-
- /**
- * Intermediate builder for State.
- */
- private record StateBuilder(
- @NonNull HashMap<String, PrivateKey> privateKeys,
- @NonNull HashMap<String, TrustedCertificate> trustedCertificates) {
-
- StateBuilder {
- requireNonNull(privateKeys);
- requireNonNull(trustedCertificates);
- }
-
- @NonNull State build() {
- return new State(Map.copyOf(privateKeys), Map.copyOf(trustedCertificates));
- }
- }
-
- private static final class SecurityHelper {
- private CertificateFactory certFactory;
- private KeyFactory dsaFactory;
- private KeyFactory rsaFactory;
-
- java.security.PrivateKey getJavaPrivateKey(final String base64PrivateKey) throws GeneralSecurityException {
- final var keySpec = new PKCS8EncodedKeySpec(base64Decode(base64PrivateKey));
-
- if (rsaFactory == null) {
- rsaFactory = KeyFactory.getInstance("RSA");
- }
- try {
- return rsaFactory.generatePrivate(keySpec);
- } catch (InvalidKeySpecException ignore) {
- // Ignored
- }
-
- if (dsaFactory == null) {
- dsaFactory = KeyFactory.getInstance("DSA");
- }
- return dsaFactory.generatePrivate(keySpec);
- }
-
- private X509Certificate getCertificate(final String base64Certificate) throws GeneralSecurityException {
- // TODO: https://stackoverflow.com/questions/43809909/is-certificatefactory-getinstancex-509-thread-safe
- // indicates this is thread-safe in most cases, but can we get a better assurance?
- if (certFactory == null) {
- certFactory = CertificateFactory.getInstance("X.509");
- }
- return (X509Certificate) certFactory.generateCertificate(
- new ByteArrayInputStream(base64Decode(base64Certificate)));
- }
- }
-
- private static final Logger LOG = LoggerFactory.getLogger(DefaultSslHandlerFactoryProvider.class);
+public final class DefaultSslHandlerFactoryProvider extends AbstractNetconfKeystore
+ implements SslHandlerFactoryProvider, AutoCloseable {
private static final char[] EMPTY_CHARS = { };
private final @NonNull SslHandlerFactory nospecFactory = new SslHandlerFactoryImpl(this, Set.of());
- private final @NonNull Registration reg;
- private volatile @NonNull State state = new State(Map.of(), Map.of());
+ private volatile @NonNull State state = State.EMPTY;
@Inject
@Activate
public DefaultSslHandlerFactoryProvider(@Reference final DataBroker dataBroker) {
- reg = dataBroker.registerTreeChangeListener(
- DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)), this);
+ start(dataBroker);
}
@Deactivate
@PreDestroy
@Override
public void close() {
- reg.close();
+ stop();
+ }
+
+ @Override
+ protected void onStateUpdated(final State newState) {
+ state = newState;
}
@Override
KeyStore getJavaKeyStore(final Set<String> allowedKeys) throws GeneralSecurityException, IOException {
requireNonNull(allowedKeys);
final var current = state;
- if (current.privateKeys.isEmpty()) {
+ if (current.privateKeys().isEmpty()) {
throw new KeyStoreException("No keystore private key found");
}
final var helper = new SecurityHelper();
// Private keys first
- for (var entry : current.privateKeys.entrySet()) {
+ for (var entry : current.privateKeys().entrySet()) {
final var alias = entry.getKey();
if (!allowedKeys.isEmpty() && !allowedKeys.contains(alias)) {
continue;
keyStore.setKeyEntry(alias, key, EMPTY_CHARS, chain);
}
- for (var entry : current.trustedCertificates.entrySet()) {
+ for (var entry : current.trustedCertificates().entrySet()) {
keyStore.setCertificateEntry(entry.getKey(), helper.getCertificate(entry.getValue().getCertificate()));
}
return keyStore;
}
-
- private static byte[] base64Decode(final String base64) {
- return Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.US_ASCII));
- }
-
- @Override
- public void onDataTreeChanged(final List<DataTreeModification<Keystore>> changes) {
- LOG.debug("Starting update with {} changes", changes.size());
- final var builder = state.newBuilder();
- onDataTreeChanged(builder, changes);
- state = builder.build();
- LOG.debug("Update finished");
- }
-
- private static void onDataTreeChanged(final StateBuilder builder,
- final List<DataTreeModification<Keystore>> changes) {
- for (var change : changes) {
- LOG.debug("Processing change {}", change);
- final var rootNode = change.getRootNode();
-
- for (var changedChild : rootNode.modifiedChildren()) {
- if (changedChild.dataType().equals(PrivateKey.class)) {
- onPrivateKeyChanged(builder.privateKeys, (DataObjectModification<PrivateKey>)changedChild);
- } else if (changedChild.dataType().equals(TrustedCertificate.class)) {
- onTrustedCertificateChanged(builder.trustedCertificates,
- (DataObjectModification<TrustedCertificate>)changedChild);
- }
- }
- }
- }
-
- private static void onPrivateKeyChanged(final HashMap<String, PrivateKey> privateKeys,
- final DataObjectModification<PrivateKey> objectModification) {
- switch (objectModification.modificationType()) {
- case SUBTREE_MODIFIED:
- case WRITE:
- final var privateKey = objectModification.dataAfter();
- privateKeys.put(privateKey.getName(), privateKey);
- break;
- case DELETE:
- privateKeys.remove(objectModification.dataBefore().getName());
- break;
- default:
- break;
- }
- }
-
- private static void onTrustedCertificateChanged(final HashMap<String, TrustedCertificate> trustedCertificates,
- final DataObjectModification<TrustedCertificate> objectModification) {
- switch (objectModification.modificationType()) {
- case SUBTREE_MODIFIED:
- case WRITE:
- final var trustedCertificate = objectModification.dataAfter();
- trustedCertificates.put(trustedCertificate.getName(), trustedCertificate);
- break;
- case DELETE:
- trustedCertificates.remove(objectModification.dataBefore().getName());
- break;
- default:
- break;
- }
- }
}
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
import java.security.KeyStoreException;
import java.util.ArrayList;
import java.util.List;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.junit.jupiter.MockitoExtension;
import org.opendaylight.mdsal.binding.api.DataBroker;
import org.opendaylight.mdsal.binding.api.DataObjectModification;
-import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
+import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
import org.opendaylight.mdsal.binding.api.DataTreeModification;
import org.opendaylight.netconf.api.xml.XmlUtil;
import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
import org.opendaylight.yangtools.concepts.Registration;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-@RunWith(MockitoJUnitRunner.StrictStubs.class)
-public class DefaultSslHandlerFactoryProviderTest {
+@ExtendWith(MockitoExtension.class)
+class DefaultSslHandlerFactoryProviderTest {
private static final String XML_ELEMENT_PRIVATE_KEY = "private-key";
private static final String XML_ELEMENT_NAME = "name";
private static final String XML_ELEMENT_DATA = "data";
private DataBroker dataBroker;
@Mock
private Registration listenerRegistration;
+ @Mock
+ private DataTreeModification<Keystore> dataTreeModification1;
+ @Mock
+ private DataTreeModification<Keystore> dataTreeModification2;
+ @Mock
+ private DataObjectModification<Keystore> keystoreObjectModification1;
+ @Mock
+ private DataObjectModification<Keystore> keystoreObjectModification2;
+ @Mock
+ private DataObjectModification<PrivateKey> privateKeyModification;
+ @Mock
+ private DataObjectModification<TrustedCertificate> trustedCertificateModification;
+
+ private DataTreeChangeListener<Keystore> listener;
- @Before
- public void setUp() {
- doReturn(listenerRegistration).when(dataBroker)
- .registerTreeChangeListener(any(DataTreeIdentifier.class), any(DefaultSslHandlerFactoryProvider.class));
+ @BeforeEach
+ void beforeEach() {
+ doAnswer(inv -> {
+ listener = inv.getArgument(1);
+ return listenerRegistration;
+ }).when(dataBroker).registerTreeChangeListener(any(), any());
}
@Test
- public void testKeystoreAdapterInit() throws Exception {
- final DefaultSslHandlerFactoryProvider keystoreAdapter = new DefaultSslHandlerFactoryProvider(dataBroker);
- final var ex = assertThrows(KeyStoreException.class, keystoreAdapter::getJavaKeyStore);
- assertThat(ex.getMessage(), startsWith("No keystore private key found"));
+ void testKeystoreAdapterInit() throws Exception {
+ try (var keystoreAdapter = new DefaultSslHandlerFactoryProvider(dataBroker)) {
+ final var ex = assertThrows(KeyStoreException.class, keystoreAdapter::getJavaKeyStore);
+ assertThat(ex.getMessage(), startsWith("No keystore private key found"));
+ }
}
- @SuppressWarnings("unchecked")
@Test
- public void testWritePrivateKey() throws Exception {
- DataTreeModification<Keystore> dataTreeModification = mock(DataTreeModification.class);
- DataObjectModification<Keystore> keystoreObjectModification = mock(DataObjectModification.class);
- doReturn(keystoreObjectModification).when(dataTreeModification).getRootNode();
-
- DataObjectModification<?> childObjectModification = mock(DataObjectModification.class);
- doReturn(List.of(childObjectModification)).when(keystoreObjectModification).modifiedChildren();
- doReturn(PrivateKey.class).when(childObjectModification).dataType();
-
- doReturn(DataObjectModification.ModificationType.WRITE).when(childObjectModification).modificationType();
+ void testWritePrivateKey() throws Exception {
+ doReturn(keystoreObjectModification1).when(dataTreeModification1).getRootNode();
+ doReturn(List.of(privateKeyModification)).when(keystoreObjectModification1)
+ .getModifiedChildren(PrivateKey.class);
+ doReturn(DataObjectModification.ModificationType.WRITE).when(privateKeyModification).modificationType();
final var privateKey = getPrivateKey();
- doReturn(privateKey).when(childObjectModification).dataAfter();
+ doReturn(privateKey).when(privateKeyModification).dataAfter();
- final var keystoreAdapter = new DefaultSslHandlerFactoryProvider(dataBroker);
- keystoreAdapter.onDataTreeChanged(List.of(dataTreeModification));
+ try (var keystoreAdapter = new DefaultSslHandlerFactoryProvider(dataBroker)) {
+ listener.onDataTreeChanged(List.of(dataTreeModification1));
- final var keyStore = keystoreAdapter.getJavaKeyStore();
- assertTrue(keyStore.containsAlias(privateKey.getName()));
+ final var keyStore = keystoreAdapter.getJavaKeyStore();
+ assertTrue(keyStore.containsAlias(privateKey.getName()));
+ }
}
- @SuppressWarnings("unchecked")
@Test
- public void testWritePrivateKeyAndTrustedCertificate() throws Exception {
+ void testWritePrivateKeyAndTrustedCertificate() throws Exception {
// Prepare PrivateKey configuration
- DataTreeModification<Keystore> dataTreeModification1 = mock(DataTreeModification.class);
- DataObjectModification<Keystore> keystoreObjectModification1 = mock(DataObjectModification.class);
doReturn(keystoreObjectModification1).when(dataTreeModification1).getRootNode();
- DataObjectModification<?> childObjectModification1 = mock(DataObjectModification.class);
- doReturn(List.of(childObjectModification1)).when(keystoreObjectModification1).modifiedChildren();
- doReturn(PrivateKey.class).when(childObjectModification1).dataType();
-
- doReturn(DataObjectModification.ModificationType.WRITE).when(childObjectModification1).modificationType();
+ doReturn(List.of(privateKeyModification)).when(keystoreObjectModification1)
+ .getModifiedChildren(PrivateKey.class);
+ doReturn(DataObjectModification.ModificationType.WRITE).when(privateKeyModification).modificationType();
final var privateKey = getPrivateKey();
- doReturn(privateKey).when(childObjectModification1).dataAfter();
+ doReturn(privateKey).when(privateKeyModification).dataAfter();
// Prepare TrustedCertificate configuration
- DataTreeModification<Keystore> dataTreeModification2 = mock(DataTreeModification.class);
- DataObjectModification<Keystore> keystoreObjectModification2 = mock(DataObjectModification.class);
doReturn(keystoreObjectModification2).when(dataTreeModification2).getRootNode();
- DataObjectModification<?> childObjectModification2 = mock(DataObjectModification.class);
- doReturn(List.of(childObjectModification2)).when(keystoreObjectModification2).modifiedChildren();
- doReturn(TrustedCertificate.class).when(childObjectModification2).dataType();
+ doReturn(List.of()).when(keystoreObjectModification2).getModifiedChildren(PrivateKey.class);
+ doReturn(List.of(trustedCertificateModification)).when(keystoreObjectModification2)
+ .getModifiedChildren(TrustedCertificate.class);
+ doReturn(DataObjectModification.ModificationType.WRITE).when(trustedCertificateModification).modificationType();
- doReturn(DataObjectModification.ModificationType.WRITE)
- .when(childObjectModification2).modificationType();
+ final var trustedCertificate = getTrustedCertificate();
+ doReturn(trustedCertificate).when(trustedCertificateModification).dataAfter();
- final var trustedCertificate = geTrustedCertificate();
- doReturn(trustedCertificate).when(childObjectModification2).dataAfter();
+ try (var keystoreAdapter = new DefaultSslHandlerFactoryProvider(dataBroker)) {
+ // Apply configurations
+ listener.onDataTreeChanged(List.of(dataTreeModification1, dataTreeModification2));
- // Apply configurations
- final var keystoreAdapter = new DefaultSslHandlerFactoryProvider(dataBroker);
- keystoreAdapter.onDataTreeChanged(List.of(dataTreeModification1, dataTreeModification2));
-
- // Check result
- final var keyStore = keystoreAdapter.getJavaKeyStore();
- assertTrue(keyStore.containsAlias(privateKey.getName()));
- assertTrue(keyStore.containsAlias(trustedCertificate.getName()));
+ // Check result
+ final var keyStore = keystoreAdapter.getJavaKeyStore();
+ assertTrue(keyStore.containsAlias(privateKey.getName()));
+ assertTrue(keyStore.containsAlias(trustedCertificate.getName()));
+ }
}
- private PrivateKey getPrivateKey() throws Exception {
- final List<PrivateKey> privateKeys = new ArrayList<>();
- final Document document = readKeystoreXML();
- final NodeList nodeList = document.getElementsByTagName(XML_ELEMENT_PRIVATE_KEY);
+ private static PrivateKey getPrivateKey() throws Exception {
+ final var privateKeys = new ArrayList<PrivateKey>();
+ final var document = readKeystoreXML();
+ final var nodeList = document.getElementsByTagName(XML_ELEMENT_PRIVATE_KEY);
for (int i = 0; i < nodeList.getLength(); i++) {
- final Node node = nodeList.item(i);
- if (node.getNodeType() != Node.ELEMENT_NODE) {
- continue;
- }
- final Element element = (Element)node;
- final String keyName = element.getElementsByTagName(XML_ELEMENT_NAME).item(0).getTextContent();
- final String keyData = element.getElementsByTagName(XML_ELEMENT_DATA).item(0).getTextContent();
- final NodeList certNodes = element.getElementsByTagName(XML_ELEMENT_CERT_CHAIN);
- final List<String> certChain = new ArrayList<>();
- for (int j = 0; j < certNodes.getLength(); j++) {
- final Node certNode = certNodes.item(j);
- if (certNode.getNodeType() != Node.ELEMENT_NODE) {
- continue;
+ if (nodeList.item(i) instanceof Element element) {
+ final var keyName = element.getElementsByTagName(XML_ELEMENT_NAME).item(0).getTextContent();
+ final var keyData = element.getElementsByTagName(XML_ELEMENT_DATA).item(0).getTextContent();
+ final var certNodes = element.getElementsByTagName(XML_ELEMENT_CERT_CHAIN);
+ final var certChain = new ArrayList<String>();
+ for (int j = 0; j < certNodes.getLength(); j++) {
+ if (certNodes.item(j) instanceof Element certNode) {
+ certChain.add(certNode.getTextContent());
+ }
}
- certChain.add(certNode.getTextContent());
- }
- final PrivateKey privateKey = new PrivateKeyBuilder()
+ privateKeys.add(new PrivateKeyBuilder()
.withKey(new PrivateKeyKey(keyName))
.setName(keyName)
.setData(keyData)
.setCertificateChain(certChain)
- .build();
- privateKeys.add(privateKey);
+ .build());
+ }
}
return privateKeys.get(0);
}
- private TrustedCertificate geTrustedCertificate() throws Exception {
- final List<TrustedCertificate> trustedCertificates = new ArrayList<>();
- final Document document = readKeystoreXML();
- final NodeList nodeList = document.getElementsByTagName(XML_ELEMENT_TRUSTED_CERT);
+ private static TrustedCertificate getTrustedCertificate() throws Exception {
+ final var trustedCertificates = new ArrayList<TrustedCertificate>();
+ final var document = readKeystoreXML();
+ final var nodeList = document.getElementsByTagName(XML_ELEMENT_TRUSTED_CERT);
for (int i = 0; i < nodeList.getLength(); i++) {
- final Node node = nodeList.item(i);
- if (node.getNodeType() != Node.ELEMENT_NODE) {
- continue;
- }
- final Element element = (Element)node;
- final String certName = element.getElementsByTagName(XML_ELEMENT_NAME).item(0).getTextContent();
- final String certData = element.getElementsByTagName(XML_ELEMENT_CERT).item(0).getTextContent();
+ if (nodeList.item(i) instanceof Element element) {
+ final var certName = element.getElementsByTagName(XML_ELEMENT_NAME).item(0).getTextContent();
+ final var certData = element.getElementsByTagName(XML_ELEMENT_CERT).item(0).getTextContent();
- final TrustedCertificate certificate = new TrustedCertificateBuilder()
+ trustedCertificates.add(new TrustedCertificateBuilder()
.withKey(new TrustedCertificateKey(certName))
.setName(certName)
.setCertificate(certData)
- .build();
- trustedCertificates.add(certificate);
+ .build());
+ }
}
return trustedCertificates.get(0);
}
- private Document readKeystoreXML() throws Exception {
- return XmlUtil.readXmlToDocument(getClass().getResourceAsStream("/netconf-keystore.xml"));
+ private static Document readKeystoreXML() throws Exception {
+ return XmlUtil.readXmlToDocument(
+ DefaultSslHandlerFactoryProviderTest.class.getResourceAsStream("/netconf-keystore.xml"));
}
}