This is an automated email from the ASF dual-hosted git repository.

jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git


The following commit(s) were added to refs/heads/main by this push:
     new d66ff81ea8 fixes #6909 ssh: extend test coverage
d66ff81ea8 is described below

commit d66ff81ea854384b910be4caa47b74b07dea9d01
Author: Jiri Ondrusek <[email protected]>
AuthorDate: Wed Jan 15 10:54:46 2025 +0100

    fixes #6909 ssh: extend test coverage
---
 .../component/ssh/deployment/SshProcessor.java     |  29 ++++-
 .../ssh/runtime/SubstituteEdDSAEngine.java         |  78 ++++++++++++++
 .../ssh/runtime/SubstituteSecurityUtils.java       |  65 ------------
 integration-tests/ssh/generate-certs.sh            |  50 +++++++++
 integration-tests/ssh/pom.xml                      |  27 ++++-
 .../quarkus/component/ssh/it/SshResource.java      |  93 +++++++++++++---
 .../camel/quarkus/component/ssh/it/SshRoutes.java  |  81 ++++++++++++++
 .../ssh/src/main/resources/application.properties  |  17 +++
 .../ssh/src/main/resources/edDSA/known_hosts       |   1 +
 .../camel/quarkus/component/ssh/it/SshTest.java    |  88 ++++++++++++++++
 .../quarkus/component/ssh/it/SshTestResource.java  |  64 +++++++++--
 .../component/ssh/it/TestEchoCommandFactory.java   | 117 +++++++++++++++++++++
 .../ssh/src/test/resources/edDSA/key_ed25519.pem   |   7 ++
 .../src/test/resources/edDSA/key_ed25519.pem.pub   |   1 +
 pom.xml                                            |   1 +
 15 files changed, 627 insertions(+), 92 deletions(-)

diff --git 
a/extensions/ssh/deployment/src/main/java/org/apache/camel/quarkus/component/ssh/deployment/SshProcessor.java
 
b/extensions/ssh/deployment/src/main/java/org/apache/camel/quarkus/component/ssh/deployment/SshProcessor.java
index c1ed5f65dd..7935194f56 100644
--- 
a/extensions/ssh/deployment/src/main/java/org/apache/camel/quarkus/component/ssh/deployment/SshProcessor.java
+++ 
b/extensions/ssh/deployment/src/main/java/org/apache/camel/quarkus/component/ssh/deployment/SshProcessor.java
@@ -26,14 +26,18 @@ import javax.crypto.Mac;
 
 import io.quarkus.deployment.annotations.BuildProducer;
 import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
 import io.quarkus.deployment.builditem.FeatureBuildItem;
+import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
 import 
io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem;
 import 
io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
 import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
+import net.i2p.crypto.eddsa.EdDSAEngine;
 import org.apache.sshd.common.channel.ChannelListener;
 import org.apache.sshd.common.forward.PortForwardingEventListener;
 import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory;
 import org.apache.sshd.common.session.SessionListener;
+import org.jboss.jandex.IndexView;
 
 class SshProcessor {
 
@@ -51,9 +55,10 @@ class SshProcessor {
                         KeyAgreement.class,
                         KeyFactory.class,
                         Signature.class,
-                        Mac.class).methods().build());
-        reflectiveClasses.produce(
-                
ReflectiveClassBuildItem.builder(Nio2ServiceFactoryFactory.class).build());
+                        Mac.class,
+                        Nio2ServiceFactoryFactory.class,
+                        EdDSAEngine.class,
+                        
net.i2p.crypto.eddsa.KeyFactory.class).methods().build());
     }
 
     @BuildStep
@@ -71,4 +76,22 @@ class SshProcessor {
         }
     }
 
+    @BuildStep
+    ReflectiveClassBuildItem registerForReflection(CombinedIndexBuildItem 
combinedIndex) {
+        IndexView index = combinedIndex.getIndex();
+
+        String[] dtos = index.getKnownClasses().stream()
+                .map(ci -> ci.name().toString())
+                .filter(n -> 
n.startsWith("org.bouncycastle.crypto.signers.Ed25519"))
+                .sorted()
+                .toArray(String[]::new);
+
+        return 
ReflectiveClassBuildItem.builder(dtos).methods().fields().build();
+    }
+
+    @BuildStep
+    IndexDependencyBuildItem registerDependencyForIndex2() {
+        return new IndexDependencyBuildItem("org.bouncycastle", 
"bcprov-jdk18on");
+    }
+
 }
