Refresh IETF client/server models
[netconf.git] / transport / transport-tls / src / main / java / org / opendaylight / netconf / transport / tls / 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.tls;
9
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;
18
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;
26 import java.util.Map;
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;
39
40 final class ConfigUtils {
41
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";
45
46     private ConfigUtils() {
47         // utility class
48     }
49
50     /**
51      * Builds X.509 certificates based on configuration data provided then sets them to given key store.
52      *
53      * @param keyStore key store
54      * @param caCerts CA certificates configuration
55      * @param eeCerts EE certificates configuration
56      * @throws UnsupportedConfigurationException if error occurs
57      */
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-"))
64                 .build();
65         for (var entry : certMap.entrySet()) {
66             try {
67                 keyStore.setCertificateEntry(entry.getKey(), entry.getValue());
68             } catch (KeyStoreException e) {
69                 throw new UnsupportedConfigurationException("Failed to load certificate", e);
70             }
71         }
72     }
73
74     private static Map<String, Certificate> extractCertificates(
75             @Nullable final InlineOrTruststoreCertsGrouping certs,
76             @NonNull final String aliasPrefix) throws UnsupportedConfigurationException {
77         if (certs == null) {
78             return Map.of();
79         }
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);
86         }
87         final var mapBuilder = ImmutableMap.<String, Certificate>builder();
88         for (var cert : inlineDef.nonnullCertificate().values()) {
89             try {
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);
94             }
95         }
96         return mapBuilder.build();
97     }
98
99     /**
100      * Builds asymmetric key pair from configuration data provided, validates it then puts into given key store.
101      *
102      * @param keyStore keystore
103      * @param input configuration
104      * @throws UnsupportedConfigurationException if key pair is not set to key store
105      */
106     static void setAsymmetricKey(final @NonNull KeyStore keyStore,
107             final @NonNull InlineOrKeystoreAsymmetricKeyGrouping input)
108             throws UnsupportedConfigurationException {
109
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);
116         }
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());
122         try {
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);
128         }
129     }
130
131     /**
132      * Builds asymmetric key pair and associated certificate from configuration data provided, validates
133      * then puts into given key store.
134      *
135      * @param keyStore key store
136      * @param input configuration
137      * @throws UnsupportedConfigurationException if key pair and certificate are not set to key store
138      */
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);
148         }
149         final var keyPair = extractKeyPair(inlineDef);
150         final Certificate certificate;
151         try {
152             certificate = buildX509Certificate(inlineDef.requireCertData().getValue());
153         } catch (IOException | CertificateException e) {
154             throw new UnsupportedConfigurationException("Failed to load certificate" + inlineDef, e);
155         }
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);
161         try {
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);
167         }
168     }
169
170     private static KeyPair extractKeyPair(final AsymmetricKeyPairGrouping input)
171             throws UnsupportedConfigurationException {
172
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;
179         } else {
180             throw new UnsupportedConfigurationException("Unsupported private key format " + privateKeyFormat);
181         }
182         final byte[] privateKeyBytes;
183         if (input.getPrivateKeyType() instanceof CleartextPrivateKey clearText) {
184             privateKeyBytes = clearText.requireCleartextPrivateKey();
185         } else {
186             throw new UnsupportedConfigurationException("Unsupported private key type " + input.getPrivateKeyType());
187         }
188         final var privateKey = buildPrivateKey(keyAlgorithm, privateKeyBytes);
189
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;
196         } else {
197             throw new UnsupportedConfigurationException("Unsupported public key format " + publicKeyFormat);
198         }
199         final var publicKey = isSshPublicKey ? buildPublicKeyFromSshEncoding(input.getPublicKey())
200                 : buildX509PublicKey(keyAlgorithm, input.getPublicKey());
201         return new KeyPair(publicKey, privateKey);
202     }
203
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());
209         }
210         return expectedType.cast(obj);
211     }
212 }