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

Reply via email to