From 2d25b3bccca229e1c7072d3404f62042ef5d2640 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Sun, 20 Sep 2026 14:32:49 +0200 Subject: [PATCH 1/3] [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 --- .../filtered-resources/resources/bin/client | 4 +++ .../resources/bin/client.bat | 4 +++ .../resources/etc/org.apache.karaf.shell.cfg | 4 +-- .../publickey/PublickeyLoginModule.java | 36 +++++++++++++++++++ .../publickey/PublicKeyEncodingTest.java | 20 +++++++++++ .../org/apache/karaf/shell/ssh/Activator.java | 2 +- 6 files changed, 67 insertions(+), 3 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 f40d95c3b31..c4b5f58979e 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 ec3578258e7..daa55a7f3b1 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 ac26a847057..dcefd84b98a 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,sk-ecdsa-sha2-nistp256@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521 +# Default: ssh-rsa,rsa-sha2-256,rsa-sha2-512,sk-ecdsa-sha2-nistp256@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,sk-ssh-ed25519@openssh.com # -# sigAlgorithms = ssh-rsa,rsa-sha2-256,rsa-sha2-512,sk-ecdsa-sha2-nistp256@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521 +# sigAlgorithms = ssh-rsa,rsa-sha2-256,rsa-sha2-512,sk-ecdsa-sha2-nistp256@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,sk-ssh-ed25519@openssh.com # # 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 644fa4f1b43..5154350e57a 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.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; @@ -64,6 +66,12 @@ public class PublickeyLoginModule extends AbstractKarafLoginModule { private 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 nistSecMap; static { @@ -239,6 +247,23 @@ public static boolean equals(PublicKey key, String storedKey) throws FailedLogin 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(); + return encoded != null && Arrays.equals(encoded, generatedPublicKey.getEncoded()); } else { throw new FailedLoginException("Unsupported key type " + key.getClass().toString()); } @@ -247,6 +272,17 @@ public static boolean equals(PublicKey key, String storedKey) throws FailedLogin } } + /** + * 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}. + */ + private static byte[] x509Ed25519(byte[] rawKey) { + 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 e85304f01b4..c79becb62b4 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.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,22 @@ public void testEC256_2() throws FailedLoginException, NoSuchAlgorithmException, 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)); + } + } 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 e0657bef24a..2746c9aaf59 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,7 @@ protected SshServer createSshServer(SessionFactory sessionFactory) { 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,sk-ecdsa-sha2-nistp256@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521"); + String[] sigAlgorithms = getStringArray("sigAlgorithms", "ssh-rsa,rsa-sha2-256,rsa-sha2-512,sk-ecdsa-sha2-nistp256@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,sk-ssh-ed25519@openssh.com"); String welcomeBanner = getString("welcomeBanner", null); String moduliUrl = getString("moduli-url", null); boolean sftpEnabled = getBoolean("sftpEnabled", true); From 88032041fe5d17534012d3e67a25db9783fb3400 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Sun, 20 Sep 2026 16:54:52 +0200 Subject: [PATCH 2/3] review comments --- .../modules/publickey/PublickeyLoginModule.java | 15 ++++++++++++--- .../modules/publickey/PublicKeyEncodingTest.java | 4 ++++ 2 files changed, 16 insertions(+), 3 deletions(-) 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 5154350e57a..e34220ec23a 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 @@ -63,7 +63,7 @@ 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"; @@ -263,7 +263,11 @@ public static boolean equals(PublicKey key, String storedKey) throws FailedLogin PublicKey generatedPublicKey = keyFactory.generatePublic(publicKeySpec); byte[] encoded = key.getEncoded(); - return encoded != null && Arrays.equals(encoded, generatedPublicKey.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()); } @@ -274,9 +278,14 @@ public static boolean equals(PublicKey key, String storedKey) throws FailedLogin /** * 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}. + * 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); 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 c79becb62b4..e84464adb45 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 @@ -227,6 +227,10 @@ public void testEd25519() throws FailedLoginException, NoSuchAlgorithmException, // 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)); } } From b30878a776c085c28c2c1d8d5ff73f095a8f3a2b Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Sun, 20 Sep 2026 17:18:47 +0200 Subject: [PATCH 3/3] review comments --- .../ssh/src/main/java/org/apache/karaf/shell/ssh/Activator.java | 2 ++ 1 file changed, 2 insertions(+) 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 2746c9aaf59..cbab5e97971 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,6 +169,8 @@ protected SshServer createSshServer(SessionFactory sessionFactory) { 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"); + // 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,sk-ecdsa-sha2-nistp256@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,sk-ssh-ed25519@openssh.com"); String welcomeBanner = getString("welcomeBanner", null); String moduliUrl = getString("moduli-url", null);