0eb6c9d8548dd75fb259f09739dbe3930f557aad
[netconf.git] / apps / callhome-provider / src / main / java / org / opendaylight / netconf / callhome / mount / tls / CallHomeMountTlsAuthProvider.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 com.google.common.collect.ImmutableMultimap;
11 import io.netty.handler.ssl.SslContext;
12 import java.net.SocketAddress;
13 import java.security.PublicKey;
14 import java.util.List;
15 import java.util.Map;
16 import java.util.Set;
17 import java.util.concurrent.ConcurrentHashMap;
18 import java.util.concurrent.ConcurrentMap;
19 import javax.annotation.PreDestroy;
20 import javax.inject.Inject;
21 import javax.inject.Singleton;
22 import org.opendaylight.mdsal.binding.api.DataBroker;
23 import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
24 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
25 import org.opendaylight.mdsal.binding.api.DataTreeModification;
26 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
27 import org.opendaylight.netconf.callhome.server.tls.CallHomeTlsAuthProvider;
28 import org.opendaylight.netconf.client.SslContextFactory;
29 import org.opendaylight.netconf.client.mdsal.api.SslContextFactoryProvider;
30 import org.opendaylight.netconf.keystore.legacy.NetconfKeystore;
31 import org.opendaylight.netconf.keystore.legacy.NetconfKeystoreService;
32 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.NetconfCallhomeServer;
33 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.AllowedDevices;
34 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.allowed.devices.Device;
35 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.allowed.devices.device.transport.Tls;
36 import org.opendaylight.yangtools.concepts.Registration;
37 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
38 import org.osgi.service.component.annotations.Activate;
39 import org.osgi.service.component.annotations.Component;
40 import org.osgi.service.component.annotations.Deactivate;
41 import org.osgi.service.component.annotations.Reference;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45
46 @Component(service = CallHomeTlsAuthProvider.class, immediate = true)
47 @Singleton
48 public final class CallHomeMountTlsAuthProvider extends CallHomeTlsAuthProvider implements AutoCloseable {
49     private static final Logger LOG = LoggerFactory.getLogger(CallHomeMountTlsAuthProvider.class);
50
51     private final ConcurrentMap<String, String> deviceToPrivateKey = new ConcurrentHashMap<>();
52     private final ConcurrentMap<String, String> deviceToCertificate = new ConcurrentHashMap<>();
53     private final Registration allowedDevicesReg;
54     private final Registration certificatesReg;
55     private final SslContextFactory sslContextFactory;
56
57     private volatile ImmutableMultimap<PublicKey, String> publicKeyToName = ImmutableMultimap.of();
58
59     @Inject
60     @Activate
61     public CallHomeMountTlsAuthProvider(@Reference final SslContextFactoryProvider sslContextFactoryProvider,
62             @Reference final DataBroker dataBroker, @Reference final NetconfKeystoreService keystoreService) {
63         sslContextFactory = sslContextFactoryProvider.getSslContextFactory(null);
64         allowedDevicesReg = dataBroker.registerTreeChangeListener(
65             DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION,
66                 InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class)),
67             new AllowedDevicesMonitor());
68         certificatesReg = keystoreService.registerKeystoreConsumer(this::updateCertificates);
69     }
70
71     @PreDestroy
72     @Deactivate
73     @Override
74     public void close() {
75         allowedDevicesReg.close();
76         certificatesReg.close();
77     }
78
79     private void updateCertificates(final NetconfKeystore keystore) {
80         final var builder = ImmutableMultimap.<PublicKey, String>builder();
81         keystore.trustedCertificates().forEach((name, cert) -> builder.put(cert.getPublicKey(), name));
82         publicKeyToName = builder.build();
83     }
84
85     @Override
86     public String idFor(final PublicKey key) {
87         // Find certificate names by the public key
88         final var certificateNames = publicKeyToName.get(key);
89
90         // Find devices names associated with a certificate name
91         final var deviceNames = deviceToCertificate.entrySet().stream()
92             .filter(v -> certificateNames.contains(v.getValue()))
93             .map(Map.Entry::getKey)
94             .toList();
95
96         // In real world scenario it is not possible to have multiple certificates with the same private/public key,
97         // but in theory/synthetic tests it is practically possible to generate multiple certificates from a single
98         // private key. In such case it's not possible to pin certificate to particular device.
99         return switch (deviceNames.size()) {
100             case 0 -> null;
101             case 1 -> deviceNames.get(0);
102             default -> {
103                 LOG.error("Unable to find device by provided certificate. Possible reason: one certificate configured "
104                     + "with multiple devices/names or multiple certificates contain same public key");
105                 yield null;
106             }
107         };
108     }
109
110     @Override
111     protected SslContext getSslContext(final SocketAddress remoteAddress) {
112         return sslContextFactory.createSslContext(Set.copyOf(deviceToPrivateKey.values()));
113     }
114
115     private final class AllowedDevicesMonitor implements DataTreeChangeListener<Device> {
116         @Override
117         public void onDataTreeChanged(final List<DataTreeModification<Device>> mods) {
118             for (var dataTreeModification : mods) {
119                 final var deviceMod = dataTreeModification.getRootNode();
120                 switch (deviceMod.modificationType()) {
121                     case DELETE -> deleteDevice(deviceMod.dataBefore());
122                     case SUBTREE_MODIFIED, WRITE -> {
123                         deleteDevice(deviceMod.dataBefore());
124                         writeDevice(deviceMod.dataAfter());
125                     }
126                     default -> {
127                         // Should never happen
128                     }
129                 }
130             }
131         }
132
133         private void deleteDevice(final Device dataBefore) {
134             if (dataBefore != null && dataBefore.getTransport() instanceof Tls) {
135                 LOG.debug("Removing device {}", dataBefore.getUniqueId());
136                 deviceToPrivateKey.remove(dataBefore.getUniqueId());
137                 deviceToCertificate.remove(dataBefore.getUniqueId());
138             }
139         }
140
141         private void writeDevice(final Device dataAfter) {
142             if (dataAfter != null && dataAfter.getTransport() instanceof Tls tls) {
143                 LOG.debug("Adding device {}", dataAfter.getUniqueId());
144                 final var tlsClientParams = tls.getTlsClientParams();
145                 deviceToPrivateKey.putIfAbsent(dataAfter.getUniqueId(), tlsClientParams.getKeyId());
146                 deviceToCertificate.putIfAbsent(dataAfter.getUniqueId(), tlsClientParams.getCertificateId());
147             }
148         }
149     }
150 }