Fix callhome device "DISCONNECTED" status
[netconf.git] / apps / callhome-provider / src / main / java / org / opendaylight / netconf / topology / callhome / AuthorizedKeysDecoder.java
1 /*
2  * Copyright (c) 2016 Brocade Communication Systems and others.  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.topology.callhome;
9
10 import com.google.common.collect.ImmutableMap;
11 import java.io.ByteArrayOutputStream;
12 import java.io.DataOutput;
13 import java.io.DataOutputStream;
14 import java.io.IOException;
15 import java.math.BigInteger;
16 import java.nio.charset.StandardCharsets;
17 import java.security.GeneralSecurityException;
18 import java.security.KeyFactory;
19 import java.security.NoSuchAlgorithmException;
20 import java.security.Provider;
21 import java.security.PublicKey;
22 import java.security.Security;
23 import java.security.interfaces.DSAPublicKey;
24 import java.security.interfaces.RSAPublicKey;
25 import java.security.spec.DSAPublicKeySpec;
26 import java.security.spec.ECPoint;
27 import java.security.spec.ECPublicKeySpec;
28 import java.security.spec.RSAPublicKeySpec;
29 import java.util.Arrays;
30 import org.bouncycastle.jce.ECNamedCurveTable;
31 import org.bouncycastle.jce.ECPointUtil;
32 import org.bouncycastle.jce.interfaces.ECPublicKey;
33 import org.bouncycastle.jce.provider.BouncyCastleProvider;
34 import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
35 import org.bouncycastle.jce.spec.ECNamedCurveSpec;
36 import org.eclipse.jdt.annotation.NonNull;
37 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.SshPublicKey;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 final class AuthorizedKeysDecoder {
42     private static final Logger LOG = LoggerFactory.getLogger(AuthorizedKeysDecoder.class);
43
44     private static final ImmutableMap<String, String> ECDSA_CURVES = ImmutableMap.<String, String>builder()
45         .put("nistp256", "secp256r1")
46         .put("nistp384", "secp384r1")
47         .put("nistp512", "secp512r1")
48         .build();
49
50     private static final String ECDSA_SUPPORTED_CURVE_NAME = "nistp256";
51     private static final byte[] ECDSA_SUPPORTED_CURVE_NAME_BYTES =
52         ECDSA_SUPPORTED_CURVE_NAME.getBytes(StandardCharsets.UTF_8);
53     private static final String ECDSA_SUPPORTED_CURVE_NAME_SPEC = ECDSA_CURVES.get(ECDSA_SUPPORTED_CURVE_NAME);
54
55     private static final String KEY_TYPE_RSA = "ssh-rsa";
56     private static final byte[] KEY_TYPE_RSA_BYTES = KEY_TYPE_RSA.getBytes(StandardCharsets.UTF_8);
57
58     private static final String KEY_TYPE_DSA = "ssh-dss";
59     private static final byte[] KEY_TYPE_DSA_BYTES = KEY_TYPE_DSA.getBytes(StandardCharsets.UTF_8);
60     private static final String KEY_TYPE_ECDSA = "ecdsa-sha2-" + ECDSA_SUPPORTED_CURVE_NAME;
61     private static final byte[] KEY_TYPE_ECDSA_BYTES = KEY_TYPE_ECDSA.getBytes(StandardCharsets.UTF_8);
62
63     private static final KeyFactory RSA_FACTORY;
64     private static final KeyFactory DSA_FACTORY;
65     private static final KeyFactory EC_FACTORY;
66
67     static {
68         // Default provider fails to validate keys, see https://jira.opendaylight.org/browse/NETCONF-1249
69         var bcprov = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME);
70         if (bcprov == null) {
71             bcprov = new BouncyCastleProvider();
72         }
73         RSA_FACTORY = loadOrWarn("RSA", bcprov);
74         DSA_FACTORY = loadOrWarn("DSA", bcprov);
75         EC_FACTORY = loadOrWarn("EC", bcprov);
76     }
77
78     private static KeyFactory loadOrWarn(final String algorithm, final Provider provider) {
79         try {
80             return KeyFactory.getInstance(algorithm, provider);
81         } catch (NoSuchAlgorithmException e) {
82             LOG.warn("KeyFactory for {} not found", algorithm, e);
83             return null;
84         }
85     }
86
87     private final byte[] bytes;
88     private int pos;
89
90     private AuthorizedKeysDecoder(final SshPublicKey keyLine) {
91         bytes = keyLine.getValue();
92     }
93
94     static @NonNull PublicKey decodePublicKey(final @NonNull SshPublicKey keyLine) throws GeneralSecurityException {
95         final var instance = new AuthorizedKeysDecoder(keyLine);
96         final var type = instance.decodeType();
97         return switch (type) {
98             case KEY_TYPE_RSA -> instance.decodeAsRSA();
99             case KEY_TYPE_DSA -> instance.decodeAsDSA();
100             case KEY_TYPE_ECDSA -> instance.decodeAsEcDSA();
101             default -> throw new NoSuchAlgorithmException("Unknown decode key type " + type + " in " + keyLine);
102         };
103     }
104
105     static @NonNull SshPublicKey encodePublicKey(final PublicKey publicKey) throws IOException {
106         final var baos = new ByteArrayOutputStream();
107
108         try (var dout = new DataOutputStream(baos)) {
109             if (publicKey instanceof RSAPublicKey rsa) {
110                 dout.writeInt(KEY_TYPE_RSA_BYTES.length);
111                 dout.write(KEY_TYPE_RSA_BYTES);
112                 encodeBigInt(dout, rsa.getPublicExponent());
113                 encodeBigInt(dout, rsa.getModulus());
114             } else if (publicKey instanceof DSAPublicKey dsa) {
115                 final var dsaParams = dsa.getParams();
116                 dout.writeInt(KEY_TYPE_DSA_BYTES.length);
117                 dout.write(KEY_TYPE_DSA_BYTES);
118                 encodeBigInt(dout, dsaParams.getP());
119                 encodeBigInt(dout, dsaParams.getQ());
120                 encodeBigInt(dout, dsaParams.getG());
121                 encodeBigInt(dout, dsa.getY());
122             } else if (publicKey instanceof ECPublicKey ec) {
123                 dout.writeInt(KEY_TYPE_ECDSA_BYTES.length);
124                 dout.write(KEY_TYPE_ECDSA_BYTES);
125                 dout.writeInt(ECDSA_SUPPORTED_CURVE_NAME_BYTES.length);
126                 dout.write(ECDSA_SUPPORTED_CURVE_NAME_BYTES);
127
128                 final var q = ec.getQ();
129                 final var coordX = q.getAffineXCoord().getEncoded();
130                 final var coordY = q.getAffineYCoord().getEncoded();
131                 dout.writeInt(coordX.length + coordY.length + 1);
132                 dout.writeByte(0x04);
133                 dout.write(coordX);
134                 dout.write(coordY);
135             } else {
136                 throw new IOException("Unknown public key encoding: " + publicKey);
137             }
138         }
139         return new SshPublicKey(baos.toByteArray());
140     }
141
142     private @NonNull PublicKey decodeAsEcDSA() throws GeneralSecurityException {
143         if (EC_FACTORY == null) {
144             throw new NoSuchAlgorithmException("ECDSA keys are not supported");
145         }
146
147         ECNamedCurveParameterSpec spec256r1 = ECNamedCurveTable.getParameterSpec(ECDSA_SUPPORTED_CURVE_NAME_SPEC);
148         ECNamedCurveSpec params256r1 = new ECNamedCurveSpec(
149             ECDSA_SUPPORTED_CURVE_NAME_SPEC, spec256r1.getCurve(), spec256r1.getG(), spec256r1.getN());
150         // copy last 65 bytes from ssh key.
151         ECPoint point = ECPointUtil.decodePoint(params256r1.getCurve(), Arrays.copyOfRange(bytes, 39, bytes.length));
152         return EC_FACTORY.generatePublic(new ECPublicKeySpec(point, params256r1));
153     }
154
155     private @NonNull PublicKey decodeAsDSA() throws GeneralSecurityException {
156         if (DSA_FACTORY == null) {
157             throw new NoSuchAlgorithmException("RSA keys are not supported");
158         }
159
160         final var p = decodeBigInt();
161         final var q = decodeBigInt();
162         final var g = decodeBigInt();
163         final var y = decodeBigInt();
164         return DSA_FACTORY.generatePublic(new DSAPublicKeySpec(y, p, q, g));
165     }
166
167     private @NonNull PublicKey decodeAsRSA() throws GeneralSecurityException {
168         if (RSA_FACTORY == null) {
169             throw new NoSuchAlgorithmException("RSA keys are not supported");
170         }
171
172         final var exponent = decodeBigInt();
173         final var modulus = decodeBigInt();
174         return RSA_FACTORY.generatePublic(new RSAPublicKeySpec(modulus, exponent));
175     }
176
177     private String decodeType() {
178         int len = decodeInt();
179         String type = new String(bytes, pos, len, StandardCharsets.UTF_8);
180         pos += len;
181         return type;
182     }
183
184     private int decodeInt() {
185         return (bytes[pos++] & 0xFF) << 24 | (bytes[pos++] & 0xFF) << 16
186                 | (bytes[pos++] & 0xFF) << 8 | bytes[pos++] & 0xFF;
187     }
188
189     private BigInteger decodeBigInt() {
190         int len = decodeInt();
191         byte[] bigIntBytes = new byte[len];
192         System.arraycopy(bytes, pos, bigIntBytes, 0, len);
193         pos += len;
194         return new BigInteger(bigIntBytes);
195     }
196
197     private static void encodeBigInt(final DataOutput out, final BigInteger value) throws IOException {
198         final var bytes = value.toByteArray();
199         out.writeInt(bytes.length);
200         out.write(bytes);
201     }
202 }