This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch camel-3.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit 3a33784c579bb4b89edd3147f34a6f212b0310ca Author: Dennis Schwarz <[email protected]> AuthorDate: Fri Jan 27 15:40:14 2023 +0100 CAMEL-18917 camel-as2: add validating signature support to server (#9219) * CAMEL-18917 - Implement Signature verfication 1st step * CAMEL-18917 - Implement Signature verfication 2nd step --- .../component/as2/api/AS2ServerConnection.java | 26 ++-- .../AS2MessageDispositionNotificationEntity.java | 7 +- ...spositionNotificationMultipartReportEntity.java | 8 +- .../as2/api/entity/MultipartSignedEntity.java | 53 -------- .../component/as2/api/protocol/ResponseMDN.java | 8 +- .../component/as2/api/util/HttpMessageUtils.java | 73 ++++++++--- .../camel/component/as2/api/util/MicUtils.java | 6 +- .../camel/component/as2/api/util/SigningUtils.java | 63 ++++++++++ .../camel/component/as2/api/AS2MessageTest.java | 18 +-- .../camel/component/as2/api/util/MicUtilsTest.java | 2 +- .../component/as2/api/util/SigningUtilsTest.java | 110 ++++++++++++++++ ...ientManagerEndpointConfigurationConfigurer.java | 7 ++ .../component/as2/AS2ConfigurationConfigurer.java | 7 ++ .../camel/component/as2/AS2EndpointConfigurer.java | 7 ++ .../camel/component/as2/AS2EndpointUriFactory.java | 3 +- ...rverManagerEndpointConfigurationConfigurer.java | 7 ++ .../org/apache/camel/component/as2/as2.json | 1 + .../camel/component/as2/AS2Configuration.java | 14 +++ .../apache/camel/component/as2/AS2Consumer.java | 6 +- .../as2/internal/AS2ConnectionHelper.java | 3 +- .../camel/component/as2/AS2ClientManagerIT.java | 140 ++++++--------------- .../camel/component/as2/AS2ServerManagerIT.java | 95 +++++++++++++- 22 files changed, 460 insertions(+), 204 deletions(-) diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java index 1dd94f28483..1e82ac09142 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java @@ -68,14 +68,16 @@ public class AS2ServerConnection { Certificate[] signingCertificateChain, PrivateKey signingPrivateKey, PrivateKey decryptingPrivateKey, - String mdnMessageTemplate) - throws IOException { + String mdnMessageTemplate, + Certificate[] validateSigningCertificateChain) + throws IOException { setName(REQUEST_LISTENER_THREAD_NAME_PREFIX + port); serversocket = new ServerSocket(port); // Set up HTTP protocol processor for incoming connections final HttpProcessor inhttpproc = initProtocolProcessor(as2Version, originServer, serverFqdn, - signatureAlgorithm, signingCertificateChain, signingPrivateKey, decryptingPrivateKey, mdnMessageTemplate); + signatureAlgorithm, signingCertificateChain, signingPrivateKey, decryptingPrivateKey, mdnMessageTemplate, + validateSigningCertificateChain); reqistry = new UriHttpRequestHandlerMapper(); @@ -194,6 +196,7 @@ public class AS2ServerConnection { private PrivateKey signingPrivateKey; private PrivateKey decryptingPrivateKey; private String mdnMessageTemplate; + private Certificate[] validateSigningCertificateChain; public AS2ServerConnection(String as2Version, String originServer, @@ -203,8 +206,9 @@ public class AS2ServerConnection { Certificate[] signingCertificateChain, PrivateKey signingPrivateKey, PrivateKey decryptingPrivateKey, - String mdnMessageTemplate) - throws IOException { + String mdnMessageTemplate, + Certificate[] validateSigningCertificateChain) + throws IOException { this.as2Version = ObjectHelper.notNull(as2Version, "as2Version"); this.originServer = ObjectHelper.notNull(originServer, "userAgent"); this.serverFqdn = ObjectHelper.notNull(serverFqdn, "serverFqdn"); @@ -214,15 +218,20 @@ public class AS2ServerConnection { this.signingPrivateKey = signingPrivateKey; this.decryptingPrivateKey = decryptingPrivateKey; this.mdnMessageTemplate = mdnMessageTemplate; + this.validateSigningCertificateChain = validateSigningCertificateChain; listenerThread = new RequestListenerThread( this.as2Version, this.originServer, this.serverFqdn, this.serverPortNumber, this.signingAlgorithm, this.signingCertificateChain, this.signingPrivateKey, - this.decryptingPrivateKey, this.mdnMessageTemplate); + this.decryptingPrivateKey, this.mdnMessageTemplate, validateSigningCertificateChain); listenerThread.setDaemon(true); listenerThread.start(); } + public Certificate[] getValidateSigningCertificateChain() { + return validateSigningCertificateChain; + } + public PrivateKey getSigningPrivateKey() { return signingPrivateKey; } @@ -267,12 +276,13 @@ public class AS2ServerConnection { Certificate[] signingCertificateChain, PrivateKey signingPrivateKey, PrivateKey decryptingPrivateKey, - String mdnMessageTemplate) { + String mdnMessageTemplate, + Certificate[] validateSigningCertificateChain) { return HttpProcessorBuilder.create().add(new ResponseContent(true)).add(new ResponseServer(originServer)) .add(new ResponseDate()).add(new ResponseConnControl()).add(new ResponseMDN( as2Version, serverFqdn, signatureAlgorithm, signingCertificateChain, signingPrivateKey, decryptingPrivateKey, - mdnMessageTemplate)) + mdnMessageTemplate, validateSigningCertificateChain)) .build(); } diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/AS2MessageDispositionNotificationEntity.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/AS2MessageDispositionNotificationEntity.java index 31789498555..24591a849f6 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/AS2MessageDispositionNotificationEntity.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/AS2MessageDispositionNotificationEntity.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; +import java.security.cert.Certificate; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; @@ -78,7 +79,8 @@ public class AS2MessageDispositionNotificationEntity extends MimeEntity { Map<String, String> extensionFields, String charset, boolean isMainBody, - PrivateKey decryptingPrivateKey) throws HttpException { + PrivateKey decryptingPrivateKey, + Certificate[] validateSigningCertificateChain) throws HttpException { setMainBody(isMainBody); setContentType(ContentType.create(AS2MimeType.MESSAGE_DISPOSITION_NOTIFICATION, charset)); @@ -89,7 +91,8 @@ public class AS2MessageDispositionNotificationEntity extends MimeEntity { this.originalMessageId = HttpMessageUtils.getHeaderValue(request, AS2Header.MESSAGE_ID); - this.receivedContentMic = MicUtils.createReceivedContentMic(request, decryptingPrivateKey); + this.receivedContentMic + = MicUtils.createReceivedContentMic(request, validateSigningCertificateChain, decryptingPrivateKey); this.reportingUA = HttpMessageUtils.getHeaderValue(response, AS2Header.SERVER); diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/DispositionNotificationMultipartReportEntity.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/DispositionNotificationMultipartReportEntity.java index b94b8ab60e4..9da207edf94 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/DispositionNotificationMultipartReportEntity.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/DispositionNotificationMultipartReportEntity.java @@ -18,6 +18,7 @@ package org.apache.camel.component.as2.api.entity; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; +import java.security.cert.Certificate; import java.util.Map; import org.apache.camel.component.as2.api.AS2Header; @@ -51,8 +52,9 @@ public class DispositionNotificationMultipartReportEntity extends MultipartRepor String boundary, boolean isMainBody, PrivateKey decryptingPrivateKey, - String mdnMessage) - throws HttpException { + String mdnMessage, + Certificate[] validateSigningCertificateChain) + throws HttpException { super(charset, isMainBody, boundary); removeHeaders(AS2Header.CONTENT_TYPE); setContentType(getContentTypeValue(boundary)); @@ -64,7 +66,7 @@ public class DispositionNotificationMultipartReportEntity extends MultipartRepor addPart(new AS2MessageDispositionNotificationEntity( request, response, dispositionMode, dispositionType, dispositionModifier, failureFields, errorFields, warningFields, extensionFields, charset, false, - decryptingPrivateKey)); + decryptingPrivateKey, validateSigningCertificateChain)); } public String getMainMessageContentType() { diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/MultipartSignedEntity.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/MultipartSignedEntity.java index bf5f7d5685e..0b38996dd7f 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/MultipartSignedEntity.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/entity/MultipartSignedEntity.java @@ -16,23 +16,8 @@ */ package org.apache.camel.component.as2.api.entity; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.security.cert.X509Certificate; -import java.util.Collection; - import org.apache.camel.component.as2.api.AS2SignedDataGenerator; import org.apache.http.HttpException; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.cms.CMSProcessable; -import org.bouncycastle.cms.CMSProcessableByteArray; -import org.bouncycastle.cms.CMSSignedData; -import org.bouncycastle.cms.SignerInformation; -import org.bouncycastle.cms.SignerInformationStore; -import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; -import org.bouncycastle.util.Store; public class MultipartSignedEntity extends MultipartMimeEntity { @@ -51,44 +36,6 @@ public class MultipartSignedEntity extends MultipartMimeEntity { this.isMainBody = isMainBody; } - public boolean isValid() { - MimeEntity signedEntity = getSignedDataEntity(); - ApplicationPkcs7SignatureEntity applicationPkcs7SignatureEntity = getSignatureEntity(); - - if (signedEntity == null || applicationPkcs7SignatureEntity == null) { - return false; - } - - try { - ByteArrayOutputStream outstream = new ByteArrayOutputStream(); - signedEntity.writeTo(outstream); - CMSProcessable signedContent = new CMSProcessableByteArray(outstream.toByteArray()); - - byte[] signature = applicationPkcs7SignatureEntity.getSignature(); - InputStream is = new ByteArrayInputStream(signature); - - CMSSignedData signedData = new CMSSignedData(signedContent, is); - - Store<X509CertificateHolder> store = signedData.getCertificates(); - SignerInformationStore signers = signedData.getSignerInfos(); - - for (SignerInformation signer : signers.getSigners()) { - @SuppressWarnings("unchecked") - Collection<X509CertificateHolder> certCollection = store.getMatches(signer.getSID()); - - X509CertificateHolder certHolder = certCollection.iterator().next(); - X509Certificate cert = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder); - if (!signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(cert))) { - return false; - } - } - } catch (Exception e) { - return false; - } - - return true; - } - public MimeEntity getSignedDataEntity() { if (getPartCount() > 0) { return getPart(0); diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/protocol/ResponseMDN.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/protocol/ResponseMDN.java index 1cb577e25a0..5cfb275ece0 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/protocol/ResponseMDN.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/protocol/ResponseMDN.java @@ -83,12 +83,13 @@ public class ResponseMDN implements HttpResponseInterceptor { private PrivateKey signingPrivateKey; private PrivateKey decryptingPrivateKey; private String mdnMessageTemplate; + private Certificate[] validateSigningCertificateChain; private VelocityEngine velocityEngine; public ResponseMDN(String as2Version, String serverFQDN, AS2SignatureAlgorithm signingAlgorithm, Certificate[] signingCertificateChain, PrivateKey signingPrivateKey, PrivateKey decryptingPrivateKey, - String mdnMessageTemplate) { + String mdnMessageTemplate, Certificate[] validateSigningCertificateChain) { this.as2Version = as2Version; this.serverFQDN = serverFQDN; this.signingAlgorithm = signingAlgorithm; @@ -102,6 +103,7 @@ public class ResponseMDN implements HttpResponseInterceptor { } else { this.mdnMessageTemplate = DEFAULT_MDN_MESSAGE_TEMPLATE; } + this.validateSigningCertificateChain = validateSigningCertificateChain; } @Override @@ -145,7 +147,7 @@ public class ResponseMDN implements HttpResponseInterceptor { multipartReportEntity = new DispositionNotificationMultipartReportEntity( httpEntityEnclosingRequest, response, DispositionMode.AUTOMATIC_ACTION_MDN_SENT_AUTOMATICALLY, AS2DispositionType.FAILED, null, null, null, null, null, StandardCharsets.US_ASCII.name(), boundary, true, - decryptingPrivateKey, mdnMessage); + decryptingPrivateKey, mdnMessage, validateSigningCertificateChain); } else { String mdnMessage = createMdnDescription(httpEntityEnclosingRequest, response, DispositionMode.AUTOMATIC_ACTION_MDN_SENT_AUTOMATICALLY, @@ -155,7 +157,7 @@ public class ResponseMDN implements HttpResponseInterceptor { httpEntityEnclosingRequest, response, DispositionMode.AUTOMATIC_ACTION_MDN_SENT_AUTOMATICALLY, AS2DispositionType.PROCESSED, null, null, null, null, null, StandardCharsets.US_ASCII.name(), boundary, true, - decryptingPrivateKey, mdnMessage); + decryptingPrivateKey, mdnMessage, validateSigningCertificateChain); } DispositionNotificationOptions dispositionNotificationOptions = DispositionNotificationOptionsParser diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/HttpMessageUtils.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/HttpMessageUtils.java index e8068e9f7ec..b2027569d4f 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/HttpMessageUtils.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/HttpMessageUtils.java @@ -17,6 +17,7 @@ package org.apache.camel.component.as2.api.util; import java.security.PrivateKey; +import java.security.cert.Certificate; import java.util.Objects; import org.apache.camel.component.as2.api.AS2Header; @@ -126,7 +127,8 @@ public final class HttpMessageUtils { return null; } - public static ApplicationEDIEntity extractEdiPayload(HttpMessage message, PrivateKey privateKey) throws HttpException { + public static ApplicationEDIEntity extractEdiPayload(HttpMessage message, DecrpytingAndSigningInfo decrpytingAndSigningInfo) + throws HttpException { String contentTypeString = getHeaderValue(message, AS2Header.CONTENT_TYPE); if (contentTypeString == null) { @@ -144,17 +146,17 @@ public final class HttpMessageUtils { break; } case AS2MimeType.MULTIPART_SIGNED: { - ediEntity = extractMultipartSigned(message); + ediEntity = extractMultipartSigned(message, decrpytingAndSigningInfo); break; } case AS2MimeType.APPLICATION_PKCS7_MIME: { switch (contentType.getParameter("smime-type")) { case "compressed-data": { - ediEntity = extractCompressedData(message); + ediEntity = extractCompressedData(message, decrpytingAndSigningInfo); break; } case "enveloped-data": { - ediEntity = extractEnvelopedData(message, privateKey); + ediEntity = extractEnvelopedData(message, decrpytingAndSigningInfo); break; } default: @@ -174,9 +176,11 @@ public final class HttpMessageUtils { } - private static ApplicationEDIEntity extractEnvelopedData(HttpMessage message, PrivateKey privateKey) throws HttpException { + private static ApplicationEDIEntity extractEnvelopedData( + HttpMessage message, DecrpytingAndSigningInfo decrpytingAndSigningInfo) + throws HttpException { ApplicationEDIEntity ediEntity; - if (privateKey == null) { + if (decrpytingAndSigningInfo.getDecryptingPrivateKey() == null) { throw new HttpException( "Failed to extract EDI payload: private key can not be null for AS2 enveloped message"); } @@ -185,11 +189,13 @@ public final class HttpMessageUtils { Objects.requireNonNull(envelopedDataEntity, "Failed to extract EDI payload: the enveloped data entity is null"); - ediEntity = extractEdiPayloadFromEnvelopedEntity(envelopedDataEntity, privateKey); + ediEntity = extractEdiPayloadFromEnvelopedEntity(envelopedDataEntity, decrpytingAndSigningInfo); return ediEntity; } - private static ApplicationEDIEntity extractCompressedData(HttpMessage message) throws HttpException { + private static ApplicationEDIEntity extractCompressedData( + HttpMessage message, DecrpytingAndSigningInfo decrpytingAndSigningInfo) + throws HttpException { ApplicationEDIEntity ediEntity; ApplicationPkcs7MimeCompressedDataEntity compressedDataEntity = getEntity(message, ApplicationPkcs7MimeCompressedDataEntity.class); @@ -197,11 +203,13 @@ public final class HttpMessageUtils { Objects.requireNonNull(compressedDataEntity, "Failed to extract the EDI payload: the compressed data entity is null"); - ediEntity = extractEdiPayloadFromCompressedEntity(compressedDataEntity); + ediEntity = extractEdiPayloadFromCompressedEntity(compressedDataEntity, decrpytingAndSigningInfo); return ediEntity; } - private static ApplicationEDIEntity extractMultipartSigned(HttpMessage message) throws HttpException { + private static ApplicationEDIEntity extractMultipartSigned( + HttpMessage message, DecrpytingAndSigningInfo decrpytingAndSigningInfo) + throws HttpException { ApplicationEDIEntity ediEntity; MultipartSignedEntity multipartSignedEntity = getEntity(message, MultipartSignedEntity.class); @@ -209,13 +217,18 @@ public final class HttpMessageUtils { Objects.requireNonNull(multipartSignedEntity, "Failed to extract EDI payload: the multipart signed entity is null"); + if (decrpytingAndSigningInfo.getValidateSigningCertificateChain() != null && !SigningUtils + .isValid(multipartSignedEntity, decrpytingAndSigningInfo.getValidateSigningCertificateChain())) { + throw new HttpException("Failed to validate the signature"); + } + MimeEntity mimeEntity = multipartSignedEntity.getSignedDataEntity(); if (mimeEntity instanceof ApplicationEDIEntity) { ediEntity = (ApplicationEDIEntity) mimeEntity; } else if (mimeEntity instanceof ApplicationPkcs7MimeCompressedDataEntity) { ApplicationPkcs7MimeCompressedDataEntity compressedDataEntity = (ApplicationPkcs7MimeCompressedDataEntity) mimeEntity; - ediEntity = extractEdiPayloadFromCompressedEntity(compressedDataEntity); + ediEntity = extractEdiPayloadFromCompressedEntity(compressedDataEntity, decrpytingAndSigningInfo); } else { throw new HttpException( "Failed to extract EDI payload: invalid content type '" + mimeEntity.getContentTypeValue() @@ -225,11 +238,11 @@ public final class HttpMessageUtils { } private static ApplicationEDIEntity extractEdiPayloadFromEnvelopedEntity( - ApplicationPkcs7MimeEnvelopedDataEntity envelopedDataEntity, PrivateKey privateKey) + ApplicationPkcs7MimeEnvelopedDataEntity envelopedDataEntity, DecrpytingAndSigningInfo decrpytingAndSigningInfo) throws HttpException { ApplicationEDIEntity ediEntity = null; - MimeEntity entity = envelopedDataEntity.getEncryptedEntity(privateKey); + MimeEntity entity = envelopedDataEntity.getEncryptedEntity(decrpytingAndSigningInfo.getDecryptingPrivateKey()); String contentTypeString = entity.getContentTypeValue(); if (contentTypeString == null) { throw new HttpException("Failed to extract EDI message: content type missing from encrypted entity"); @@ -245,13 +258,18 @@ public final class HttpMessageUtils { } case AS2MimeType.MULTIPART_SIGNED: { MultipartSignedEntity multipartSignedEntity = (MultipartSignedEntity) entity; + if (decrpytingAndSigningInfo.getValidateSigningCertificateChain() != null && !SigningUtils + .isValid(multipartSignedEntity, decrpytingAndSigningInfo.getValidateSigningCertificateChain())) { + throw new HttpException("Failed to validate the signature"); + } + MimeEntity mimeEntity = multipartSignedEntity.getSignedDataEntity(); if (mimeEntity instanceof ApplicationEDIEntity) { ediEntity = (ApplicationEDIEntity) mimeEntity; } else if (mimeEntity instanceof ApplicationPkcs7MimeCompressedDataEntity) { ApplicationPkcs7MimeCompressedDataEntity compressedDataEntity = (ApplicationPkcs7MimeCompressedDataEntity) mimeEntity; - ediEntity = extractEdiPayloadFromCompressedEntity(compressedDataEntity); + ediEntity = extractEdiPayloadFromCompressedEntity(compressedDataEntity, decrpytingAndSigningInfo); } else { throw new HttpException( @@ -268,7 +286,7 @@ public final class HttpMessageUtils { } ApplicationPkcs7MimeCompressedDataEntity compressedDataEntity = (ApplicationPkcs7MimeCompressedDataEntity) entity; - ediEntity = extractEdiPayloadFromCompressedEntity(compressedDataEntity); + ediEntity = extractEdiPayloadFromCompressedEntity(compressedDataEntity, decrpytingAndSigningInfo); break; } default: @@ -281,7 +299,7 @@ public final class HttpMessageUtils { } public static ApplicationEDIEntity extractEdiPayloadFromCompressedEntity( - ApplicationPkcs7MimeCompressedDataEntity compressedDataEntity) + ApplicationPkcs7MimeCompressedDataEntity compressedDataEntity, DecrpytingAndSigningInfo decrpytingAndSigningInfo) throws HttpException { ApplicationEDIEntity ediEntity = null; @@ -301,6 +319,11 @@ public final class HttpMessageUtils { } case AS2MimeType.MULTIPART_SIGNED: { MultipartSignedEntity multipartSignedEntity = (MultipartSignedEntity) entity; + if (decrpytingAndSigningInfo.getValidateSigningCertificateChain() != null && !SigningUtils + .isValid(multipartSignedEntity, decrpytingAndSigningInfo.getValidateSigningCertificateChain())) { + throw new HttpException("Failed to validate the signature"); + } + MimeEntity mimeEntity = multipartSignedEntity.getSignedDataEntity(); if (mimeEntity instanceof ApplicationEDIEntity) { ediEntity = (ApplicationEDIEntity) mimeEntity; @@ -321,4 +344,22 @@ public final class HttpMessageUtils { return ediEntity; } + public static class DecrpytingAndSigningInfo { + private Certificate[] validateSigningCertificateChain; + private PrivateKey decryptingPrivateKey; + + public DecrpytingAndSigningInfo(Certificate[] validateSigningCertificateChain, PrivateKey decryptingPrivateKey) { + this.validateSigningCertificateChain = validateSigningCertificateChain; + this.decryptingPrivateKey = decryptingPrivateKey; + } + + public Certificate[] getValidateSigningCertificateChain() { + return validateSigningCertificateChain; + } + + public PrivateKey getDecryptingPrivateKey() { + return decryptingPrivateKey; + } + + } } diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/MicUtils.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/MicUtils.java index af84d5da6ad..87f42229fa1 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/MicUtils.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/MicUtils.java @@ -21,6 +21,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; +import java.security.cert.Certificate; import org.apache.camel.component.as2.api.AS2Header; import org.apache.camel.component.as2.api.AS2MicAlgorithm; @@ -79,7 +80,7 @@ public final class MicUtils { } public static ReceivedContentMic createReceivedContentMic( - HttpEntityEnclosingRequest request, PrivateKey decryptingPrivateKey) + HttpEntityEnclosingRequest request, Certificate[] validateSigningCertificateChain, PrivateKey decryptingPrivateKey) throws HttpException { String dispositionNotificationOptionsString @@ -97,7 +98,8 @@ public final class MicUtils { return null; } - HttpEntity entity = HttpMessageUtils.extractEdiPayload(request, decryptingPrivateKey); + HttpEntity entity = HttpMessageUtils.extractEdiPayload(request, + new HttpMessageUtils.DecrpytingAndSigningInfo(validateSigningCertificateChain, decryptingPrivateKey)); byte[] content = EntityUtils.getContent(entity); diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/SigningUtils.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/SigningUtils.java index 7a17387a1ef..f2cdf8f44c0 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/SigningUtils.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/SigningUtils.java @@ -16,6 +16,9 @@ */ package org.apache.camel.component.as2.api.util; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; @@ -24,6 +27,9 @@ import java.util.Arrays; import org.apache.camel.component.as2.api.AS2SignatureAlgorithm; import org.apache.camel.component.as2.api.AS2SignedDataGenerator; +import org.apache.camel.component.as2.api.entity.ApplicationPkcs7SignatureEntity; +import org.apache.camel.component.as2.api.entity.MimeEntity; +import org.apache.camel.component.as2.api.entity.MultipartSignedEntity; import org.apache.camel.util.ObjectHelper; import org.apache.http.HttpException; import org.bouncycastle.asn1.ASN1EncodableVector; @@ -36,11 +42,21 @@ import org.bouncycastle.asn1.smime.SMIMEEncryptionKeyPreferenceAttribute; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerId; import org.bouncycastle.cms.SignerInfoGenerator; +import org.bouncycastle.cms.SignerInformationVerifier; +import org.bouncycastle.cms.SignerInformationVerifierProvider; import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public final class SigningUtils { + private static final Logger LOG = LoggerFactory.getLogger(SigningUtils.class); + private SigningUtils() { } @@ -93,4 +109,51 @@ public final class SigningUtils { return gen; } + + public static boolean isValidSigned(byte[] signedContent, byte[] signature, Certificate[] signingCertificateChain) { + if (signedContent == null || signature == null || signingCertificateChain == null) { + return false; + } + + try { + CMSSignedData signedData + = new CMSSignedData(new CMSProcessableByteArray(signedContent), new ByteArrayInputStream(signature)); + + SignerInformationVerifierProvider sivp = (SignerId sid) -> { + for (Certificate knownCert : signingCertificateChain) { + SignerInformationVerifier siv + = new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build((X509Certificate) knownCert); + if (siv.getAssociatedCertificate().getIssuer().equals(sid.getIssuer()) + && siv.getAssociatedCertificate().getSerialNumber().equals(sid.getSerialNumber())) { + return siv; + } + } + throw new RuntimeException("Signature was created with an unknown certificate"); + }; + + return signedData.verifySignatures(sivp); + } catch (CMSException e) { + //Probably the signature was created with an unknown certificate or something else is wrong with the signature + LOG.debug(e.getMessage(), e); + } catch (Exception e) { + LOG.debug(e.getMessage(), e); + } + return false; + } + + public static boolean isValid(MultipartSignedEntity multipartSignedEntity, Certificate[] signingCertificateChain) { + MimeEntity signedEntity = multipartSignedEntity.getSignedDataEntity(); + ApplicationPkcs7SignatureEntity applicationPkcs7SignatureEntity = multipartSignedEntity.getSignatureEntity(); + if (signedEntity == null || applicationPkcs7SignatureEntity == null) { + return false; + } + + try (ByteArrayOutputStream o = new ByteArrayOutputStream()) { + signedEntity.writeTo(o); + return isValidSigned(o.toByteArray(), applicationPkcs7SignatureEntity.getSignature(), signingCertificateChain); + } catch (IOException e) { + return false; + } + } + } 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 138338b7fe0..303b437b2f3 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 @@ -57,6 +57,7 @@ import org.apache.camel.component.as2.api.util.EntityUtils; import org.apache.camel.component.as2.api.util.HttpMessageUtils; import org.apache.camel.component.as2.api.util.MicUtils; import org.apache.camel.component.as2.api.util.MicUtils.ReceivedContentMic; +import org.apache.camel.component.as2.api.util.SigningUtils; import org.apache.camel.test.AvailablePortFinder; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; @@ -139,6 +140,7 @@ public class AS2MessageTest { private static final Duration HTTP_CONNECTION_TIMEOUT = Duration.ofSeconds(5); private static final Integer HTTP_CONNECTION_POOL_SIZE = 5; private static final Duration HTTP_CONNECTION_POOL_TTL = Duration.ofMinutes(15); + private static final Certificate[] VALIDATE_SIGNING_CERTIFICATE_CHAIN = null; private static final String RECIPIENT_DELIVERY_ADDRESS = "http://localhost:" + TARGET_PORT + "/handle-receipts"; private static final String AS2_VERSION = "1.1"; private static final String USER_AGENT = "Camel AS2 Endpoint"; @@ -213,7 +215,8 @@ public class AS2MessageTest { testServer = new AS2ServerConnection( AS2_VERSION, "MyServer-HTTP/1.1", SERVER_FQDN, TARGET_PORT, AS2SignatureAlgorithm.SHA256WITHRSA, - certList.toArray(new Certificate[0]), signingKP.getPrivate(), decryptingKP.getPrivate(), MDN_MESSAGE_TEMPLATE); + certList.toArray(new Certificate[0]), signingKP.getPrivate(), decryptingKP.getPrivate(), MDN_MESSAGE_TEMPLATE, + VALIDATE_SIGNING_CERTIFICATE_CHAIN); testServer.listen("*", new HttpRequestHandler() { @Override public void handle(HttpRequest request, HttpResponse response, HttpContext context) @@ -222,8 +225,9 @@ public class AS2MessageTest { org.apache.camel.component.as2.api.entity.EntityParser.parseAS2MessageEntity(request); context.setAttribute(AS2ServerManager.SUBJECT, SUBJECT); context.setAttribute(AS2ServerManager.FROM, AS2_NAME); - LOG.debug(AS2Utils.printMessage(request)); - ediEntity = HttpMessageUtils.extractEdiPayload(request, testServer.getDecryptingPrivateKey()); + LOG.debug("{}", AS2Utils.printMessage(request)); + ediEntity = HttpMessageUtils.extractEdiPayload(request, new HttpMessageUtils.DecrpytingAndSigningInfo( + testServer.getValidateSigningCertificateChain(), testServer.getDecryptingPrivateKey())); } catch (Exception e) { throw new HttpException("Failed to parse AS2 Message Entity", e); } @@ -657,7 +661,7 @@ public class AS2MessageTest { assertNotNull(signatureEntity, "Multipart signed entity does not contain signature entity"); // Validate Signature - assertTrue(multipartSignedEntity.isValid(), "Signature is invalid"); + assertTrue(SigningUtils.isValid(multipartSignedEntity, new Certificate[] { signingCert }), "Signature is invalid"); } @@ -698,7 +702,7 @@ public class AS2MessageTest { assertNotNull(signatureEntity, "Signature Entity"); // Validate Signature - assertTrue(responseSignedEntity.isValid(), "Signature is invalid"); + assertTrue(SigningUtils.isValid(responseSignedEntity, new Certificate[] { signingCert }), "Signature is invalid"); } @Test @@ -740,7 +744,7 @@ public class AS2MessageTest { request, response, DispositionMode.AUTOMATIC_ACTION_MDN_SENT_AUTOMATICALLY, AS2DispositionType.PROCESSED, dispositionModifier, failureFields, errorFields, warningFields, extensionFields, null, "boundary", true, - null, "Got ya message!"); + null, "Got ya message!", null); // Send MDN HttpCoreContext httpContext = mdnManager.send(mdn, RECIPIENT_DELIVERY_ADDRESS); @@ -765,7 +769,7 @@ public class AS2MessageTest { assertArrayEquals(errorFields, mdnEntity.getErrorFields(), "Unexpected value for Error Fields"); assertArrayEquals(warningFields, mdnEntity.getWarningFields(), "Unexpected value for Warning Fields"); assertEquals(extensionFields, mdnEntity.getExtensionFields(), "Unexpected value for Extension Fields"); - ReceivedContentMic expectedMic = MicUtils.createReceivedContentMic(request, null); + ReceivedContentMic expectedMic = MicUtils.createReceivedContentMic(request, null, null); ReceivedContentMic mdnMic = mdnEntity.getReceivedContentMic(); assertEquals(expectedMic.getEncodedMessageDigest(), mdnMic.getEncodedMessageDigest(), "Unexpected value for Received Content Mic"); diff --git a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/MicUtilsTest.java b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/MicUtilsTest.java index 84629b94893..ad85477c12d 100644 --- a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/MicUtilsTest.java +++ b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/MicUtilsTest.java @@ -101,7 +101,7 @@ public class MicUtilsTest { basicEntity.setContentType(CONTENT_TYPE_VALUE); request.setEntity(basicEntity); - ReceivedContentMic receivedContentMic = MicUtils.createReceivedContentMic(request, null); + ReceivedContentMic receivedContentMic = MicUtils.createReceivedContentMic(request, null, null); assertNotNull(receivedContentMic, "Failed to create Received Content MIC"); LOG.debug("Digest Algorithm: " + receivedContentMic.getDigestAlgorithmId()); assertEquals(EXPECTED_MESSAGE_DIGEST_ALGORITHM, receivedContentMic.getDigestAlgorithmId(), diff --git a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/SigningUtilsTest.java b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/SigningUtilsTest.java new file mode 100644 index 00000000000..8533efe3b9a --- /dev/null +++ b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/SigningUtilsTest.java @@ -0,0 +1,110 @@ +/* + * 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.component.as2.api.util; + +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +import org.apache.camel.component.as2.api.AS2SignatureAlgorithm; +import org.apache.camel.component.as2.api.AS2SignedDataGenerator; +import org.apache.camel.component.as2.api.Utils; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerId; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SigningUtilsTest { + + private static KeyPair signingKP; + private static X509Certificate signingCert; + private static X509Certificate evilSigningCert; + private static final String MESSAGE = "Test message to be signed"; + + @BeforeEach + public void setUp() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + setupKeysAndCertificates(); + } + + @AfterEach + public void tearDown() throws Exception { + } + + @Test + public void createSigningGeneratorTest() throws Exception { + AS2SignedDataGenerator gen = SigningUtils.createSigningGenerator(AS2SignatureAlgorithm.SHA1WITHRSA, + new Certificate[] { signingCert }, signingKP.getPrivate()); + CMSProcessableByteArray sData = new CMSProcessableByteArray(MESSAGE.getBytes(StandardCharsets.UTF_8)); + CMSSignedData signedData = gen.generate(sData, true); + + assertTrue(signedData.verifySignatures((SignerId sid) -> { + return new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(signingCert); + }), "Message was wrongly signed"); + } + + @Test + public void isValidSignedTest() throws Exception { + AS2SignedDataGenerator gen = SigningUtils.createSigningGenerator(AS2SignatureAlgorithm.SHA1WITHRSA, + new Certificate[] { signingCert }, signingKP.getPrivate()); + CMSProcessableByteArray sData = new CMSProcessableByteArray(MESSAGE.getBytes(StandardCharsets.UTF_8)); + CMSSignedData signedData = gen.generate(sData, true); + assertTrue(SigningUtils.isValidSigned(MESSAGE.getBytes(StandardCharsets.UTF_8), signedData.getEncoded(), + new Certificate[] { signingCert }), "Message must be valid"); + assertFalse(SigningUtils.isValidSigned(MESSAGE.getBytes(StandardCharsets.UTF_8), signedData.getEncoded(), + new Certificate[] { evilSigningCert }), "Message must be invalid"); + } + + private static void setupKeysAndCertificates() throws Exception { + // + // set up our certificates + // + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); + kpg.initialize(1024, new SecureRandom()); + // + // certificate we sign against + // + { + String signingDN = "O=AS2 Test Orgaisation, C=US"; + signingKP = kpg.generateKeyPair(); + signingCert = Utils.makeCertificate( + signingKP, signingDN, signingKP, signingDN); + } + + // + // certificate some else signed against + // + { + String evilSigningDN = "O=Evil Haxor Coorp, C=RU"; + KeyPair evilSigningKP = kpg.generateKeyPair(); + evilSigningCert = Utils.makeCertificate( + evilSigningKP, evilSigningDN, evilSigningKP, evilSigningDN); + } + } + +} diff --git a/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2ClientManagerEndpointConfigurationConfigurer.java b/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2ClientManagerEndpointConfigurationConfigurer.java index 374bbfa4bef..25bfcad9f8e 100644 --- a/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2ClientManagerEndpointConfigurationConfigurer.java +++ b/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2ClientManagerEndpointConfigurationConfigurer.java @@ -55,6 +55,7 @@ public class AS2ClientManagerEndpointConfigurationConfigurer extends org.apache. map.put("TargetHostname", java.lang.String.class); map.put("TargetPortNumber", java.lang.Integer.class); map.put("UserAgent", java.lang.String.class); + map.put("ValidateSigningCertificateChain", java.security.cert.Certificate[].class); ALL_OPTIONS = map; } @@ -132,6 +133,8 @@ public class AS2ClientManagerEndpointConfigurationConfigurer extends org.apache. case "TargetPortNumber": target.setTargetPortNumber(property(camelContext, java.lang.Integer.class, value)); return true; case "useragent": case "UserAgent": target.setUserAgent(property(camelContext, java.lang.String.class, value)); return true; + case "validatesigningcertificatechain": + case "ValidateSigningCertificateChain": target.setValidateSigningCertificateChain(property(camelContext, java.security.cert.Certificate[].class, value)); return true; default: return false; } } @@ -214,6 +217,8 @@ public class AS2ClientManagerEndpointConfigurationConfigurer extends org.apache. case "TargetPortNumber": return java.lang.Integer.class; case "useragent": case "UserAgent": return java.lang.String.class; + case "validatesigningcertificatechain": + case "ValidateSigningCertificateChain": return java.security.cert.Certificate[].class; default: return null; } } @@ -292,6 +297,8 @@ public class AS2ClientManagerEndpointConfigurationConfigurer extends org.apache. case "TargetPortNumber": return target.getTargetPortNumber(); case "useragent": case "UserAgent": return target.getUserAgent(); + case "validatesigningcertificatechain": + case "ValidateSigningCertificateChain": return target.getValidateSigningCertificateChain(); default: return null; } } diff --git a/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2ConfigurationConfigurer.java b/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2ConfigurationConfigurer.java index 46225f9739a..2924d31cacd 100644 --- a/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2ConfigurationConfigurer.java +++ b/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2ConfigurationConfigurer.java @@ -53,6 +53,7 @@ public class AS2ConfigurationConfigurer extends org.apache.camel.support.compone map.put("TargetHostname", java.lang.String.class); map.put("TargetPortNumber", java.lang.Integer.class); map.put("UserAgent", java.lang.String.class); + map.put("ValidateSigningCertificateChain", java.security.cert.Certificate[].class); ALL_OPTIONS = map; } @@ -126,6 +127,8 @@ public class AS2ConfigurationConfigurer extends org.apache.camel.support.compone case "TargetPortNumber": target.setTargetPortNumber(property(camelContext, java.lang.Integer.class, value)); return true; case "useragent": case "UserAgent": target.setUserAgent(property(camelContext, java.lang.String.class, value)); return true; + case "validatesigningcertificatechain": + case "ValidateSigningCertificateChain": target.setValidateSigningCertificateChain(property(camelContext, java.security.cert.Certificate[].class, value)); return true; default: return false; } } @@ -204,6 +207,8 @@ public class AS2ConfigurationConfigurer extends org.apache.camel.support.compone case "TargetPortNumber": return java.lang.Integer.class; case "useragent": case "UserAgent": return java.lang.String.class; + case "validatesigningcertificatechain": + case "ValidateSigningCertificateChain": return java.security.cert.Certificate[].class; default: return null; } } @@ -278,6 +283,8 @@ public class AS2ConfigurationConfigurer extends org.apache.camel.support.compone case "TargetPortNumber": return target.getTargetPortNumber(); case "useragent": case "UserAgent": return target.getUserAgent(); + case "validatesigningcertificatechain": + case "ValidateSigningCertificateChain": return target.getValidateSigningCertificateChain(); default: return null; } } diff --git a/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2EndpointConfigurer.java b/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2EndpointConfigurer.java index a6252b60c7c..0222151dcde 100644 --- a/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2EndpointConfigurer.java +++ b/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2EndpointConfigurer.java @@ -54,6 +54,7 @@ public class AS2EndpointConfigurer extends PropertyConfigurerSupport implements map.put("targetHostname", java.lang.String.class); map.put("targetPortNumber", java.lang.Integer.class); map.put("userAgent", java.lang.String.class); + map.put("validateSigningCertificateChain", java.security.cert.Certificate[].class); map.put("exceptionHandler", org.apache.camel.spi.ExceptionHandler.class); map.put("exchangePattern", org.apache.camel.ExchangePattern.class); map.put("lazyStartProducer", boolean.class); @@ -131,6 +132,8 @@ public class AS2EndpointConfigurer extends PropertyConfigurerSupport implements case "targetPortNumber": target.getConfiguration().setTargetPortNumber(property(camelContext, java.lang.Integer.class, value)); return true; case "useragent": case "userAgent": target.getConfiguration().setUserAgent(property(camelContext, java.lang.String.class, value)); return true; + case "validatesigningcertificatechain": + case "validateSigningCertificateChain": target.getConfiguration().setValidateSigningCertificateChain(property(camelContext, java.security.cert.Certificate[].class, value)); return true; default: return false; } } @@ -210,6 +213,8 @@ public class AS2EndpointConfigurer extends PropertyConfigurerSupport implements case "targetPortNumber": return java.lang.Integer.class; case "useragent": case "userAgent": return java.lang.String.class; + case "validatesigningcertificatechain": + case "validateSigningCertificateChain": return java.security.cert.Certificate[].class; default: return null; } } @@ -285,6 +290,8 @@ public class AS2EndpointConfigurer extends PropertyConfigurerSupport implements case "targetPortNumber": return target.getConfiguration().getTargetPortNumber(); case "useragent": case "userAgent": return target.getConfiguration().getUserAgent(); + case "validatesigningcertificatechain": + case "validateSigningCertificateChain": return target.getConfiguration().getValidateSigningCertificateChain(); default: return null; } } diff --git a/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2EndpointUriFactory.java b/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2EndpointUriFactory.java index 3d138d2b9d8..2528eb6bf04 100644 --- a/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2EndpointUriFactory.java +++ b/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2EndpointUriFactory.java @@ -21,7 +21,7 @@ public class AS2EndpointUriFactory extends org.apache.camel.support.component.En private static final Set<String> SECRET_PROPERTY_NAMES; private static final Set<String> MULTI_VALUE_PREFIXES; static { - Set<String> props = new HashSet<>(40); + Set<String> props = new HashSet<>(41); props.add("apiName"); props.add("as2From"); props.add("as2MessageStructure"); @@ -62,6 +62,7 @@ public class AS2EndpointUriFactory extends org.apache.camel.support.component.En props.add("targetHostname"); props.add("targetPortNumber"); props.add("userAgent"); + props.add("validateSigningCertificateChain"); PROPERTY_NAMES = Collections.unmodifiableSet(props); SECRET_PROPERTY_NAMES = Collections.emptySet(); MULTI_VALUE_PREFIXES = Collections.emptySet(); diff --git a/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2ServerManagerEndpointConfigurationConfigurer.java b/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2ServerManagerEndpointConfigurationConfigurer.java index 7952b980868..e4ca3775905 100644 --- a/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2ServerManagerEndpointConfigurationConfigurer.java +++ b/components/camel-as2/camel-as2-component/src/generated/java/org/apache/camel/component/as2/AS2ServerManagerEndpointConfigurationConfigurer.java @@ -54,6 +54,7 @@ public class AS2ServerManagerEndpointConfigurationConfigurer extends org.apache. map.put("TargetHostname", java.lang.String.class); map.put("TargetPortNumber", java.lang.Integer.class); map.put("UserAgent", java.lang.String.class); + map.put("ValidateSigningCertificateChain", java.security.cert.Certificate[].class); ALL_OPTIONS = map; } @@ -129,6 +130,8 @@ public class AS2ServerManagerEndpointConfigurationConfigurer extends org.apache. case "TargetPortNumber": target.setTargetPortNumber(property(camelContext, java.lang.Integer.class, value)); return true; case "useragent": case "UserAgent": target.setUserAgent(property(camelContext, java.lang.String.class, value)); return true; + case "validatesigningcertificatechain": + case "ValidateSigningCertificateChain": target.setValidateSigningCertificateChain(property(camelContext, java.security.cert.Certificate[].class, value)); return true; default: return false; } } @@ -209,6 +212,8 @@ public class AS2ServerManagerEndpointConfigurationConfigurer extends org.apache. case "TargetPortNumber": return java.lang.Integer.class; case "useragent": case "UserAgent": return java.lang.String.class; + case "validatesigningcertificatechain": + case "ValidateSigningCertificateChain": return java.security.cert.Certificate[].class; default: return null; } } @@ -285,6 +290,8 @@ public class AS2ServerManagerEndpointConfigurationConfigurer extends org.apache. case "TargetPortNumber": return target.getTargetPortNumber(); case "useragent": case "UserAgent": return target.getUserAgent(); + case "validatesigningcertificatechain": + case "ValidateSigningCertificateChain": return target.getValidateSigningCertificateChain(); default: return null; } } diff --git a/components/camel-as2/camel-as2-component/src/generated/resources/org/apache/camel/component/as2/as2.json b/components/camel-as2/camel-as2-component/src/generated/resources/org/apache/camel/component/as2/as2.json index d0288b227c8..313b8b6cab0 100644 --- a/components/camel-as2/camel-as2-component/src/generated/resources/org/apache/camel/component/as2/as2.json +++ b/components/camel-as2/camel-as2-component/src/generated/resources/org/apache/camel/component/as2/as2.json @@ -64,6 +64,7 @@ "targetHostname": { "kind": "parameter", "displayName": "Target Hostname", "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 host name (IP or DNS name) of target host." }, "targetPortNumber": { "kind": "parameter", "displayName": "Target 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 target host. -1 indicates the scheme default port." }, "userAgent": { "kind": "parameter", "displayName": "User Agent", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "Camel AS2 Client Endpoint", "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "The value included in the User-Agent message header identifying the AS2 user agent." }, + "validateSigningCertificateChain": { "kind": "parameter", "displayName": "Validate Signing Certificate Chain", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.security.cert.Certificate[]", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.as2.AS2Configuration", "configurationField": "configuration", "description": "Certifiates to validate the messages signature against. If not supplied, v [...] "exceptionHandler": { "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By default the con [...] "exchangePattern": { "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut", "InOptionalOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." }, "lazyStartProducer": { "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may other [...] diff --git a/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Configuration.java b/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Configuration.java index 67616c10655..6cf642091c6 100644 --- a/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Configuration.java +++ b/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Configuration.java @@ -108,6 +108,8 @@ public class AS2Configuration { private Integer httpConnectionPoolSize = 5; @UriParam(defaultValue = "15m") private Duration httpConnectionPoolTtl = Duration.ofMinutes(15); + @UriParam + private Certificate[] validateSigningCertificateChain; public AS2ApiName getApiName() { return apiName; @@ -490,4 +492,16 @@ public class AS2Configuration { this.httpConnectionPoolTtl = httpConnectionPoolTtl; } + public Certificate[] getValidateSigningCertificateChain() { + return validateSigningCertificateChain; + } + + /** + * Certifiates to validate the messages signature against. If not supplied, validation will not take place. Server: + * validates the received message. Client: not yet implemented, should validate the MDN + */ + public void setValidateSigningCertificateChain(Certificate[] validateSigningCertificateChain) { + this.validateSigningCertificateChain = validateSigningCertificateChain; + } + } diff --git a/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Consumer.java b/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Consumer.java index 2c6142b1eda..540fe9c0242 100644 --- a/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Consumer.java +++ b/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/AS2Consumer.java @@ -120,9 +120,11 @@ public class AS2Consumer extends AbstractApiConsumer<AS2ApiName, AS2Configuratio apiProxy.handleMDNResponse(context, getEndpoint().getSubject(), ofNullable(getEndpoint().getFrom()).orElse(getEndpoint().getConfiguration().getServer())); } - ApplicationEDIEntity ediEntity - = HttpMessageUtils.extractEdiPayload(request, as2ServerConnection.getDecryptingPrivateKey()); + = HttpMessageUtils.extractEdiPayload(request, + new HttpMessageUtils.DecrpytingAndSigningInfo( + as2ServerConnection.getValidateSigningCertificateChain(), + as2ServerConnection.getDecryptingPrivateKey())); // Set AS2 Interchange property and EDI message into body of input message. Exchange exchange = createExchange(false); diff --git a/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/internal/AS2ConnectionHelper.java b/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/internal/AS2ConnectionHelper.java index 13f5146ef18..da1b01e2eb4 100644 --- a/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/internal/AS2ConnectionHelper.java +++ b/components/camel-as2/camel-as2-component/src/main/java/org/apache/camel/component/as2/internal/AS2ConnectionHelper.java @@ -73,7 +73,8 @@ public final class AS2ConnectionHelper { configuration.getAs2Version(), configuration.getServer(), configuration.getServerFqdn(), configuration.getServerPortNumber(), configuration.getSigningAlgorithm(), configuration.getSigningCertificateChain(), configuration.getSigningPrivateKey(), - configuration.getDecryptingPrivateKey(), configuration.getMdnMessageTemplate()); + configuration.getDecryptingPrivateKey(), configuration.getMdnMessageTemplate(), + configuration.getValidateSigningCertificateChain()); serverConnections.put(configuration.getServerPortNumber(), serverConnection); } return serverConnection; diff --git a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ClientManagerIT.java b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ClientManagerIT.java index f055938ca76..bb3eec87c38 100644 --- a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ClientManagerIT.java +++ b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ClientManagerIT.java @@ -24,9 +24,7 @@ import java.security.SecureRandom; import java.security.Security; import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.apache.camel.CamelException; @@ -43,7 +41,6 @@ import org.apache.camel.component.as2.api.AS2MimeType; import org.apache.camel.component.as2.api.AS2ServerConnection; import org.apache.camel.component.as2.api.AS2ServerManager; import org.apache.camel.component.as2.api.AS2SignatureAlgorithm; -import org.apache.camel.component.as2.api.AS2SignedDataGenerator; import org.apache.camel.component.as2.api.entity.AS2DispositionModifier; import org.apache.camel.component.as2.api.entity.AS2DispositionType; import org.apache.camel.component.as2.api.entity.AS2MessageDispositionNotificationEntity; @@ -60,6 +57,7 @@ import org.apache.camel.component.as2.api.util.EntityUtils; import org.apache.camel.component.as2.api.util.HttpMessageUtils; import org.apache.camel.component.as2.api.util.MicUtils; import org.apache.camel.component.as2.api.util.MicUtils.ReceivedContentMic; +import org.apache.camel.component.as2.api.util.SigningUtils; import org.apache.camel.component.as2.internal.AS2ApiCollection; import org.apache.camel.component.as2.internal.AS2ClientManagerApiMethod; import org.apache.camel.http.common.HttpMessage; @@ -79,21 +77,10 @@ import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpCoreContext; import org.apache.http.protocol.HttpDateGenerator; import org.apache.http.protocol.HttpRequestHandler; -import org.bouncycastle.asn1.ASN1EncodableVector; -import org.bouncycastle.asn1.cms.AttributeTable; -import org.bouncycastle.asn1.cms.IssuerAndSerialNumber; -import org.bouncycastle.asn1.smime.SMIMECapabilitiesAttribute; -import org.bouncycastle.asn1.smime.SMIMECapability; -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.JcaSimpleSignerInfoGeneratorBuilder; import org.bouncycastle.cms.jcajce.ZlibExpanderProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -164,63 +151,14 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { private static final String REPORTING_UA = "Server Responding with MDN"; private static AS2ServerConnection serverConnection; - private static KeyPair serverSigningKP; - private static List<X509Certificate> serverCertList; + private static KeyPair serverKP; + private static X509Certificate serverCert; private static RequestHandler requestHandler; private static final HttpDateGenerator DATE_GENERATOR = new HttpDateGenerator(); - private KeyPair issueKP; - private X509Certificate issueCert; - - private KeyPair signingKP; - private KeyPair decryptingKP; - private X509Certificate signingCert; - private List<X509Certificate> certList; - private AS2SignedDataGenerator gen; - - @Override - @BeforeEach - public void setUp() throws Exception { - super.setUp(); - Security.addProvider(new BouncyCastleProvider()); - - setupKeysAndCertificates(); - - // Create and populate certificate store. - JcaCertStore certs = new JcaCertStore(certList); - - // Create capabilities vector - SMIMECapabilityVector capabilities = new SMIMECapabilityVector(); - capabilities.addCapability(SMIMECapability.dES_EDE3_CBC); - capabilities.addCapability(SMIMECapability.rC2_CBC, 128); - capabilities.addCapability(SMIMECapability.dES_CBC); - - // Create signing attributes - ASN1EncodableVector attributes = new ASN1EncodableVector(); - attributes.add(new SMIMEEncryptionKeyPreferenceAttribute( - new IssuerAndSerialNumber(new X500Name(signingCert.getIssuerDN().getName()), signingCert.getSerialNumber()))); - attributes.add(new SMIMECapabilitiesAttribute(capabilities)); - - for (String signingAlgorithmName : AS2SignedDataGenerator - .getSupportedSignatureAlgorithmNamesForKey(signingKP.getPrivate())) { - try { - this.gen = new AS2SignedDataGenerator(); - this.gen.addSignerInfoGenerator(new JcaSimpleSignerInfoGeneratorBuilder().setProvider("BC") - .setSignedAttributeGenerator(new AttributeTable(attributes)) - .build(signingAlgorithmName, signingKP.getPrivate(), signingCert)); - this.gen.addCertificates(certs); - break; - } catch (Exception e) { - this.gen = null; - continue; - } - } - - if (this.gen == null) { - throw new Exception("failed to create signing generator"); - } - } + private static KeyPair clientKeyPair; + private static X509Certificate clientCert; @Test public void plainMessageSendTest() throws Exception { @@ -409,7 +347,7 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { // parameter type is org.apache.camel.component.as2.api.AS2EncryptionAlgorithm headers.put("CamelAS2.encryptingAlgorithm", AS2EncryptionAlgorithm.AES128_CBC); // parameter type is java.security.cert.Certificate[] - headers.put("CamelAS2.encryptingCertificateChain", certList); + headers.put("CamelAS2.encryptingCertificateChain", new Certificate[] { clientCert }); // parameter type is String headers.put("CamelAS2.attachedFileName", ""); @@ -428,7 +366,7 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { assertTrue(entity instanceof ApplicationPkcs7MimeEnvelopedDataEntity, "Request body does not contain ApplicationPkcs7Mime entity"); MimeEntity envelopeEntity - = ((ApplicationPkcs7MimeEnvelopedDataEntity) entity).getEncryptedEntity(decryptingKP.getPrivate()); + = ((ApplicationPkcs7MimeEnvelopedDataEntity) entity).getEncryptedEntity(clientKeyPair.getPrivate()); assertTrue(envelopeEntity instanceof ApplicationEDIEntity, "Enveloped entity is not an EDI entity"); String ediMessage = ((ApplicationEDIEntity) envelopeEntity).getEdiMessage(); assertEquals(EDI_MESSAGE.replaceAll("[\n\r]", ""), ediMessage.replaceAll("[\n\r]", ""), "EDI message is different"); @@ -499,9 +437,9 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { // parameter type is org.apache.camel.component.as2.api.AS2SignatureAlgorithm headers.put("CamelAS2.signingAlgorithm", AS2SignatureAlgorithm.SHA512WITHRSA); // parameter type is java.security.cert.Certificate[] - headers.put("CamelAS2.signingCertificateChain", certList.toArray(new Certificate[0])); + headers.put("CamelAS2.signingCertificateChain", new Certificate[] { clientCert }); // parameter type is java.security.PrivateKey - headers.put("CamelAS2.signingPrivateKey", signingKP.getPrivate()); + headers.put("CamelAS2.signingPrivateKey", clientKeyPair.getPrivate()); // parameter type is String headers.put("CamelAS2.dispositionNotificationTo", "[email protected]"); // parameter type is String[] @@ -546,7 +484,8 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { assertNotNull(responseEntity, "Response entity"); assertTrue(responseEntity instanceof MultipartSignedEntity, "Unexpected response entity type"); MultipartSignedEntity responseSignedEntity = (MultipartSignedEntity) responseEntity; - assertTrue(responseSignedEntity.isValid(), "Signature for response entity is invalid"); + assertTrue(SigningUtils.isValid(responseSignedEntity, new Certificate[] { serverCert }), + "Signature for response entity is invalid"); MimeEntity responseSignedDataEntity = responseSignedEntity.getSignedDataEntity(); assertTrue(responseSignedDataEntity instanceof DispositionNotificationMultipartReportEntity, "Signed entity wrong type"); @@ -579,7 +518,8 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { ReceivedContentMic receivedContentMic = messageDispositionNotificationEntity.getReceivedContentMic(); ReceivedContentMic computedContentMic - = MicUtils.createReceivedContentMic((HttpEntityEnclosingRequest) request, decryptingKP.getPrivate()); + = MicUtils.createReceivedContentMic((HttpEntityEnclosingRequest) request, new Certificate[] { clientCert }, + clientKeyPair.getPrivate()); assertEquals(computedContentMic.getEncodedMessageDigest(), receivedContentMic.getEncodedMessageDigest(), "Received content MIC does not match computed"); } @@ -651,7 +591,8 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { assertNotNull(responseEntity, "Response entity"); assertTrue(responseEntity instanceof MultipartSignedEntity, "Unexpected response entity type"); MultipartSignedEntity responseSignedEntity = (MultipartSignedEntity) responseEntity; - assertTrue(responseSignedEntity.isValid(), "Signature for response entity is invalid"); + assertTrue(SigningUtils.isValid(responseSignedEntity, new Certificate[] { serverCert }), + "Signature for response entity is invalid"); MimeEntity responseSignedDataEntity = responseSignedEntity.getSignedDataEntity(); assertTrue(responseSignedDataEntity instanceof DispositionNotificationMultipartReportEntity, "Signed entity wrong type"); @@ -684,7 +625,8 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { ReceivedContentMic receivedContentMic = messageDispositionNotificationEntity.getReceivedContentMic(); ReceivedContentMic computedContentMic - = MicUtils.createReceivedContentMic((HttpEntityEnclosingRequest) request, decryptingKP.getPrivate()); + = MicUtils.createReceivedContentMic((HttpEntityEnclosingRequest) request, new Certificate[] { clientCert }, + clientKeyPair.getPrivate()); assertEquals(computedContentMic.getEncodedMessageDigest(), receivedContentMic.getEncodedMessageDigest(), "Received content MIC does not match computed"); } @@ -698,7 +640,7 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { private void runAsyncMDNTest() throws CamelException, HttpException { AS2AsynchronousMDNManager mdnManager = new AS2AsynchronousMDNManager( AS2_VERSION, ORIGIN_SERVER_NAME, SERVER_FQDN, - certList.toArray(new X509Certificate[0]), signingKP.getPrivate()); + new Certificate[] { clientCert }, clientKeyPair.getPrivate()); // Create plain edi request message to acknowledge ApplicationEDIEntity ediEntity = EntityUtils.createEDIEntity(EDI_MESSAGE, @@ -733,7 +675,7 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { request, response, DispositionMode.AUTOMATIC_ACTION_MDN_SENT_AUTOMATICALLY, AS2DispositionType.PROCESSED, dispositionModifier, failureFields, errorFields, warningFields, extensionFields, null, "boundary", - true, serverSigningKP.getPrivate(), "Got your message!"); + true, serverKP.getPrivate(), "Got your message!", new Certificate[] { clientCert }); // Send MDN @SuppressWarnings("unused") @@ -743,6 +685,7 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { @BeforeAll public static void setupTest() throws Exception { setupServerKeysAndCertificates(); + setupClientKeysAndCertificates(); receiveTestMessages(); } @@ -803,11 +746,15 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { } }; // test route for send - from("direct://SEND").to("as2://" + PATH_PREFIX + "/send?inBody=ediMessage"); + from("direct://SEND") + .to("as2://" + PATH_PREFIX + "/send?inBody=ediMessage&httpSocketTimeout=5m&httpConnectionTimeout=5m"); // test route for send2 - from("direct://SEND2").toF("as2://" + PATH_PREFIX + "/send?inBody=ediMessage&as2From=%s&as2To=%s", AS2_NAME, - AS2_NAME); + from("direct://SEND2") + .toF("as2://" + PATH_PREFIX + + "/send?inBody=ediMessage&as2From=%s&as2To=%s&httpSocketTimeout=5m&httpConnectionTimeout=5m", + AS2_NAME, + AS2_NAME); from("jetty:http://localhost:" + MDN_TARGET_PORT + "/handle-receipts").process(proc); @@ -834,27 +781,21 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { // certificate we sign against // String signingDN = "CN=William J. Collins, [email protected], O=Punkhorn Software, C=US"; - serverSigningKP = kpg.generateKeyPair(); - X509Certificate signingCert = Utils.makeCertificate( - serverSigningKP, signingDN, issueKP, issueDN); - - serverCertList = new ArrayList<>(); - - serverCertList.add(signingCert); - serverCertList.add(issueCert); + serverKP = kpg.generateKeyPair(); + serverCert = Utils.makeCertificate(serverKP, signingDN, issueKP, issueDN); } private static void receiveTestMessages() throws IOException { serverConnection = new AS2ServerConnection( AS2_VERSION, ORIGIN_SERVER_NAME, SERVER_FQDN, PARTNER_TARGET_PORT, AS2SignatureAlgorithm.SHA256WITHRSA, - serverCertList.toArray(new Certificate[0]), serverSigningKP.getPrivate(), serverSigningKP.getPrivate(), - MDN_MESSAGE_TEMPLATE); + new Certificate[] { serverCert }, serverKP.getPrivate(), serverKP.getPrivate(), + MDN_MESSAGE_TEMPLATE, new Certificate[] { clientCert }); requestHandler = new RequestHandler(); serverConnection.listen("/", requestHandler); } - private void setupKeysAndCertificates() throws Exception { + private static void setupClientKeysAndCertificates() throws Exception { // // set up our certificates // @@ -863,24 +804,15 @@ public class AS2ClientManagerIT extends AbstractAS2ITSupport { kpg.initialize(1024, new SecureRandom()); String issueDN = "O=Punkhorn Software, C=US"; - issueKP = kpg.generateKeyPair(); - issueCert = Utils.makeCertificate( + KeyPair issueKP = kpg.generateKeyPair(); + X509Certificate issueCert = Utils.makeCertificate( issueKP, issueDN, issueKP, issueDN); // // certificate we sign against // String signingDN = "CN=William J. Collins, [email protected], O=Punkhorn Software, C=US"; - signingKP = kpg.generateKeyPair(); - signingCert = Utils.makeCertificate( - signingKP, signingDN, issueKP, issueDN); - - certList = new ArrayList<>(); - - certList.add(signingCert); - certList.add(issueCert); - - // keys used to encrypt/decrypt - decryptingKP = signingKP; + clientKeyPair = kpg.generateKeyPair(); + clientCert = Utils.makeCertificate(clientKeyPair, signingDN, issueKP, issueDN); } } 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 aed3295aca1..cfb4feae095 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 @@ -289,7 +289,100 @@ public class AS2ServerManagerIT extends AbstractAS2ITSupport { assertFalse(signatureEntity.isMainBody(), "First mime type set as main body of request"); // Validate Signature - assertTrue(signedEntity.isValid(), "Signature is invalid"); + assertTrue(SigningUtils.isValid(signedEntity, new Certificate[] { signingCert }), "Signature is invalid"); + + String rcvdMessage = message.getBody(String.class); + assertEquals(EDI_MESSAGE.replaceAll("[\n\r]", ""), rcvdMessage.replaceAll("[\n\r]", ""), + "Unexpected content for enveloped mime part"); + } + + @Test + public void receiveMultipartInvalidSignedMessageTest() 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); + AS2ClientManager clientManager = new AS2ClientManager(clientConnection); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); + + kpg.initialize(1024, new SecureRandom()); + String hackerIssueDN = "O=Hackers Unlimited Ltd., C=US"; + var hackerIssueKP = kpg.generateKeyPair(); + var hackerissueCert = Utils.makeCertificate( + hackerIssueKP, hackerIssueDN, hackerIssueKP, hackerIssueDN); + String hackerSigningDN = "CN=John Doe, [email protected], O=Self Signed, C=US"; + var hackerSigningKP = kpg.generateKeyPair(); + var hackerSigningCert = Utils.makeCertificate( + hackerSigningKP, hackerSigningDN, hackerIssueKP, hackerIssueDN); + + clientManager.send(EDI_MESSAGE, REQUEST_URI, SUBJECT, FROM, AS2_NAME, AS2_NAME, AS2MessageStructure.SIGNED, + ContentType.create(AS2MediaType.APPLICATION_EDIFACT, StandardCharsets.US_ASCII), null, + AS2SignatureAlgorithm.SHA256WITHRSA, + new Certificate[] { hackerSigningCert }, hackerSigningKP.getPrivate(), null, DISPOSITION_NOTIFICATION_TO, + SIGNED_RECEIPT_MIC_ALGORITHMS, null, null, null); + + MockEndpoint mockEndpoint = getMockEndpoint("mock:as2RcvMsgs"); + mockEndpoint.expectedMinimumMessageCount(1); + mockEndpoint.setResultWaitTime(TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS)); + mockEndpoint.assertIsSatisfied(); + + final List<Exchange> exchanges = mockEndpoint.getExchanges(); + assertNotNull(exchanges, "listen result"); + assertFalse(exchanges.isEmpty(), "listen result"); + LOG.debug("poll result: " + exchanges); + + Exchange exchange = exchanges.get(0); + Message message = exchange.getIn(); + assertNotNull(message, "exchange message"); + HttpCoreContext coreContext = exchange.getProperty(AS2Constants.AS2_INTERCHANGE, HttpCoreContext.class); + assertNotNull(coreContext, "context"); + HttpRequest request = coreContext.getRequest(); + assertNotNull(request, "request"); + assertEquals(METHOD, request.getRequestLine().getMethod(), "Unexpected method value"); + assertEquals(REQUEST_URI, request.getRequestLine().getUri(), "Unexpected request URI value"); + assertEquals(HttpVersion.HTTP_1_1, request.getRequestLine().getProtocolVersion(), "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"); + assertTrue(request.getFirstHeader(AS2Header.CONTENT_TYPE).getValue().startsWith(AS2MediaType.MULTIPART_SIGNED), + "Unexpected content type for message"); + + assertTrue(request instanceof BasicHttpEntityEnclosingRequest, "Request does not contain entity"); + HttpEntity entity = ((BasicHttpEntityEnclosingRequest) request).getEntity(); + assertNotNull(entity, "Request does not contain entity"); + assertTrue(entity instanceof MultipartSignedEntity, "Unexpected request entity type"); + MultipartSignedEntity signedEntity = (MultipartSignedEntity) entity; + assertTrue(signedEntity.isMainBody(), "Entity not set as main body of request"); + assertEquals(2, signedEntity.getPartCount(), "Request contains invalid number of mime parts"); + + // Validated first mime part. + assertTrue(signedEntity.getPart(0) instanceof ApplicationEDIFACTEntity, "First mime part incorrect type "); + ApplicationEDIFACTEntity ediEntity = (ApplicationEDIFACTEntity) signedEntity.getPart(0); + assertTrue(ediEntity.getContentType().getValue().startsWith(AS2MediaType.APPLICATION_EDIFACT), + "Unexpected content type for first mime part"); + assertFalse(ediEntity.isMainBody(), "First mime type set as main body of request"); + + // Validate second mime part. + assertTrue(signedEntity.getPart(1) instanceof ApplicationPkcs7SignatureEntity, "Second mime part incorrect type "); + ApplicationPkcs7SignatureEntity signatureEntity = (ApplicationPkcs7SignatureEntity) signedEntity.getPart(1); + assertTrue(signatureEntity.getContentType().getValue().startsWith(AS2MediaType.APPLICATION_PKCS7_SIGNATURE), + "Unexpected content type for second mime part"); + assertFalse(signatureEntity.isMainBody(), "First mime type set as main body of request"); + + // Validate Signature + assertFalse(SigningUtils.isValid(signedEntity, new Certificate[] { signingCert }), "Signature must be invalid"); String rcvdMessage = message.getBody(String.class); assertEquals(EDI_MESSAGE.replaceAll("[\n\r]", ""), rcvdMessage.replaceAll("[\n\r]", ""),
