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.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;
41 final class ConfigUtils {
43 private static final int KEEP_ALIVE_DEFAULT_MAX_WAIT = 30; // seconds
44 private static final int KEEP_ALIVE_DEFAULT_ATTEMPTS = 3;
46 private ConfigUtils() {
50 static void setTransportParams(final @NonNull BaseBuilder<?, ?> builder,
51 final @Nullable TransportParamsGrouping params, final @NonNull KexFactoryProvider kexProvider)
52 throws UnsupportedConfigurationException {
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()));
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));
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());
87 return listBuilder.build();
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);
100 return extractKeyPair(inlineDef);
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;
112 throw new UnsupportedConfigurationException("Unsupported private key format " + keyFormat);
114 final byte[] privateKeyBytes;
115 if (input.getPrivateKeyType() instanceof CleartextPrivateKey clearText) {
116 privateKeyBytes = clearText.requireCleartextPrivateKey();
118 throw new UnsupportedConfigurationException("Unsupported private key type " + input.getPrivateKeyType());
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;
129 throw new UnsupportedConfigurationException("Unsupported public key format " + publicKeyFormat);
132 final var privateKey = KeyUtils.buildPrivateKey(privateKeyAlgorithm, privateKeyBytes);
133 final var publicKey = isSshPublicKey ? KeyUtils.buildPublicKeyFromSshEncoding(publicKeyBytes)
134 : KeyUtils.buildX509PublicKey(privateKeyAlgorithm, publicKeyBytes);
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."
140 KeyUtils.validateKeyPair(publicKey, privateKey);
141 return new KeyPair(publicKey, privateKey);
144 static List<Certificate> extractCertificates(final @Nullable InlineOrTruststoreCertsGrouping input)
145 throws UnsupportedConfigurationException {
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);
156 final var listBuilder = ImmutableList.<Certificate>builder();
157 for (var cert : inlineDef.nonnullCertificate().values()) {
158 listBuilder.add(KeyUtils.buildX509Certificate(cert.requireCertData().getValue()));
160 return listBuilder.build();
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);
172 final var keyPair = extractKeyPair(inlineDef);
173 final var certificate = KeyUtils.buildX509Certificate(inlineDef.requireCertData().getValue());
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."
179 KeyUtils.validatePublicKey(keyPair.getPublic(), certificate);
180 return new SimpleImmutableEntry<>(keyPair, List.of(certificate));
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());
189 return expectedType.cast(obj);
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);
203 final var publicKey = inlineDef.getPublicKey();
204 if (publicKey == null) {
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");
213 listBuilder.add(KeyUtils.buildPublicKeyFromSshEncoding(entry.getValue().getPublicKey()));
215 return listBuilder.build();
218 static List<PublicKey> extractPublicKeys(final @Nullable SshHostKeys sshHostKeys)
219 throws UnsupportedConfigurationException {
220 return sshHostKeys == null ? List.of() : extractPublicKeys(sshHostKeys.getInlineOrTruststore());
224 interface KexFactoryProvider {
225 List<KeyExchangeFactory> getKexFactories(KeyExchange input) throws UnsupportedConfigurationException;