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);