c269465d5d5c30a3f9ed759cb9dec17d0ed6f1ff
[netconf.git] / transport / transport-ssh / src / main / java / org / opendaylight / netconf / transport / ssh / ConfigUtils.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech s.r.o. 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.transport.ssh;
9
10 import com.google.common.collect.ImmutableList;
11 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
12 import java.security.KeyPair;
13 import java.security.PublicKey;
14 import java.security.cert.Certificate;
15 import java.security.cert.X509Certificate;
16 import java.time.Duration;
17 import java.util.AbstractMap.SimpleImmutableEntry;
18 import java.util.List;
19 import java.util.Map;
20 import org.eclipse.jdt.annotation.NonNull;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.opendaylight.netconf.shaded.sshd.client.ClientFactoryManager;
23 import org.opendaylight.netconf.shaded.sshd.common.FactoryManager;
24 import org.opendaylight.netconf.shaded.sshd.common.kex.KeyExchangeFactory;
25 import org.opendaylight.netconf.shaded.sshd.common.session.SessionHeartbeatController;
26 import org.opendaylight.netconf.shaded.sshd.server.ServerFactoryManager;
27 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
28 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.AsymmetricKeyPairGrouping;
29 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.EcPrivateKeyFormat;
30 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.RsaPrivateKeyFormat;
31 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.SshPublicKeyFormat;
32 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.SubjectPublicKeyInfoFormat;
33 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.asymmetric.key.pair.grouping._private.key.type.CleartextPrivateKey;
34 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev230417.InlineOrKeystoreEndEntityCertWithKeyGrouping;
35 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.server.authentication.SshHostKeys;
36 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.common.rev230417.TransportParamsGrouping;
37 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.common.rev230417.transport.params.grouping.KeyExchange;
38 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.truststore.rev230417.InlineOrTruststoreCertsGrouping;
39 import org.opendaylight.yangtools.yang.common.Uint16;
40 import org.opendaylight.yangtools.yang.common.Uint8;
41
42 final class ConfigUtils {
43
44     private static final int KEEP_ALIVE_DEFAULT_MAX_WAIT = 30; // seconds
45     private static final int KEEP_ALIVE_DEFAULT_ATTEMPTS = 3;
46
47     private ConfigUtils() {
48         // utility class
49     }
50
51     static void setTransportParams(final @NonNull ClientFactoryManager factoryMgr,
52             final @Nullable TransportParamsGrouping params) throws UnsupportedConfigurationException {
53         setTransportParams(factoryMgr, params, TransportUtils::getClientKexFactories);
54     }
55
56     static void setTransportParams(final @NonNull ServerFactoryManager factoryMgr,
57             final @Nullable TransportParamsGrouping params) throws UnsupportedConfigurationException {
58         setTransportParams(factoryMgr, params, TransportUtils::getServerKexFactories);
59     }
60
61     static void setTransportParams(final @NonNull FactoryManager factoryMgr,
62             final @Nullable TransportParamsGrouping params, final @NonNull KexFactoryProvider kexProvider)
63             throws UnsupportedConfigurationException {
64
65         factoryMgr.setCipherFactories(
66                 TransportUtils.getCipherFactories(params == null ? null : params.getEncryption()));
67         factoryMgr.setSignatureFactories(
68                 TransportUtils.getSignatureFactories(params == null ? null : params.getHostKey()));
69         factoryMgr.setKeyExchangeFactories(
70                 kexProvider.getKexFactories(params == null ? null : params.getKeyExchange()));
71         factoryMgr.setMacFactories(
72                 TransportUtils.getMacFactories(params == null ? null : params.getMac()));
73     }
74
75     static void setKeepAlives(final @NonNull ServerFactoryManager factoryMgr,
76             final org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417
77                     .ssh.server.grouping.Keepalives keepAlives) {
78         setKeepAlives(factoryMgr,
79                 keepAlives == null ? null : keepAlives.getMaxWait(),
80                 keepAlives == null ? null : keepAlives.getMaxAttempts());
81     }
82
83     static void setKeepAlives(final @NonNull ClientFactoryManager factoryMgr,
84             final org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417
85                     .ssh.client.grouping.Keepalives keepAlives) {
86         setKeepAlives(factoryMgr,
87                 keepAlives == null ? null : keepAlives.getMaxWait(),
88                 keepAlives == null ? null : keepAlives.getMaxAttempts());
89     }
90
91     @SuppressFBWarnings(value = "DLS_DEAD_LOCAL_STORE", justification = "maxAttempts usage need clarification")
92     private static void setKeepAlives(final @NonNull FactoryManager factoryMgr, final @Nullable Uint16 cfgMaxWait,
93             final @Nullable Uint8 cfgMaxAttempts) {
94         // FIXME: utilize max attempts
95         final var maxAttempts = cfgMaxAttempts == null ? KEEP_ALIVE_DEFAULT_ATTEMPTS : cfgMaxAttempts.intValue();
96         final var maxWait = cfgMaxWait == null ? KEEP_ALIVE_DEFAULT_MAX_WAIT : cfgMaxWait.intValue();
97         factoryMgr.setSessionHeartbeat(SessionHeartbeatController.HeartbeatType.RESERVED, Duration.ofSeconds(maxWait));
98     }
99
100     static List<KeyPair> extractServerHostKeys(
101             final List<org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417
102                     .ssh.server.grouping.server.identity.HostKey> serverHostKeys)
103             throws UnsupportedConfigurationException {
104         var listBuilder = ImmutableList.<KeyPair>builder();
105         for (var hostKey : serverHostKeys) {
106             if (hostKey.getHostKeyType()
107                     instanceof org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417
108                     .ssh.server.grouping.server.identity.host.key.host.key.type.PublicKey publicKey
109                     && publicKey.getPublicKey() != null) {
110                 listBuilder.add(extractKeyPair(publicKey.getPublicKey().getInlineOrKeystore()));
111             } else if (hostKey.getHostKeyType()
112                     instanceof org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417
113                     .ssh.server.grouping.server.identity.host.key.host.key.type.Certificate certificate
114                     && certificate.getCertificate() != null) {
115                 listBuilder.add(extractCertificateEntry(certificate.getCertificate()).getKey());
116             }
117         }
118         return listBuilder.build();
119     }
120
121     static KeyPair extractKeyPair(
122             final org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev230417
123                     .inline.or.keystore.asymmetric.key.grouping.InlineOrKeystore input)
124             throws UnsupportedConfigurationException {
125         final var inline = ofType(org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev230417
126                 .inline.or.keystore.asymmetric.key.grouping.inline.or.keystore.Inline.class, input);
127         final var inlineDef = inline.getInlineDefinition();
128         if (inlineDef == null) {
129             throw new UnsupportedConfigurationException("Missing inline definition in " + inline);
130         }
131         return extractKeyPair(inlineDef);
132     }
133
134     private static KeyPair extractKeyPair(final AsymmetricKeyPairGrouping input)
135             throws UnsupportedConfigurationException {
136         final var keyFormat = input.getPrivateKeyFormat();
137         final String privateKeyAlgorithm;
138         if (EcPrivateKeyFormat.VALUE.equals(keyFormat)) {
139             privateKeyAlgorithm = KeyUtils.EC_ALGORITHM;
140         } else if (RsaPrivateKeyFormat.VALUE.equals(input.getPrivateKeyFormat())) {
141             privateKeyAlgorithm = KeyUtils.RSA_ALGORITHM;
142         } else {
143             throw new UnsupportedConfigurationException("Unsupported private key format " + keyFormat);
144         }
145         final byte[] privateKeyBytes;
146         if (input.getPrivateKeyType() instanceof CleartextPrivateKey clearText) {
147             privateKeyBytes = clearText.requireCleartextPrivateKey();
148         } else {
149             throw new UnsupportedConfigurationException("Unsupported private key type " + input.getPrivateKeyType());
150         }
151
152         final var publicKeyFormat = input.getPublicKeyFormat();
153         final var publicKeyBytes = input.getPublicKey();
154         final boolean isSshPublicKey;
155         if (SubjectPublicKeyInfoFormat.VALUE.equals(publicKeyFormat)) {
156             isSshPublicKey = false;
157         } else if (SshPublicKeyFormat.VALUE.equals(publicKeyFormat)) {
158             isSshPublicKey = true;
159         } else {
160             throw new UnsupportedConfigurationException("Unsupported public key format " + publicKeyFormat);
161         }
162
163         final var privateKey = KeyUtils.buildPrivateKey(privateKeyAlgorithm, privateKeyBytes);
164         final var publicKey = isSshPublicKey ? KeyUtils.buildPublicKeyFromSshEncoding(publicKeyBytes)
165                 : KeyUtils.buildX509PublicKey(privateKeyAlgorithm, publicKeyBytes);
166         /*
167             ietf-crypto-types:grouping asymmetric-key-pair-grouping
168             "A private key and its associated public key.  Implementations
169             SHOULD ensure that the two keys are a matching pair."
170          */
171         KeyUtils.validateKeyPair(publicKey, privateKey);
172         return new KeyPair(publicKey, privateKey);
173     }
174
175     static List<Certificate> extractCertificates(final @Nullable InlineOrTruststoreCertsGrouping input)
176             throws UnsupportedConfigurationException {
177         if (input == null) {
178             return List.of();
179         }
180         final var inline = ofType(org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.truststore
181                         .rev230417.inline.or.truststore.certs.grouping.inline.or.truststore.Inline.class,
182                 input.getInlineOrTruststore());
183         final var inlineDef = inline.getInlineDefinition();
184         if (inlineDef == null) {
185             throw new UnsupportedConfigurationException("Missing inline definition in " + inline);
186         }
187         final var listBuilder = ImmutableList.<Certificate>builder();
188         for (var cert : inlineDef.nonnullCertificate().values()) {
189             listBuilder.add(KeyUtils.buildX509Certificate(cert.requireCertData().getValue()));
190         }
191         return listBuilder.build();
192     }
193
194     private static Map.Entry<KeyPair, List<X509Certificate>> extractCertificateEntry(
195             final InlineOrKeystoreEndEntityCertWithKeyGrouping input) throws UnsupportedConfigurationException {
196         final var inline = ofType(org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev230417
197                         .inline.or.keystore.end.entity.cert.with.key.grouping.inline.or.keystore.Inline.class,
198                 input.getInlineOrKeystore());
199         final var inlineDef = inline.getInlineDefinition();
200         if (inlineDef == null) {
201             throw new UnsupportedConfigurationException("Missing inline definition in " + inline);
202         }
203         final var keyPair = extractKeyPair(inlineDef);
204         final var certificate = KeyUtils.buildX509Certificate(inlineDef.requireCertData().getValue());
205         /*
206           ietf-crypto-types:asymmetric-key-pair-with-cert-grouping
207           "A private/public key pair and an associated certificate.
208           Implementations SHOULD assert that certificates contain the matching public key."
209          */
210         KeyUtils.validatePublicKey(keyPair.getPublic(), certificate);
211         return new SimpleImmutableEntry<>(keyPair, List.of(certificate));
212     }
213
214     private static <T> T ofType(final Class<T> expectedType, final Object obj)
215             throws UnsupportedConfigurationException {
216         if (!expectedType.isInstance(obj)) {
217             throw new UnsupportedConfigurationException("Expected type: " + expectedType
218                     + " actual: " + obj.getClass());
219         }
220         return expectedType.cast(obj);
221     }
222
223     static List<PublicKey> extractPublicKeys(
224             final org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.truststore.rev230417
225                     .inline.or.truststore._public.keys.grouping.InlineOrTruststore input)
226             throws UnsupportedConfigurationException {
227         final var inline = ofType(org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.truststore.rev230417
228                 .inline.or.truststore._public.keys.grouping.inline.or.truststore.Inline.class, input);
229         final var inlineDef = inline.getInlineDefinition();
230         if (inlineDef == null) {
231             throw new UnsupportedConfigurationException("Missing inline definition in " + inline);
232         }
233
234         final var publicKey = inlineDef.getPublicKey();
235         if (publicKey == null) {
236             return List.of();
237         }
238
239         final var listBuilder = ImmutableList.<PublicKey>builder();
240         for (var entry : publicKey.entrySet()) {
241             if (!SshPublicKeyFormat.VALUE.equals(entry.getValue().getPublicKeyFormat())) {
242                 throw new UnsupportedConfigurationException("ssh public key format is expected");
243             }
244             listBuilder.add(KeyUtils.buildPublicKeyFromSshEncoding(entry.getValue().getPublicKey()));
245         }
246         return listBuilder.build();
247     }
248
249     static List<PublicKey> extractPublicKeys(final @Nullable SshHostKeys sshHostKeys)
250             throws UnsupportedConfigurationException {
251         return sshHostKeys == null ? List.of() : extractPublicKeys(sshHostKeys.getInlineOrTruststore());
252     }
253
254     @FunctionalInterface
255     private interface KexFactoryProvider {
256         List<KeyExchangeFactory> getKexFactories(KeyExchange input) throws UnsupportedConfigurationException;
257     }
258 }