Call-Home provider migration to transport-api
[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 static java.nio.charset.StandardCharsets.US_ASCII;
11
12 import io.netty.channel.Channel;
13 import io.netty.handler.ssl.SslHandler;
14 import java.io.ByteArrayInputStream;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.security.PublicKey;
18 import java.security.cert.CertificateException;
19 import java.security.cert.CertificateFactory;
20 import java.util.Base64;
21 import java.util.Collection;
22 import java.util.Map;
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 javax.annotation.PreDestroy;
28 import javax.inject.Inject;
29 import javax.inject.Singleton;
30 import org.eclipse.jdt.annotation.NonNull;
31 import org.opendaylight.mdsal.binding.api.ClusteredDataTreeChangeListener;
32 import org.opendaylight.mdsal.binding.api.DataBroker;
33 import org.opendaylight.mdsal.binding.api.DataObjectModification;
34 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
35 import org.opendaylight.mdsal.binding.api.DataTreeModification;
36 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
37 import org.opendaylight.netconf.callhome.server.tls.CallHomeTlsAuthProvider;
38 import org.opendaylight.netconf.client.SslHandlerFactory;
39 import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider;
40 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
41 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.NetconfCallhomeServer;
43 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.AllowedDevices;
44 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.Device;
45 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev230428.netconf.callhome.server.allowed.devices.device.transport.Tls;
46 import org.opendaylight.yangtools.concepts.Registration;
47 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
48 import org.osgi.service.component.annotations.Activate;
49 import org.osgi.service.component.annotations.Component;
50 import org.osgi.service.component.annotations.Deactivate;
51 import org.osgi.service.component.annotations.Reference;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55
56 @Component(service = CallHomeTlsAuthProvider.class, immediate = true)
57 @Singleton
58 public final class CallHomeMountTlsAuthProvider implements CallHomeTlsAuthProvider, AutoCloseable {
59     private static final Logger LOG = LoggerFactory.getLogger(CallHomeMountTlsAuthProvider.class);
60
61     private final ConcurrentMap<String, String> deviceToPrivateKey = new ConcurrentHashMap<>();
62     private final ConcurrentMap<String, String> deviceToCertificate = new ConcurrentHashMap<>();
63     private final ConcurrentMap<String, PublicKey> certificateToPublicKey = new ConcurrentHashMap<>();
64     private final Registration allowedDevicesReg;
65     private final Registration certificatesReg;
66     private final SslHandlerFactory sslHandlerFactory;
67
68     @Inject
69     @Activate
70     public CallHomeMountTlsAuthProvider(
71             final @Reference SslHandlerFactoryProvider sslHandlerFactoryProvider,
72             final @Reference DataBroker dataBroker) {
73         this.sslHandlerFactory = sslHandlerFactoryProvider.getSslHandlerFactory(null);
74         allowedDevicesReg = dataBroker.registerDataTreeChangeListener(
75             DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION,
76                 InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class)),
77             new AllowedDevicesMonitor());
78         certificatesReg = dataBroker.registerDataTreeChangeListener(
79             DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
80             new CertificatesMonitor());
81     }
82
83     @PreDestroy
84     @Deactivate
85     @Override
86     public void close() {
87         allowedDevicesReg.close();
88         certificatesReg.close();
89     }
90
91     @Override
92     public String idFor(final PublicKey key) {
93         // Find certificate names by the public key
94         final var certificates = certificateToPublicKey.entrySet().stream()
95             .filter(v -> key.equals(v.getValue()))
96             .map(Map.Entry::getKey)
97             .collect(Collectors.toSet());
98
99         // Find devices names associated with a certificate name
100         final var deviceNames = deviceToCertificate.entrySet().stream()
101             .filter(v -> certificates.contains(v.getValue()))
102             .map(Map.Entry::getKey)
103             .toList();
104
105         // In real world scenario it is not possible to have multiple certificates with the same private/public key,
106         // but in theory/synthetic tests it is practically possible to generate multiple certificates from a single
107         // private key. In such case it's not possible to pin certificate to particular device.
108         if (deviceNames.size() > 1) {
109             LOG.error("Unable to find device by provided certificate. Possible reason: one certificate configured "
110                 + "with multiple devices/names or multiple certificates contain same public key");
111             return null;
112         }
113         return deviceNames.isEmpty() ? null : deviceNames.get(0);
114     }
115
116     @Override
117     public SslHandler createSslHandler(final Channel channel) {
118         return sslHandlerFactory.createSslHandler(Set.copyOf(deviceToPrivateKey.values()));
119     }
120
121     private final class CertificatesMonitor implements ClusteredDataTreeChangeListener<Keystore> {
122         @Override
123         public void onDataTreeChanged(@NonNull final Collection<DataTreeModification<Keystore>> changes) {
124             changes.stream().map(DataTreeModification::getRootNode)
125                 .flatMap(v -> v.getModifiedChildren().stream())
126                 .filter(v -> v.getDataType().equals(TrustedCertificate.class))
127                 .map(v -> (DataObjectModification<TrustedCertificate>) v)
128                 .forEach(this::updateCertificate);
129         }
130
131         private void updateCertificate(final DataObjectModification<TrustedCertificate> change) {
132             switch (change.getModificationType()) {
133                 case DELETE:
134                     deleteCertificate(change.getDataBefore());
135                     break;
136                 case SUBTREE_MODIFIED:
137                 case WRITE:
138                     deleteCertificate(change.getDataBefore());
139                     writeCertificate(change.getDataAfter());
140                     break;
141                 default:
142                     break;
143             }
144         }
145
146         private void deleteCertificate(final TrustedCertificate dataBefore) {
147             if (dataBefore != null) {
148                 LOG.debug("Removing public key mapping for certificate {}", dataBefore.getName());
149                 certificateToPublicKey.remove(dataBefore.getName());
150             }
151         }
152
153         private void writeCertificate(final TrustedCertificate dataAfter) {
154             if (dataAfter != null) {
155                 LOG.debug("Adding public key mapping for certificate {}", dataAfter.getName());
156                 certificateToPublicKey.putIfAbsent(dataAfter.getName(), buildPublicKey(dataAfter.getCertificate()));
157             }
158         }
159
160         private PublicKey buildPublicKey(final String encoded) {
161             final byte[] decoded = Base64.getMimeDecoder().decode(encoded.getBytes(US_ASCII));
162             try {
163                 final CertificateFactory factory = CertificateFactory.getInstance("X.509");
164                 try (InputStream in = new ByteArrayInputStream(decoded)) {
165                     return factory.generateCertificate(in).getPublicKey();
166                 }
167             } catch (final CertificateException | IOException e) {
168                 LOG.error("Unable to build X.509 certificate from encoded value: {}", e.getLocalizedMessage());
169             }
170             return null;
171         }
172     }
173
174     private final class AllowedDevicesMonitor implements ClusteredDataTreeChangeListener<Device> {
175         @Override
176         public void onDataTreeChanged(final Collection<DataTreeModification<Device>> mods) {
177             for (final DataTreeModification<Device> dataTreeModification : mods) {
178                 final DataObjectModification<Device> deviceMod = dataTreeModification.getRootNode();
179                 switch (deviceMod.getModificationType()) {
180                     case DELETE:
181                         deleteDevice(deviceMod.getDataBefore());
182                         break;
183                     case SUBTREE_MODIFIED:
184                     case WRITE:
185                         deleteDevice(deviceMod.getDataBefore());
186                         writeDevice(deviceMod.getDataAfter());
187                         break;
188                     default:
189                         break;
190                 }
191             }
192         }
193
194         private void deleteDevice(final Device dataBefore) {
195             if (dataBefore != null && dataBefore.getTransport() instanceof Tls) {
196                 LOG.debug("Removing device {}", dataBefore.getUniqueId());
197                 deviceToPrivateKey.remove(dataBefore.getUniqueId());
198                 deviceToCertificate.remove(dataBefore.getUniqueId());
199             }
200         }
201
202         private void writeDevice(final Device dataAfter) {
203             if (dataAfter != null && dataAfter.getTransport() instanceof Tls tls) {
204                 LOG.debug("Adding device {}", dataAfter.getUniqueId());
205                 final var tlsClientParams = tls.getTlsClientParams();
206                 deviceToPrivateKey.putIfAbsent(dataAfter.getUniqueId(), tlsClientParams.getKeyId());
207                 deviceToCertificate.putIfAbsent(dataAfter.getUniqueId(), tlsClientParams.getCertificateId());
208             }
209         }
210     }
211 }