Split out keystore-legacy 85/110085/5
authorRobert Varga <robert.varga@pantheon.tech>
Mon, 29 Jan 2024 19:30:25 +0000 (20:30 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Tue, 30 Jan 2024 00:15:48 +0000 (01:15 +0100)
Tracking down the lifecycle of SslContexts brings us to our handling of
key material.

This is currently tangled netconf-client-mdsal using our home-grown (and
problematic) model.

Split out netconf-keystore.yang and the baseline implementation into
keystore-legacy. Now that it sits side-by-side with keystore-api, we can
compare the two.

Since this change requires us to intercept a hidden object, rework the
test in terms of JUnit5.

JIRA: NETCONF-1237
Change-Id: Id9d410e88ec588e148c5f1dff3aad574b3cc8328
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
artifacts/pom.xml
keystore/keystore-legacy/pom.xml [new file with mode: 0644]
keystore/keystore-legacy/src/main/java/org/opendaylight/netconf/keystore/legacy/AbstractNetconfKeystore.java [new file with mode: 0644]
keystore/keystore-legacy/src/main/java/org/opendaylight/netconf/keystore/legacy/ConfigListener.java [new file with mode: 0644]
keystore/keystore-legacy/src/main/java/org/opendaylight/netconf/keystore/legacy/SecurityHelper.java [new file with mode: 0644]
keystore/keystore-legacy/src/main/yang/netconf-keystore.yang [moved from plugins/netconf-client-mdsal/src/main/yang/netconf-keystore.yang with 100% similarity]
keystore/pom.xml
plugins/netconf-client-mdsal/pom.xml
plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslHandlerFactoryProvider.java
plugins/netconf-client-mdsal/src/test/java/org/opendaylight/netconf/client/mdsal/impl/DefaultSslHandlerFactoryProviderTest.java

index 74175125b092976fd76990a6c11343cab9661a92..8edbc87c1a542ef521884fa00f2b32f2c103b728 100644 (file)
                 <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>
diff --git a/keystore/keystore-legacy/pom.xml b/keystore/keystore-legacy/pom.xml
new file mode 100644 (file)
index 0000000..637f400
--- /dev/null
@@ -0,0 +1,38 @@
+<?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>
diff --git a/keystore/keystore-legacy/src/main/java/org/opendaylight/netconf/keystore/legacy/AbstractNetconfKeystore.java b/keystore/keystore-legacy/src/main/java/org/opendaylight/netconf/keystore/legacy/AbstractNetconfKeystore.java
new file mode 100644 (file)
index 0000000..0cae9c5
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * 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)
+    }
+}
diff --git a/keystore/keystore-legacy/src/main/java/org/opendaylight/netconf/keystore/legacy/ConfigListener.java b/keystore/keystore-legacy/src/main/java/org/opendaylight/netconf/keystore/legacy/ConfigListener.java
new file mode 100644 (file)
index 0000000..35d3c45
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * 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
diff --git a/keystore/keystore-legacy/src/main/java/org/opendaylight/netconf/keystore/legacy/SecurityHelper.java b/keystore/keystore-legacy/src/main/java/org/opendaylight/netconf/keystore/legacy/SecurityHelper.java
new file mode 100644 (file)
index 0000000..621240b
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * 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
index 71212f7bb16b233e2855ec9b7fe824c66b5177b1..d277fd0d4496885dd04f138bdd86f285d53e60f5 100644 (file)
@@ -30,6 +30,7 @@
 
     <modules>
         <module>keystore-api</module>
+        <module>keystore-legacy</module>
         <module>keystore-none</module>
     </modules>
 </project>
index 7f2e04ca3e58b4f091a3f73eab9fcb4d58c6c6a2..b225cd928ed24226948912cf90b04f92a8cf28f5 100644 (file)
             <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>
index 165e5ab710cae74e4c90cfd24f6f584125abde28..64752f664928fc4b44dc5d449ae9bc53427723a0 100644 (file)
@@ -9,142 +9,55 @@ package org.opendaylight.netconf.client.mdsal.impl;
 
 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
@@ -187,7 +100,7 @@ public final class DefaultSslHandlerFactoryProvider
     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");
         }
 
@@ -197,7 +110,7 @@ public final class DefaultSslHandlerFactoryProvider
         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;
@@ -219,72 +132,10 @@ public final class DefaultSslHandlerFactoryProvider
             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;
-        }
-    }
 }
index aabe79a73073d47b8748f33e1080eead2f0ac037..c59f62805a1bd27f43e34712bb8f8e1225d77370 100644 (file)
@@ -12,20 +12,20 @@ import static org.hamcrest.MatcherAssert.assertThat;
 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;
@@ -38,11 +38,9 @@ import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.
 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";
@@ -54,144 +52,139 @@ public class DefaultSslHandlerFactoryProviderTest {
     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"));
     }
 }