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..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 @@ -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; @@ -61,9 +63,15 @@ 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 nistSecMap; static { @@ -239,6 +247,27 @@ 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(); + 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 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}. 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 e85304f01b4..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 @@ -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,26 @@ 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)); + + // 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 e0657bef24a..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,7 +169,9 @@ 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"); + // 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); boolean sftpEnabled = getBoolean("sftpEnabled", true);