This is an automated email from the ASF dual-hosted git repository. twolf pushed a commit to branch dev_3.0 in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit a8439661b0d5ee8e965f650546725065ea6442fd Author: Thomas Wolf <[email protected]> AuthorDate: Sat Sep 13 20:34:32 2025 +0200 GH-585: Enable using java's built-in ed25519 Add a key type detector for ed25519 keys. Different libraries return different algorithms from Key.getAlgorithm(): * Bouncy Castle returns algorithm "Ed25519". * net.i2p returns algorithm "EdDSA", and we know it only does ed25519. * JDK returns algorithm "EdDSA", and we have to get the name of the NamedParameterSpec of the key to check whether it is indeed ed25519. Fix some tests, and make the Java15+ JcePublicKeyFactory also work (theoretically) for ed448. Use maven-failsafe-plugin instead of maven-surefire-plugin to test the MR JAR. We need to run the tests from the JAR, not from target/classes. --- sshd-common/pom.xml | 8 +++- .../sshd/common/config/keys/BuiltinIdentities.java | 3 ++ .../apache/sshd/common/config/keys/KeyUtils.java | 6 +-- .../sshd/common/util/security/SecurityUtils.java | 24 +++++----- .../eddsa/generic/EdDSAKeyTypeDetector.java | 46 +++++++++++++++++++ .../eddsa/generic/EdDSAKeyTypeDetector.java | 52 ++++++++++++++++++++++ .../security/eddsa/jce/JcePublicKeyFactory.java | 12 ++--- .../openssh/OpenSSHKeyPairResourceWriterTest.java | 12 ++--- .../util/security/eddsa/Ed25519VectorsTest.java | 6 +-- .../sshd/util/test/CommonTestSupportUtils.java | 2 +- 10 files changed, 135 insertions(+), 36 deletions(-) diff --git a/sshd-common/pom.xml b/sshd-common/pom.xml index d27a26add..3582c74f6 100644 --- a/sshd-common/pom.xml +++ b/sshd-common/pom.xml @@ -118,13 +118,14 @@ <build> <plugins> <plugin> + <!-- MR JAR needs to run tests from the built JAR, so use failsafe instead of surefire --> <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-surefire-plugin</artifactId> + <artifactId>maven-failsafe-plugin</artifactId> <executions> <execution> <id>jce</id> <goals> - <goal>test</goal> + <goal>integration-test</goal> </goals> <configuration> <redirectTestOutputToFile>true</redirectTestOutputToFile> @@ -133,6 +134,9 @@ <org.apache.sshd.security.provider.EdDSA.enabled>false</org.apache.sshd.security.provider.EdDSA.enabled> <org.apache.sshd.security.provider.BC.enabled>false</org.apache.sshd.security.provider.BC.enabled> </systemPropertyVariables> + <includes> + <include>**/*Test.java</include> + </includes> </configuration> </execution> </executions> diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java index f4afc957f..a43b9737b 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java @@ -158,6 +158,9 @@ public enum BuiltinIdentities implements Identity { } private static BuiltinIdentities fromKeyType(String type) { + if (type == null) { + return null; + } return VALUES.stream().filter(b -> b.getSupportedKeyTypes().contains(type)).findFirst().orElse(null); } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java index 3796e9f4f..b05ec3b8d 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java @@ -85,6 +85,7 @@ import org.apache.sshd.common.util.buffer.ByteArrayBuffer; import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.common.util.security.SecurityUtils; import org.apache.sshd.common.util.security.eddsa.generic.Ed25519PublicKeyDecoder; +import org.apache.sshd.common.util.security.eddsa.generic.EdDSAKeyTypeDetector; /** * Utility class for keys @@ -856,10 +857,7 @@ public final class KeyUtils { } private static boolean isEd25519(Key k) { - return k != null - && (SecurityUtils.ED25519.equals(k.getAlgorithm()) // - || (SecurityUtils.EDDSA.equalsIgnoreCase(k.getAlgorithm()) - && k.getClass().getCanonicalName().startsWith("net.i2p."))); + return EdDSAKeyTypeDetector.isEd25519(k); } public static boolean compareKeys(PublicKey k1, PublicKey k2) { diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityUtils.java index e9eff3a6f..2bf9881a3 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityUtils.java @@ -94,12 +94,14 @@ public final class SecurityUtils { public static final String BOUNCY_CASTLE = "BC"; /** - * EDDSA (net.i2p) {@link SecurityProviderRegistrar} name. + * EDDSA (net.i2p) {@link SecurityProviderRegistrar} name. Also the algorithm name that + * {@link java.security.Key#getAlgorithm()} returns for ed25519 keys generated by net.i2p or by SunEC. */ public static final String EDDSA = "EdDSA"; /** - * Key and signature algorithm name for ed25519. + * Key and signature algorithm name for ed25519. A Bouncy Castle Ed25519 key returns this as + * {@link java.security.Key#getAlgorithm()}. */ public static final String ED25519 = "Ed25519"; @@ -560,19 +562,13 @@ public final class SecurityUtils { Boolean isSupported = EDDSA_SUPPORT_PRESENT.get(); if (isSupported == null) { register(); - // Currently we can only support ed25519 through libraries and thus we require a registrar. - // TODO: GH-585 (needs a multi-release JAR) - boolean supported = false; - synchronized (REGISTERED_PROVIDERS) { - for (SecurityProviderRegistrar r : REGISTERED_PROVIDERS.values()) { - supported = r.isEnabled() && r.isSupported() && r.isKeyFactorySupported(ED25519); - if (supported) { - break; - } - } + try { + KeyFactory factory = getKeyFactory(ED25519); + isSupported = Boolean.valueOf(factory != null); + } catch (GeneralSecurityException e) { + isSupported = Boolean.FALSE; } - isSupported = Boolean.valueOf(supported); - if (!EDDSA_SUPPORT_PRESENT.compareAndSet(null, Boolean.valueOf(supported))) { + if (!EDDSA_SUPPORT_PRESENT.compareAndSet(null, isSupported)) { isSupported = EDDSA_SUPPORT_PRESENT.get(); } } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/generic/EdDSAKeyTypeDetector.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/generic/EdDSAKeyTypeDetector.java new file mode 100644 index 000000000..f35d1a43b --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/generic/EdDSAKeyTypeDetector.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.common.util.security.eddsa.generic; + +import java.security.Key; + +import org.apache.sshd.common.util.security.SecurityUtils; + +public final class EdDSAKeyTypeDetector { + + private EdDSAKeyTypeDetector() { + throw new IllegalStateException("No instantiation"); + } + + public static boolean isEd25519(Key key) { + if (key == null) { + return false; + } + String algorithm = key.getAlgorithm(); + if (SecurityUtils.ED25519.equals(algorithm)) { + return true; + } + if (SecurityUtils.EDDSA.equalsIgnoreCase(algorithm)) { + if (key.getClass().getCanonicalName().startsWith("net.i2p.")) { + return true; + } + } + return false; + } +} diff --git a/sshd-common/src/main/java15/org/apache/sshd/common/util/security/eddsa/generic/EdDSAKeyTypeDetector.java b/sshd-common/src/main/java15/org/apache/sshd/common/util/security/eddsa/generic/EdDSAKeyTypeDetector.java new file mode 100644 index 000000000..bf6cc143d --- /dev/null +++ b/sshd-common/src/main/java15/org/apache/sshd/common/util/security/eddsa/generic/EdDSAKeyTypeDetector.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.common.util.security.eddsa.generic; + +import java.security.Key; +import java.security.interfaces.EdECKey; +import java.security.spec.NamedParameterSpec; + +import org.apache.sshd.common.util.security.SecurityUtils; + +public final class EdDSAKeyTypeDetector { + + private EdDSAKeyTypeDetector() { + throw new IllegalStateException("No instantiation"); + } + + public static boolean isEd25519(Key key) { + if (key == null) { + return false; + } + String algorithm = key.getAlgorithm(); + if (SecurityUtils.ED25519.equals(algorithm)) { + return true; + } + if (SecurityUtils.EDDSA.equalsIgnoreCase(algorithm)) { + if (key.getClass().getCanonicalName().startsWith("net.i2p.")) { + return true; + } + if (key instanceof EdECKey) { + NamedParameterSpec params = ((EdECKey) key).getParams(); + return SecurityUtils.ED25519.equals(params.getName()); + } + } + return false; + } +} diff --git a/sshd-common/src/main/java15/org/apache/sshd/common/util/security/eddsa/jce/JcePublicKeyFactory.java b/sshd-common/src/main/java15/org/apache/sshd/common/util/security/eddsa/jce/JcePublicKeyFactory.java index 4b637b1e8..06fb1b9f5 100644 --- a/sshd-common/src/main/java15/org/apache/sshd/common/util/security/eddsa/jce/JcePublicKeyFactory.java +++ b/sshd-common/src/main/java15/org/apache/sshd/common/util/security/eddsa/jce/JcePublicKeyFactory.java @@ -24,6 +24,7 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; +import java.security.interfaces.EdECKey; import java.security.spec.NamedParameterSpec; import org.apache.sshd.common.util.security.PublicKeyFactory; @@ -39,13 +40,14 @@ public class JcePublicKeyFactory implements PublicKeyFactory { @Override public PublicKey getPublicKey(PrivateKey key) { - if (SecurityUtils.ED25519.equals(key.getAlgorithm())) { - return recoverEd25519PublicKey(key); + if (SecurityUtils.EDDSA.equals(key.getAlgorithm()) && (key instanceof EdECKey)) { + NamedParameterSpec params = ((EdECKey) key).getParams(); + return recoverEd25519PublicKey(key, params); } return null; } - private static PublicKey recoverEd25519PublicKey(PrivateKey key) { + private static PublicKey recoverEd25519PublicKey(PrivateKey key, NamedParameterSpec params) { byte[] rawPrivateKey = EdDSAUtils.getBytes(key); // An EdDSA private key is just cryptographically secure random data, which the JDK implementation obtains from // the given SecureRandom. By making that random generator return not new random bytes but the bytes of the @@ -62,8 +64,8 @@ public class JcePublicKeyFactory implements PublicKeyFactory { // // Note that NamedParameterSpec was introduced in Java 11, and the ED25519 constant in Java 15. try { - KeyPairGenerator gen = KeyPairGenerator.getInstance("Ed25519"); - gen.initialize(NamedParameterSpec.ED25519, new SecureRandom() { + KeyPairGenerator gen = KeyPairGenerator.getInstance(params.getName()); + gen.initialize(params, new SecureRandom() { private static final long serialVersionUID = 1L; diff --git a/sshd-common/src/test/java/org/apache/sshd/common/config/keys/writer/openssh/OpenSSHKeyPairResourceWriterTest.java b/sshd-common/src/test/java/org/apache/sshd/common/config/keys/writer/openssh/OpenSSHKeyPairResourceWriterTest.java index 514f09f51..a123fec71 100644 --- a/sshd-common/src/test/java/org/apache/sshd/common/config/keys/writer/openssh/OpenSSHKeyPairResourceWriterTest.java +++ b/sshd-common/src/test/java/org/apache/sshd/common/config/keys/writer/openssh/OpenSSHKeyPairResourceWriterTest.java @@ -58,12 +58,12 @@ class OpenSSHKeyPairResourceWriterTest extends JUnitTestSupport { static Collection<TestData> parameters() { List<TestData> result = new ArrayList<>(); - result.add(new TestData("RSA", 1024)); - result.add(new TestData("RSA", 2048)); - result.add(new TestData("DSA", 1024)); - result.add(new TestData("ECDSA", 256)); - result.add(new TestData("ECDSA", 384)); - result.add(new TestData("ECDSA", 521)); + result.add(new TestData(KeyUtils.RSA_ALGORITHM, 1024)); + result.add(new TestData(KeyUtils.RSA_ALGORITHM, 2048)); + result.add(new TestData(KeyUtils.DSS_ALGORITHM, 1024)); + result.add(new TestData(KeyUtils.EC_ALGORITHM, 256)); + result.add(new TestData(KeyUtils.EC_ALGORITHM, 384)); + result.add(new TestData(KeyUtils.EC_ALGORITHM, 521)); if (SecurityUtils.isEDDSACurveSupported()) { result.add(new TestData(SecurityUtils.ED25519, -1)); } diff --git a/sshd-common/src/test/java/org/apache/sshd/common/util/security/eddsa/Ed25519VectorsTest.java b/sshd-common/src/test/java/org/apache/sshd/common/util/security/eddsa/Ed25519VectorsTest.java index 29ad12af8..afe1ff87e 100644 --- a/sshd-common/src/test/java/org/apache/sshd/common/util/security/eddsa/Ed25519VectorsTest.java +++ b/sshd-common/src/test/java/org/apache/sshd/common/util/security/eddsa/Ed25519VectorsTest.java @@ -77,10 +77,7 @@ class Ed25519VectorsTest extends JUnitTestSupport { if (registrar == null) { registrar = SecurityUtils.getRegisteredProvider(SecurityUtils.BOUNCY_CASTLE); } - if (registrar == null) { - throw new IllegalStateException("Neither net.i2p nor BC registered"); - } - String supportClassName = registrar.getClass().getSimpleName(); + String supportClassName = registrar != null ? registrar.getClass().getSimpleName() : "JCE"; parameters.add(new Object[] { supportClassName + " TEST1 - empty message", "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", @@ -243,6 +240,7 @@ class Ed25519VectorsTest extends JUnitTestSupport { String name, String prvKey, String pubKey, String msg, String signature) throws Exception { initEd25519VectorsTest(name, prvKey, pubKey, msg, signature); PublicKey recoveredKey = SecurityUtils.recoverEDDSAPublicKey(privateKey); + assertNotNull(recoveredKey, "Public key could not be recovered"); assertTrue(SecurityUtils.compareEDDSAPPublicKeys(publicKey, recoveredKey), "Recovered key is not equal"); byte[] recoveredBytes = EdDSAUtils.getBytes(recoveredKey); assertArrayEquals(pubBytes, recoveredBytes, "Mismatched public seed value"); diff --git a/sshd-common/src/test/java/org/apache/sshd/util/test/CommonTestSupportUtils.java b/sshd-common/src/test/java/org/apache/sshd/util/test/CommonTestSupportUtils.java index 32a209157..45ad951da 100644 --- a/sshd-common/src/test/java/org/apache/sshd/util/test/CommonTestSupportUtils.java +++ b/sshd-common/src/test/java/org/apache/sshd/util/test/CommonTestSupportUtils.java @@ -422,7 +422,7 @@ public final class CommonTestSupportUtils { throw new InvalidKeySpecException("Unknown curve for key size=" + keySize); } gen.initialize(curve.getParameters()); - } else { + } else if (!SecurityUtils.ED25519.equals(algorithm)) { gen.initialize(keySize); }
