This is an automated email from the ASF dual-hosted git repository. gansheer pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit 192797e6dd88f7cd8b5b6b6040f3733359cac2c1 Author: Gaelle Fournier <[email protected]> AuthorDate: Thu Apr 16 16:00:11 2026 +0200 feat: Add Camel SFTP certificate based authentication --- .../test/support/sftp/SftpTestResource.java | 25 +++- .../quarkus/component/sftp/it/SftpResource.java | 138 +++++++++++++++++++++ .../ftp/src/main/resources/application.properties | 2 + .../main/resources/certs/generate-certificates.sh | 66 ++++++++++ .../src/main/resources/certs/test-key-rsa-cert.pub | 1 + .../ftp/src/main/resources/certs/test-key-rsa.key | 27 ++++ .../camel/quarkus/component/sftp/it/SftpTest.java | 76 ++++++++++++ 7 files changed, 334 insertions(+), 1 deletion(-) diff --git a/integration-tests-support/sftp/src/main/java/org/apache/camel/quarkus/test/support/sftp/SftpTestResource.java b/integration-tests-support/sftp/src/main/java/org/apache/camel/quarkus/test/support/sftp/SftpTestResource.java index 1a1994d1c6..784daf8293 100644 --- a/integration-tests-support/sftp/src/main/java/org/apache/camel/quarkus/test/support/sftp/SftpTestResource.java +++ b/integration-tests-support/sftp/src/main/java/org/apache/camel/quarkus/test/support/sftp/SftpTestResource.java @@ -23,13 +23,18 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.Comparator; +import java.util.List; import java.util.Map; import java.util.stream.Stream; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; import org.apache.camel.quarkus.test.AvailablePortFinder; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.config.keys.OpenSshCertificate; import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; import org.apache.sshd.common.keyprovider.FileKeyPairProvider; +import org.apache.sshd.common.signature.BuiltinSignatures; +import org.apache.sshd.common.signature.Signature; import org.apache.sshd.scp.server.ScpCommandFactory; import org.apache.sshd.server.SshServer; import org.apache.sshd.sftp.server.SftpSubsystemFactory; @@ -68,7 +73,25 @@ public class SftpTestResource implements QuarkusTestResourceLifecycleManager { sshServer.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); sshServer.setCommandFactory(new ScpCommandFactory()); sshServer.setPasswordAuthenticator((username, password, session) -> true); - sshServer.setPublickeyAuthenticator((username, key, session) -> true); + + // Accept both regular public keys and OpenSSH certificates + sshServer.setPublickeyAuthenticator((username, key, session) -> { + if (key instanceof OpenSshCertificate) { + LOGGER.debug("Accepting OpenSSH certificate authentication for user '" + username + "'"); + } else { + LOGGER.debug("Accepting public key authentication for user '" + username + "'"); + } + return true; + }); + + // Add certificate signature factories to support OpenSSH certificates + List<NamedFactory<Signature>> signatureFactories = sshServer.getSignatureFactories(); + signatureFactories.add(BuiltinSignatures.rsa_cert); + signatureFactories.add(BuiltinSignatures.rsaSHA256_cert); + signatureFactories.add(BuiltinSignatures.rsaSHA512_cert); + signatureFactories.add(BuiltinSignatures.ed25519_cert); + sshServer.setSignatureFactories(signatureFactories); + sshServer.setFileSystemFactory(factory); sshServer.start(); diff --git a/integration-tests/ftp/src/main/java/org/apache/camel/quarkus/component/sftp/it/SftpResource.java b/integration-tests/ftp/src/main/java/org/apache/camel/quarkus/component/sftp/it/SftpResource.java index a87d8a358a..14de2c50c5 100644 --- a/integration-tests/ftp/src/main/java/org/apache/camel/quarkus/component/sftp/it/SftpResource.java +++ b/integration-tests/ftp/src/main/java/org/apache/camel/quarkus/component/sftp/it/SftpResource.java @@ -16,6 +16,7 @@ */ package org.apache.camel.quarkus.component.sftp.it; +import java.io.InputStream; import java.net.URI; import jakarta.enterprise.context.ApplicationScoped; @@ -30,9 +31,12 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; 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.apache.camel.component.file.remote.SftpConfiguration; +import org.apache.camel.component.file.remote.SftpEndpoint; @Path("/sftp") @ApplicationScoped @@ -40,6 +44,9 @@ public class SftpResource { private static final long TIMEOUT_MS = 1000; + @Inject + CamelContext context; + @Inject ProducerTemplate producerTemplate; @@ -87,4 +94,135 @@ public class SftpResource { TIMEOUT_MS, String.class); } + + @Path("/certificate/create/{fileName}") + @POST + @Consumes(MediaType.TEXT_PLAIN) + public Response createFileWithCertificate(@PathParam("fileName") String fileName, String fileContent) + throws Exception { + producerTemplate.sendBodyAndHeader( + "sftp://admin@localhost:{{camel.sftp.test-port}}/sftp?privateKeyUri=certs/test-key-rsa.key&certUri=certs/test-key-rsa-cert.pub", + fileContent, + Exchange.FILE_NAME, fileName); + return Response + .created(new URI("https://camel.apache.org/")) + .build(); + } + + @Path("/certificate/get/{fileName}") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getFileWithCertificate(@PathParam("fileName") String fileName) { + return consumerTemplate.receiveBody( + "sftp://admin@localhost:{{camel.sftp.test-port}}/sftp?privateKeyUri=certs/test-key-rsa.key&certUri=certs/test-key-rsa-cert.pub&localWorkDirectory=target&fileName=" + + fileName, + TIMEOUT_MS, + String.class); + } + + @Path("/certificateFile/create/{fileName}") + @POST + @Consumes(MediaType.TEXT_PLAIN) + public Response createFileWithCertificateFile(@PathParam("fileName") String fileName, String fileContent) + throws Exception { + producerTemplate.sendBodyAndHeader( + "sftp://admin@localhost:{{camel.sftp.test-port}}/sftp?privateKeyFile=target/classes/certs/test-key-rsa.key&certFile=target/classes/certs/test-key-rsa-cert.pub", + fileContent, + Exchange.FILE_NAME, fileName); + return Response + .created(new URI("https://camel.apache.org/")) + .build(); + } + + @Path("/certificateFile/get/{fileName}") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getFileWithCertificateFile(@PathParam("fileName") String fileName) { + return consumerTemplate.receiveBody( + "sftp://admin@localhost:{{camel.sftp.test-port}}/sftp?privateKeyFile=target/classes/certs/test-key-rsa.key&certFile=target/classes/certs/test-key-rsa-cert.pub&localWorkDirectory=target&fileName=" + + fileName, + TIMEOUT_MS, + String.class); + } + + @Path("/certificateBytes/create/{fileName}") + @POST + @Consumes(MediaType.TEXT_PLAIN) + public Response createFileWithCertificateBytes(@PathParam("fileName") String fileName, String fileContent) + throws Exception { + + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + try (InputStream certStream = classLoader.getResourceAsStream("certs/test-key-rsa-cert.pub"); + InputStream keyStream = classLoader.getResourceAsStream("certs/test-key-rsa.key")) { + if (certStream == null) { + throw new RuntimeException("Failed reading cert file"); + } + + if (keyStream == null) { + throw new RuntimeException("Failed reading key file"); + } + + String uri = "sftp://admin@localhost:{{camel.sftp.test-port}}/sftp"; + SftpEndpoint endpoint = context.getEndpoint(uri, SftpEndpoint.class); + SftpConfiguration config = endpoint.getConfiguration(); + config.setCertBytes(certStream.readAllBytes()); + config.setPrivateKey(keyStream.readAllBytes()); + + producerTemplate.sendBodyAndHeader(endpoint, fileContent, Exchange.FILE_NAME, fileName); + return Response + .created(new URI("https://camel.apache.org/")) + .build(); + } + } + + @Path("/certificateBytes/get/{fileName}") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getFileWithCertificateBytes(@PathParam("fileName") String fileName) throws Exception { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + try (InputStream certStream = classLoader.getResourceAsStream("certs/test-key-rsa-cert.pub"); + InputStream keyStream = classLoader.getResourceAsStream("certs/test-key-rsa.key")) { + if (certStream == null) { + throw new RuntimeException("Failed reading cert file"); + } + + if (keyStream == null) { + throw new RuntimeException("Failed reading key file"); + } + + String uri = "sftp://admin@localhost:{{camel.sftp.test-port}}/sftp?localWorkDirectory=target&fileName=" + + fileName; + SftpEndpoint endpoint = context.getEndpoint(uri, SftpEndpoint.class); + SftpConfiguration config = endpoint.getConfiguration(); + config.setCertBytes(certStream.readAllBytes()); + config.setPrivateKey(keyStream.readAllBytes()); + + return consumerTemplate.receiveBody(endpoint, TIMEOUT_MS, String.class); + } + } + + @Path("/certificateWithCaAlgorithms/create/{fileName}") + @POST + @Consumes(MediaType.TEXT_PLAIN) + public Response createFileWithCertificateAndCaAlgorithms(@PathParam("fileName") String fileName, String fileContent) + throws Exception { + producerTemplate.sendBodyAndHeader( + "sftp://admin@localhost:{{camel.sftp.test-port}}/sftp?privateKeyUri=certs/test-key-rsa.key&certUri=certs/test-key-rsa-cert.pub&caSignatureAlgorithms=rsa-sha2-512,rsa-sha2-256,ssh-rsa", + fileContent, + Exchange.FILE_NAME, fileName); + return Response + .created(new URI("https://camel.apache.org/")) + .build(); + } + + @Path("/certificateWithCaAlgorithms/get/{fileName}") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String getFileWithCertificateAndCaAlgorithms(@PathParam("fileName") String fileName) { + return consumerTemplate.receiveBody( + "sftp://admin@localhost:{{camel.sftp.test-port}}/sftp?privateKeyUri=certs/test-key-rsa.key&certUri=certs/test-key-rsa-cert.pub&caSignatureAlgorithms=rsa-sha2-512,rsa-sha2-256,ssh-rsa&localWorkDirectory=target&fileName=" + + fileName, + TIMEOUT_MS, + String.class); + } } diff --git a/integration-tests/ftp/src/main/resources/application.properties b/integration-tests/ftp/src/main/resources/application.properties index 8fcaf03aa5..9d05789d61 100644 --- a/integration-tests/ftp/src/main/resources/application.properties +++ b/integration-tests/ftp/src/main/resources/application.properties @@ -17,3 +17,5 @@ # Change to INFO level to get insights about commands run on the FTP server quarkus.log.category."org.apache.ftpserver".level = WARNING + +quarkus.native.resources.includes=certs/test-key-rsa.key,certs/test-key-rsa-cert.pub diff --git a/integration-tests/ftp/src/main/resources/certs/generate-certificates.sh b/integration-tests/ftp/src/main/resources/certs/generate-certificates.sh new file mode 100755 index 0000000000..8780f21483 --- /dev/null +++ b/integration-tests/ftp/src/main/resources/certs/generate-certificates.sh @@ -0,0 +1,66 @@ +#!/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. +# + +# +# Script to generate OpenSSH certificate files for SFTP integration tests +# +# This script creates: +# - test-key-rsa.key: RSA private key (2048 bits) +# - test-key-rsa-cert.pub: OpenSSH certificate signed by a temporary CA +# +# The certificate is valid for 52 weeks and can be used for testing +# certificate-based authentication with the mina-sftp component. +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +echo "===================================================================" +echo "Generating OpenSSH Certificates for MINA SFTP Integration Tests" +echo "===================================================================" +echo "" + +echo "Cleaning up existing files..." +rm -f ca-key ca-key.pub test-key-rsa.key test-key-rsa.key.pub test-key-rsa-cert.pub +echo "Cleaned up existing files" + +echo "Generating temporary CA key pair..." +ssh-keygen -t rsa -b 2048 -f ca-key -N "" -C "test-ca" > /dev/null 2>&1 +echo "Created ca-key and ca-key.pub" + +echo "Generating user RSA key pair..." +ssh-keygen -t rsa -b 2048 -f test-key-rsa.key -N "" -C "test-rsa@test" > /dev/null 2>&1 +echo "Created test-key-rsa.key and test-key-rsa.key.pub" + +echo "Signing public key with CA to create certificate..." +ssh-keygen -s ca-key \ + -I "test-user" \ + -n testuser \ + -V +520w \ + test-key-rsa.key.pub > /dev/null 2>&1 +echo "Created test-key-rsa.key-cert.pub" + +echo "Renaming certificate to test-key-rsa-cert.pub..." +mv test-key-rsa.key-cert.pub test-key-rsa-cert.pub +echo "Renamed to test-key-rsa-cert.pub" + +echo "Cleaning up temporary files..." +rm -f ca-key ca-key.pub test-key-rsa.key.pub +echo "Removed CA keys and public key" diff --git a/integration-tests/ftp/src/main/resources/certs/test-key-rsa-cert.pub b/integration-tests/ftp/src/main/resources/certs/test-key-rsa-cert.pub new file mode 100644 index 0000000000..ba21c6c28b --- /dev/null +++ b/integration-tests/ftp/src/main/resources/certs/test-key-rsa-cert.pub @@ -0,0 +1 @@ [email protected] AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgwsB0wUJYWF7WQayMI3EFz63+rzdGxxmy6QmkXXigkbcAAAADAQABAAABAQCd2V7v8ELetZfmObUn3zP28B44AhcDnOdZHErgl1fe6e2Jmnja4BhdtZAG+XZEmKwlQnWNkRJfDs1/ryfP8xthcZIlklEb/6F2D2zmot5BnY6zzL6XWPtuTny1Ym7iah6KSPv9vS9nc2wcEA9BFu+CYHHWJYskv/PE/hwebae/upKafppCWJ97+Kdkc87Whsd1y2PxhhPPCbH/lOOOmzw2qTyxpYciHsr9NaBfoHRwvTWBdWjk6pYKtw+4gmWOmyAngOh9jcXeJ5pKUhWeJh5fmMrHL/mv/0DtrGQ2fhhrYZjIbkuha5EOFqurnXHjwwBz3Ey8JOhaF9UwQlCvOOWnAAAAAAAAAAAAAAA [...] diff --git a/integration-tests/ftp/src/main/resources/certs/test-key-rsa.key b/integration-tests/ftp/src/main/resources/certs/test-key-rsa.key new file mode 100644 index 0000000000..751cc308be --- /dev/null +++ b/integration-tests/ftp/src/main/resources/certs/test-key-rsa.key @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAndle7/BC3rWX5jm1J98z9vAeOAIXA5znWRxK4JdX3untiZp42uAY +XbWQBvl2RJisJUJ1jZESXw7Nf68nz/MbYXGSJZJRG/+hdg9s5qLeQZ2Os8y+l1j7bk58tW +Ju4moeikj7/b0vZ3NsHBAPQRbvgmBx1iWLJL/zxP4cHm2nv7qSmn6aQlife/inZHPO1obH +dctj8YYTzwmx/5Tjjps8Nqk8saWHIh7K/TWgX6B0cL01gXVo5OqWCrcPuIJljpsgJ4DofY +3F3ieaSlIVniYeX5jKxy/5r/9A7axkNn4Ya2GYyG5LoWuRDharq51x48MAc9xMvCToWhfV +MEJQrzjlpwAAA8hfVSnxX1Up8QAAAAdzc2gtcnNhAAABAQCd2V7v8ELetZfmObUn3zP28B +44AhcDnOdZHErgl1fe6e2Jmnja4BhdtZAG+XZEmKwlQnWNkRJfDs1/ryfP8xthcZIlklEb +/6F2D2zmot5BnY6zzL6XWPtuTny1Ym7iah6KSPv9vS9nc2wcEA9BFu+CYHHWJYskv/PE/h +webae/upKafppCWJ97+Kdkc87Whsd1y2PxhhPPCbH/lOOOmzw2qTyxpYciHsr9NaBfoHRw +vTWBdWjk6pYKtw+4gmWOmyAngOh9jcXeJ5pKUhWeJh5fmMrHL/mv/0DtrGQ2fhhrYZjIbk +uha5EOFqurnXHjwwBz3Ey8JOhaF9UwQlCvOOWnAAAAAwEAAQAAAQAwsx1GwqYm5vjH53r8 +I7F5GMkB96cZDsITrJZvZ1INbLfEEfwCb0wlMTyP4kw6Sq4lyrTQ6fa0jDEbmTMbxcHnVO +5FmDhc/ofWkFjFaW9P6CfcUillMWdVN3LjVUynnxzwBid0t/cVoDc1C0FhkA1x+IZ2jtu4 +iV5QoyOSwbsU/CMKSQ6WOeGLJ8MMgGQgyiZ4/Y6Wjmr4daRYlC+VlFWRqv9aqYvCPBVwim +jYQ/juwHeskk9nENOLMONx4D8zSObJmdm/QMEi0FvE6hkTNPczQez8Kq0XibsOfbBm2a1E +tfHw09buG22V1nDv3MRnIolzD/8FDXv6rwFXyNJXc0E9AAAAgG+UjO3TGmT49yoZdiKmgB +/aYftPl+Qh1BH7ucnZ9obw5xi6Y4wm15QzTBqqXSS3Src46Dt74FIWiDN1soKQh/1P9TlF +iWOWcqpBafIm26b8zK1X9BC1TaCNwrEgiNXbOvJh3jqYI4ZiQBDid/2nQD3S4CSRSjK7rp +ZsGDp5ooc2AAAAgQDZvrUm8w1FRlo6d0OcYbhLQxUOg6oUaSCikXFMQqhwnyzzx0M1F1A9 +vgASJZAcng476hV+bu99EW0XB85rUwanvigO3b2VtrI9jHx8X3Vsi1XJF8tGNLDUvzWWMP +lKtIDPwfPkBS5bV6LOUa5Fa5JTTu821FR6hMyDhOr07O9cVQAAAIEAuZTJ88ChTgdwrbWy +F2j1ah9nXaNtnKs3LNBP90U2ueq5hQp0rHUfpl1mwGM9aFSEd2jyYq91h0oMXG/baMngS+ +OP+M0AH80xDG3X9CW3PdhGEUaqXX9vozw5OQnd6RbGxW605t39XTL630p6mAHfmauYlBDe +GKKHquT0B+ueNgsAAAANdGVzdC1yc2FAdGVzdAECAwQFBg== +-----END OPENSSH PRIVATE KEY----- diff --git a/integration-tests/ftp/src/test/java/org/apache/camel/quarkus/component/sftp/it/SftpTest.java b/integration-tests/ftp/src/test/java/org/apache/camel/quarkus/component/sftp/it/SftpTest.java index 58e2e8aede..5795f21bcc 100644 --- a/integration-tests/ftp/src/test/java/org/apache/camel/quarkus/component/sftp/it/SftpTest.java +++ b/integration-tests/ftp/src/test/java/org/apache/camel/quarkus/component/sftp/it/SftpTest.java @@ -80,4 +80,80 @@ class SftpTest { .statusCode(204); } + @Test + void testCertificateAuthentication() { + RestAssured.given() + .contentType(ContentType.TEXT) + .body("Certificate authentication test") + .post("/sftp/certificate/create/certificate-test.txt") + .then() + .statusCode(201); + + RestAssured.get("/sftp/certificate/get/certificate-test.txt") + .then() + .statusCode(200) + .body(is("Certificate authentication test")); + + RestAssured.delete("/sftp/delete/certificate-test.txt") + .then() + .statusCode(204); + } + + @Test + void testCertificateAuthenticationWithFile() { + RestAssured.given() + .contentType(ContentType.TEXT) + .body("Certificate file authentication test") + .post("/sftp/certificateFile/create/certificate-file-test.txt") + .then() + .statusCode(201); + + RestAssured.get("/sftp/certificateFile/get/certificate-file-test.txt") + .then() + .statusCode(200) + .body(is("Certificate file authentication test")); + + RestAssured.delete("/sftp/delete/certificate-file-test.txt") + .then() + .statusCode(204); + } + + @Test + void testCertificateAuthenticationWithBytes() { + RestAssured.given() + .contentType(ContentType.TEXT) + .body("Certificate bytes authentication test") + .post("/sftp/certificateBytes/create/certificate-bytes-test.txt") + .then() + .statusCode(201); + + RestAssured.get("/sftp/certificateBytes/get/certificate-bytes-test.txt") + .then() + .statusCode(200) + .body(is("Certificate bytes authentication test")); + + RestAssured.delete("/sftp/delete/certificate-bytes-test.txt") + .then() + .statusCode(204); + } + + @Test + void testCertificateAuthenticationWithCaSignatureAlgorithms() { + RestAssured.given() + .contentType(ContentType.TEXT) + .body("Certificate with CA signature algorithms test") + .post("/sftp/certificateWithCaAlgorithms/create/certificate-ca-algo-test.txt") + .then() + .statusCode(201); + + RestAssured.get("/sftp/certificateWithCaAlgorithms/get/certificate-ca-algo-test.txt") + .then() + .statusCode(200) + .body(is("Certificate with CA signature algorithms test")); + + RestAssured.delete("/sftp/delete/certificate-ca-algo-test.txt") + .then() + .statusCode(204); + } + }
