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.tls;
10 import static org.opendaylight.netconf.transport.tls.KeyStoreUtils.buildX509Certificate;
11 import static org.opendaylight.netconf.transport.tls.KeyUtils.EC_ALGORITHM;
12 import static org.opendaylight.netconf.transport.tls.KeyUtils.RSA_ALGORITHM;
13 import static org.opendaylight.netconf.transport.tls.KeyUtils.buildPrivateKey;
14 import static org.opendaylight.netconf.transport.tls.KeyUtils.buildPublicKeyFromSshEncoding;
15 import static org.opendaylight.netconf.transport.tls.KeyUtils.buildX509PublicKey;
16 import static org.opendaylight.netconf.transport.tls.KeyUtils.validateKeyPair;
17 import static org.opendaylight.netconf.transport.tls.KeyUtils.validatePublicKey;
19 import com.google.common.collect.ImmutableMap;
20 import java.io.IOException;
21 import java.security.KeyPair;
22 import java.security.KeyStore;
23 import java.security.KeyStoreException;
24 import java.security.cert.Certificate;
25 import java.security.cert.CertificateException;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
30 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.AsymmetricKeyPairGrouping;
31 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.EcPrivateKeyFormat;
32 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.RsaPrivateKeyFormat;
33 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.SshPublicKeyFormat;
34 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228.SubjectPublicKeyInfoFormat;
35 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev231228._private.key.grouping._private.key.type.CleartextPrivateKey;
36 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev231228.InlineOrKeystoreAsymmetricKeyGrouping;
37 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev231228.InlineOrKeystoreEndEntityCertWithKeyGrouping;
38 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.truststore.rev231228.InlineOrTruststoreCertsGrouping;
40 final class ConfigUtils {
42 static final char[] EMPTY_SECRET = new char[0];
43 static final String DEFAULT_PRIVATE_KEY_ALIAS = "private";
44 static final String DEFAULT_CERTIFICATE_ALIAS = "certificate";
46 private ConfigUtils() {
51 * Builds X.509 certificates based on configuration data provided then sets them to given key store.
53 * @param keyStore key store
54 * @param caCerts CA certificates configuration
55 * @param eeCerts EE certificates configuration
56 * @throws UnsupportedConfigurationException if error occurs
58 static void setX509Certificates(final @NonNull KeyStore keyStore,
59 final @Nullable InlineOrTruststoreCertsGrouping caCerts,
60 final @Nullable InlineOrTruststoreCertsGrouping eeCerts) throws UnsupportedConfigurationException {
61 var certMap = ImmutableMap.<String, Certificate>builder()
62 .putAll(extractCertificates(caCerts, "ca-"))
63 .putAll(extractCertificates(eeCerts, "ee-"))
65 for (var entry : certMap.entrySet()) {
67 keyStore.setCertificateEntry(entry.getKey(), entry.getValue());
68 } catch (KeyStoreException e) {
69 throw new UnsupportedConfigurationException("Failed to load certificate", e);
74 private static Map<String, Certificate> extractCertificates(
75 @Nullable final InlineOrTruststoreCertsGrouping certs,
76 @NonNull final String aliasPrefix) throws UnsupportedConfigurationException {
80 final var inline = ofType(org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.truststore
81 .rev231228.inline.or.truststore.certs.grouping.inline.or.truststore.Inline.class,
82 certs.getInlineOrTruststore());
83 final var inlineDef = inline.getInlineDefinition();
84 if (inlineDef == null) {
85 throw new UnsupportedConfigurationException("Missing inline definition in " + inline);
87 final var mapBuilder = ImmutableMap.<String, Certificate>builder();
88 for (var cert : inlineDef.nonnullCertificate().values()) {
90 final var alias = aliasPrefix + cert.requireName();
91 mapBuilder.put(alias, buildX509Certificate(cert.requireCertData().getValue()));
92 } catch (IOException | CertificateException e) {
93 throw new UnsupportedConfigurationException("Failed to parse certificate " + cert, e);
96 return mapBuilder.build();
100 * Builds asymmetric key pair from configuration data provided, validates it then puts into given key store.
102 * @param keyStore keystore
103 * @param input configuration
104 * @throws UnsupportedConfigurationException if key pair is not set to key store
106 static void setAsymmetricKey(final @NonNull KeyStore keyStore,
107 final @NonNull InlineOrKeystoreAsymmetricKeyGrouping input)
108 throws UnsupportedConfigurationException {
110 final var inline = ofType(org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev231228
111 .inline.or.keystore.asymmetric.key.grouping.inline.or.keystore.Inline.class,
112 input.getInlineOrKeystore());
113 final var inlineDef = inline.getInlineDefinition();
114 if (inlineDef == null) {
115 throw new UnsupportedConfigurationException("Missing inline definition in " + inline);
117 final var keyPair = extractKeyPair(inlineDef);
118 // ietf-crypto-types:grouping asymmetric-key-pair-grouping
119 // "A private key and its associated public key. Implementations
120 // SHOULD ensure that the two keys are a matching pair."
121 validateKeyPair(keyPair.getPublic(), keyPair.getPrivate());
123 // FIXME: the below line throws an exception bc keyStore does not support private key without certificate
124 // chain (belongs to implementation of raw public key feature support)
125 keyStore.setKeyEntry(DEFAULT_PRIVATE_KEY_ALIAS, keyPair.getPrivate(), EMPTY_SECRET, null);
126 } catch (KeyStoreException e) {
127 throw new UnsupportedConfigurationException("Failed to load private key", e);
132 * Builds asymmetric key pair and associated certificate from configuration data provided, validates
133 * then puts into given key store.
135 * @param keyStore key store
136 * @param input configuration
137 * @throws UnsupportedConfigurationException if key pair and certificate are not set to key store
139 static void setEndEntityCertificateWithKey(final @NonNull KeyStore keyStore,
140 final @NonNull InlineOrKeystoreEndEntityCertWithKeyGrouping input)
141 throws UnsupportedConfigurationException {
142 final var inline = ofType(org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev231228
143 .inline.or.keystore.end.entity.cert.with.key.grouping.inline.or.keystore.Inline.class,
144 input.getInlineOrKeystore());
145 final var inlineDef = inline.getInlineDefinition();
146 if (inlineDef == null) {
147 throw new UnsupportedConfigurationException("Missing inline definition in " + inline);
149 final var keyPair = extractKeyPair(inlineDef);
150 final Certificate certificate;
152 certificate = buildX509Certificate(inlineDef.requireCertData().getValue());
153 } catch (IOException | CertificateException e) {
154 throw new UnsupportedConfigurationException("Failed to load certificate" + inlineDef, e);
156 // ietf-crypto-types:asymmetric-key-pair-with-cert-grouping
157 // "A private/public key pair and an associated certificate.
158 // Implementations SHOULD assert that certificates contain the matching public key."
159 validateKeyPair(keyPair.getPublic(), keyPair.getPrivate());
160 validatePublicKey(keyPair.getPublic(), certificate);
162 keyStore.setCertificateEntry(DEFAULT_CERTIFICATE_ALIAS, certificate);
163 keyStore.setKeyEntry(DEFAULT_PRIVATE_KEY_ALIAS, keyPair.getPrivate(),
164 EMPTY_SECRET, new Certificate[]{certificate});
165 } catch (KeyStoreException e) {
166 throw new UnsupportedConfigurationException("Failed to load certificate and/or private key", e);
170 private static KeyPair extractKeyPair(final AsymmetricKeyPairGrouping input)
171 throws UnsupportedConfigurationException {
173 final var privateKeyFormat = input.getPrivateKeyFormat();
174 final String keyAlgorithm;
175 if (EcPrivateKeyFormat.VALUE.equals(privateKeyFormat)) {
176 keyAlgorithm = EC_ALGORITHM;
177 } else if (RsaPrivateKeyFormat.VALUE.equals(privateKeyFormat)) {
178 keyAlgorithm = RSA_ALGORITHM;
180 throw new UnsupportedConfigurationException("Unsupported private key format " + privateKeyFormat);
182 final byte[] privateKeyBytes;
183 if (input.getPrivateKeyType() instanceof CleartextPrivateKey clearText) {
184 privateKeyBytes = clearText.requireCleartextPrivateKey();
186 throw new UnsupportedConfigurationException("Unsupported private key type " + input.getPrivateKeyType());
188 final var privateKey = buildPrivateKey(keyAlgorithm, privateKeyBytes);
190 final var publicKeyFormat = input.getPublicKeyFormat();
191 final boolean isSshPublicKey;
192 if (SubjectPublicKeyInfoFormat.VALUE.equals(publicKeyFormat)) {
193 isSshPublicKey = false;
194 } else if (SshPublicKeyFormat.VALUE.equals(publicKeyFormat)) {
195 isSshPublicKey = true;
197 throw new UnsupportedConfigurationException("Unsupported public key format " + publicKeyFormat);
199 final var publicKey = isSshPublicKey ? buildPublicKeyFromSshEncoding(input.getPublicKey())
200 : buildX509PublicKey(keyAlgorithm, input.getPublicKey());
201 return new KeyPair(publicKey, privateKey);
204 private static <T> T ofType(final Class<T> expectedType, final Object obj)
205 throws UnsupportedConfigurationException {
206 if (!expectedType.isInstance(obj)) {
207 throw new UnsupportedConfigurationException("Expected type: " + expectedType
208 + " actual: " + obj.getClass());
210 return expectedType.cast(obj);