You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by tw...@apache.org on 2025/09/21 10:52:06 UTC
(mina-sshd) 04/19: GH-585: add a PublicKeyFactory for JCE ed25519 private keys
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 <tw...@apache.org>
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);
+ }
+
+ }
+}