Identify Call-Home device connected over TLS by the provided certificate 13/90613/31
authorOleksii Mozghovyi <oleksii.mozghovyi@pantheon.tech>
Wed, 24 Jun 2020 00:59:20 +0000 (03:59 +0300)
committerTomas Cere <tomas.cere@pantheon.tech>
Mon, 2 Nov 2020 13:25:31 +0000 (13:25 +0000)
JIRA: NETCONF-5
Change-Id: Ie5008ac806e875902e1b28bc3aa94f6f7d3d466b
Signed-off-by: Oleksii Mozghovyi <oleksii.mozghovyi@pantheon.tech>
Signed-off-by: Vladyslav Marchenko <vladyslav.marchenko@pantheon.tech>
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/tls/CallHomeTlsSessionContext.java
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/tls/NetconfCallHomeTlsServer.java
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/tls/NetconfCallHomeTlsServerBuilder.java
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/tls/TlsAllowedDevicesMonitor.java [new file with mode: 0644]
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/tls/NetconfCallHomeTlsService.java
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/tls/SslHandlerFactoryAdapter.java
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/tls/TlsAllowedDevicesMonitorImpl.java [new file with mode: 0644]

index 4e9f3834d56aad61374703a8e60c2c3acda902a6..7d8334c1460db11def88b2e07710b64a5e6dd46e 100644 (file)
@@ -38,15 +38,15 @@ final class CallHomeTlsSessionContext implements CallHomeProtocolSessionContext
     private final AtomicBoolean activated = new AtomicBoolean();
     private final SslHandlerFactory sslHandlerFactory;
     private final CallHomeNetconfSubsystemListener subsystemListener;
-    private final String channelId;
+    private final String deviceId;
     private final Channel channel;
     private final PublicKey publicKey;
     private final SocketAddress socketAddress;
 
