2 * Copyright (c) 2020 Pantheon Technologies, s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.netconf.callhome.mount.tls;
10 import static java.nio.charset.StandardCharsets.US_ASCII;
12 import java.io.ByteArrayInputStream;
13 import java.io.IOException;
14 import java.io.InputStream;
15 import java.security.PublicKey;
16 import java.security.cert.CertificateException;
17 import java.security.cert.CertificateFactory;
18 import java.util.Base64;
19 import java.util.Collection;
21 import java.util.Optional;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.ConcurrentMap;
25 import java.util.stream.Collectors;
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.opendaylight.mdsal.binding.api.ClusteredDataTreeChangeListener;
28 import org.opendaylight.mdsal.binding.api.DataBroker;
29 import org.opendaylight.mdsal.binding.api.DataObjectModification;
30 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
31 import org.opendaylight.mdsal.binding.api.DataTreeModification;
32 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
33 import org.opendaylight.netconf.callhome.protocol.tls.TlsAllowedDevicesMonitor;
34 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
35 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
36 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.NetconfCallhomeServer;
37 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.AllowedDevices;
38 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.Device;
39 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.device.transport.Tls;
40 import org.opendaylight.yangtools.concepts.Registration;
41 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
45 public class TlsAllowedDevicesMonitorImpl implements TlsAllowedDevicesMonitor, AutoCloseable {
46 private static final Logger LOG = LoggerFactory.getLogger(TlsAllowedDevicesMonitorImpl.class);
48 private final ConcurrentMap<String, String> deviceToPrivateKey = new ConcurrentHashMap<>();
49 private final ConcurrentMap<String, String> deviceToCertificate = new ConcurrentHashMap<>();
50 private final ConcurrentMap<String, PublicKey> certificateToPublicKey = new ConcurrentHashMap<>();
51 private final Registration allowedDevicesReg;
52 private final Registration certificatesReg;
54 public TlsAllowedDevicesMonitorImpl(final DataBroker dataBroker) {
55 allowedDevicesReg = dataBroker.registerDataTreeChangeListener(
56 DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION,
57 InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class)),
58 new AllowedDevicesMonitor());
59 certificatesReg = dataBroker.registerDataTreeChangeListener(
60 DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
61 new CertificatesMonitor());
65 public Optional<String> findDeviceIdByPublicKey(final PublicKey key) {
66 // Find certificate names by the public key
67 final Set<String> certificates = certificateToPublicKey.entrySet().stream()
68 .filter(v -> key.equals(v.getValue()))
69 .map(Map.Entry::getKey)
70 .collect(Collectors.toSet());
72 // Find devices names associated with a certificate name
73 final Set<String> deviceNames = deviceToCertificate.entrySet().stream()
74 .filter(v -> certificates.contains(v.getValue()))
75 .map(Map.Entry::getKey)
76 .collect(Collectors.toSet());
78 // In real world scenario it is not possible to have multiple certificates with the same private/public key,
79 // but in theor/synthetic tests it is practically possible to generate mulitple certificates from a single
80 // private key. In such case it's not possible to pin certificate to particular device.
81 if (deviceNames.size() > 1) {
82 LOG.error("Unable to find device by provided certificate. Possible reason: one certificate configured "
83 + "with multiple devices/names or multiple certificates contain same public key");
84 return Optional.empty();
86 return deviceNames.stream().findFirst();
91 public Set<String> findAllowedKeys() {
92 return Set.copyOf(deviceToPrivateKey.values());
97 allowedDevicesReg.close();
98 certificatesReg.close();
101 private final class CertificatesMonitor implements ClusteredDataTreeChangeListener<Keystore> {
103 public void onDataTreeChanged(@NonNull final Collection<DataTreeModification<Keystore>> changes) {
104 changes.stream().map(DataTreeModification::getRootNode)
105 .flatMap(v -> v.getModifiedChildren().stream())
106 .filter(v -> v.getDataType().equals(TrustedCertificate.class))
107 .map(v -> (DataObjectModification<TrustedCertificate>) v)
108 .forEach(this::updateCertificate);
111 private void updateCertificate(final DataObjectModification<TrustedCertificate> change) {
112 switch (change.getModificationType()) {
114 deleteCertificate(change.getDataBefore());
116 case SUBTREE_MODIFIED:
118 deleteCertificate(change.getDataBefore());
119 writeCertificate(change.getDataAfter());
126 private void deleteCertificate(final TrustedCertificate dataBefore) {
127 if (dataBefore != null) {
128 LOG.debug("Removing public key mapping for certificate {}", dataBefore.getName());
129 certificateToPublicKey.remove(dataBefore.getName());
133 private void writeCertificate(final TrustedCertificate dataAfter) {
134 if (dataAfter != null) {
135 LOG.debug("Adding public key mapping for certificate {}", dataAfter.getName());
136 certificateToPublicKey.putIfAbsent(dataAfter.getName(), buildPublicKey(dataAfter.getCertificate()));
140 private PublicKey buildPublicKey(final String encoded) {
141 final byte[] decoded = Base64.getMimeDecoder().decode(encoded.getBytes(US_ASCII));
143 final CertificateFactory factory = CertificateFactory.getInstance("X.509");
144 try (InputStream in = new ByteArrayInputStream(decoded)) {
145 return factory.generateCertificate(in).getPublicKey();
147 } catch (final CertificateException | IOException e) {
148 LOG.error("Unable to build X.509 certificate from encoded value: {}", e.getLocalizedMessage());
154 private final class AllowedDevicesMonitor implements ClusteredDataTreeChangeListener<Device> {
156 public void onDataTreeChanged(final Collection<DataTreeModification<Device>> mods) {
157 for (final DataTreeModification<Device> dataTreeModification : mods) {
158 final DataObjectModification<Device> deviceMod = dataTreeModification.getRootNode();
159 switch (deviceMod.getModificationType()) {
161 deleteDevice(deviceMod.getDataBefore());
163 case SUBTREE_MODIFIED:
165 deleteDevice(deviceMod.getDataBefore());
166 writeDevice(deviceMod.getDataAfter());
174 private void deleteDevice(final Device dataBefore) {
175 if (dataBefore != null && dataBefore.getTransport() instanceof Tls) {
176 LOG.debug("Removing device {}", dataBefore.getUniqueId());
177 deviceToPrivateKey.remove(dataBefore.getUniqueId());
178 deviceToCertificate.remove(dataBefore.getUniqueId());
182 private void writeDevice(final Device dataAfter) {
183 if (dataAfter != null && dataAfter.getTransport() instanceof Tls tls) {
184 LOG.debug("Adding device {}", dataAfter.getUniqueId());
185 final var tlsClientParams = tls.getTlsClientParams();
186 deviceToPrivateKey.putIfAbsent(dataAfter.getUniqueId(), tlsClientParams.getKeyId());
187 deviceToCertificate.putIfAbsent(dataAfter.getUniqueId(), tlsClientParams.getCertificateId());