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");
}
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));
}
@Override
public String getSessionId() {
- return channelId;
+ return deviceId;
}
@Override
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;
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);
this.subsystemListener = requireNonNull(subsystemListener);
this.bossGroup = requireNonNull(bossGroup);
this.workerGroup = requireNonNull(workerGroup);
+ this.allowedDevicesMonitor = requireNonNull(allowedDevicesMonitor);
}
public void start() {
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();
private CallHomeNetconfSubsystemListener subsystemListener;
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
+ private TlsAllowedDevicesMonitor allowedDevicesMonitor;
public NetconfCallHomeTlsServerBuilder setHost(final String host) {
this.host = host;
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
--- /dev/null
+/*
+ * 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();
+
+}
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;
private final CallHomeNetconfSubsystemListener subsystemListener;
private final EventLoopGroup bossGroup;
private final EventLoopGroup workerGroup;
+ private final TlsAllowedDevicesMonitor allowedDevicesMonitor;
private NetconfCallHomeTlsServer server;
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() {
.setSubsystemListener(subsystemListener)
.setBossGroup(bossGroup)
.setWorkerGroup(workerGroup)
+ .setAllowedDevicesMonitor(allowedDevicesMonitor)
.build();
server.start();
@Override
public void close() {
server.stop();
+ allowedDevicesMonitor.close();
}
}
\ No newline at end of file
*/
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
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
--- /dev/null
+/*
+ * 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());
+ }
+ }
+ }
+}