Move netconf topology applications in apps/
[netconf.git] / apps / callhome-provider / src / main / java / org / opendaylight / netconf / callhome / mount / tls / TlsAllowedDevicesMonitorImpl.java
1 /*
2  * Copyright (c) 2020 Pantheon Technologies, s.r.o. and others. All rights reserved.
3  *
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
7  */
8 package org.opendaylight.netconf.callhome.mount.tls;
9
10 import static java.nio.charset.StandardCharsets.US_ASCII;
11
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;
20 import java.util.HashSet;
21 import java.util.Map;
22 import java.util.Optional;
23 import java.util.Set;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.ConcurrentMap;
26 import java.util.stream.Collectors;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.opendaylight.mdsal.binding.api.ClusteredDataTreeChangeListener;
29 import org.opendaylight.mdsal.binding.api.DataBroker;
30 import org.opendaylight.mdsal.binding.api.DataObjectModification;
31 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
32 import org.opendaylight.mdsal.binding.api.DataTreeModification;
33 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
34 import org.opendaylight.netconf.callhome.protocol.tls.TlsAllowedDevicesMonitor;
35 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
36 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
37 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.NetconfCallhomeServer;
38 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.AllowedDevices;
39 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.Device;
40 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.device.transport.Tls;
41 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.device.transport.tls.TlsClientParams;
42 import org.opendaylight.yangtools.concepts.ListenerRegistration;
43 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 public class TlsAllowedDevicesMonitorImpl implements TlsAllowedDevicesMonitor, AutoCloseable {
48
49     private static final Logger LOG = LoggerFactory.getLogger(TlsAllowedDevicesMonitorImpl.class);
50
51     private static final InstanceIdentifier<Device> ALLOWED_DEVICES_PATH =
52         InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class);
53     private static final DataTreeIdentifier<Device> ALLOWED_DEVICES =
54         DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION, ALLOWED_DEVICES_PATH);
55     private static final InstanceIdentifier<Keystore> KEYSTORE_PATH = InstanceIdentifier.create(Keystore.class);
56     private static final DataTreeIdentifier<Keystore> KEYSTORE = DataTreeIdentifier.create(
57         LogicalDatastoreType.CONFIGURATION, KEYSTORE_PATH);
58
59     private static final ConcurrentMap<String, String> DEVICE_TO_PRIVATE_KEY = new ConcurrentHashMap<>();
60     private static final ConcurrentMap<String, String> DEVICE_TO_CERTIFICATE = new ConcurrentHashMap<>();
61     private static final ConcurrentMap<String, PublicKey> CERTIFICATE_TO_PUBLIC_KEY = new ConcurrentHashMap<>();
62
63     private final ListenerRegistration<AllowedDevicesMonitor> allowedDevicesReg;
64     private final ListenerRegistration<CertificatesMonitor> certificatesReg;
65
66     public TlsAllowedDevicesMonitorImpl(final DataBroker dataBroker) {
67         allowedDevicesReg = dataBroker.registerDataTreeChangeListener(ALLOWED_DEVICES, new AllowedDevicesMonitor());
68         certificatesReg = dataBroker.registerDataTreeChangeListener(KEYSTORE, new CertificatesMonitor());
69     }
70
71     @Override
72     public Optional<String> findDeviceIdByPublicKey(final PublicKey key) {
73         // Find certificate names by the public key
74         final Set<String> certificates = CERTIFICATE_TO_PUBLIC_KEY.entrySet().stream()
75             .filter(v -> key.equals(v.getValue()))
76             .map(Map.Entry::getKey)
77             .collect(Collectors.toSet());
78
79         // Find devices names associated with a certificate name
80         final Set<String> deviceNames = DEVICE_TO_CERTIFICATE.entrySet().stream()
81             .filter(v -> certificates.contains(v.getValue()))
82             .map(Map.Entry::getKey)
83             .collect(Collectors.toSet());
84
85         // In real world scenario it is not possible to have multiple certificates with the same private/public key,
86         // but in theor/synthetic tests it is practically possible to generate mulitple certificates from a single
87         // private key. In such case it's not possible to pin certificate to particular device.
88         if (deviceNames.size() > 1) {
89             LOG.error("Unable to find device by provided certificate. Possible reason: one certificate configured "
90                 + "with multiple devices/names or multiple certificates contain same public key");
91             return Optional.empty();
92         } else {
93             return deviceNames.stream().findFirst();
94         }
95     }
96
97     @Override
98     public Set<String> findAllowedKeys() {
99         return new HashSet<>(DEVICE_TO_PRIVATE_KEY.values());
100     }
101
102     @Override
103     public void close() {
104         allowedDevicesReg.close();
105         certificatesReg.close();
106     }
107
108     private static class CertificatesMonitor implements ClusteredDataTreeChangeListener<Keystore> {
109
110         @Override
111         public void onDataTreeChanged(@NonNull final Collection<DataTreeModification<Keystore>> changes) {
112             changes.stream().map(DataTreeModification::getRootNode)
113                 .flatMap(v -> v.getModifiedChildren().stream())
114                 .filter(v -> v.getDataType().equals(TrustedCertificate.class))
115                 .map(v -> (DataObjectModification<TrustedCertificate>) v)
116                 .forEach(this::updateCertificate);
117         }
118
119         private void updateCertificate(final DataObjectModification<TrustedCertificate> change) {
120             final DataObjectModification.ModificationType modType = change.getModificationType();
121             switch (modType) {
122                 case DELETE:
123                     deleteCertificate(change.getDataBefore());
124                     break;
125                 case SUBTREE_MODIFIED:
126                 case WRITE:
127                     deleteCertificate(change.getDataBefore());
128                     writeCertificate(change.getDataAfter());
129                     break;
130                 default:
131                     break;
132             }
133         }
134
135         private void deleteCertificate(final TrustedCertificate dataBefore) {
136             if (dataBefore != null) {
137                 LOG.debug("Removing public key mapping for certificate {}", dataBefore.getName());
138                 CERTIFICATE_TO_PUBLIC_KEY.remove(dataBefore.getName());
139             }
140         }
141
142         private void writeCertificate(final TrustedCertificate dataAfter) {
143             if (dataAfter != null) {
144                 LOG.debug("Adding public key mapping for certificate {}", dataAfter.getName());
145                 CERTIFICATE_TO_PUBLIC_KEY.putIfAbsent(dataAfter.getName(), buildPublicKey(dataAfter.getCertificate()));
146             }
147         }
148
149         private PublicKey buildPublicKey(final String encoded) {
150             final byte[] decoded = Base64.getMimeDecoder().decode(encoded.getBytes(US_ASCII));
151             try {
152                 final CertificateFactory factory = CertificateFactory.getInstance("X.509");
153                 try (InputStream in = new ByteArrayInputStream(decoded)) {
154                     return factory.generateCertificate(in).getPublicKey();
155                 }
156             } catch (final CertificateException | IOException e) {
157                 LOG.error("Unable to build X.509 certificate from encoded value: {}", e.getLocalizedMessage());
158             }
159             return null;
160         }
161
162     }
163
164     private static class AllowedDevicesMonitor implements ClusteredDataTreeChangeListener<Device> {
165
166         @Override
167         public final void onDataTreeChanged(final Collection<DataTreeModification<Device>> mods) {
168             for (final DataTreeModification<Device> dataTreeModification : mods) {
169                 final DataObjectModification<Device> deviceMod = dataTreeModification.getRootNode();
170                 final DataObjectModification.ModificationType modType = deviceMod.getModificationType();
171                 switch (modType) {
172                     case DELETE:
173                         deleteDevice(deviceMod.getDataBefore());
174                         break;
175                     case SUBTREE_MODIFIED:
176                     case WRITE:
177                         deleteDevice(deviceMod.getDataBefore());
178                         writeDevice(deviceMod.getDataAfter());
179                         break;
180                     default:
181                         break;
182                 }
183             }
184         }
185
186         private void deleteDevice(final Device dataBefore) {
187             if (dataBefore != null && dataBefore.getTransport() instanceof Tls) {
188                 LOG.debug("Removing device {}", dataBefore.getUniqueId());
189                 DEVICE_TO_PRIVATE_KEY.remove(dataBefore.getUniqueId());
190                 DEVICE_TO_CERTIFICATE.remove(dataBefore.getUniqueId());
191             }
192         }
193
194         private void writeDevice(final Device dataAfter) {
195             if (dataAfter != null && dataAfter.getTransport() instanceof Tls) {
196                 LOG.debug("Adding device {}", dataAfter.getUniqueId());
197                 final TlsClientParams clientParams = ((Tls) dataAfter.getTransport()).getTlsClientParams();
198                 DEVICE_TO_PRIVATE_KEY.putIfAbsent(dataAfter.getUniqueId(), clientParams.getKeyId());
199                 DEVICE_TO_CERTIFICATE.putIfAbsent(dataAfter.getUniqueId(), clientParams.getCertificateId());
200             }
201         }
202     }
203 }