2 * Copyright (c) 2023 PANTHEON.tech s.r.o. 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.transport.ssh;
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;
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;
42 final class ConfigUtils {
44 private static final int KEEP_ALIVE_DEFAULT_MAX_WAIT = 30; // seconds
45 private static final int KEEP_ALIVE_DEFAULT_ATTEMPTS = 3;
47 private ConfigUtils() {
51 static void setTransportParams(final @NonNull ClientFactoryManager factoryMgr,
52 final @Nullable TransportParamsGrouping params) throws UnsupportedConfigurationException {
53 setTransportParams(factoryMgr, params, TransportUtils::getClientKexFactories);
56 static void setTransportParams(final @NonNull ServerFactoryManager factoryMgr,
57 final @Nullable TransportParamsGrouping params) throws UnsupportedConfigurationException {
58 setTransportParams(factoryMgr, params, TransportUtils::getServerKexFactories);
61 static void setTransportParams(final @NonNull FactoryManager factoryMgr,
62 final @Nullable TransportParamsGrouping params, final @NonNull KexFactoryProvider kexProvider)
63 throws UnsupportedConfigurationException {
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()));
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());
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());
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));
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());
118 return listBuilder.build();
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);
131 return extractKeyPair(inlineDef);
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;
143 throw new UnsupportedConfigurationException("Unsupported private key format " + keyFormat);
145 final byte[] privateKeyBytes;
146 if (input.getPrivateKeyType() instanceof CleartextPrivateKey clearText) {
147 privateKeyBytes = clearText.requireCleartextPrivateKey();
149 throw new UnsupportedConfigurationException("Unsupported private key type " + input.getPrivateKeyType());
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;
160 throw new UnsupportedConfigurationException("Unsupported public key format " + publicKeyFormat);
163 final var privateKey = KeyUtils.buildPrivateKey(privateKeyAlgorithm, privateKeyBytes);
164 final var publicKey = isSshPublicKey ? KeyUtils.buildPublicKeyFromSshEncoding(publicKeyBytes)
165 : KeyUtils.buildX509PublicKey(privateKeyAlgorithm, publicKeyBytes);
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."
171 KeyUtils.validateKeyPair(publicKey, privateKey);
172 return new KeyPair(publicKey, privateKey);
175 static List<Certificate> extractCertificates(final @Nullable InlineOrTruststoreCertsGrouping input)
176 throws UnsupportedConfigurationException {
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);
187 final var listBuilder = ImmutableList.<Certificate>builder();
188 for (var cert : inlineDef.nonnullCertificate().values()) {
189 listBuilder.add(KeyUtils.buildX509Certificate(cert.requireCertData().getValue()));
191 return listBuilder.build();
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);
203 final var keyPair = extractKeyPair(inlineDef);
204 final var certificate = KeyUtils.buildX509Certificate(inlineDef.requireCertData().getValue());
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."
210 KeyUtils.validatePublicKey(keyPair.getPublic(), certificate);
211 return new SimpleImmutableEntry<>(keyPair, List.of(certificate));
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());
220 return expectedType.cast(obj);
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);
234 final var publicKey = inlineDef.getPublicKey();
235 if (publicKey == null) {
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");
244 listBuilder.add(KeyUtils.buildPublicKeyFromSshEncoding(entry.getValue().getPublicKey()));
246 return listBuilder.build();
249 static List<PublicKey> extractPublicKeys(final @Nullable SshHostKeys sshHostKeys)
250 throws UnsupportedConfigurationException {
251 return sshHostKeys == null ? List.of() : extractPublicKeys(sshHostKeys.getInlineOrTruststore());
255 private interface KexFactoryProvider {
256 List<KeyExchangeFactory> getKexFactories(KeyExchange input) throws UnsupportedConfigurationException;