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 c36e5a57b000eceb9bc9771878992e31e9c4f23c Author: Thomas Wolf <[email protected]> AuthorDate: Sat Sep 13 13:16:01 2025 +0200 GH-585: add a PublicKeyFactory for JCE ed25519 private keys With this commit, sshd-common becomes a multirelease JAR, and so does sshd-osgi. On Java 15+, we can actually use the JDK's own ed25519 implementation, so we also need a way to recover a public key given only a private key. This recovery operation is actually only mandatory for reading an ed25519 key pair from a PKCS#8 PEM private key. For reading key pairs from an OpenSSH private key file, it's optional, since the OpenSSH format contains both the private and the public key. The MR-JAR setup requires dropping the source and target compiler options. Use only and exclusively "release" now. (We can do so since we require Java 17 for building anyway.) --- pom.xml | 48 +++--------- sshd-common/pom.xml | 19 +++++ .../sshd/common/util/security/SecurityUtils.java | 3 +- .../security/eddsa/jce/JcePublicKeyFactory.java | 37 +++++++++ .../security/eddsa/jce/JcePublicKeyFactory.java | 87 ++++++++++++++++++++++ 5 files changed, 156 insertions(+), 38 deletions(-) diff --git a/pom.xml b/pom.xml index cdc403756..894851ca6 100644 --- a/pom.xml +++ b/pom.xml @@ -84,18 +84,12 @@ <project.build.outputTimestamp>2025-08-29T21:59:28Z</project.build.outputTimestamp> <java.sdk.version>8</java.sdk.version> - <javac.source>${java.sdk.version}</javac.source> - <project.build.java.source>${javac.source}</project.build.java.source> - <maven.compiler.source>${javac.source}</maven.compiler.source> + <javac.release>${java.sdk.version}</javac.release> + <maven.compiler.release>${java.sdk.version}</maven.compiler.release> <ant.version>1.10.15</ant.version> - <ant.build.javac.source>${javac.source}</ant.build.javac.source> <build-helper-maven-plugin.version>3.6.1</build-helper-maven-plugin.version> - <javac.target>${javac.source}</javac.target> - <required.java.version>[${javac.target},)</required.java.version> - <project.build.java.target>${javac.target}</project.build.java.target> - <maven.compiler.target>${javac.target}</maven.compiler.target> - <ant.build.javac.target>${javac.target}</ant.build.javac.target> + <required.java.version>[${java.sdk.version},)</required.java.version> <groovy.version>4.0.17</groovy.version> <bouncycastle.version>1.81</bouncycastle.version> @@ -266,41 +260,23 @@ <showWarnings>true</showWarnings> </configuration> <dependencies> - <dependency> - <groupId>org.codehaus.plexus</groupId> + <dependency> + <groupId>org.codehaus.plexus</groupId> <artifactId>plexus-compiler-javac-errorprone</artifactId> - <version>2.15.0</version> - </dependency> - <!-- override plexus-compiler-javac-errorprone's dependency on Error Prone with the latest version --> - <dependency> + <version>2.15.0</version> + </dependency> + <!-- override plexus-compiler-javac-errorprone's dependency on Error Prone with the latest version --> + <dependency> <groupId>com.google.errorprone</groupId> <artifactId>error_prone_core</artifactId> - <version>2.41.0</version> + <version>2.41.0</version> </dependency> - </dependencies> + </dependencies> </plugin> </plugins> </build> </profile> - <profile> - <id>javac-jdk9+</id> - <activation> - <jdk>[9,)</jdk> - </activation> - <build> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-compiler-plugin</artifactId> - <configuration> - <release>${java.sdk.version}</release> - </configuration> - </plugin> - </plugins> - </build> - </profile> - <profile> <id>only-eclipse</id> <activation> @@ -1176,8 +1152,6 @@ <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> - <source>${javac.source}</source> - <target>${javac.target}</target> <compilerArgument>-g</compilerArgument> <!-- see http://www.javaworld.com/article/2073587/javac-s(dashdash)xlint-options.html --> <compilerArgument>-Xlint:-serial</compilerArgument> diff --git a/sshd-common/pom.xml b/sshd-common/pom.xml index b4f95fb18..d27a26add 100644 --- a/sshd-common/pom.xml +++ b/sshd-common/pom.xml @@ -190,6 +190,25 @@ <reportsDirectory>${project.build.directory}/surefire-reports-common</reportsDirectory> </configuration> </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <executions> + <execution> + <id>default-compile-15</id> + <goals> + <goal>compile</goal> + </goals> + <configuration> + <compileSourceRoots> + <compileSourceRoot>${project.basedir}/src/main/java15</compileSourceRoot> + </compileSourceRoots> + <multiReleaseOutput>true</multiReleaseOutput> + <release>15</release> + </configuration> + </execution> + </executions> + </plugin> </plugins> </build> </project> 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 891a0e012..e9eff3a6f 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 @@ -76,6 +76,7 @@ import org.apache.sshd.common.util.security.bouncycastle.BouncyCastleKeyPairReso import org.apache.sshd.common.util.security.bouncycastle.BouncyCastleRandomFactory; import org.apache.sshd.common.util.security.eddsa.generic.EdDSAUtils; import org.apache.sshd.common.util.security.eddsa.generic.OpenSSHEd25519PrivateKeyEntryDecoder; +import org.apache.sshd.common.util.security.eddsa.jce.JcePublicKeyFactory; import org.apache.sshd.common.util.threads.ThreadUtils; import org.apache.sshd.server.keyprovider.AbstractGeneratorHostKeyProvider; import org.slf4j.Logger; @@ -601,7 +602,7 @@ public final class SecurityUtils { } } } - return null; + return new JcePublicKeyFactory().getPublicKey(key); } public static KeyPair extractEDDSAKeyPair(Buffer buffer, String keyType) throws GeneralSecurityException { diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/jce/JcePublicKeyFactory.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/jce/JcePublicKeyFactory.java new file mode 100644 index 000000000..6b96e13df --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/jce/JcePublicKeyFactory.java @@ -0,0 +1,37 @@ +/* + * 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.jce; + +import java.security.PrivateKey; +import java.security.PublicKey; + +import org.apache.sshd.common.util.security.PublicKeyFactory; + +public class JcePublicKeyFactory implements PublicKeyFactory { + + public JcePublicKeyFactory() { + super(); + } + + @Override + public PublicKey getPublicKey(PrivateKey key) { + return null; + } + +} 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 new file mode 100644 index 000000000..4b637b1e8 --- /dev/null +++ b/sshd-common/src/main/java15/org/apache/sshd/common/util/security/eddsa/jce/JcePublicKeyFactory.java @@ -0,0 +1,87 @@ +/* + * 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.jce; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.NamedParameterSpec; + +import org.apache.sshd.common.util.security.PublicKeyFactory; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.apache.sshd.common.util.security.eddsa.generic.EdDSAUtils; +import org.bouncycastle.util.Arrays; + +public class JcePublicKeyFactory implements PublicKeyFactory { + + public JcePublicKeyFactory() { + super(); + } + + @Override + public PublicKey getPublicKey(PrivateKey key) { + if (SecurityUtils.ED25519.equals(key.getAlgorithm())) { + return recoverEd25519PublicKey(key); + } + return null; + } + + private static PublicKey recoverEd25519PublicKey(PrivateKey key) { + 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 + // existing private key, the library will then compute the public key for this private key. + // + // This relies on the library using the returned "random" value as-is for the private key. Theoretically a + // library would be free to process these random bytes in any way it wants before setting it as private key. + // Since any such post-processing of the random value runs the risk of introducing weaknesses through + // inadvertently loosing some randomness properties of the originally cryptographically secure random data, it + // is unlikely that an EdDSA implementation would do so, and in any case the JDK implementation doesn't. + // + // All this is just a hack to work around sun.security.ec.ed.EdDSAOperations.computePublic() not being + // accessible. + // + // 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() { + + private static final long serialVersionUID = 1L; + + @Override + public void nextBytes(byte[] bytes) { + if (bytes.length != rawPrivateKey.length) { + throw new IllegalStateException( + "Wrong array length requested; expected " + rawPrivateKey.length + " but got " + bytes.length); + } + System.arraycopy(rawPrivateKey, 0, bytes, 0, rawPrivateKey.length); + } + }); + return gen.generateKeyPair().getPublic(); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { + return null; + } finally { + Arrays.fill(rawPrivateKey, (byte) 0); + } + + } +}
