This is an automated email from the ASF dual-hosted git repository.

jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/karaf.git


The following commit(s) were added to refs/heads/main by this push:
     new f7d9d4a701 [ssh] Support ed25519 keys using bouncycastle (#2927)
f7d9d4a701 is described below

commit f7d9d4a7012e523c1825b4412e512fd7df6e9cb9
Author: Holger Friedrich <[email protected]>
AuthorDate: Tue Oct 6 12:04:09 2026 +0200

    [ssh] Support ed25519 keys using bouncycastle (#2927)
    
    * [ssh] Support ed25519 keys using bouncycastle
    
    Enable ssh-ed25519 in the default signature algorithms, match ed25519
    keys in the publickey login module, and add the bouncycastle jars from
    the distribution to the bin/client classpath.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    
    * review comments
    
    * review comments
    
    ---------
    
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .../main/filtered-resources/resources/bin/client   |  4 ++
 .../filtered-resources/resources/bin/client.bat    |  4 ++
 .../resources/etc/org.apache.karaf.shell.cfg       |  4 +-
 .../modules/publickey/PublickeyLoginModule.java    | 47 +++++++++++++++++++++-
 .../modules/publickey/PublicKeyEncodingTest.java   | 24 +++++++++++
 .../java/org/apache/karaf/shell/ssh/Activator.java |  4 +-
 6 files changed, 83 insertions(+), 4 deletions(-)

diff --git 
a/assemblies/features/base/src/main/filtered-resources/resources/bin/client 
b/assemblies/features/base/src/main/filtered-resources/resources/bin/client
index f40d95c3b3..c4b5f58979 100755
--- a/assemblies/features/base/src/main/filtered-resources/resources/bin/client
+++ b/assemblies/features/base/src/main/filtered-resources/resources/bin/client
@@ -84,6 +84,10 @@ setupClassPath() {
     
CLASSPATH="${CLASSPATH}:${KARAF_HOME}/system/org/apache/sshd/sshd-sftp/@@sshd.version@@/sshd-sftp-@@sshd.version@@.jar"
     
CLASSPATH="${CLASSPATH}:${KARAF_HOME}/system/org/fusesource/jansi/jansi/@@jansi.version@@/jansi-@@jansi.version@@.jar"
     
CLASSPATH="${CLASSPATH}:${KARAF_HOME}/system/org/jline/jline/@@jline.version@@/jline-@@jline.version@@.jar"
+    # bouncycastle provides the EdDSA support sshd needs to read ed25519 keys
+    
CLASSPATH="${CLASSPATH}:${KARAF_HOME}/system/org/bouncycastle/bcprov-jdk18on/@@bouncycastle.version@@/bcprov-jdk18on-@@bouncycastle.version@@.jar"
+    
CLASSPATH="${CLASSPATH}:${KARAF_HOME}/system/org/bouncycastle/bcpkix-jdk18on/@@bouncycastle.version@@/bcpkix-jdk18on-@@bouncycastle.version@@.jar"
+    
CLASSPATH="${CLASSPATH}:${KARAF_HOME}/system/org/bouncycastle/bcutil-jdk18on/@@bouncycastle.version@@/bcutil-jdk18on-@@bouncycastle.version@@.jar"
 }
 
 init() {
diff --git 
a/assemblies/features/base/src/main/filtered-resources/resources/bin/client.bat 
b/assemblies/features/base/src/main/filtered-resources/resources/bin/client.bat
index ec3578258e..daa55a7f3b 100644
--- 
a/assemblies/features/base/src/main/filtered-resources/resources/bin/client.bat
+++ 
b/assemblies/features/base/src/main/filtered-resources/resources/bin/client.bat
@@ -220,6 +220,10 @@ set 
CLASSPATH=%CLASSPATH%;%KARAF_HOME%\system\org\apache\sshd\sshd-scp\@@sshd.ve
 set 
CLASSPATH=%CLASSPATH%;%KARAF_HOME%\system\org\apache\sshd\sshd-sftp\@@sshd.version@@\sshd-sftp-@@sshd.version@@.jar
 set 
CLASSPATH=%CLASSPATH%;%KARAF_HOME%\system\org\jline\jline\@@jline.version@@\jline-@@jline.version@@.jar
 set 
CLASSPATH=%CLASSPATH%;%KARAF_HOME%\system\org\fusesource\jansi\jansi\@@jansi.version@@\jansi-@@jansi.version@@.jar
+rem bouncycastle provides the EdDSA support sshd needs to read ed25519 keys
+set 
CLASSPATH=%CLASSPATH%;%KARAF_HOME%\system\org\bouncycastle\bcprov-jdk18on\@@bouncycastle.version@@\bcprov-jdk18on-@@bouncycastle.version@@.jar
+set 
CLASSPATH=%CLASSPATH%;%KARAF_HOME%\system\org\bouncycastle\bcpkix-jdk18on\@@bouncycastle.version@@\bcpkix-jdk18on-@@bouncycastle.version@@.jar
+set 
CLASSPATH=%CLASSPATH%;%KARAF_HOME%\system\org\bouncycastle\bcutil-jdk18on\@@bouncycastle.version@@\bcutil-jdk18on-@@bouncycastle.version@@.jar
 
 :EXECUTE
     set arg1=%~1
diff --git 
a/instance/src/main/resources/org/apache/karaf/instance/resources/etc/org.apache.karaf.shell.cfg
 
b/instance/src/main/resources/org/apache/karaf/instance/resources/etc/org.apache.karaf.shell.cfg
index ac26a84705..dcefd84b98 100644
--- 
a/instance/src/main/resources/org/apache/karaf/instance/resources/etc/org.apache.karaf.shell.cfg
+++ 
b/instance/src/main/resources/org/apache/karaf/instance/resources/etc/org.apache.karaf.shell.cfg
@@ -107,9 +107,9 @@ completionMode = GLOBAL
 
 #
 # Override allowed SSH host key signature algorithms.
-# Default: 
ssh-rsa,rsa-sha2-256,rsa-sha2-512,[email protected],ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521
+# Default: 
ssh-rsa,rsa-sha2-256,rsa-sha2-512,[email protected],ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,[email protected]
 #
-# sigAlgorithms = 
ssh-rsa,rsa-sha2-256,rsa-sha2-512,[email protected],ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521
+# sigAlgorithms = 
ssh-rsa,rsa-sha2-256,rsa-sha2-512,[email protected],ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,[email protected]
 
 #
 # Override moduli-url.
diff --git 
a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/publickey/PublickeyLoginModule.java
 
b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/publickey/PublickeyLoginModule.java
index 644fa4f1b4..e34220ec23 100644
--- 
a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/publickey/PublickeyLoginModule.java
+++ 
b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/publickey/PublickeyLoginModule.java
@@ -37,6 +37,8 @@ import java.security.spec.InvalidKeySpecException;
 import java.security.spec.InvalidParameterSpecException;
 import java.security.spec.KeySpec;
 import java.security.spec.RSAPublicKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
 import java.util.Base64;
 import java.util.Collections;
 import java.util.HashMap;
@@ -61,9 +63,15 @@ import org.slf4j.LoggerFactory;
 
 public class PublickeyLoginModule extends AbstractKarafLoginModule {
 
-    private final Logger LOG = 
LoggerFactory.getLogger(PublickeyLoginModule.class);
+    private static final Logger LOG = 
LoggerFactory.getLogger(PublickeyLoginModule.class);
 
     private static final String USERS_FILE = "users";
+    private static final String ED25519_IDENTIFIER = "ssh-ed25519";
+    private static final int ED25519_KEY_LENGTH = 32;
+    // DER prefix of a X.509 SubjectPublicKeyInfo holding a 32 bytes long 
ed25519 key (RFC 8410)
+    private static final byte[] ED25519_X509_PREFIX = {
+        0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00
+    };
     private static final Map<String, String> nistSecMap;
 
     static {
@@ -239,6 +247,27 @@ public class PublickeyLoginModule extends 
AbstractKarafLoginModule {
                 PublicKey generatedPublicKey = 
keyFactory.generatePublic(keySpec);
 
                 return key.equals(generatedPublicKey);
+            } else if (ED25519_IDENTIFIER.equals(identifier)) {
+                // OpenSSH stores an ed25519 key as the raw 32 bytes of the 
compressed point.
+                // The key implementation depends on the registered provider 
(for instance
+                // BouncyCastle), so compare the X.509 encodings instead of 
the key objects.
+                int size = dis.readInt();
+                if (size != ED25519_KEY_LENGTH) {
+                    return false;
+                }
+                byte[] bytes = new byte[size];
+                dis.readFully(bytes);
+
+                KeyFactory keyFactory = KeyFactory.getInstance("Ed25519");
+                KeySpec publicKeySpec = new 
X509EncodedKeySpec(x509Ed25519(bytes));
+                PublicKey generatedPublicKey = 
keyFactory.generatePublic(publicKeySpec);
+
+                byte[] encoded = key.getEncoded();
+                if (encoded == null) {
+                    LOG.debug("Unable to compare ed25519 key, the provider 
does not support getEncoded()");
+                    return false;
+                }
+                return Arrays.equals(encoded, generatedPublicKey.getEncoded());
             } else {
                 throw new FailedLoginException("Unsupported key type " + 
key.getClass().toString());
             }
@@ -247,6 +276,22 @@ public class PublickeyLoginModule extends 
AbstractKarafLoginModule {
         }
     }
 
+    /**
+     * Wraps the raw bytes of an ed25519 public key in a X.509 
SubjectPublicKeyInfo structure,
+     * so that it can be read by a {@link KeyFactory}. The prefix encodes the 
total length,
+     * so the key has to be exactly {@link #ED25519_KEY_LENGTH} bytes long.
+     */
+    private static byte[] x509Ed25519(byte[] rawKey) {
+        if (rawKey.length != ED25519_KEY_LENGTH) {
+            throw new IllegalArgumentException("An ed25519 key must be " + 
ED25519_KEY_LENGTH
+                + " bytes long, got " + rawKey.length);
+        }
+        byte[] encoded = new byte[ED25519_X509_PREFIX.length + rawKey.length];
+        System.arraycopy(ED25519_X509_PREFIX, 0, encoded, 0, 
ED25519_X509_PREFIX.length);
+        System.arraycopy(rawKey, 0, encoded, ED25519_X509_PREFIX.length, 
rawKey.length);
+        return encoded;
+    }
+
     private static String readString(DataInputStream dis) throws IOException {
         int size = dis.readInt();
         byte[] bytes = new byte[size];
diff --git 
a/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/publickey/PublicKeyEncodingTest.java
 
b/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/publickey/PublicKeyEncodingTest.java
index e85304f01b..e84464adb4 100644
--- 
a/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/publickey/PublicKeyEncodingTest.java
+++ 
b/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/publickey/PublicKeyEncodingTest.java
@@ -33,6 +33,8 @@ import java.security.spec.InvalidKeySpecException;
 import java.security.spec.InvalidParameterSpecException;
 import java.security.spec.KeySpec;
 import java.security.spec.RSAPublicKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
 
 import javax.security.auth.login.FailedLoginException;
 
@@ -209,4 +211,26 @@ public class PublicKeyEncodingTest {
         assertFalse(PublickeyLoginModule.equals(publicKey, differentKey));
     }
 
+    @Test
+    public void testEd25519() throws FailedLoginException, 
NoSuchAlgorithmException, InvalidKeySpecException {
+        // Generated using: ssh-keygen -t ed25519
+        String storedKey = 
"AAAAC3NzaC1lZDI1NTE5AAAAIOiBWR+V72VeSjf4d2spgw2jmh95+LgE8GkCmZiFZQCd";
+        // the same key as X.509 SubjectPublicKeyInfo
+        String x509Key = 
"MCowBQYDK2VwAyEA6IFZH5XvZV5KN/h3aymDDaOaH3n4uATwaQKZmIVlAJ0=";
+
+        KeyFactory keyFactory = KeyFactory.getInstance("Ed25519");
+        KeySpec keySpec = new 
X509EncodedKeySpec(Base64.getDecoder().decode(x509Key));
+        PublicKey publicKey = keyFactory.generatePublic(keySpec);
+
+        assertTrue(PublickeyLoginModule.equals(publicKey, storedKey));
+
+        // Make sure a different stored key does not work
+        String differentKey = 
"AAAAC3NzaC1lZDI1NTE5AAAAIH0XVMRvA3FXSjqjRzqCIpqWaSRH5HxWRfwWqKEXayqu";
+        assertFalse(PublickeyLoginModule.equals(publicKey, differentKey));
+
+        // A key of the wrong length is rejected instead of being passed to 
the key factory
+        String truncatedKey = 
"AAAAC3NzaC1lZDI1NTE5AAAAHwABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4=";
+        assertFalse(PublickeyLoginModule.equals(publicKey, truncatedKey));
+    }
+
 }
diff --git a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/Activator.java 
b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/Activator.java
index e0657bef24..cbab5e9797 100644
--- a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/Activator.java
+++ b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/Activator.java
@@ -169,7 +169,9 @@ public class Activator extends BaseActivator implements 
ManagedService {
         String[] macs               = getStringArray("macs", 
"hmac-sha2-512,hmac-sha2-256");
         String[] ciphers            = getStringArray("ciphers", 
"aes256-ctr,aes192-ctr,aes128-ctr");
         String[] kexAlgorithms      = getStringArray("kexAlgorithms", 
"ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256");
-        String[] sigAlgorithms      = getStringArray("sigAlgorithms", 
"ssh-rsa,rsa-sha2-256,rsa-sha2-512,[email protected],ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521");
+        // the sk-* algorithms are accepted by the server, but hardware backed 
keys are not
+        // supported by the publickey login module yet
+        String[] sigAlgorithms      = getStringArray("sigAlgorithms", 
"ssh-rsa,rsa-sha2-256,rsa-sha2-512,[email protected],ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,[email protected]");
         String welcomeBanner        = getString("welcomeBanner", null);
         String moduliUrl            = getString("moduli-url", null);
         boolean sftpEnabled         = getBoolean("sftpEnabled", true);

Reply via email to