This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push: new 477eb74e8a7 Camel 20759 allow client compression before signing (#14110) 477eb74e8a7 is described below commit 477eb74e8a754c749a7cf81a0cf27e0f0896d480 Author: Jono Morris <j...@apache.org> AuthorDate: Mon May 13 16:49:58 2024 +1200 Camel 20759 allow client compression before signing (#14110) * CAMEL-20759 allow client compression for signing * CAMEL-20759 add generated changes --------- Co-authored-by: Claus Ibsen <claus.ib...@gmail.com> --- .../org/apache/camel/catalog/components/as2.json | 4 +- .../camel/component/as2/api/AS2ClientManager.java | 68 ++++++++- .../component/as2/api/AS2MessageStructure.java | 4 +- .../camel/component/as2/api/AS2MessageTest.java | 119 ++++++++++++++- .../org/apache/camel/component/as2/as2.json | 4 +- .../camel/component/as2/AS2ServerManagerIT.java | 166 +++++++++++++++++++++ .../ROOT/pages/camel-4x-upgrade-guide-4_7.adoc | 6 + 7 files changed, 360 insertions(+), 11 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/as2.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/as2.json index a4c4ba24786..45dffabaaba 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/as2.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/as2.json @@ -33,7 +33,7 @@ "apiName": { "index": 0, "kind": "path", "displayName": "Api Name", "group": "common", "label": "", "required": true, "type": "object", "javaType": "org.apache.camel.component.as2.internal.AS2ApiName", "enum": [ "CLIENT", "SERVER", "RECEIPT" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "What kind of operation to perform" }, "methodName": { "index": 1, "kind": "path", "displayName": "Method Name", "group": "common", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "What sub operation to use for the selected operation" }, "as2From": { "index": 2, "kind": "parameter", "displayName": "As2 From", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "The value of the AS2From header of AS2 message." }, - "as2MessageStructure": { "index": 3, "kind": "parameter", "displayName": "As2 Message Structure", "group": "common", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.component.as2.api.AS2MessageStructure", "enum": [ "PLAIN", "SIGNED", "ENCRYPTED", "SIGNED_ENCRYPTED", "PLAIN_COMPRESSED", "SIGNED_COMPRESSED", "ENCRYPTED_COMPRESSED", "ENCRYPTED_COMPRESSED_SIGNED" ], "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache [...] + "as2MessageStructure": { "index": 3, "kind": "parameter", "displayName": "As2 Message Structure", "group": "common", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.component.as2.api.AS2MessageStructure", "enum": [ "PLAIN", "SIGNED", "ENCRYPTED", "SIGNED_ENCRYPTED", "PLAIN_COMPRESSED", "COMPRESSED_SIGNED", "SIGNED_COMPRESSED", "ENCRYPTED_COMPRESSED", "ENCRYPTED_COMPRESSED_SIGNED", "ENCRYPTED_SIGNED_COMPRESSED" ], "deprecated": false, "autowired": false [...] "as2To": { "index": 4, "kind": "parameter", "displayName": "As2 To", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "The value of the AS2To header of AS2 message." }, "as2Version": { "index": 5, "kind": "parameter", "displayName": "As2 Version", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "enum": [ "1.0", "1.1" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "1.1", "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "The version of the AS2 protocol." }, "asyncMdnPortNumber": { "index": 6, "kind": "parameter", "displayName": "Async Mdn Port Number", "group": "common", "label": "", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "The port number of asynchronous MDN server." }, @@ -79,7 +79,7 @@ "server": { "consumerOnly": true, "producerOnly": false, "description": "Receives EDI Messages over HTTP", "methods": { "listen": { "description": "", "signatures": [ "void listen(String requestUriPattern, org.apache.hc.core5.http.io.HttpRequestHandler handler)" ] } } } }, "apiProperties": { - "client": { "methods": { "send": { "properties": { "as2From": { "index": 0, "kind": "parameter", "displayName": "As2 From", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "AS2 name of sender", "optional": false }, "as2MessageStructure": { "index": 1, "kind": "parameter", "displayName": "As2 Message Structure", "group": "producer", "label": "", "required": f [...] + "client": { "methods": { "send": { "properties": { "as2From": { "index": 0, "kind": "parameter", "displayName": "As2 From", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "AS2 name of sender", "optional": false }, "as2MessageStructure": { "index": 1, "kind": "parameter", "displayName": "As2 Message Structure", "group": "producer", "label": "", "required": f [...] "receipt": { "methods": { "receive": { "properties": { "requestUriPattern": { "index": 0, "kind": "parameter", "displayName": "Request Uri Pattern", "group": "consumer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "", "optional": false } } } } }, "server": { "methods": { "listen": { "properties": { "requestUriPattern": { "index": 0, "kind": "parameter", "displayName": "Request Uri Pattern", "group": "consumer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "", "optional": false } } } } } } diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientManager.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientManager.java index eb8ec12e346..e2188dcd577 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientManager.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientManager.java @@ -288,6 +288,10 @@ public class AS2ClientManager { signedCompressed(httpContext, applicationEntity, request); break; } + case COMPRESSED_SIGNED: { + compressedSigned(httpContext, applicationEntity, request); + break; + } case ENCRYPTED_COMPRESSED: { encryptedCompressed(httpContext, applicationEntity, request); break; @@ -296,6 +300,10 @@ public class AS2ClientManager { encryptedCompressedSigned(httpContext, applicationEntity, request); break; } + case ENCRYPTED_SIGNED_COMPRESSED: { + encryptedSignedCompressed(httpContext, applicationEntity, request); + break; + } default: throw new HttpException("Unknown AS2 Message Structure"); } @@ -324,13 +332,44 @@ public class AS2ClientManager { return response; } + // payload is compressed before being signed and encrypted private void encryptedCompressedSigned( HttpCoreContext httpContext, ApplicationEntity applicationEntity, BasicClassicHttpRequest request) throws HttpException { + + // Create Compressed Entity containing the EDI Entity + CMSCompressedDataGenerator compressedDataGenerator = createCompressorGenerator(); + OutputCompressor compressor = createCompressor(httpContext); + ApplicationPkcs7MimeCompressedDataEntity pkcs7MimeCompressedDataEntity + = new ApplicationPkcs7MimeCompressedDataEntity( + applicationEntity, compressedDataGenerator, compressor, AS2TransferEncoding.BASE64, false); + + // Create Multipart Signed Entity containing the Compressed Entity + AS2SignedDataGenerator signingGenerator = createSigningGenerator(httpContext); + MultipartSignedEntity multipartSignedEntity = new MultipartSignedEntity( + pkcs7MimeCompressedDataEntity, signingGenerator, + StandardCharsets.US_ASCII.name(), AS2TransferEncoding.BASE64, false, null); + + // Create Enveloped Entity containing th Signed Entity + CMSEnvelopedDataGenerator envelopedDataGenerator = createEncryptingGenerator(httpContext); + OutputEncryptor encryptor = createEncryptor(httpContext); + ApplicationPkcs7MimeEnvelopedDataEntity pkcs7MimeEnvelopedDataEntity + = new ApplicationPkcs7MimeEnvelopedDataEntity( + multipartSignedEntity, envelopedDataGenerator, encryptor, AS2TransferEncoding.BASE64, + true); + + // Add Enveloped Entity to main body of request + EntityUtils.setMessageEntity(request, pkcs7MimeEnvelopedDataEntity); + } + + // payload is signed before being compressed and encrypted. + private void encryptedSignedCompressed( + HttpCoreContext httpContext, ApplicationEntity applicationEntity, BasicClassicHttpRequest request) + throws HttpException { // Create Multipart Signed Entity containing EDI Entity - AS2SignedDataGenerator signingGenrator = createSigningGenerator(httpContext); + AS2SignedDataGenerator signingGenerator = createSigningGenerator(httpContext); MultipartSignedEntity multipartSignedEntity = new MultipartSignedEntity( - applicationEntity, signingGenrator, + applicationEntity, signingGenerator, StandardCharsets.US_ASCII.name(), AS2TransferEncoding.BASE64, false, null); // Create Compressed Entity containing Multipart Signed Entity @@ -374,14 +413,35 @@ public class AS2ClientManager { EntityUtils.setMessageEntity(request, pkcs7MimeEnvelopedDataEntity); } + private void compressedSigned( + HttpCoreContext httpContext, ApplicationEntity applicationEntity, BasicClassicHttpRequest request) + throws HttpException { + + // Create Compressed Entity containing the EDI Entity + CMSCompressedDataGenerator compressedDataGenerator = createCompressorGenerator(); + OutputCompressor compressor = createCompressor(httpContext); + ApplicationPkcs7MimeCompressedDataEntity pkcs7MimeCompressedDataEntity + = new ApplicationPkcs7MimeCompressedDataEntity( + applicationEntity, compressedDataGenerator, compressor, AS2TransferEncoding.BASE64, false); + + // Create Multipart Signed Entity containing the Compressed Entity + AS2SignedDataGenerator signingGenerator = createSigningGenerator(httpContext); + MultipartSignedEntity multipartSignedEntity = new MultipartSignedEntity( + pkcs7MimeCompressedDataEntity, + signingGenerator, StandardCharsets.US_ASCII.name(), AS2TransferEncoding.BASE64, true, null); + + // Add Compressed Entity to main body of request. + EntityUtils.setMessageEntity(request, multipartSignedEntity); + } + private void signedCompressed( HttpCoreContext httpContext, ApplicationEntity applicationEntity, BasicClassicHttpRequest request) throws HttpException { // Create Multipart Signed Entity containing EDI Entity - AS2SignedDataGenerator signingGenrator = createSigningGenerator(httpContext); + AS2SignedDataGenerator signingGenerator = createSigningGenerator(httpContext); MultipartSignedEntity multipartSignedEntity = new MultipartSignedEntity( applicationEntity, - signingGenrator, StandardCharsets.US_ASCII.name(), AS2TransferEncoding.BASE64, false, null); + signingGenerator, StandardCharsets.US_ASCII.name(), AS2TransferEncoding.BASE64, false, null); // Create Compressed Entity containing Multipart Signed Entity CMSCompressedDataGenerator compressedDataGenerator = createCompressorGenerator(); diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2MessageStructure.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2MessageStructure.java index dc73ded378f..d40877f433c 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2MessageStructure.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2MessageStructure.java @@ -22,9 +22,11 @@ public enum AS2MessageStructure { ENCRYPTED(false, true, false), SIGNED_ENCRYPTED(true, true, false), PLAIN_COMPRESSED(false, false, true), + COMPRESSED_SIGNED(true, false, true), SIGNED_COMPRESSED(true, false, true), ENCRYPTED_COMPRESSED(false, true, true), - ENCRYPTED_COMPRESSED_SIGNED(true, true, true); + ENCRYPTED_COMPRESSED_SIGNED(true, true, true), + ENCRYPTED_SIGNED_COMPRESSED(true, true, true); private final boolean isSigned; private final boolean isEncrypted; diff --git a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2MessageTest.java b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2MessageTest.java index 6e11be2ee8f..1e8c1937b53 100644 --- a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2MessageTest.java +++ b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2MessageTest.java @@ -19,6 +19,7 @@ package org.apache.camel.component.as2.api; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.security.KeyPair; @@ -836,7 +837,7 @@ public class AS2MessageTest { } @Test - public void compressedAndSignedMessageTest() throws Exception { + public void signedAndCompressedMessageTest() throws Exception { AS2ClientManager clientManager = createDefaultClientManager(); LOG.info("Key Algorithm: {}", signingKP.getPrivate().getAlgorithm()); @@ -960,6 +961,67 @@ public class AS2MessageTest { "Unexpected content for enveloped mime part"); } + // Verify that the payload is compressed before being signed. + @Test + public void compressedAndSignedMessageTest() throws Exception { + AS2ClientManager clientManager = createDefaultClientManager(); + HttpCoreContext httpContext = clientManager.send(EDI_MESSAGE, REQUEST_URI, SUBJECT, FROM, AS2_NAME, AS2_NAME, + AS2MessageStructure.COMPRESSED_SIGNED, + ContentType.create(AS2MediaType.APPLICATION_EDIFACT, StandardCharsets.US_ASCII), "base64", + AS2SignatureAlgorithm.SHA256WITHRSA, certList.toArray(new Certificate[0]), signingKP.getPrivate(), + AS2CompressionAlgorithm.ZLIB, + DISPOSITION_NOTIFICATION_TO, SIGNED_RECEIPT_MIC_ALGORITHMS, null, + null, "file.txt", null); + + HttpRequest request = httpContext.getRequest(); + verifyRequest(request); + + assertTrue(request.getFirstHeader(AS2Header.CONTENT_TYPE).getValue().startsWith(AS2MimeType.MULTIPART_SIGNED), + "Unexpected content type for message"); + assertTrue(request instanceof ClassicHttpRequest, "Request does not contain entity"); + HttpEntity entity = ((ClassicHttpRequest) request).getEntity(); + assertNotNull(entity, "Request does not contain entity"); + assertTrue(entity instanceof MultipartSignedEntity, "Unexpected request entity type"); + MultipartSignedEntity multipartSignedEntity = (MultipartSignedEntity) entity; + assertTrue(multipartSignedEntity.isMainBody(), "Entity not set as main body of request"); + + verifyCompressedSignedEntity(multipartSignedEntity); + } + + // Verify that the payload is compressed before being signed and encrypted. + @Test + public void envelopedSignedAndCompressedMessageTest() throws Exception { + AS2ClientManager clientManager = createDefaultClientManager(); + HttpCoreContext httpContext = clientManager.send(EDI_MESSAGE, REQUEST_URI, SUBJECT, FROM, AS2_NAME, AS2_NAME, + AS2MessageStructure.ENCRYPTED_COMPRESSED_SIGNED, + ContentType.create(AS2MediaType.APPLICATION_EDIFACT, StandardCharsets.US_ASCII), null, + AS2SignatureAlgorithm.SHA256WITHRSA, certList.toArray(new Certificate[0]), signingKP.getPrivate(), + AS2CompressionAlgorithm.ZLIB, DISPOSITION_NOTIFICATION_TO, SIGNED_RECEIPT_MIC_ALGORITHMS, + AS2EncryptionAlgorithm.AES128_CBC, certList.toArray(new Certificate[0]), "file.txt", null); + + HttpRequest request = httpContext.getRequest(); + verifyRequest(request); + + assertTrue(request.getFirstHeader(AS2Header.CONTENT_TYPE).getValue().startsWith(AS2MimeType.APPLICATION_PKCS7_MIME), + "Unexpected content type for message"); + assertTrue(request instanceof ClassicHttpRequest, "Request does not contain entity"); + HttpEntity entity = ((ClassicHttpRequest) request).getEntity(); + assertNotNull(entity, "Request does not contain entity"); + assertTrue(entity instanceof ApplicationPkcs7MimeEnvelopedDataEntity, "Unexpected request entity type"); + ApplicationPkcs7MimeEnvelopedDataEntity envelopedEntity = (ApplicationPkcs7MimeEnvelopedDataEntity) entity; + assertTrue(envelopedEntity.isMainBody(), "Entity not set as main body of request"); + + // Validated enveloped part. + MimeEntity decryptedEntity = envelopedEntity.getEncryptedEntity(signingKP.getPrivate()); + assertTrue(decryptedEntity instanceof MultipartSignedEntity, "Enveloped mime part incorrect type "); + MultipartSignedEntity multipartSignedEntity = (MultipartSignedEntity) decryptedEntity; + assertTrue(multipartSignedEntity.getContentType().startsWith(AS2MimeType.MULTIPART_SIGNED), + "Unexpected content type for compressed mime part"); + assertFalse(multipartSignedEntity.isMainBody(), "Enveloped mime type set as main body of request"); + + verifyCompressedSignedEntity(multipartSignedEntity); + } + @Test public void envelopedCompressedAndSignedMessageTest() throws Exception { AS2ClientManager clientManager = createDefaultClientManager(); @@ -967,7 +1029,7 @@ public class AS2MessageTest { LOG.info("Key Algorithm: {}", signingKP.getPrivate().getAlgorithm()); HttpCoreContext httpContext = clientManager.send(EDI_MESSAGE, REQUEST_URI, SUBJECT, FROM, AS2_NAME, AS2_NAME, - AS2MessageStructure.ENCRYPTED_COMPRESSED_SIGNED, + AS2MessageStructure.ENCRYPTED_SIGNED_COMPRESSED, ContentType.create(AS2MediaType.APPLICATION_EDIFACT, StandardCharsets.US_ASCII), null, AS2SignatureAlgorithm.SHA256WITHRSA, certList.toArray(new Certificate[0]), signingKP.getPrivate(), AS2CompressionAlgorithm.ZLIB, DISPOSITION_NOTIFICATION_TO, SIGNED_RECEIPT_MIC_ALGORITHMS, @@ -1076,6 +1138,59 @@ public class AS2MessageTest { assertEquals(EDI_MESSAGE, ediEntity.getEdiMessage().replaceAll("\r", "")); } + private void verifyRequest(HttpRequest request) throws URISyntaxException { + assertEquals(METHOD, request.getMethod(), "Unexpected method value"); + assertEquals(REQUEST_URI, request.getUri().getPath(), "Unexpected request URI value"); + assertEquals(HttpVersion.HTTP_1_1, request.getVersion(), "Unexpected HTTP version value"); + + assertEquals(SUBJECT, request.getFirstHeader(AS2Header.SUBJECT).getValue(), "Unexpected subject value"); + assertEquals(FROM, request.getFirstHeader(AS2Header.FROM).getValue(), "Unexpected from value"); + assertEquals(AS2_VERSION, request.getFirstHeader(AS2Header.AS2_VERSION).getValue(), "Unexpected AS2 version value"); + assertEquals(AS2_NAME, request.getFirstHeader(AS2Header.AS2_FROM).getValue(), "Unexpected AS2 from value"); + assertEquals(AS2_NAME, request.getFirstHeader(AS2Header.AS2_TO).getValue(), "Unexpected AS2 to value"); + assertTrue(request.getFirstHeader(AS2Header.MESSAGE_ID).getValue().endsWith(CLIENT_FQDN + ">"), + "Unexpected message id value"); + assertEquals(TARGET_HOST + ":" + TARGET_PORT, request.getFirstHeader(AS2Header.TARGET_HOST).getValue(), + "Unexpected target host value"); + assertEquals(USER_AGENT, request.getFirstHeader(AS2Header.USER_AGENT).getValue(), "Unexpected user agent value"); + assertNotNull(request.getFirstHeader(AS2Header.DATE), "Date value missing"); + assertNotNull(request.getFirstHeader(AS2Header.CONTENT_LENGTH), "Content length value missing"); + } + + private void verifyCompressedSignedEntity(MultipartSignedEntity multipartSignedEntity) throws HttpException { + assertEquals(2, multipartSignedEntity.getPartCount(), "Request contains invalid number of mime parts"); + + // Verify first mime part. + assertTrue(multipartSignedEntity.getPart(0) instanceof ApplicationPkcs7MimeCompressedDataEntity, + "First mime part incorrect type "); + ApplicationPkcs7MimeCompressedDataEntity compressedDataEntity + = (ApplicationPkcs7MimeCompressedDataEntity) multipartSignedEntity.getPart(0); + assertTrue(compressedDataEntity.getContentType().startsWith(AS2MediaType.APPLICATION_PKCS7_MIME_COMPRESSED), + "Unexpected content type for first mime part"); + assertFalse(compressedDataEntity.isMainBody(), "First mime type set as main body of request"); + + // Verify compressed entity. + verifyEdiFactEntity(compressedDataEntity.getCompressedEntity(new ZlibExpanderProvider())); + + // Verify second mime part. + assertTrue(multipartSignedEntity.getPart(1) instanceof ApplicationPkcs7SignatureEntity, + "Second mime part incorrect type "); + ApplicationPkcs7SignatureEntity signatureEntity = (ApplicationPkcs7SignatureEntity) multipartSignedEntity.getPart(1); + assertTrue(signatureEntity.getContentType().startsWith(AS2MediaType.APPLICATION_PKCS7_SIGNATURE), + "Unexpected content type for second mime part"); + assertFalse(signatureEntity.isMainBody(), "First mime type set as main body of request"); + + // Verify Signature + assertTrue(SigningUtils.isValid(multipartSignedEntity, new Certificate[] { signingCert }), "Signature must be invalid"); + } + + private void verifyEdiFactEntity(MimeEntity entity) { + assertTrue(entity instanceof ApplicationEDIFACTEntity, "Enveloped mime part incorrect type "); + ApplicationEDIFACTEntity ediEntity = (ApplicationEDIFACTEntity) entity; + assertTrue(ediEntity.getContentType().startsWith(AS2MediaType.APPLICATION_EDIFACT), + "Unexpected content type for compressed entity"); + } + private AS2ClientManager createDefaultClientManager() throws IOException { AS2ClientConnection clientConnection = new AS2ClientConnection( AS2_VERSION, USER_AGENT, CLIENT_FQDN, diff --git a/components/camel-as2/camel-as2-component/src/generated/resources/META-INF/org/apache/camel/component/as2/as2.json b/components/camel-as2/camel-as2-component/src/generated/resources/META-INF/org/apache/camel/component/as2/as2.json index a4c4ba24786..45dffabaaba 100644 --- a/components/camel-as2/camel-as2-component/src/generated/resources/META-INF/org/apache/camel/component/as2/as2.json +++ b/components/camel-as2/camel-as2-component/src/generated/resources/META-INF/org/apache/camel/component/as2/as2.json @@ -33,7 +33,7 @@ "apiName": { "index": 0, "kind": "path", "displayName": "Api Name", "group": "common", "label": "", "required": true, "type": "object", "javaType": "org.apache.camel.component.as2.internal.AS2ApiName", "enum": [ "CLIENT", "SERVER", "RECEIPT" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "What kind of operation to perform" }, "methodName": { "index": 1, "kind": "path", "displayName": "Method Name", "group": "common", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "What sub operation to use for the selected operation" }, "as2From": { "index": 2, "kind": "parameter", "displayName": "As2 From", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "The value of the AS2From header of AS2 message." }, - "as2MessageStructure": { "index": 3, "kind": "parameter", "displayName": "As2 Message Structure", "group": "common", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.component.as2.api.AS2MessageStructure", "enum": [ "PLAIN", "SIGNED", "ENCRYPTED", "SIGNED_ENCRYPTED", "PLAIN_COMPRESSED", "SIGNED_COMPRESSED", "ENCRYPTED_COMPRESSED", "ENCRYPTED_COMPRESSED_SIGNED" ], "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache [...] + "as2MessageStructure": { "index": 3, "kind": "parameter", "displayName": "As2 Message Structure", "group": "common", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.component.as2.api.AS2MessageStructure", "enum": [ "PLAIN", "SIGNED", "ENCRYPTED", "SIGNED_ENCRYPTED", "PLAIN_COMPRESSED", "COMPRESSED_SIGNED", "SIGNED_COMPRESSED", "ENCRYPTED_COMPRESSED", "ENCRYPTED_COMPRESSED_SIGNED", "ENCRYPTED_SIGNED_COMPRESSED" ], "deprecated": false, "autowired": false [...] "as2To": { "index": 4, "kind": "parameter", "displayName": "As2 To", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "The value of the AS2To header of AS2 message." }, "as2Version": { "index": 5, "kind": "parameter", "displayName": "As2 Version", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "enum": [ "1.0", "1.1" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "1.1", "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "The version of the AS2 protocol." }, "asyncMdnPortNumber": { "index": 6, "kind": "parameter", "displayName": "Async Mdn Port Number", "group": "common", "label": "", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "The port number of asynchronous MDN server." }, @@ -79,7 +79,7 @@ "server": { "consumerOnly": true, "producerOnly": false, "description": "Receives EDI Messages over HTTP", "methods": { "listen": { "description": "", "signatures": [ "void listen(String requestUriPattern, org.apache.hc.core5.http.io.HttpRequestHandler handler)" ] } } } }, "apiProperties": { - "client": { "methods": { "send": { "properties": { "as2From": { "index": 0, "kind": "parameter", "displayName": "As2 From", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "AS2 name of sender", "optional": false }, "as2MessageStructure": { "index": 1, "kind": "parameter", "displayName": "As2 Message Structure", "group": "producer", "label": "", "required": f [...] + "client": { "methods": { "send": { "properties": { "as2From": { "index": 0, "kind": "parameter", "displayName": "As2 From", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "AS2 name of sender", "optional": false }, "as2MessageStructure": { "index": 1, "kind": "parameter", "displayName": "As2 Message Structure", "group": "producer", "label": "", "required": f [...] "receipt": { "methods": { "receive": { "properties": { "requestUriPattern": { "index": 0, "kind": "parameter", "displayName": "Request Uri Pattern", "group": "consumer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "", "optional": false } } } } }, "server": { "methods": { "listen": { "properties": { "requestUriPattern": { "index": 0, "kind": "parameter", "displayName": "Request Uri Pattern", "group": "consumer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "", "optional": false } } } } } } diff --git a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerManagerIT.java b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerManagerIT.java index 846b824e72e..83e040a2c76 100644 --- a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerManagerIT.java +++ b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerManagerIT.java @@ -16,6 +16,7 @@ */ package org.apache.camel.component.as2; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -37,6 +38,7 @@ import org.apache.camel.Processor; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.as2.api.AS2ClientConnection; import org.apache.camel.component.as2.api.AS2ClientManager; +import org.apache.camel.component.as2.api.AS2CompressionAlgorithm; import org.apache.camel.component.as2.api.AS2EncryptionAlgorithm; import org.apache.camel.component.as2.api.AS2Header; import org.apache.camel.component.as2.api.AS2MediaType; @@ -45,6 +47,7 @@ import org.apache.camel.component.as2.api.AS2MimeType; import org.apache.camel.component.as2.api.AS2SignatureAlgorithm; import org.apache.camel.component.as2.api.AS2SignedDataGenerator; import org.apache.camel.component.as2.api.entity.ApplicationEDIFACTEntity; +import org.apache.camel.component.as2.api.entity.ApplicationPkcs7MimeCompressedDataEntity; import org.apache.camel.component.as2.api.entity.ApplicationPkcs7MimeEnvelopedDataEntity; import org.apache.camel.component.as2.api.entity.ApplicationPkcs7SignatureEntity; import org.apache.camel.component.as2.api.entity.ApplicationXMLEntity; @@ -64,6 +67,7 @@ import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.HttpStatus; @@ -77,6 +81,7 @@ import org.bouncycastle.asn1.smime.SMIMECapabilityVector; import org.bouncycastle.asn1.smime.SMIMEEncryptionKeyPreferenceAttribute; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.jcajce.ZlibExpanderProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeAll; @@ -486,6 +491,167 @@ public class AS2ServerManagerIT extends AbstractAS2ITSupport { "Unexpected content for enveloped mime part"); } + // Verify that the payload is compressed before being signed. + @Test + public void receiveMultipartCompressedAndSignedMessageTest() throws Exception { + AS2ClientConnection clientConnection + = new AS2ClientConnection( + AS2_VERSION, USER_AGENT, CLIENT_FQDN, TARGET_HOST, TARGET_PORT, HTTP_SOCKET_TIMEOUT, + HTTP_CONNECTION_TIMEOUT, HTTP_CONNECTION_POOL_SIZE, HTTP_CONNECTION_POOL_TTL, clientSslContext, + null); + AS2ClientManager clientManager = new AS2ClientManager(clientConnection); + + clientManager.send(EDI_MESSAGE, REQUEST_URI, SUBJECT, FROM, AS2_NAME, AS2_NAME, AS2MessageStructure.COMPRESSED_SIGNED, + ContentType.create(AS2MediaType.APPLICATION_EDIFACT, StandardCharsets.US_ASCII), null, + AS2SignatureAlgorithm.SHA256WITHRSA, + certList.toArray(new Certificate[0]), signingKP.getPrivate(), AS2CompressionAlgorithm.ZLIB, + DISPOSITION_NOTIFICATION_TO, + SIGNED_RECEIPT_MIC_ALGORITHMS, null, null, null, null); + + MockEndpoint mockEndpoint = getMockEndpoint("mock:as2RcvMsgs"); + verifyMock(mockEndpoint); + + final List<Exchange> exchanges = mockEndpoint.getExchanges(); + verifyExchanges(exchanges); + + HttpCoreContext coreContext = exchanges.get(0).getProperty(AS2Constants.AS2_INTERCHANGE, HttpCoreContext.class); + assertNotNull(coreContext, "context"); + + HttpRequest request = coreContext.getRequest(); + verifyRequest(request); + + assertTrue(request.getFirstHeader(AS2Header.CONTENT_TYPE).getValue().startsWith(AS2MediaType.MULTIPART_SIGNED), + "Unexpected content type for message"); + assertTrue(request instanceof ClassicHttpRequest, "Request does not contain entity"); + HttpEntity entity = ((ClassicHttpRequest) request).getEntity(); + assertNotNull(entity, "Request does not contain entity"); + assertTrue(entity instanceof MultipartSignedEntity, "Unexpected request entity type"); + MultipartSignedEntity multipartSignedEntity = (MultipartSignedEntity) entity; + assertTrue(multipartSignedEntity.isMainBody(), "Entity not set as main body of request"); + + verifyCompressedSignedEntity(multipartSignedEntity); + } + + // Verify that the payload is compressed before being signed and encrypted. + @Test + public void receiveEnvelopedCompressedAndSignedMessageTest() throws Exception { + AS2ClientConnection clientConnection + = new AS2ClientConnection( + AS2_VERSION, USER_AGENT, CLIENT_FQDN, TARGET_HOST, TARGET_PORT, HTTP_SOCKET_TIMEOUT, + HTTP_CONNECTION_TIMEOUT, HTTP_CONNECTION_POOL_SIZE, HTTP_CONNECTION_POOL_TTL, clientSslContext, + null); + AS2ClientManager clientManager = new AS2ClientManager(clientConnection); + + clientManager.send(EDI_MESSAGE, REQUEST_URI, SUBJECT, FROM, AS2_NAME, AS2_NAME, + AS2MessageStructure.ENCRYPTED_COMPRESSED_SIGNED, + ContentType.create(AS2MediaType.APPLICATION_EDIFACT, StandardCharsets.US_ASCII), null, + AS2SignatureAlgorithm.SHA256WITHRSA, certList.toArray(new Certificate[0]), signingKP.getPrivate(), + AS2CompressionAlgorithm.ZLIB, DISPOSITION_NOTIFICATION_TO, SIGNED_RECEIPT_MIC_ALGORITHMS, + AS2EncryptionAlgorithm.AES128_CBC, + certList.toArray(new Certificate[0]), null, null); + + MockEndpoint mockEndpoint = getMockEndpoint("mock:as2RcvMsgs"); + verifyMock(mockEndpoint); + + final List<Exchange> exchanges = mockEndpoint.getExchanges(); + verifyExchanges(exchanges); + + HttpCoreContext coreContext = exchanges.get(0).getProperty(AS2Constants.AS2_INTERCHANGE, HttpCoreContext.class); + assertNotNull(coreContext, "context"); + + HttpRequest request = coreContext.getRequest(); + verifyRequest(request); + + assertTrue(request.getFirstHeader(AS2Header.CONTENT_TYPE).getValue().startsWith(AS2MimeType.APPLICATION_PKCS7_MIME), + "Unexpected content type for message"); + assertTrue(request instanceof ClassicHttpRequest, "Request does not contain entity"); + HttpEntity entity = ((ClassicHttpRequest) request).getEntity(); + assertNotNull(entity, "Request does not contain entity"); + assertTrue(entity instanceof ApplicationPkcs7MimeEnvelopedDataEntity, "Unexpected request entity type"); + ApplicationPkcs7MimeEnvelopedDataEntity envelopedEntity = (ApplicationPkcs7MimeEnvelopedDataEntity) entity; + assertTrue(envelopedEntity.isMainBody(), "Entity not set as main body of request"); + + // Validated enveloped part. + MimeEntity decryptedEntity = envelopedEntity.getEncryptedEntity(decryptingKP.getPrivate()); + assertTrue(decryptedEntity instanceof MultipartSignedEntity, "Enveloped mime part incorrect type "); + MultipartSignedEntity multipartSignedEntity = (MultipartSignedEntity) decryptedEntity; + assertTrue(multipartSignedEntity.getContentType().startsWith(AS2MediaType.MULTIPART_SIGNED), + "Unexpected content type for enveloped mime part"); + assertFalse(multipartSignedEntity.isMainBody(), "Enveloped mime type set as main body of request"); + + verifyCompressedSignedEntity(multipartSignedEntity); + } + + private void verifyRequest(HttpRequest request) throws URISyntaxException { + assertNotNull(request, "request"); + assertEquals(METHOD, request.getMethod(), "Unexpected method value"); + assertEquals(REQUEST_URI, request.getUri().getPath(), "Unexpected request URI value"); + assertEquals(HttpVersion.HTTP_1_1, request.getVersion(), "Unexpected HTTP version value"); + assertEquals(SUBJECT, request.getFirstHeader(AS2Header.SUBJECT).getValue(), "Unexpected subject value"); + assertEquals(FROM, request.getFirstHeader(AS2Header.FROM).getValue(), "Unexpected from value"); + assertEquals(AS2_VERSION, request.getFirstHeader(AS2Header.AS2_VERSION).getValue(), "Unexpected AS2 version value"); + assertEquals(AS2_NAME, request.getFirstHeader(AS2Header.AS2_FROM).getValue(), "Unexpected AS2 from value"); + assertEquals(AS2_NAME, request.getFirstHeader(AS2Header.AS2_TO).getValue(), "Unexpected AS2 to value"); + assertTrue(request.getFirstHeader(AS2Header.MESSAGE_ID).getValue().endsWith(CLIENT_FQDN + ">"), + "Unexpected message id value"); + assertEquals(TARGET_HOST + ":" + TARGET_PORT, request.getFirstHeader(AS2Header.TARGET_HOST).getValue(), + "Unexpected target host value"); + assertEquals(USER_AGENT, request.getFirstHeader(AS2Header.USER_AGENT).getValue(), "Unexpected user agent value"); + assertNotNull(request.getFirstHeader(AS2Header.DATE), "Date value missing"); + assertNotNull(request.getFirstHeader(AS2Header.CONTENT_LENGTH), "Content length value missing"); + } + + private void verifyMock(MockEndpoint mockEndpoint) throws InterruptedException { + mockEndpoint.expectedMinimumMessageCount(1); + mockEndpoint.setResultWaitTime(TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS)); + mockEndpoint.assertIsSatisfied(); + } + + private void verifyExchanges(List<Exchange> exchanges) { + assertNotNull(exchanges, "listen result"); + assertFalse(exchanges.isEmpty(), "listen result"); + Exchange exchange = exchanges.get(0); + Message message = exchange.getIn(); + assertNotNull(message, "exchange message"); + String rcvdMessage = message.getBody(String.class); + assertEquals(EDI_MESSAGE.replaceAll("[\n\r]", ""), rcvdMessage.replaceAll("[\n\r]", ""), + "Unexpected content for enveloped mime part"); + } + + private void verifyCompressedSignedEntity(MultipartSignedEntity multipartSignedEntity) throws HttpException { + assertEquals(2, multipartSignedEntity.getPartCount(), "Request contains invalid number of mime parts"); + + // Verify first mime part. + assertTrue(multipartSignedEntity.getPart(0) instanceof ApplicationPkcs7MimeCompressedDataEntity, + "First mime part incorrect type "); + ApplicationPkcs7MimeCompressedDataEntity compressedDataEntity + = (ApplicationPkcs7MimeCompressedDataEntity) multipartSignedEntity.getPart(0); + assertTrue(compressedDataEntity.getContentType().startsWith(AS2MediaType.APPLICATION_PKCS7_MIME_COMPRESSED), + "Unexpected content type for first mime part"); + assertFalse(compressedDataEntity.isMainBody(), "First mime type set as main body of request"); + + // Verify compressed entity. + verifyEdiFactEntity(compressedDataEntity.getCompressedEntity(new ZlibExpanderProvider())); + + // Verify second mime part. + assertTrue(multipartSignedEntity.getPart(1) instanceof ApplicationPkcs7SignatureEntity, + "Second mime part incorrect type "); + ApplicationPkcs7SignatureEntity signatureEntity = (ApplicationPkcs7SignatureEntity) multipartSignedEntity.getPart(1); + assertTrue(signatureEntity.getContentType().startsWith(AS2MediaType.APPLICATION_PKCS7_SIGNATURE), + "Unexpected content type for second mime part"); + assertFalse(signatureEntity.isMainBody(), "First mime type set as main body of request"); + + // Verify Signature + assertTrue(SigningUtils.isValid(multipartSignedEntity, new Certificate[] { signingCert }), "Signature must be invalid"); + } + + private void verifyEdiFactEntity(MimeEntity entity) { + assertTrue(entity instanceof ApplicationEDIFACTEntity, "Enveloped mime part incorrect type "); + ApplicationEDIFACTEntity ediEntity = (ApplicationEDIFACTEntity) entity; + assertTrue(ediEntity.getContentType().startsWith(AS2MediaType.APPLICATION_EDIFACT), + "Unexpected content type for compressed entity"); + } + @Test public void receiveEnvelopedMessageTest() throws Exception { AS2ClientConnection clientConnection diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_7.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_7.adoc index a595510a771..0ff669f76da 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_7.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_7.adoc @@ -81,6 +81,11 @@ authorizationPolicy.setAuthorizationManager(AuthorityAuthorizationManager.hasRol This new pattern supports a more expressive language to define your own authorization rules, exposing the full power of the Spring Security framework to Camel route policies. See the https://docs.spring.io/spring-security/reference/5.8/migration/servlet/authorization.html#servlet-replace-permissionevaluator-bean-with-methodsecurityexpression-handler[spring documentation] for further details on how to migrate your custom code from `AccessDecisionManager` to `AuthorizationManager`. +=== camel-as2 + +The `camel-as2` component has been updated so that the client can compress a MIME body before signing or compress a MIME body before signing and encrypting as described in +sections https://datatracker.ietf.org/doc/html/rfc5402/#section-3.2[3.2] and https://datatracker.ietf.org/doc/html/rfc5402/#section-3.5[3.5] of https://datatracker.ietf.org/doc/html/rfc5402/[rfc 5402]. + == Camel Spring Boot @@ -88,3 +93,4 @@ See the https://docs.spring.io/spring-security/reference/5.8/migration/servlet/a Using camel debugger with Spring Boot is now moved from `camel-spring-boot` into `camel-debug-starter` where you can configure the debugger via `camel.debug.` options in `application.properties`. +