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 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.List;
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.opendaylight.mdsal.binding.api.DataBroker;
31 import org.opendaylight.mdsal.binding.api.DataObjectModification;
32 import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
33 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
34 import org.opendaylight.mdsal.binding.api.DataTreeModification;
35 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
36 import org.opendaylight.netconf.callhome.server.tls.CallHomeTlsAuthProvider;
37 import org.opendaylight.netconf.client.SslHandlerFactory;
38 import org.opendaylight.netconf.client.mdsal.api.SslHandlerFactoryProvider;
39 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
40 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
41 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.NetconfCallhomeServer;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.AllowedDevices;
43 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.allowed.devices.Device;
44 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.allowed.devices.device.transport.Tls;
45 import org.opendaylight.yangtools.concepts.Registration;
46 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
47 import org.osgi.service.component.annotations.Activate;
48 import org.osgi.service.component.annotations.Component;
49 import org.osgi.service.component.annotations.Deactivate;
50 import org.osgi.service.component.annotations.Reference;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 @Component(service = CallHomeTlsAuthProvider.class, immediate = true)
57 public final class CallHomeMountTlsAuthProvider implements CallHomeTlsAuthProvider, AutoCloseable {
58 private static final Logger LOG = LoggerFactory.getLogger(CallHomeMountTlsAuthProvider.class);
60 private final ConcurrentMap<String, String> deviceToPrivateKey = new ConcurrentHashMap<>();
61 private final ConcurrentMap<String, String> deviceToCertificate = new ConcurrentHashMap<>();
62 private final ConcurrentMap<String, PublicKey> certificateToPublicKey = new ConcurrentHashMap<>();
63 private final Registration allowedDevicesReg;
64 private final Registration certificatesReg;
65 private final SslHandlerFactory sslHandlerFactory;
69 public CallHomeMountTlsAuthProvider(
70 final @Reference SslHandlerFactoryProvider sslHandlerFactoryProvider,
71 final @Reference DataBroker dataBroker) {
72 sslHandlerFactory = sslHandlerFactoryProvider.getSslHandlerFactory(null);
73 allowedDevicesReg = dataBroker.registerTreeChangeListener(
74 DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION,
75 InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class)),
76 new AllowedDevicesMonitor());
77 certificatesReg = dataBroker.registerTreeChangeListener(
78 DataTreeIdentifier.of(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
79 new CertificatesMonitor());
86 allowedDevicesReg.close();
87 certificatesReg.close();
91 public String idFor(final PublicKey key) {
92 // Find certificate names by the public key
93 final var certificates = certificateToPublicKey.entrySet().stream()
94 .filter(v -> key.equals(v.getValue()))
95 .map(Map.Entry::getKey)
96 .collect(Collectors.toSet());
98 // Find devices names associated with a certificate name
99 final var deviceNames = deviceToCertificate.entrySet().stream()
100 .filter(v -> certificates.contains(v.getValue()))
101 .map(Map.Entry::getKey)
104 // In real world scenario it is not possible to have multiple certificates with the same private/public key,
105 // but in theory/synthetic tests it is practically possible to generate multiple certificates from a single
106 // private key. In such case it's not possible to pin certificate to particular device.
107 return switch (deviceNames.size()) {
109 case 1 -> deviceNames.get(0);
111 LOG.error("Unable to find device by provided certificate. Possible reason: one certificate configured "
112 + "with multiple devices/names or multiple certificates contain same public key");
119 public SslHandler createSslHandler(final Channel channel) {
120 return sslHandlerFactory.createSslHandler(Set.copyOf(deviceToPrivateKey.values()));
123 private final class CertificatesMonitor implements DataTreeChangeListener<Keystore> {
125 public void onDataTreeChanged(final List<DataTreeModification<Keystore>> changes) {
127 .map(DataTreeModification::getRootNode)
128 .flatMap(v -> v.modifiedChildren().stream())
129 .filter(v -> v.dataType().equals(TrustedCertificate.class))
130 .map(v -> (DataObjectModification<TrustedCertificate>) v)
131 .forEach(this::updateCertificate);
134 private void updateCertificate(final DataObjectModification<TrustedCertificate> change) {
135 switch (change.modificationType()) {
136 case DELETE -> deleteCertificate(change.dataBefore());
137 case SUBTREE_MODIFIED, WRITE -> {
138 deleteCertificate(change.dataBefore());
139 writeCertificate(change.dataAfter());
142 // Should never happen
147 private void deleteCertificate(final TrustedCertificate dataBefore) {
148 if (dataBefore != null) {
149 LOG.debug("Removing public key mapping for certificate {}", dataBefore.getName());
150 certificateToPublicKey.remove(dataBefore.getName());
154 private void writeCertificate(final TrustedCertificate dataAfter) {
155 if (dataAfter != null) {
156 LOG.debug("Adding public key mapping for certificate {}", dataAfter.getName());
157 certificateToPublicKey.putIfAbsent(dataAfter.getName(), buildPublicKey(dataAfter.getCertificate()));
161 private PublicKey buildPublicKey(final String encoded) {
162 final byte[] decoded = Base64.getMimeDecoder().decode(encoded.getBytes(US_ASCII));
164 final CertificateFactory factory = CertificateFactory.getInstance("X.509");
165 try (InputStream in = new ByteArrayInputStream(decoded)) {
166 return factory.generateCertificate(in).getPublicKey();
168 } catch (final CertificateException | IOException e) {
169 LOG.error("Unable to build X.509 certificate from encoded value: {}", e.getLocalizedMessage());
175 private final class AllowedDevicesMonitor implements DataTreeChangeListener<Device> {
177 public void onDataTreeChanged(final List<DataTreeModification<Device>> mods) {
178 for (var dataTreeModification : mods) {
179 final var deviceMod = dataTreeModification.getRootNode();
180 switch (deviceMod.modificationType()) {
181 case DELETE -> deleteDevice(deviceMod.dataBefore());
182 case SUBTREE_MODIFIED, WRITE -> {
183 deleteDevice(deviceMod.dataBefore());
184 writeDevice(deviceMod.dataAfter());
187 // Should never happen
193 private void deleteDevice(final Device dataBefore) {
194 if (dataBefore != null && dataBefore.getTransport() instanceof Tls) {
195 LOG.debug("Removing device {}", dataBefore.getUniqueId());
196 deviceToPrivateKey.remove(dataBefore.getUniqueId());
197 deviceToCertificate.remove(dataBefore.getUniqueId());
201 private void writeDevice(final Device dataAfter) {
202 if (dataAfter != null && dataAfter.getTransport() instanceof Tls tls) {
203 LOG.debug("Adding device {}", dataAfter.getUniqueId());
204 final var tlsClientParams = tls.getTlsClientParams();
205 deviceToPrivateKey.putIfAbsent(dataAfter.getUniqueId(), tlsClientParams.getKeyId());
206 deviceToCertificate.putIfAbsent(dataAfter.getUniqueId(), tlsClientParams.getCertificateId());