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