diff --git 
a/extensions/ssh/runtime/src/main/java/org/apache/camel/quarkus/component/ssh/runtime/SubstituteEdDSAEngine.java
 
b/extensions/ssh/runtime/src/main/java/org/apache/camel/quarkus/component/ssh/runtime/SubstituteEdDSAEngine.java
new file mode 100644
index 0000000000..3054c049cc
--- /dev/null
+++ 
b/extensions/ssh/runtime/src/main/java/org/apache/camel/quarkus/component/ssh/runtime/SubstituteEdDSAEngine.java
@@ -0,0 +1,78 @@
+/*
+ * 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.camel.quarkus.component.ssh.runtime;
+
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+import com.oracle.svm.core.annotate.Alias;
+import com.oracle.svm.core.annotate.Substitute;
+import com.oracle.svm.core.annotate.TargetClass;
+import net.i2p.crypto.eddsa.EdDSAEngine;
+import net.i2p.crypto.eddsa.EdDSAKey;
+import net.i2p.crypto.eddsa.EdDSAPublicKey;
+
+/**
+ * We're substituting those offending methods that would require the presence 
of
+ * net.i2p.crypto:eddsa library which is not supported by Camel SSH component
+ */
+@TargetClass(EdDSAEngine.class)
+final class SubstituteEdDSAEngine {
+
+    @Alias
+    private MessageDigest digest;
+
+    @Alias
+    private EdDSAKey key;
+
+    @Alias
+    private void reset() {
+    }
+
+    @Substitute
+    protected void engineInitVerify(PublicKey publicKey) throws 
InvalidKeyException {
+        reset();
+        if (publicKey instanceof EdDSAPublicKey) {
+            key = (EdDSAPublicKey) publicKey;
+
+            if (digest == null) {
+                // Instantiate the digest from the key parameters
+                try {
+                    digest = 
MessageDigest.getInstance(key.getParams().getHashAlgorithm());
+                } catch (NoSuchAlgorithmException e) {
+                    throw new InvalidKeyException(
+                            "cannot get required digest " + 
key.getParams().getHashAlgorithm() + " for private key.");
+                }
+            } else if 
(!key.getParams().getHashAlgorithm().equals(digest.getAlgorithm()))
+                throw new InvalidKeyException("Key hash algorithm does not 
match chosen digest");
+        } //following line differs from the original method
+        else if (publicKey.getFormat().equals("X.509")) {
+            // X509Certificate will sometimes contain an X509Key rather than 
the EdDSAPublicKey itself; the contained
+            // key is valid but needs to be instanced as an EdDSAPublicKey 
before it can be used.
+            EdDSAPublicKey parsedPublicKey;
+            try {
+                parsedPublicKey = new EdDSAPublicKey(new 
X509EncodedKeySpec(publicKey.getEncoded()));
+            } catch (InvalidKeySpecException ex) {
+                throw new InvalidKeyException("cannot handle X.509 EdDSA 
public key: " + publicKey.getAlgorithm());
+            }
+            engineInitVerify(parsedPublicKey);
+        } else {
+            throw new InvalidKeyException("cannot identify EdDSA public key: " 
+ publicKey.getClass());
+        }
+    }
+}
diff --git 
a/extensions/ssh/runtime/src/main/java/org/apache/camel/quarkus/component/ssh/runtime/SubstituteSecurityUtils.java
 
b/extensions/ssh/runtime/src/main/java/org/apache/camel/quarkus/component/ssh/runtime/SubstituteSecurityUtils.java
deleted file mode 100644
index 2bca5bc96a..0000000000
--- 
a/extensions/ssh/runtime/src/main/java/org/apache/camel/quarkus/component/ssh/runtime/SubstituteSecurityUtils.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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.camel.quarkus.component.ssh.runtime;
-
-import java.security.GeneralSecurityException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-
-import com.oracle.svm.core.annotate.Substitute;
-import com.oracle.svm.core.annotate.TargetClass;
-import org.apache.sshd.common.util.buffer.Buffer;
-import org.apache.sshd.common.util.security.SecurityUtils;
-
-/**
- * We're substituting those offending methods that would require the presence 
of
- * net.i2p.crypto:eddsa library which is not supported by Camel SSH component
- */
-@TargetClass(SecurityUtils.class)
-final class SubstituteSecurityUtils {
-
-    @Substitute
-    public static boolean compareEDDSAPPublicKeys(PublicKey k1, PublicKey k2) {
-        throw new UnsupportedOperationException("EdDSA Signer not available");
-    }
-
-    @Substitute
-    public static boolean compareEDDSAPrivateKeys(PrivateKey k1, PrivateKey 
k2) {
-        throw new UnsupportedOperationException("EdDSA Signer not available");
-    }
-
-    @Substitute
-    public static PublicKey generateEDDSAPublicKey(String keyType, byte[] 
seed) throws GeneralSecurityException {
-        throw new UnsupportedOperationException("EdDSA Signer not available");
-    }
-
-    @Substitute
-    public static org.apache.sshd.common.signature.Signature getEDDSASigner() {
-        throw new UnsupportedOperationException("EdDSA Signer not available");
-    }
-
-    @Substitute
-    public static <B extends Buffer> B putRawEDDSAPublicKey(B buffer, 
PublicKey key) {
-        throw new UnsupportedOperationException("EdDSA Signer not available");
-    }
-
-    @Substitute
-    public static PublicKey recoverEDDSAPublicKey(PrivateKey key) throws 
GeneralSecurityException {
-        throw new UnsupportedOperationException("EdDSA Signer not available");
-    }
-
-}
diff --git a/integration-tests/ssh/generate-certs.sh 
b/integration-tests/ssh/generate-certs.sh
new file mode 100755
index 0000000000..b25265c620
--- /dev/null
+++ b/integration-tests/ssh/generate-certs.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+#
+# 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.
+#
+
+
+set -e
+set -x
+
+keyType="ed25519"
+
+destinationDir="src/main/resources/edDSA"
+testDestinationDir="src/test/resources/edDSA"
+
+# see https://stackoverflow.com/a/54924640
+export MSYS_NO_PATHCONV=1
+
+if ! [[ -x "$(command -v openssl)" ]] ; then
+  echo 'Error: openssl is not installed.' >&2
+  exit 1
+fi
+
+mkdir -p "$destinationDir"
+mkdir -p "$testDestinationDir"
+
+# Ed25519  private key
+#openssl genpkey -algorithm ed25519   -out "$destinationDir/key_ed25519.pem"
+ssh-keygen -t $keyType -o -a 100 -N "" -f 
"$testDestinationDir/key_ed25519.pem" -C "test@localhost"
+
+# Ed25519  public key
+#openssl pkey -in "$destinationDir/key_ed25519.pem" -pubout -out 
"$destinationDir/key_ed25519.pem.pub"
+ssh-keygen -y -f "$testDestinationDir/key_ed25519.pem" > 
"$testDestinationDir/key_ed25519.pem.pub"
+
+#generate known-hosts
+echo -n "127.0.0.1 $(sed 's/\(.*\) \([^ ]*\)$/\1/' 
"$testDestinationDir/key_ed25519.pem.pub")" >> "$destinationDir/known_hosts"
+
+
diff --git a/integration-tests/ssh/pom.xml b/integration-tests/ssh/pom.xml
index 42b639eb61..9f03ec2cd1 100644
--- a/integration-tests/ssh/pom.xml
+++ b/integration-tests/ssh/pom.xml
@@ -35,10 +35,18 @@
             <groupId>org.apache.camel.quarkus</groupId>
             <artifactId>camel-quarkus-ssh</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-direct</artifactId>
+        </dependency>
         <dependency>
             <groupId>io.quarkus</groupId>
             <artifactId>quarkus-resteasy</artifactId>
         </dependency>
+        <dependency>
+            <groupId>io.quarkus</groupId>
+            <artifactId>quarkus-resteasy-jackson</artifactId>
+        </dependency>
 
         <!-- test dependencies -->
         <dependency>
@@ -67,9 +75,13 @@
             <artifactId>quarkus-junit4-mock</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            
<artifactId>camel-quarkus-integration-tests-support-certificate-generator</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
-
     <profiles>
         <profile>
             <id>native</id>
@@ -120,6 +132,19 @@
                         </exclusion>
                     </exclusions>
                 </dependency>
+                <dependency>
+                    <groupId>org.apache.camel.quarkus</groupId>
+                    <artifactId>camel-quarkus-direct-deployment</artifactId>
+                    <version>${project.version}</version>
+                    <type>pom</type>
+                    <scope>test</scope>
+                    <exclusions>
+                        <exclusion>
+                            <groupId>*</groupId>
+                            <artifactId>*</artifactId>
+                        </exclusion>
+                    </exclusions>
+                </dependency>
             </dependencies>
         </profile>
         <profile>
diff --git 
a/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshResource.java
 
b/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshResource.java
index 858666f740..2b316d4e0d 100644
--- 
a/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshResource.java
+++ 
b/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshResource.java
@@ -16,19 +16,22 @@
  */
 package org.apache.camel.quarkus.component.ssh.it;
 
+import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
-import jakarta.ws.rs.Consumes;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.*;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.Response;
+import org.apache.camel.CamelContext;
+import org.apache.camel.ConsumerTemplate;
+import org.apache.camel.Exchange;
 import org.apache.camel.ProducerTemplate;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 
@@ -36,17 +39,32 @@ import 
org.eclipse.microprofile.config.inject.ConfigProperty;
 @ApplicationScoped
 public class SshResource {
 
-    private final String user = "test";
-    private final String password = "password";
+    public enum ServerType {
+        userPassword, user01Key, edKey
+    };
 
     @ConfigProperty(name = "quarkus.ssh.host")
-    private String host;
+    String host;
     @ConfigProperty(name = "quarkus.ssh.port")
-    private String port;
+    String port;
+    @ConfigProperty(name = "quarkus.ssh.secured-port")
+    String securedPort;
+    @ConfigProperty(name = "quarkus.ssh.ed-port")
+    String edPort;
+    @ConfigProperty(name = "ssh.username")
+    String username;
+    @ConfigProperty(name = "ssh.password")
+    String password;
+
+    @Inject
+    CamelContext camelContext;
 
     @Inject
     ProducerTemplate producerTemplate;
 
+    @Inject
+    ConsumerTemplate consumerTemplate;
+
     @POST
     @Path("/file/{fileName}")
     @Consumes(MediaType.TEXT_PLAIN)
@@ -55,7 +73,7 @@ public class SshResource {
 
         String sshWriteFileCommand = String.format("printf \"%s\" > %s", 
content, fileName);
         producerTemplate.sendBody(
-                String.format("ssh:%s:%s?username=%s&password=%s", host, port, 
user, password),
+                String.format("ssh:%s:%s?username=%s&password=%s", host, port, 
username, password),
                 sshWriteFileCommand);
 
         return Response
@@ -69,13 +87,60 @@ public class SshResource {
     public Response readFile(@PathParam("fileName") String fileName) throws 
URISyntaxException {
 
         String sshReadFileCommand = String.format("cat %s", fileName);
-        String content = producerTemplate.requestBody(
-                String.format("ssh:%s:%s?username=%s&password=%s", host, port, 
user, password),
-                sshReadFileCommand,
+        String content = consumerTemplate.receiveBody(
+                
String.format("ssh:%s:%s?username=%s&password=%s&pollCommand=%s", host, port, 
username, password,
+                        sshReadFileCommand),
                 String.class);
 
         return Response
                 .ok(content)
                 .build();
     }
+
+    @POST
+    @Path("/send")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Map<String, String> send(@QueryParam("command") String command,
+            @QueryParam("component") @DefaultValue("ssh") String component,
+            @QueryParam("serverType") @DefaultValue("userPassword") String 
serverType,
+            @QueryParam("pathSuffix") String pathSuffix,
+            Map<String, Object> headers)
+            throws URISyntaxException {
+
+        var port = switch (ServerType.valueOf(serverType)) {
+        case userPassword -> this.port;
+        case edKey -> edPort;
+        case user01Key -> securedPort;
+        };
+
+        String url = String.format("%s:%s@%s:%s", component, username, host, 
port);
+        if (pathSuffix != null) {
+            url += "?" + pathSuffix;
+        }
+        Exchange exchange = producerTemplate.request(url,
+                e -> {
+                    e.getIn().setHeaders(headers == null ? 
Collections.emptyMap() : headers);
+                    e.getIn().setBody(command == null ? "" : command);
+                });
+
+        Map<String, String> result = new HashMap<>();
+        result.put("body", exchange.getMessage().getBody(String.class));
+        result.putAll(exchange.getMessage().getHeaders().entrySet()
+                .stream()
+                .collect(Collectors.toMap(
+                        Map.Entry::getKey,
+                        //convert inputStreams
+                        entry -> String.valueOf(entry.getValue() instanceof 
InputStream
+                                ? 
camelContext.getTypeConverter().convertTo(String.class, entry.getValue())
+                                : entry.getValue()))));
+
+        return result;
+    }
+
+    @Path("/sendToDirect/{direct}")
+    @POST
+    public String sendToDirect(@PathParam("direct") String direct, String 
body) throws Exception {
+        return producerTemplate.requestBody("direct:" + direct, body, 
String.class).trim();
+    }
+
 }
diff --git 
a/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshRoutes.java
 
b/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshRoutes.java
new file mode 100644
index 0000000000..569c103cb5
--- /dev/null
+++ 
b/integration-tests/ssh/src/main/java/org/apache/camel/quarkus/component/ssh/it/SshRoutes.java
@@ -0,0 +1,81 @@
+/*
+ * 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.camel.quarkus.component.ssh.it;
+
+import java.nio.file.Paths;
+import java.security.Security;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Named;
+import net.i2p.crypto.eddsa.EdDSASecurityProvider;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.ssh.SshComponent;
+import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+@ApplicationScoped
+public class SshRoutes extends RouteBuilder {
+
+    @ConfigProperty(name = "quarkus.ssh.host")
+    String host;
+    @ConfigProperty(name = "quarkus.ssh.port")
+    String port;
+    @ConfigProperty(name = "ssh.username")
+    String username;
+    @ConfigProperty(name = "ssh.password")
+    String password;
+
+    @PostConstruct
+    public void init() {
+        Security.addProvider(new EdDSASecurityProvider());
+    }
+
+    @Override
+    public void configure() throws Exception {
+        // Route without SSL
+        from("direct:exampleProducer")
+                .toF("ssh://%s:%s@%s:%s", username, password, host, port);
+
+    }
+
+    /**
+     * We need to implement some conditional configuration of the {@link 
SshComponent} thus we create it
+     * programmatically and publish via CDI.
+     *
+     * @return a configured {@link SshComponent}
+     */
+    @Named("ssh-with-key-provider")
+    SshComponent sshWithKeyProvider() throws IllegalAccessException, 
NoSuchFieldException, InstantiationException {
+        final SshComponent sshComponent = new SshComponent();
+        sshComponent.setCamelContext(getContext());
+        sshComponent.getConfiguration()
+                .setKeyPairProvider(new 
FileKeyPairProvider(Paths.get("target/certs/user01.key")));
+        sshComponent.getConfiguration().setKeyType(KeyPairProvider.SSH_RSA);
+        return sshComponent;
+    }
+
+    @Named("ssh-cert")
+    SshComponent sshCert() throws IllegalAccessException, 
NoSuchFieldException, InstantiationException {
+        final SshComponent sshComponent = new SshComponent();
+        sshComponent.setCamelContext(getContext());
+        sshComponent.getConfiguration().setKeyType(null);
+        return sshComponent;
+    }
+
+}
diff --git a/integration-tests/ssh/src/main/resources/application.properties 
b/integration-tests/ssh/src/main/resources/application.properties
new file mode 100644
index 0000000000..f4421ffda3
--- /dev/null
+++ b/integration-tests/ssh/src/main/resources/application.properties
@@ -0,0 +1,17 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+quarkus.native.resources.includes=edDSA/known_hosts
diff --git a/integration-tests/ssh/src/main/resources/edDSA/known_hosts 
b/integration-tests/ssh/src/main/resources/edDSA/known_hosts
new file mode 100644
index 0000000000..97799187d0
--- /dev/null
+++ b/integration-tests/ssh/src/main/resources/edDSA/known_hosts
@@ -0,0 +1 @@
+127.0.0.1 ssh-ed25519 
AAAAC3NzaC1lZDI1NTE5AAAAIAZTZxr7m1h9J0LJ4eyVYVqjlaL7wGE5rT0w3TGjC7dQ
\ No newline at end of file
diff --git 
a/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTest.java
 
b/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTest.java
index 4681ee0a94..dd79848030 100644
--- 
a/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTest.java
+++ 
b/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTest.java
@@ -16,14 +16,27 @@
  */
 package org.apache.camel.quarkus.component.ssh.it;
 
+import java.util.Map;
+
 import io.quarkus.test.common.QuarkusTestResource;
 import io.quarkus.test.junit.QuarkusTest;
 import io.restassured.RestAssured;
 import io.restassured.http.ContentType;
+import io.smallrye.certs.Format;
+import io.smallrye.certs.junit5.Certificate;
+import org.apache.camel.component.ssh.SshConstants;
+import org.apache.camel.quarkus.test.DisabledIfFipsMode;
+import org.apache.camel.quarkus.test.support.certificate.TestCertificates;
+import org.hamcrest.Matchers;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
+@TestCertificates(certificates = {
+        @Certificate(name = "user01", formats = {
+                Format.PEM }, password = "changeit"),
+        @Certificate(name = "eddsa", formats = {
+                Format.PEM }, password = "changeit") })
 @QuarkusTest
 @QuarkusTestResource(SshTestResource.class)
 class SshTest {
@@ -50,4 +63,79 @@ class SshTest {
         assertEquals(fileContent, sshFileContent);
     }
 
+    @Test
+    public void testHeaders() {
+        RestAssured.given()
+                .contentType(ContentType.JSON)
+                .body(Map.of(SshConstants.USERNAME_HEADER, 
SshTestResource.USERNAME, SshConstants.PASSWORD_HEADER,
+                        SshTestResource.PASSWORD))
+                .queryParam("command", "wrong")
+                .post("/ssh/send/")
+                .then()
+                .statusCode(200)
+                .body("", Matchers.hasEntry(SshConstants.EXIT_VALUE, "127"))
+                .body("", 
Matchers.hasEntry(Matchers.matchesRegex(SshConstants.STDERR),
+                        Matchers.containsString("command not found")));
+    }
+
+    @Test
+    public void testProducerInRoute() {
+        RestAssured.given()
+                .body("echo Hello World")
+                .post("/ssh/sendToDirect/exampleProducer")
+                .then()
+                .statusCode(200)
+                .body(Matchers.equalTo("Hello World"));
+    }
+
+    @Test
+    public void testKeyProvider() {
+        RestAssured.given()
+                .contentType(ContentType.JSON)
+                .queryParam("component", "ssh-with-key-provider")
+                .queryParam("command", "echo test")
+                .queryParam("serverType", "user01Key")
+                .post("/ssh/send")
+                .then()
+                .statusCode(200)
+                .body("", Matchers.hasEntry(SshConstants.EXIT_VALUE, "0"))
+                //expecting error from command factory
+                .body("", Matchers.hasEntry(SshConstants.STDERR, "Expected 
Error:echo test"));
+    }
+
+    @Test
+    public void testCertificate() {
+        RestAssured.given()
+                .contentType(ContentType.JSON)
+                .queryParam("component", "ssh-cert")
+                .queryParam("command", "echo test")
+                .queryParam("serverType", "user01Key")
+                .queryParam("pathSuffix", 
"certResource=file:target/certs/user01.key&certResourcePassword=changeit")
+                .post("/ssh/send")
+                .then()
+                .statusCode(200)
+                .body("", Matchers.hasEntry(SshConstants.EXIT_VALUE, "0"))
+                //expecting error from command factory
+                .body("", Matchers.hasEntry(SshConstants.STDERR, "Expected 
Error:echo test"));
+    }
+
+    @DisabledIfFipsMode //ED25519 keys are not allowed in FIPS mode
+    @Test
+    public void testProducerWithEdDSAKeyType() {
+        RestAssured.given()
+                .contentType(ContentType.JSON)
+                .queryParam("command", "echo Hello!")
+                .queryParam("serverType", "edKey")
+                .queryParam("pathSuffix",
+                        
"timeout=3000&knownHostsResource=/edDSA/known_hosts&failOnUnknownHost=true")
+                .body(Map.of(SshConstants.USERNAME_HEADER, 
SshTestResource.USERNAME, SshConstants.PASSWORD_HEADER,
+                        SshTestResource.PASSWORD))
+                .post("/ssh/send")
+                .then()
+                .statusCode(200)
+                .body("", Matchers.hasEntry(SshConstants.EXIT_VALUE, "0"))
+                //expecting error from command factory
+                .body("", Matchers.hasEntry(SshConstants.STDERR, "Expected 
Error:echo Hello!"));
+    }
+
 }
diff --git 
a/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTestResource.java
 
b/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTestResource.java
index 56db5ec472..186d1eb434 100644
--- 
a/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTestResource.java
+++ 
b/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/SshTestResource.java
@@ -16,10 +16,15 @@
  */
 package org.apache.camel.quarkus.component.ssh.it;
 
+import java.nio.file.Paths;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 
 import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
-import org.apache.camel.util.CollectionHelper;
+import org.apache.camel.quarkus.test.AvailablePortFinder;
+import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
+import org.apache.sshd.server.SshServer;
 import org.eclipse.microprofile.config.ConfigProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -33,8 +38,12 @@ public class SshTestResource implements 
QuarkusTestResourceLifecycleManager {
     private static final int SSH_PORT = 2222;
     private static final String SSH_IMAGE = 
ConfigProvider.getConfig().getValue("openssh-server.container.image",
             String.class);
+    static final String USERNAME = "user01";
+    static final String PASSWORD = "changeit";
 
     private GenericContainer container;
+    protected List<SshServer> sshds = new LinkedList<>();
+    protected int securedPort, edPort;
 
     @Override
     public Map<String, String> start() {
@@ -45,8 +54,8 @@ public class SshTestResource implements 
QuarkusTestResourceLifecycleManager {
             container = new GenericContainer(SSH_IMAGE)
                     .withExposedPorts(SSH_PORT)
                     .withEnv("PASSWORD_ACCESS", "true")
-                    .withEnv("USER_NAME", "test")
-                    .withEnv("USER_PASSWORD", "password")
+                    .withEnv("USER_NAME", USERNAME)
+                    .withEnv("USER_PASSWORD", PASSWORD)
                     .waitingFor(Wait.forListeningPort());
 
             container.start();
@@ -54,11 +63,41 @@ public class SshTestResource implements 
QuarkusTestResourceLifecycleManager {
             LOGGER.info("Started SSH container to {}:{}", container.getHost(),
                     container.getMappedPort(SSH_PORT).toString());
 
-            return CollectionHelper.mapOf(
-                    "quarkus.ssh.host",
-                    container.getHost(),
-                    "quarkus.ssh.port",
-                    container.getMappedPort(SSH_PORT).toString());
+            securedPort = AvailablePortFinder.getNextAvailable();
+
+            var sshd = SshServer.setUpDefaultServer();
+            sshd.setPort(securedPort);
+            sshd.setKeyPairProvider(new 
FileKeyPairProvider(Paths.get("target/certs/user01.key")));
+            sshd.setCommandFactory(new TestEchoCommandFactory());
+            sshd.setPasswordAuthenticator((username, password, session) -> 
true);
+            sshd.setPublickeyAuthenticator((username, key, session) -> true);
+            sshd.start();
+
+            sshds.add(sshd);
+
+            edPort = AvailablePortFinder.getNextAvailable();
+
+            sshd = SshServer.setUpDefaultServer();
+            sshd.setPort(edPort);
+            sshd.setKeyPairProvider(new FileKeyPairProvider(
+                    
Paths.get(Thread.currentThread().getContextClassLoader().getResource("edDSA/key_ed25519.pem").toURI())));
+            sshd.setCommandFactory(new TestEchoCommandFactory());
+            sshd.setPasswordAuthenticator((username, password, session) -> 
true);
+            sshd.setPublickeyAuthenticator((username, key, session) -> true);
+            sshd.start();
+
+            sshds.add(sshd);
+
+            LOGGER.info("Started SSHD server to {}:{}", container.getHost(),
+                    securedPort);
+
+            return Map.of(
+                    "quarkus.ssh.host", "localhost",
+                    "quarkus.ssh.port", 
container.getMappedPort(SSH_PORT).toString(),
+                    "quarkus.ssh.secured-port", securedPort + "",
+                    "quarkus.ssh.ed-port", edPort + "",
+                    "ssh.username", USERNAME,
+                    "ssh.password", PASSWORD);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
@@ -66,12 +105,19 @@ public class SshTestResource implements 
QuarkusTestResourceLifecycleManager {
 
     @Override
     public void stop() {
-        LOGGER.info("Stopping SSH container");
+        LOGGER.info("Stopping SSH container and servers");
 
         try {
             if (container != null) {
                 container.stop();
             }
+            sshds.forEach(s -> {
+                try {
+                    s.stop(true);
+                } catch (Exception e) {
+                    // ignored
+                }
+            });
         } catch (Exception e) {
             // ignored
         }
diff --git 
a/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/TestEchoCommandFactory.java
 
b/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/TestEchoCommandFactory.java
new file mode 100644
index 0000000000..26af147266
--- /dev/null
+++ 
b/integration-tests/ssh/src/test/java/org/apache/camel/quarkus/component/ssh/it/TestEchoCommandFactory.java
@@ -0,0 +1,117 @@
+/*
+ * 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.camel.quarkus.component.ssh.it;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.CountDownLatch;
+
+import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.command.Command;
+import org.apache.sshd.server.command.CommandFactory;
+
+public class TestEchoCommandFactory implements CommandFactory {
+
+    @Override
+    public Command createCommand(ChannelSession channelSession, String 
command) {
+        return new TestEchoCommand(command);
+    }
+
+    public static class TestEchoCommand extends EchoCommand {
+        public static CountDownLatch latch = new CountDownLatch(1);
+
+        public TestEchoCommand(String command) {
+            super(command);
+        }
+
+        @Override
+        public void destroy(ChannelSession channelSession) throws Exception {
+            if (latch != null) {
+                latch.countDown();
+            }
+            super.destroy(channelSession);
+        }
+    }
+
+    protected static class EchoCommand implements Command, Runnable {
+        private String command;
+        private OutputStream out;
+        private OutputStream err;
+        private ExitCallback callback;
+        private Thread thread;
+
+        public EchoCommand(String command) {
+            this.command = command;
+        }
+
+        @Override
+        public void setInputStream(InputStream in) {
+        }
+
+        @Override
+        public void setOutputStream(OutputStream out) {
+            this.out = out;
+        }
+
+        @Override
+        public void setErrorStream(OutputStream err) {
+            this.err = err;
+        }
+
+        @Override
+        public void setExitCallback(ExitCallback callback) {
+            this.callback = callback;
+        }
+
+        @Override
+        public void start(ChannelSession channelSession, Environment 
environment) throws IOException {
+            thread = new Thread(this, "EchoCommand");
+            thread.start();
+        }
+
+        @Override
+        public void destroy(ChannelSession channelSession) throws Exception {
+            // noop
+        }
+
+        @Override
+        public void run() {
+            boolean succeeded = true;
+            String message = null;
+            try {
+                // we set the error with the same command message
+                err.write("Expected Error:".getBytes());
+                err.write(command.getBytes());
+                err.flush();
+                out.write(command.getBytes());
+                out.flush();
+            } catch (Exception e) {
+                succeeded = false;
+                message = e.toString();
+            } finally {
+                if (succeeded) {
+                    callback.onExit(0);
+                } else {
+                    callback.onExit(1, message);
+                }
+            }
+        }
+    }
+}
diff --git a/integration-tests/ssh/src/test/resources/edDSA/key_ed25519.pem 
b/integration-tests/ssh/src/test/resources/edDSA/key_ed25519.pem
new file mode 100644
index 0000000000..9d29f1c2ec
--- /dev/null
+++ b/integration-tests/ssh/src/test/resources/edDSA/key_ed25519.pem
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAGU2ca+5tYfSdCyeHslWFao5Wi+8BhOa09MN0xowu3UAAAAJjAekuswHpL
+rAAAAAtzc2gtZWQyNTUxOQAAACAGU2ca+5tYfSdCyeHslWFao5Wi+8BhOa09MN0xowu3UA
+AAAEA9x2k0q80wlvdiEsXoPCYMSFcD1qQu8IPHlwhCyYBEEgZTZxr7m1h9J0LJ4eyVYVqj
+laL7wGE5rT0w3TGjC7dQAAAADnRlc3RAbG9jYWxob3N0AQIDBAUGBw==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/integration-tests/ssh/src/test/resources/edDSA/key_ed25519.pem.pub 
b/integration-tests/ssh/src/test/resources/edDSA/key_ed25519.pem.pub
new file mode 100644
index 0000000000..5463e28814
--- /dev/null
+++ b/integration-tests/ssh/src/test/resources/edDSA/key_ed25519.pem.pub
@@ -0,0 +1 @@
+ssh-ed25519 
AAAAC3NzaC1lZDI1NTE5AAAAIAZTZxr7m1h9J0LJ4eyVYVqjlaL7wGE5rT0w3TGjC7dQ 
test@localhost
diff --git a/pom.xml b/pom.xml
index 58f78ed57f..a7a80fd3c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -604,6 +604,7 @@
                                     <exclude>**/my-property</exclude>
                                     <exclude>release-utils/*.sh</exclude>
                                     <exclude>**/*.wasm</exclude>
+                                    <exclude>**/known_hosts</exclude>
                                 </excludes>
                             </licenseSet>
                         </licenseSets>


Reply via email to