-    CallHomeTlsSessionContext(final Channel channel, final SslHandlerFactory sslHandlerFactory,
+    CallHomeTlsSessionContext(final String deviceId, final Channel channel, final SslHandlerFactory sslHandlerFactory,
                               final CallHomeNetconfSubsystemListener subsystemListener) {
         this.channel = requireNonNull(channel, "channel");
-        this.channelId = channel.id().asLongText();
+        this.deviceId = deviceId;
         this.socketAddress = channel.remoteAddress();
         this.publicKey = createPublicKey(channel);
         this.sslHandlerFactory = requireNonNull(sslHandlerFactory, "sslHandlerFactory");
@@ -58,7 +58,7 @@ final class CallHomeTlsSessionContext implements CallHomeProtocolSessionContext
     }
 
     void openNetconfChannel(final Channel ch) {
-        LOG.debug("Opening NETCONF Subsystem on TLS connection {}", channelId);
+        LOG.debug("Opening NETCONF Subsystem on TLS connection {}", deviceId);
         subsystemListener.onNetconfSubsystemOpened(this, listener -> doActivate(ch, listener));
     }
 
@@ -94,7 +94,7 @@ final class CallHomeTlsSessionContext implements CallHomeProtocolSessionContext
 
     @Override
     public String getSessionId() {
-        return channelId;
+        return deviceId;
     }
 
     @Override
index ce3111ccd0d32249c15d54ffa8f68ca18c266d12..4e5daf9b33b490b0ca63e5a8e4881b3de9ce01c8 100644 (file)
@@ -15,9 +15,13 @@ import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelOption;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.ssl.SslHandler;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.GenericFutureListener;
 import java.net.InetSocketAddress;
+import java.security.PublicKey;
+import java.security.cert.Certificate;
+import java.util.Optional;
 import org.opendaylight.netconf.callhome.protocol.CallHomeNetconfSubsystemListener;
 import org.opendaylight.netconf.client.SslHandlerFactory;
 import org.slf4j.Logger;
@@ -34,13 +38,15 @@ public class NetconfCallHomeTlsServer {
     private final CallHomeNetconfSubsystemListener subsystemListener;
     private final EventLoopGroup bossGroup;
     private final EventLoopGroup workerGroup;
+    private final TlsAllowedDevicesMonitor allowedDevicesMonitor;
 
     private ChannelFuture cf;
 
     NetconfCallHomeTlsServer(final String host, final Integer port, final Integer timeout, final Integer maxConnections,
                              final SslHandlerFactory sslHandlerFactory,
                              final CallHomeNetconfSubsystemListener subsystemListener,
-                             final EventLoopGroup bossGroup, final EventLoopGroup workerGroup) {
+                             final EventLoopGroup bossGroup, final EventLoopGroup workerGroup,
+                             final TlsAllowedDevicesMonitor allowedDevicesMonitor) {
         this.host = requireNonNull(host);
         this.port = requireNonNull(port);
         this.timeout = requireNonNull(timeout);
@@ -49,6 +55,7 @@ public class NetconfCallHomeTlsServer {
         this.subsystemListener = requireNonNull(subsystemListener);
         this.bossGroup = requireNonNull(bossGroup);
         this.workerGroup = requireNonNull(workerGroup);
+        this.allowedDevicesMonitor = requireNonNull(allowedDevicesMonitor);
     }
 
     public void start() {
@@ -71,9 +78,19 @@ public class NetconfCallHomeTlsServer {
             if (future.isSuccess()) {
                 LOG.debug("SSL handshake completed successfully, accepting connection...");
                 final Channel channel = future.get();
-                final CallHomeTlsSessionContext tlsSessionContext = new CallHomeTlsSessionContext(channel,
-                    sslHandlerFactory, subsystemListener);
-                tlsSessionContext.openNetconfChannel(channel);
+                // If the ssl handshake was successful it is expected that session contains peer certificate(s)
+                final Certificate cert = channel.pipeline().get(SslHandler.class).engine().getSession()
+                    .getPeerCertificates()[0];
+                final PublicKey publicKey = cert.getPublicKey();
+                final Optional<String> deviceId = allowedDevicesMonitor.findDeviceIdByPublicKey(publicKey);
+                if (deviceId.isEmpty()) {
+                    LOG.error("Unable to identify connected device by provided certificate");
+                    channel.close();
+                } else {
+                    final CallHomeTlsSessionContext tlsSessionContext = new CallHomeTlsSessionContext(deviceId.get(),
+                        channel, sslHandlerFactory, subsystemListener);
+                    tlsSessionContext.openNetconfChannel(channel);
+                }
             } else {
                 LOG.debug("SSL handshake failed, rejecting connection...");
                 future.get().close();
index 9740065768d4d4e977157f4087db6fd8b26820b6..4fe3cb9ff74668036f3db3afda51fc1854648831 100644 (file)
@@ -20,6 +20,7 @@ public class NetconfCallHomeTlsServerBuilder {
     private CallHomeNetconfSubsystemListener subsystemListener;
     private EventLoopGroup bossGroup;
     private EventLoopGroup workerGroup;
+    private TlsAllowedDevicesMonitor allowedDevicesMonitor;
 
     public NetconfCallHomeTlsServerBuilder setHost(final String host) {
         this.host = host;
@@ -61,8 +62,13 @@ public class NetconfCallHomeTlsServerBuilder {
         return this;
     }
 
+    public NetconfCallHomeTlsServerBuilder setAllowedDevicesMonitor(final TlsAllowedDevicesMonitor devicesMonitor) {
+        this.allowedDevicesMonitor = devicesMonitor;
+        return this;
+    }
+
     public NetconfCallHomeTlsServer build() {
         return new NetconfCallHomeTlsServer(host, port, timeout, maxConnections, sslHandlerFactory, subsystemListener,
-            bossGroup, workerGroup);
+            bossGroup, workerGroup, allowedDevicesMonitor);
     }
 }
\ No newline at end of file
diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/tls/TlsAllowedDevicesMonitor.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/tls/TlsAllowedDevicesMonitor.java
new file mode 100644 (file)
index 0000000..5c71dde
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2020 Pantheon Technologies, 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.callhome.protocol.tls;
+
+import java.security.PublicKey;
+import java.util.Optional;
+import java.util.Set;
+
+public interface TlsAllowedDevicesMonitor extends AutoCloseable {
+
+    /**
+     * Returns a Call-Home Device ID by the public key.
+     */
+    Optional<String> findDeviceIdByPublicKey(PublicKey publicKey);
+
+    /**
+     * Returns a set of IDs for the keys associated with Call-Home devices.
+     */
+    Set<String> findAllowedKeys();
+
+    @Override
+    void close();
+
+}
index 8ea24652b25554d6f862a8e2fe7fb11a38c1b035..7465c6cf65b1236f4827aad07ab6e45b8732e4da 100644 (file)
@@ -14,6 +14,7 @@ import org.opendaylight.mdsal.binding.api.DataBroker;
 import org.opendaylight.netconf.callhome.protocol.CallHomeNetconfSubsystemListener;
 import org.opendaylight.netconf.callhome.protocol.tls.NetconfCallHomeTlsServer;
 import org.opendaylight.netconf.callhome.protocol.tls.NetconfCallHomeTlsServerBuilder;
+import org.opendaylight.netconf.callhome.protocol.tls.TlsAllowedDevicesMonitor;
 import org.opendaylight.netconf.client.SslHandlerFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -26,6 +27,7 @@ public class NetconfCallHomeTlsService implements AutoCloseable {
     private final CallHomeNetconfSubsystemListener subsystemListener;
     private final EventLoopGroup bossGroup;
     private final EventLoopGroup workerGroup;
+    private final TlsAllowedDevicesMonitor allowedDevicesMonitor;
 
     private NetconfCallHomeTlsServer server;
 
@@ -38,7 +40,8 @@ public class NetconfCallHomeTlsService implements AutoCloseable {
         this.subsystemListener = requireNonNull(subsystemListener);
         this.bossGroup = requireNonNull(bossGroup);
         this.workerGroup = requireNonNull(workerGroup);
-        this.sslHandlerFactory = new SslHandlerFactoryAdapter(dataBroker);
+        this.allowedDevicesMonitor = new TlsAllowedDevicesMonitorImpl(dataBroker);
+        this.sslHandlerFactory = new SslHandlerFactoryAdapter(dataBroker, allowedDevicesMonitor);
     }
 
     public void init() {
@@ -53,6 +56,7 @@ public class NetconfCallHomeTlsService implements AutoCloseable {
             .setSubsystemListener(subsystemListener)
             .setBossGroup(bossGroup)
             .setWorkerGroup(workerGroup)
+            .setAllowedDevicesMonitor(allowedDevicesMonitor)
             .build();
         server.start();
 
@@ -62,5 +66,6 @@ public class NetconfCallHomeTlsService implements AutoCloseable {
     @Override
     public void close() {
         server.stop();
+        allowedDevicesMonitor.close();
     }
 }
\ No newline at end of file
index 3dfe21b849fc493a974751c5d95f2b9889531f5d..a012ff7a103822fcc65904628d644b5e6e7981cd 100644 (file)
@@ -7,50 +7,26 @@
  */
 package org.opendaylight.netconf.callhome.mount.tls;
 
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.common.collect.ImmutableSet;
 import io.netty.handler.ssl.SslHandler;
-import java.util.Collection;
 import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
 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.callhome.protocol.tls.TlsAllowedDevicesMonitor;
 import org.opendaylight.netconf.client.SslHandlerFactory;
 import org.opendaylight.netconf.sal.connect.netconf.sal.NetconfKeystoreAdapter;
 import org.opendaylight.netconf.sal.connect.util.SslHandlerFactoryImpl;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.NetconfCallhomeServer;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.AllowedDevices;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.Device;
-import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.Tls;
-import org.opendaylight.yangtools.concepts.AbstractRegistration;
-import org.opendaylight.yangtools.concepts.Registration;
-import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class SslHandlerFactoryAdapter extends AbstractRegistration implements SslHandlerFactory {
-    private static final DataTreeIdentifier<Device> ALLOWED_DEVICES =
-        DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION,
-            InstanceIdentifier.builder(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class)
-                .build());
-
+public class SslHandlerFactoryAdapter implements SslHandlerFactory {
     private static final Logger LOG = LoggerFactory.getLogger(SslHandlerFactoryAdapter.class);
 
-    private final DeviceListener deviceListener = new DeviceListener();
-    private final NetconfKeystoreAdapter keystoreAdapter;
+    private final TlsAllowedDevicesMonitor allowedDevicesMonitor;
     private final SslHandlerFactory sslHandlerFactory;
-    private final Registration deviceListenerReg;
 
-    public SslHandlerFactoryAdapter(final DataBroker dataBroker) {
-        this.keystoreAdapter = new NetconfKeystoreAdapter(dataBroker);
+    public SslHandlerFactoryAdapter(final DataBroker dataBroker, final TlsAllowedDevicesMonitor allowedDevicesMonitor) {
+        final NetconfKeystoreAdapter keystoreAdapter = new NetconfKeystoreAdapter(dataBroker);
         this.sslHandlerFactory = new SslHandlerFactoryImpl(keystoreAdapter);
-        this.deviceListenerReg = dataBroker.registerDataTreeChangeListener(ALLOWED_DEVICES, deviceListener);
+        this.allowedDevicesMonitor = allowedDevicesMonitor;
     }
 
     @Override
@@ -63,57 +39,11 @@ public class SslHandlerFactoryAdapter extends AbstractRegistration implements Ss
         return createSslHandlerFilteredByKeys();
     }
 
-    @Override
-    protected void removeRegistration() {
-        deviceListenerReg.close();
-    }
-
     private SslHandler createSslHandlerFilteredByKeys() {
-        return sslHandlerFactory.createSslHandler(deviceListener.getAllowedKeys());
-    }
-
-    private static final class DeviceListener implements DataTreeChangeListener<Device> {
-        private final ConcurrentMap<String, String> allowedKeys = new ConcurrentHashMap<>();
-
-        @Override
-        public void onDataTreeChanged(final Collection<DataTreeModification<Device>> mods) {
-            for (final DataTreeModification<Device> dataTreeModification : mods) {
-                final DataObjectModification<Device> deviceMod = dataTreeModification.getRootNode();
-                final DataObjectModification.ModificationType modType = deviceMod.getModificationType();
-                switch (modType) {
-                    case DELETE:
-                        deleteDevice(deviceMod.getDataBefore());
-                        break;
-                    case SUBTREE_MODIFIED:
-                    case WRITE:
-                        deleteDevice(deviceMod.getDataBefore());
-                        writeDevice(deviceMod.getDataAfter());
-                        break;
-                    default:
-                        throw new IllegalStateException("Unhandled modification type " + modType);
-                }
-            }
-        }
-
-        Set<String> getAllowedKeys() {
-            final Set<String> ret = ImmutableSet.copyOf(allowedKeys.values());
-            checkState(!ret.isEmpty(), "No associated keys for TLS authentication were found");
-            return ret;
-        }
-
-        private void deleteDevice(final Device dataBefore) {
-            if (dataBefore != null && dataBefore.getTransport() instanceof Tls) {
-                LOG.debug("Removing device {}", dataBefore.getUniqueId());
-                allowedKeys.remove(dataBefore.getUniqueId());
-            }
-        }
-
-        private void writeDevice(final Device dataAfter) {
-            if (dataAfter != null && dataAfter.getTransport() instanceof Tls) {
-                LOG.debug("Adding device {}", dataAfter.getUniqueId());
-                final String tlsKeyId = ((Tls) dataAfter.getTransport()).getTlsClientParams().getKeyId();
-                allowedKeys.putIfAbsent(dataAfter.getUniqueId(), tlsKeyId);
-            }
+        if (allowedDevicesMonitor.findAllowedKeys().isEmpty()) {
+            LOG.error("No associated keys for TLS authentication were found");
+            throw new IllegalStateException("No associated keys for TLS authentication were found");
         }
+        return sslHandlerFactory.createSslHandler(allowedDevicesMonitor.findAllowedKeys());
     }
 }
\ No newline at end of file
diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/tls/TlsAllowedDevicesMonitorImpl.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/tls/TlsAllowedDevicesMonitorImpl.java
new file mode 100644 (file)
index 0000000..78163ae
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2020 Pantheon Technologies, 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.callhome.mount.tls;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.PublicKey;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.stream.Collectors;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.mdsal.binding.api.ClusteredDataTreeChangeListener;
+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.DataTreeModification;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.netconf.callhome.protocol.tls.TlsAllowedDevicesMonitor;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.NetconfCallhomeServer;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.AllowedDevices;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.Device;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.Tls;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.tls.TlsClientParams;
+import org.opendaylight.yangtools.concepts.ListenerRegistration;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TlsAllowedDevicesMonitorImpl implements TlsAllowedDevicesMonitor, AutoCloseable {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TlsAllowedDevicesMonitorImpl.class);
+
+    private static final InstanceIdentifier<Device> ALLOWED_DEVICES_PATH =
+        InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class);
+    private static final DataTreeIdentifier<Device> ALLOWED_DEVICES =
+        DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION, ALLOWED_DEVICES_PATH);
+    private static final InstanceIdentifier<Keystore> KEYSTORE_PATH = InstanceIdentifier.create(Keystore.class);
+    private static final DataTreeIdentifier<Keystore> KEYSTORE = DataTreeIdentifier.create(
+        LogicalDatastoreType.CONFIGURATION, KEYSTORE_PATH);
+
+    private static final ConcurrentMap<String, String> DEVICE_TO_PRIVATE_KEY = new ConcurrentHashMap<>();
+    private static final ConcurrentMap<String, String> DEVICE_TO_CERTIFICATE = new ConcurrentHashMap<>();
+    private static final ConcurrentMap<String, PublicKey> CERTIFICATE_TO_PUBLIC_KEY = new ConcurrentHashMap<>();
+
+    private final ListenerRegistration<AllowedDevicesMonitor> allowedDevicesReg;
+    private final ListenerRegistration<CertificatesMonitor> certificatesReg;
+
+    public TlsAllowedDevicesMonitorImpl(final DataBroker dataBroker) {
+        allowedDevicesReg = dataBroker.registerDataTreeChangeListener(ALLOWED_DEVICES, new AllowedDevicesMonitor());
+        certificatesReg = dataBroker.registerDataTreeChangeListener(KEYSTORE, new CertificatesMonitor());
+    }
+
+    @Override
+    public Optional<String> findDeviceIdByPublicKey(@NonNull final PublicKey key) {
+        // Find certificate names by the public key
+        final Set<String> certificates = CERTIFICATE_TO_PUBLIC_KEY.entrySet().stream()
+            .filter(v -> key.equals(v.getValue()))
+            .map(Map.Entry::getKey)
+            .collect(Collectors.toSet());
+
+        // Find devices names associated with a certificate name
+        final Set<String> deviceNames = DEVICE_TO_CERTIFICATE.entrySet().stream()
+            .filter(v -> certificates.contains(v.getValue()))
+            .map(Map.Entry::getKey)
+            .collect(Collectors.toSet());
+
+        // In real world scenario it is not possible to have multiple certificates with the same private/public key,
+        // but in theor/synthetic tests it is practically possible to generate mulitple certificates from a single
+        // private key. In such case it's not possible to pin certificate to particular device.
+        if (deviceNames.size() > 1) {
+            LOG.error("Unable to find device by provided certificate. Possible reason: one certificate configured "
+                + "with multiple devices/names or multiple certificates contain same public key");
+            return Optional.empty();
+        } else {
+            return deviceNames.stream().findFirst();
+        }
+    }
+
+    @Override
+    public Set<String> findAllowedKeys() {
+        return new HashSet<>(DEVICE_TO_PRIVATE_KEY.values());
+    }
+
+    @Override
+    public void close() {
+        allowedDevicesReg.close();
+        certificatesReg.close();
+    }
+
+    private static class CertificatesMonitor implements ClusteredDataTreeChangeListener<Keystore> {
+
+        @Override
+        public void onDataTreeChanged(@NonNull final Collection<DataTreeModification<Keystore>> changes) {
+            changes.stream().map(DataTreeModification::getRootNode)
+                .flatMap(v -> v.getModifiedChildren().stream())
+                .filter(v -> v.getDataType().equals(TrustedCertificate.class))
+                .map(v -> (DataObjectModification<TrustedCertificate>) v)
+                .forEach(this::updateCertificate);
+        }
+
+        private void updateCertificate(final DataObjectModification<TrustedCertificate> change) {
+            final DataObjectModification.ModificationType modType = change.getModificationType();
+            switch (modType) {
+                case DELETE:
+                    deleteCertificate(change.getDataBefore());
+                    break;
+                case SUBTREE_MODIFIED:
+                case WRITE:
+                    deleteCertificate(change.getDataBefore());
+                    writeCertificate(change.getDataAfter());
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        private void deleteCertificate(final TrustedCertificate dataBefore) {
+            if (dataBefore != null) {
+                LOG.debug("Removing public key mapping for certificate {}", dataBefore.getName());
+                CERTIFICATE_TO_PUBLIC_KEY.remove(dataBefore.getName());
+            }
+        }
+
+        private void writeCertificate(final TrustedCertificate dataAfter) {
+            if (dataAfter != null) {
+                LOG.debug("Adding public key mapping for certificate {}", dataAfter.getName());
+                CERTIFICATE_TO_PUBLIC_KEY.putIfAbsent(dataAfter.getName(), buildPublicKey(dataAfter.getCertificate()));
+            }
+        }
+
+        private PublicKey buildPublicKey(final String encoded) {
+            final byte[] decoded = Base64.getMimeDecoder().decode(encoded.getBytes(US_ASCII));
+            try {
+                final CertificateFactory factory = CertificateFactory.getInstance("X.509");
+                try (InputStream in = new ByteArrayInputStream(decoded)) {
+                    return factory.generateCertificate(in).getPublicKey();
+                }
+            } catch (final CertificateException | IOException e) {
+                LOG.error("Unable to build X.509 certificate from encoded value: {}", e.getLocalizedMessage());
+            }
+            return null;
+        }
+
+    }
+
+    private static class AllowedDevicesMonitor implements ClusteredDataTreeChangeListener<Device> {
+
+        @Override
+        public final void onDataTreeChanged(final Collection<DataTreeModification<Device>> mods) {
+            for (final DataTreeModification<Device> dataTreeModification : mods) {
+                final DataObjectModification<Device> deviceMod = dataTreeModification.getRootNode();
+                final DataObjectModification.ModificationType modType = deviceMod.getModificationType();
+                switch (modType) {
+                    case DELETE:
+                        deleteDevice(deviceMod.getDataBefore());
+                        break;
+                    case SUBTREE_MODIFIED:
+                    case WRITE:
+                        deleteDevice(deviceMod.getDataBefore());
+                        writeDevice(deviceMod.getDataAfter());
+                        break;
+                    default:
+                        break;
+                }
+            }
+        }
+
+        private void deleteDevice(final Device dataBefore) {
+            if (dataBefore != null && dataBefore.getTransport() instanceof Tls) {
+                LOG.debug("Removing device {}", dataBefore.getUniqueId());
+                DEVICE_TO_PRIVATE_KEY.remove(dataBefore.getUniqueId());
+                DEVICE_TO_CERTIFICATE.remove(dataBefore.getUniqueId());
+            }
+        }
+
+        private void writeDevice(final Device dataAfter) {
+            if (dataAfter != null && dataAfter.getTransport() instanceof Tls) {
+                LOG.debug("Adding device {}", dataAfter.getUniqueId());
+                final TlsClientParams clientParams = ((Tls) dataAfter.getTransport()).getTlsClientParams();
+                DEVICE_TO_PRIVATE_KEY.putIfAbsent(dataAfter.getUniqueId(), clientParams.getKeyId());
+                DEVICE_TO_CERTIFICATE.putIfAbsent(dataAfter.getUniqueId(), clientParams.getCertificateId());
+            }
+        }
+    }
+}