f0e5ac5ae2b055a73d5009040cf98230bca41421
[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.Map;
21 import java.util.Optional;
22 import java.util.Set;
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;
44
45 public class TlsAllowedDevicesMonitorImpl implements TlsAllowedDevicesMonitor, AutoCloseable {
46     private static final Logger LOG = LoggerFactory.getLogger(TlsAllowedDevicesMonitorImpl.class);
47
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;
53
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());
62     }
63
64     @Override
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());
71
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());
77
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();
85         } else {
86             return deviceNames.stream().findFirst();
87         }
88     }
89
90     @Override
91     public Set<String> findAllowedKeys() {
92         return Set.copyOf(deviceToPrivateKey.values());
93     }
94
95     @Override
96     public void close() {
97         allowedDevicesReg.close();
98         certificatesReg.close();
99     }
100
101     private final class CertificatesMonitor implements ClusteredDataTreeChangeListener<Keystore> {
102         @Override
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);
109         }
110
111         private void updateCertificate(final DataObjectModification<TrustedCertificate> change) {
112             switch (change.getModificationType()) {
113                 case DELETE:
114                     deleteCertificate(change.getDataBefore());
115                     break;
116                 case SUBTREE_MODIFIED:
117                 case WRITE:
118                     deleteCertificate(change.getDataBefore());
119                     writeCertificate(change.getDataAfter());
120                     break;
121                 default:
122                     break;
123             }
124         }
125
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());
130             }
131         }
132
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()));
137             }
138         }
139
140         private PublicKey buildPublicKey(final String encoded) {
141             final byte[] decoded = Base64.getMimeDecoder().decode(encoded.getBytes(US_ASCII));
142             try {
143                 final CertificateFactory factory = CertificateFactory.getInstance("X.509");
144                 try (InputStream in = new ByteArrayInputStream(decoded)) {
145                     return factory.generateCertificate(in).getPublicKey();
146                 }
147             } catch (final CertificateException | IOException e) {
148                 LOG.error("Unable to build X.509 certificate from encoded value: {}", e.getLocalizedMessage());
149             }
150             return null;
151         }
152     }
153
154     private final class AllowedDevicesMonitor implements ClusteredDataTreeChangeListener<Device> {
155         @Override
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()) {
160                     case DELETE:
161                         deleteDevice(deviceMod.getDataBefore());
162                         break;
163                     case SUBTREE_MODIFIED:
164                     case WRITE:
165                         deleteDevice(deviceMod.getDataBefore());
166                         writeDevice(deviceMod.getDataAfter());
167                         break;
168                     default:
169                         break;
170                 }
171             }
172         }
173
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());
179             }
180         }
181
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());
188             }
189         }
190     }
191 }