Switch ssh-host-key to binary 92/110092/5
authorRobert Varga <robert.varga@pantheon.tech>
Tue, 30 Jan 2024 10:52:10 +0000 (11:52 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 31 Jan 2024 11:08:35 +0000 (12:08 +0100)
Using a String with Base64 is a rather lacking employ of YANG, as 'type
binary' is encoded in JSON and XML as Base64, so we get codecs for free.

JIRA: NETCONF-1243
Change-Id: I90158893570e2dd6d80a69a78acf8fe99f84ae1e
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
apps/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/AuthorizedKeysDecoder.java
apps/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSshAuthProvider.java
apps/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountStatusReporter.java
netconf/callhome-model/src/main/yang/odl-netconf-callhome-server.yang

index bafb0f6aa118b58b1dbc29d7c78b845856fed695..6828c0ad8c430087ff8f9c9182e783f19b594e25 100644 (file)
@@ -25,12 +25,13 @@ import java.security.spec.ECPoint;
 import java.security.spec.ECPublicKeySpec;
 import java.security.spec.RSAPublicKeySpec;
 import java.util.Arrays;
-import java.util.Base64;
 import org.bouncycastle.jce.ECNamedCurveTable;
 import org.bouncycastle.jce.ECPointUtil;
 import org.bouncycastle.jce.interfaces.ECPublicKey;
 import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
 import org.bouncycastle.jce.spec.ECNamedCurveSpec;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.SshPublicKey;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -75,15 +76,8 @@ public class AuthorizedKeysDecoder {
     private byte[] bytes = new byte[0];
     private int pos = 0;
 
-    public PublicKey decodePublicKey(final String keyLine) throws GeneralSecurityException {
-
-        // look for the Base64 encoded part of the line to decode
-        // both ssh-rsa and ssh-dss begin with "AAAA" due to the length bytes
-        bytes = Base64.getDecoder().decode(keyLine.getBytes(StandardCharsets.UTF_8));
-        if (bytes.length == 0) {
-            throw new IllegalArgumentException("No Base64 part to decode in " + keyLine);
-        }
-
+    public PublicKey decodePublicKey(final SshPublicKey keyLine) throws GeneralSecurityException {
+        bytes = keyLine.getValue();
         pos = 0;
 
         final var type = decodeType();
@@ -156,7 +150,7 @@ public class AuthorizedKeysDecoder {
         out.write(bytes);
     }
 
-    public static String encodePublicKey(final PublicKey publicKey) throws IOException {
+    public static @NonNull SshPublicKey encodePublicKey(final PublicKey publicKey) throws IOException {
         final var baos = new ByteArrayOutputStream();
 
         try (var dout = new DataOutputStream(baos)) {
@@ -187,9 +181,9 @@ public class AuthorizedKeysDecoder {
                 dout.write(coordX);
                 dout.write(coordY);
             } else {
-                throw new IllegalArgumentException("Unknown public key encoding: " + publicKey);
+                throw new IOException("Unknown public key encoding: " + publicKey);
             }
         }
-        return Base64.getEncoder().encodeToString(baos.toByteArray());
+        return new SshPublicKey(baos.toByteArray());
     }
 }
index d4a5857f2808ae93470dced180e7789d8bc60828..0bd92411cbf81dbde44e56f4db582d4ff459e1c3 100644 (file)
@@ -27,6 +27,7 @@ import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.netconf.callhome.server.ssh.CallHomeSshAuthProvider;
 import org.opendaylight.netconf.callhome.server.ssh.CallHomeSshAuthSettings;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.NetconfCallhomeServer;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.SshPublicKey;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.credentials.Credentials;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.AllowedDevices;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.Global;
@@ -156,10 +157,9 @@ public final class CallHomeMountSshAuthProvider implements CallHomeSshAuthProvid
 
         private void deleteDevice(final Device dataBefore) {
             if (dataBefore != null) {
-                final String publicKey = getHostPublicKey(dataBefore);
-                if (publicKey != null) {
+                if (dataBefore.getTransport() instanceof Ssh ssh) {
                     LOG.debug("Removing device {}", dataBefore.getUniqueId());
-                    removeDevice(publicKey, dataBefore);
+                    removeDevice(ssh.nonnullSshClientParams().requireHostKey(), dataBefore);
                 } else {
                     LOG.debug("Ignoring removal of device {}, no host key present", dataBefore.getUniqueId());
                 }
@@ -167,22 +167,17 @@ public final class CallHomeMountSshAuthProvider implements CallHomeSshAuthProvid
         }
 
         private void writeDevice(final Device dataAfter) {
-            final String publicKey = getHostPublicKey(dataAfter);
-            if (publicKey != null) {
+            if (dataAfter.getTransport() instanceof Ssh ssh) {
                 LOG.debug("Adding device {}", dataAfter.getUniqueId());
-                addDevice(publicKey, dataAfter);
+                addDevice(ssh.nonnullSshClientParams().requireHostKey(), dataAfter);
             } else {
                 LOG.debug("Ignoring addition of device {}, no host key present", dataAfter.getUniqueId());
             }
         }
 
-        private static String getHostPublicKey(final Device device) {
-            return device.getTransport() instanceof Ssh ssh ? ssh.nonnullSshClientParams().getHostKey() : null;
-        }
-
-        abstract void addDevice(String publicKey, Device device);
+        abstract void addDevice(SshPublicKey publicKey, Device device);
 
-        abstract void removeDevice(String publicKey, Device device);
+        abstract void removeDevice(SshPublicKey publicKey, Device device);
     }
 
     private static final class DeviceConfig extends AbstractDeviceListener {
@@ -194,7 +189,7 @@ public final class CallHomeMountSshAuthProvider implements CallHomeSshAuthProvid
         }
 
         @Override
-        void addDevice(final String publicKey, final Device device) {
+        void addDevice(final SshPublicKey publicKey, final Device device) {
             final PublicKey key = publicKey(publicKey, device);
             if (key != null) {
                 byPublicKey.put(key, device);
@@ -202,14 +197,14 @@ public final class CallHomeMountSshAuthProvider implements CallHomeSshAuthProvid
         }
 
         @Override
-        void removeDevice(final String publicKey, final Device device) {
+        void removeDevice(final SshPublicKey publicKey, final Device device) {
             final PublicKey key = publicKey(publicKey, device);
             if (key != null) {
                 byPublicKey.remove(key);
             }
         }
 
-        private PublicKey publicKey(final String hostKey, final Device device) {
+        private PublicKey publicKey(final SshPublicKey hostKey, final Device device) {
             try {
                 return keyDecoder.decodePublicKey(hostKey);
             } catch (GeneralSecurityException e) {
@@ -220,13 +215,13 @@ public final class CallHomeMountSshAuthProvider implements CallHomeSshAuthProvid
     }
 
     private static final class DeviceOp extends AbstractDeviceListener {
-        private final ConcurrentMap<String, Device> byPublicKey = new ConcurrentHashMap<>();
+        private final ConcurrentMap<SshPublicKey, Device> byPublicKey = new ConcurrentHashMap<>();
 
         Device get(final PublicKey serverKey) {
-            final String skey;
+            final SshPublicKey skey;
             try {
                 skey = AuthorizedKeysDecoder.encodePublicKey(serverKey);
-            } catch (IOException | IllegalArgumentException e) {
+            } catch (IOException e) {
                 LOG.error("Unable to encode server key: {}", serverKey, e);
                 return null;
             }
@@ -235,12 +230,12 @@ public final class CallHomeMountSshAuthProvider implements CallHomeSshAuthProvid
         }
 
         @Override
-        void removeDevice(final String publicKey, final Device device) {
+        void removeDevice(final SshPublicKey publicKey, final Device device) {
             byPublicKey.remove(publicKey);
         }
 
         @Override
-        void addDevice(final String publicKey, final Device device) {
+        void addDevice(final SshPublicKey publicKey, final Device device) {
             byPublicKey.put(publicKey, device);
         }
     }
index 6244662068d846de3296b66cffc56238e63a83b2..c479a69505e189c1d1f6c4f4050b6fce9a57f6d3 100644 (file)
@@ -26,6 +26,7 @@ import org.opendaylight.mdsal.common.api.CommitInfo;
 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.netconf.callhome.server.CallHomeStatusRecorder;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.NetconfCallhomeServer;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.SshPublicKey;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.AllowedDevices;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.allowed.devices.Device;
 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev240129.netconf.callhome.server.allowed.devices.Device.DeviceStatus;
@@ -127,17 +128,22 @@ public final class CallHomeMountStatusReporter implements CallHomeStatusRecorder
      * @param status initial device status
      */
     public void reportNewSshDevice(final String id, final PublicKey serverKey, final DeviceStatus status) {
-        writeDevice(buildInstanceIdentifier(id), newSshDevice(id, serverKey, status));
+        final var device = newSshDevice(id, serverKey, status);
+        if (device != null) {
+            writeDevice(buildInstanceIdentifier(id), device);
+        }
     }
 
     private static Device newSshDevice(final String id, final PublicKey serverKey, final DeviceStatus status) {
         // used only for netconf devices that are connected via SSH transport and global credentials
-        String sshEncodedKey = serverKey.toString();
+        final SshPublicKey sshEncodedKey;
         try {
             sshEncodedKey = AuthorizedKeysDecoder.encodePublicKey(serverKey);
         } catch (IOException e) {
-            LOG.warn("Unable to encode public key to ssh format.", e);
+            LOG.warn("Unable to encode public key to ssh format, skipping update", e);
+            return null;
         }
+
         return new DeviceBuilder()
             .setUniqueId(id)
             .withKey(new DeviceKey(id))
index 74989cf6b7d6ceaaa8ca71a74b347824283f06b6..87ebfc0290e738da390329c7765e8695b9d108d6 100644 (file)
@@ -17,7 +17,8 @@ module odl-netconf-callhome-server {
       "A number of improvements to the sematics of this model. In concrete terms:
        - every device now has to have a transport
        - previously-deprecated 'ssh-host-key' is now obsolete
-       - 'credentials; is obsoleted as well";
+       - 'credentials; is obsoleted as well
+       - 'host-key' is now a dedicated typedef based on 'type binary'";
   }
 
   revision 2023-04-28 {
@@ -35,6 +36,15 @@ module odl-netconf-callhome-server {
     description "Initial version";
   }
 
+  typedef ssh-public-key {
+    description "An SSH public key encoded in RFC4253 format";
+    reference "RFC4253 section 6.6";
+    type binary {
+      // Note: the format requires at least 8 bytes for length of the algo and its bytes
+      length "8..max";
+    }
+  }
+
   grouping credentials {
     container credentials {
       presence "Credentials to device.";
@@ -90,8 +100,8 @@ module odl-netconf-callhome-server {
             container ssh-client-params {
               leaf host-key {
                 mandatory true;
-                description "BASE-64 encoded public key which device will use during connection.";
-                type string;
+                description "Public key which device will use during connection.";
+                type ssh-public-key;
               }
               uses credentials;
             }