diff --git a/README.md b/README.md index 1fa2c07..4d413ed 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ To create the signed message the following call is required: ```java String signedMessaged=new SignedCertificateMessageBuilder() .withSigningCertificate(signingCert,signingCertPrivateKey) - .withPayloadCertificate(inputCert) + .withPayload(inputCert) .buildAsString(); ``` @@ -232,9 +232,9 @@ It is also possible to create a detached signature. Just pass the boolean value or ```buildAsString()``` method. ```java -String detachedSignature=new SignedCertificateMessageBuilder() +String detachedSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(signingCert,signingCertPrivateKey) - .withPayloadCertificate(inputCert) + .withPayload(inputCert) .buildAsString(true); ``` @@ -245,13 +245,13 @@ When a certificate is received it needs to be "unpacked". To do so the SignedCer Simply instantiate ```SignedCertificateMessageParser``` with the base64 encoded String. ```java -SignedCertificateMessageParser parser=new SignedCertificateMessageParser(inputString); +SignedCertificateMessageParser parser = new SignedCertificateMessageParser(inputString); ``` The constructor accepts different formats as incoming message. Also detached signatures are accepted. ```java -SignedCertificateMessageParser parser=new SignedCertificateMessageParser(payloadByteArray,signatureString); +SignedCertificateMessageParser parser = new SignedCertificateMessageParser(payloadByteArray, signatureString); ``` All combinations of String and byte[] as parameter for signature and payload are possible. Please be aware that the @@ -260,7 +260,7 @@ payload will be always base64 encoded (even if it is passed as byte[]). The parser will immediately parse the message. The result can be obtained from ```java -parser.getParserState() +parser.getParserState(); ``` If the state is ```SUCCESS``` the syntax of the message was correct and the certificate could be parsed. @@ -269,14 +269,14 @@ Also the parser checks if the signature of the CMS message was created by the em result of this check just read the property ```java -parser.isSignatureVerified() +parser.isSignatureVerified(); ``` The signer certificate and the containing certificate can be accessed by ```java -parser.getSigningCertificate() - parser.getPayloadCertificate() +parser.getSigningCertificate(); +parser.getPayload(); ``` Also a detached signature can be gained from parsed message @@ -285,6 +285,20 @@ Also a detached signature can be gained from parsed message parser.getSignature() ``` +#### Signing and Parsing of other data types + +It is also possible to sign and parse CMS messages with other data types. +Therefore, implementations of ```SignedMessageBuilder``` and ```SignedMessageParser``` are required. + +The DGC-Lib currently contains Implementations for the following classes: + +| Data Type | Parser Implementation | Builder Implementation | +| --- | --- | --- | +| java.lang.String | SignedStringMessageParser | SignedStringMessageBuilder | +| org.bouncycastle.cert.X509CertificateHolder | SignedCertificateMessageParser | SignedCertificateMessageBuilder | + +The usage of Parser and Builder can be seen in the chapter above. Basic usage is equal for all implementations of Parser and Builder. + ### Utils ### Certificate Utils @@ -363,12 +377,9 @@ The following channels are available for discussions, feedback, and support requ | Type | Channel | | ------------------------- | ------------------------------------------------------ | -| **DGC Gateway -issues** | | -| **DGC Lib -issues** | | -| **Other -requests** | | +| **DGC Gateway issues** | | +| **DGC Lib issues** | | +| **Other requests** | | ## How to contribute diff --git a/src/main/java/eu/europa/ec/dgc/signing/SignedCertificateMessageBuilder.java b/src/main/java/eu/europa/ec/dgc/signing/SignedCertificateMessageBuilder.java index 7cffacc..0bed794 100644 --- a/src/main/java/eu/europa/ec/dgc/signing/SignedCertificateMessageBuilder.java +++ b/src/main/java/eu/europa/ec/dgc/signing/SignedCertificateMessageBuilder.java @@ -21,49 +21,25 @@ package eu.europa.ec.dgc.signing; import java.io.IOException; -import java.security.PrivateKey; -import java.security.Security; -import java.util.Base64; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cms.CMSException; -import org.bouncycastle.cms.CMSProcessableByteArray; -import org.bouncycastle.cms.CMSSignedData; -import org.bouncycastle.cms.CMSSignedDataGenerator; -import org.bouncycastle.cms.SignerInfoGenerator; -import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.DefaultAlgorithmNameFinder; -import org.bouncycastle.operator.DigestCalculatorProvider; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; /** * Utility to build a CMS signed message containing a DER encoded X509 certificate. */ @Slf4j @NoArgsConstructor -public class SignedCertificateMessageBuilder { +public class SignedCertificateMessageBuilder + extends SignedMessageBuilder { - private X509CertificateHolder payloadCertificate; - - private X509CertificateHolder signingCertificate; - - private PrivateKey signingCertificatePrivateKey; + @Override + byte[] convertToBytes(X509CertificateHolder payload) throws IOException { + return payload.getEncoded(); + } - /** - * Add a signing certificate to MessageBuilder instance. - * - * @param certificate X509 Certificate to sign the message with - * @param privateKey Private key for given X509 Certificate. - */ - public SignedCertificateMessageBuilder withSigningCertificate( - X509CertificateHolder certificate, PrivateKey privateKey) { - signingCertificate = certificate; - signingCertificatePrivateKey = privateKey; + @Override + SignedCertificateMessageBuilder getThis() { return this; } @@ -71,84 +47,11 @@ public SignedCertificateMessageBuilder withSigningCertificate( * Add a payload certificate to MessageBuilder instance. * * @param certificate X509 certificate for payload. + * @deprecated Use .withPayload(X509CertificateHolder) instead */ + @Deprecated public SignedCertificateMessageBuilder withPayloadCertificate(X509CertificateHolder certificate) { - payloadCertificate = certificate; - return this; + return withPayload(certificate); } - /** - *

Builds the CMS signed certificate message.

- *

payloadCertificate and SigningCertificate needs to be set previously.

- * - * @param detached flag whether only the signature should be returned (detached signature) - * @return Bytes of signed CMS message. - */ - public byte[] build(boolean detached) { - Security.addProvider(new BouncyCastleProvider()); - - if (payloadCertificate == null || signingCertificate == null || signingCertificatePrivateKey == null) { - throw new RuntimeException("Message Builder is not ready"); - } - - byte[] messageBytes; - CMSSignedDataGenerator signedDataGenerator = new CMSSignedDataGenerator(); - - try { - DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build(); - - String signingAlgorithmName = - new DefaultAlgorithmNameFinder().getAlgorithmName(signingCertificate.getSignatureAlgorithm()); - - ContentSigner contentSigner = - new JcaContentSignerBuilder(signingAlgorithmName).build(signingCertificatePrivateKey); - - SignerInfoGenerator signerInfoGenerator = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider) - .build(contentSigner, signingCertificate); - - signedDataGenerator.addSignerInfoGenerator(signerInfoGenerator); - - signedDataGenerator.addCertificate(signingCertificate); - - CMSSignedData signedData = signedDataGenerator.generate( - new CMSProcessableByteArray(payloadCertificate.getEncoded()), !detached); - - messageBytes = signedData.getEncoded(); - } catch (OperatorCreationException | CMSException | IOException e) { - throw new RuntimeException("Failed to create signed message"); - } - - return messageBytes; - } - - /** - *

Builds the CMS signed certificate message.

- *

payloadCertificate and SigningCertificate needs to be set previously.

- * - * @return Bytes of signed CMS message. - */ - public byte[] build() { - return build(false); - } - - /** - *

Builds the CMS signed certificate message.

- *

payloadCertificate and SigningCertificate needs to be set previously.

- * - * @param detached flag whether only the signature should be returned (detached signature) - * @return Base64 encoded String of CMS message. - */ - public String buildAsString(boolean detached) { - return Base64.getEncoder().encodeToString(build(detached)); - } - - /** - *

Builds the CMS signed certificate message.

- *

payloadCertificate and SigningCertificate needs to be set previously.

- * - * @return Base64 encoded String of signed CMS message. - */ - public String buildAsString() { - return Base64.getEncoder().encodeToString(build(false)); - } } diff --git a/src/main/java/eu/europa/ec/dgc/signing/SignedCertificateMessageParser.java b/src/main/java/eu/europa/ec/dgc/signing/SignedCertificateMessageParser.java index 27ab0fb..b96ac29 100644 --- a/src/main/java/eu/europa/ec/dgc/signing/SignedCertificateMessageParser.java +++ b/src/main/java/eu/europa/ec/dgc/signing/SignedCertificateMessageParser.java @@ -20,74 +20,31 @@ package eu.europa.ec.dgc.signing; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.Security; -import java.security.cert.CertificateException; -import java.util.Base64; -import java.util.Collection; -import lombok.Getter; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; -import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cms.CMSException; -import org.bouncycastle.cms.CMSProcessableByteArray; -import org.bouncycastle.cms.CMSSignedData; -import org.bouncycastle.cms.CMSSignedDataGenerator; -import org.bouncycastle.cms.SignerInformation; -import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.OperatorCreationException; /** * Utility to parse a CMS signed message containing a DER encoded X509 Certificate. */ @Slf4j -public class SignedCertificateMessageParser { +public class SignedCertificateMessageParser extends SignedMessageParser { - private final byte[] raw; - private final byte[] rawPayload; + @Override + X509CertificateHolder convertFromBytes(byte[] bytes) throws Exception { + return new X509CertificateHolder(bytes); + } /** * The extracted payload certificate. - */ - @Getter - private X509CertificateHolder payloadCertificate; - - /** - * The certificate which was used to sign the message. - */ - @Getter - private X509CertificateHolder signingCertificate; - - /** - *

The result of parsing the CMS message.

* - *

Result of SUCCESS does not mean that the signature of the signed message is valid. - * Pay attention to the value of signatureVerified. Only in conjunction of - * parserState == ParserState.SUCCESS and signatureVerified == true a valid certificate CMS - * message was passed to the parser.

+ * @deprecated use .getPayload() instead. + * @return certificate. */ - @Getter - private ParserState parserState = ParserState.IDLE; - - /** - *

Result of the integrity check of the cms message.

- * - *

The result just proofs, that the message was signed with the attached certificate. - * The integrity of the signer certificate is not proven

- */ - @Getter - private boolean signatureVerified = false; - - /** - *

Base64 encoded signature of the cms message.

- * - *

This string contains only the signature which signs the message.

- */ - @Getter - private String signature; + @Deprecated + public X509CertificateHolder getPayloadCertificate() { + return getPayload(); + } /** * Create a new instance of {@link SignedCertificateMessageParser} and starts the parsing process. @@ -96,9 +53,7 @@ public class SignedCertificateMessageParser { * @param cmsMessage base64 encoded CMS message bytes. */ public SignedCertificateMessageParser(@NonNull byte[] cmsMessage) { - raw = cmsMessage; - rawPayload = null; - afterPropertiesSet(); + super(cmsMessage); } /** @@ -109,9 +64,7 @@ public SignedCertificateMessageParser(@NonNull byte[] cmsMessage) { * @param cmsPayload base64 encoded CMS message payload. */ public SignedCertificateMessageParser(@NonNull byte[] cmsSignature, @NonNull byte[] cmsPayload) { - raw = cmsSignature; - rawPayload = cmsPayload; - afterPropertiesSet(); + super(cmsSignature, cmsPayload); } /** @@ -121,9 +74,7 @@ public SignedCertificateMessageParser(@NonNull byte[] cmsSignature, @NonNull byt * @param cmsMessage base64 encoded CMS message string. */ public SignedCertificateMessageParser(@NonNull String cmsMessage) { - raw = cmsMessage.getBytes(StandardCharsets.UTF_8); - rawPayload = null; - afterPropertiesSet(); + super(cmsMessage); } /** @@ -134,9 +85,7 @@ public SignedCertificateMessageParser(@NonNull String cmsMessage) { * @param cmsPayload base64 encoded CMS message payload string. */ public SignedCertificateMessageParser(@NonNull String cmsSignature, @NonNull String cmsPayload) { - raw = cmsSignature.getBytes(StandardCharsets.UTF_8); - rawPayload = cmsPayload.getBytes(StandardCharsets.UTF_8); - afterPropertiesSet(); + super(cmsSignature, cmsPayload); } /** @@ -147,9 +96,7 @@ public SignedCertificateMessageParser(@NonNull String cmsSignature, @NonNull Str * @param cmsPayload base64 encoded CMS message payload string. */ public SignedCertificateMessageParser(@NonNull byte[] cmsSignature, @NonNull String cmsPayload) { - raw = cmsSignature; - rawPayload = cmsPayload.getBytes(StandardCharsets.UTF_8); - afterPropertiesSet(); + super(cmsSignature, cmsPayload); } /** @@ -160,120 +107,6 @@ public SignedCertificateMessageParser(@NonNull byte[] cmsSignature, @NonNull Str * @param cmsPayload base64 encoded CMS message payload bytes. */ public SignedCertificateMessageParser(@NonNull String cmsSignature, @NonNull byte[] cmsPayload) { - raw = cmsSignature.getBytes(StandardCharsets.UTF_8); - rawPayload = cmsPayload; - afterPropertiesSet(); - } - - private void afterPropertiesSet() { - Security.addProvider(new BouncyCastleProvider()); - - // Parse Base64 - byte[] cmsBytes; - byte[] cmsPayloadBytes = null; - try { - cmsBytes = Base64.getDecoder().decode(raw); - - if (rawPayload != null) { - cmsPayloadBytes = Base64.getDecoder().decode(rawPayload); - } - - } catch (IllegalArgumentException e) { - parserState = ParserState.FAILURE_INVALID_BASE64; - return; - } - - // Parse CMS Message; - CMSSignedData cmsSignedData; - try { - if (rawPayload == null) { - cmsSignedData = new CMSSignedData(cmsBytes); - } else { - CMSProcessableByteArray cmsProcessablePayload = new CMSProcessableByteArray(cmsPayloadBytes); - cmsSignedData = new CMSSignedData(cmsProcessablePayload, cmsBytes); - } - } catch (CMSException e) { - parserState = ParserState.FAILURE_INVALID_CMS; - return; - } - - // Check Payload of CMS Message - if (cmsSignedData.getSignedContent().getContentType() != CMSObjectIdentifiers.data) { - parserState = ParserState.FAILURE_INVALID_CMS_BODY; - return; - } - - // Extract Certificate from Payload - try { - payloadCertificate = new X509CertificateHolder( - (byte[]) cmsSignedData.getSignedContent().getContent()); - } catch (IOException e) { - parserState = ParserState.FAILURE_CMS_BODY_NO_CERTIFICATE; - return; - } - - // Get signer certificate - Collection certificateHolderCollection = - cmsSignedData.getCertificates().getMatches(null); - - if (certificateHolderCollection.size() != 1) { - log.error("Signed Message contains more than 1 certificate"); - parserState = ParserState.FAILURE_CMS_SIGNING_CERT_INVALID; - return; - } - signingCertificate = certificateHolderCollection.iterator().next(); - - // Try to extract detached CMS Signature - try { - signature = Base64.getEncoder().encodeToString(repackToDetachedCms(cmsSignedData).getEncoded()); - } catch (IOException | CMSException e) { - signature = null; - log.error("Failed to repack CMS to get detached signature."); - } - - // Get signer information and verify signature - if (cmsSignedData.getSignerInfos().size() != 1) { - log.error("Signed Message contains more than 1 signer information"); - parserState = ParserState.FAILURE_CMS_SIGNER_INFO; - return; - } - SignerInformation signerInformation = cmsSignedData.getSignerInfos().iterator().next(); - try { - signatureVerified = signerInformation.verify( - new JcaSimpleSignerInfoVerifierBuilder().build(signingCertificate) - ); - } catch (CMSException | OperatorCreationException | CertificateException e) { - log.error("Failed to validate Signature"); - } - - parserState = ParserState.SUCCESS; - } - - /** - * Recreates a CMS without encapsulated Data. - * - * @param input input CMS Message - * @return CMS message without encapsulated data. - * @throws CMSException if repacking fails. - */ - private CMSSignedData repackToDetachedCms(CMSSignedData input) throws CMSException { - CMSSignedDataGenerator cmsGenerator = new CMSSignedDataGenerator(); - cmsGenerator.addCertificates(input.getCertificates()); - cmsGenerator.addSigners(input.getSignerInfos()); - cmsGenerator.addAttributeCertificates(input.getAttributeCertificates()); - cmsGenerator.addCRLs(input.getCRLs()); - - return cmsGenerator.generate(input.getSignedContent(), false); - } - - public enum ParserState { - IDLE, - SUCCESS, - FAILURE_INVALID_BASE64, - FAILURE_INVALID_CMS, - FAILURE_INVALID_CMS_BODY, - FAILURE_CMS_BODY_NO_CERTIFICATE, - FAILURE_CMS_SIGNER_INFO, - FAILURE_CMS_SIGNING_CERT_INVALID + super(cmsSignature, cmsPayload); } } diff --git a/src/main/java/eu/europa/ec/dgc/signing/SignedMessageBuilder.java b/src/main/java/eu/europa/ec/dgc/signing/SignedMessageBuilder.java new file mode 100644 index 0000000..cb2ad74 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/signing/SignedMessageBuilder.java @@ -0,0 +1,172 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed 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. + * ---license-end + */ + +package eu.europa.ec.dgc.signing; + +import java.io.IOException; +import java.security.PrivateKey; +import java.security.Security; +import java.util.Base64; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.SignerInfoGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DefaultAlgorithmNameFinder; +import org.bouncycastle.operator.DigestCalculatorProvider; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; + +/** + *

Abstract class to build Typed CMS Message builder.

+ *

+ * T -> DataType which needs to be signed with CMS.
+ * R -> Implemented Subclass + *

+ */ +@Slf4j +@NoArgsConstructor +public abstract class SignedMessageBuilder> { + + private T payload; + + private X509CertificateHolder signingCertificate; + + private PrivateKey signingCertificatePrivateKey; + + /** + * Method to convert the payload into bytes which can be used for signing. + * + * @param payload payload to convert in original data type. + * @return payload representation as byte array. + */ + abstract byte[] convertToBytes(T payload) throws IOException; + + /** + * Method which needs to return the instance of the subclass. + * This special workaround is needed to enable the builder style. + */ + abstract R getThis(); + + /** + * Add a signing certificate to MessageBuilder instance. + * + * @param certificate X509 Certificate to sign the message with + * @param privateKey Private key for given X509 Certificate. + */ + public R withSigningCertificate( + X509CertificateHolder certificate, PrivateKey privateKey) { + signingCertificate = certificate; + signingCertificatePrivateKey = privateKey; + return getThis(); + } + + /** + * Add a payload to MessageBuilder instance. + * + * @param payload to be added. + */ + public R withPayload(T payload) { + this.payload = payload; + return getThis(); + } + + /** + *

Builds the CMS signed message.

+ *

payload and SigningCertificate needs to be set previously.

+ * + * @param detached flag whether only the signature should be returned (detached signature) + * @return Bytes of signed CMS message. + */ + public byte[] build(boolean detached) { + Security.addProvider(new BouncyCastleProvider()); + + if (payload == null || signingCertificate == null || signingCertificatePrivateKey == null) { + throw new RuntimeException("Message Builder is not ready"); + } + + byte[] messageBytes; + CMSSignedDataGenerator signedDataGenerator = new CMSSignedDataGenerator(); + + try { + DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build(); + + String signingAlgorithmName = + new DefaultAlgorithmNameFinder().getAlgorithmName(signingCertificate.getSignatureAlgorithm()); + + ContentSigner contentSigner = + new JcaContentSignerBuilder(signingAlgorithmName).build(signingCertificatePrivateKey); + + SignerInfoGenerator signerInfoGenerator = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider) + .build(contentSigner, signingCertificate); + + signedDataGenerator.addSignerInfoGenerator(signerInfoGenerator); + + signedDataGenerator.addCertificate(signingCertificate); + + CMSSignedData signedData = signedDataGenerator.generate( + new CMSProcessableByteArray(convertToBytes(payload)), !detached); + + messageBytes = signedData.getEncoded(); + } catch (OperatorCreationException | CMSException | IOException e) { + throw new RuntimeException("Failed to create signed message"); + } + + return messageBytes; + } + + /** + *

Builds the CMS signed message.

+ *

payload and SigningCertificate needs to be set previously.

+ * + * @return Bytes of signed CMS message. + */ + public byte[] build() { + return build(false); + } + + /** + *

Builds the CMS signed message.

+ *

payload and SigningCertificate needs to be set previously.

+ * + * @param detached flag whether only the signature should be returned (detached signature) + * @return Base64 encoded String of CMS message. + */ + public String buildAsString(boolean detached) { + return Base64.getEncoder().encodeToString(build(detached)); + } + + /** + *

Builds the CMS signed message.

+ *

payload and SigningCertificate needs to be set previously.

+ * + * @return Base64 encoded String of signed CMS message. + */ + public String buildAsString() { + return Base64.getEncoder().encodeToString(build(false)); + } +} diff --git a/src/main/java/eu/europa/ec/dgc/signing/SignedMessageParser.java b/src/main/java/eu/europa/ec/dgc/signing/SignedMessageParser.java new file mode 100644 index 0000000..5429b13 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/signing/SignedMessageParser.java @@ -0,0 +1,294 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed 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. + * ---license-end + */ + +package eu.europa.ec.dgc.signing; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.Security; +import java.security.cert.CertificateException; +import java.util.Base64; +import java.util.Collection; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.OperatorCreationException; + +/** + * Abstract class to create typed CMS Message parsers. + */ +@Slf4j +public abstract class SignedMessageParser { + + private final byte[] raw; + private final byte[] rawPayload; + + /** + * The extracted payload. + */ + @Getter + private T payload; + + /** + * The certificate which was used to sign the message. + */ + @Getter + private X509CertificateHolder signingCertificate; + + /** + *

The result of parsing the CMS message.

+ * + *

Result of SUCCESS does not mean that the signature of the signed message is valid. + * Pay attention to the value of signatureVerified. Only in conjunction of + * parserState == ParserState.SUCCESS and signatureVerified == true a valid CMS + * message was passed to the parser.

+ */ + @Getter + private ParserState parserState = ParserState.IDLE; + + /** + *

Result of the integrity check of the cms message.

+ * + *

The result just proofs, that the message was signed with the attached certificate.

+ */ + @Getter + private boolean signatureVerified = false; + + /** + *

Base64 encoded signature of the cms message.

+ * + *

This string contains only the signature which signs the message.

+ */ + @Getter + private String signature; + + /** + * Method to convert the encoded bytes to the actual Class instance. + * + * @param bytes to convert. + * @return instance of T. + */ + abstract T convertFromBytes(byte[] bytes) throws Exception; + + /** + * Create a new instance of {@link SignedMessageParser} and starts the parsing process. + * The result of parsing process will be immediately available. + * + * @param cmsMessage base64 encoded CMS message bytes. + */ + protected SignedMessageParser(@NonNull byte[] cmsMessage) { + raw = cmsMessage; + rawPayload = null; + afterPropertiesSet(); + } + + /** + * Create a new instance of {@link SignedMessageParser} and starts the parsing process. + * The result of parsing process will be immediately available. + * + * @param cmsSignature base64 encoded detached CMS signature bytes. + * @param cmsPayload base64 encoded CMS message payload. + */ + protected SignedMessageParser(@NonNull byte[] cmsSignature, @NonNull byte[] cmsPayload) { + raw = cmsSignature; + rawPayload = cmsPayload; + afterPropertiesSet(); + } + + /** + * Create a new instance of {@link SignedMessageParser} and starts the parsing process. + * The result of parsing process will be immediately available. + * + * @param cmsMessage base64 encoded CMS message string. + */ + protected SignedMessageParser(@NonNull String cmsMessage) { + raw = cmsMessage.getBytes(StandardCharsets.UTF_8); + rawPayload = null; + afterPropertiesSet(); + } + + /** + * Create a new instance of {@link SignedMessageParser} and starts the parsing process. + * The result of parsing process will be immediately available. + * + * @param cmsSignature base64 encoded detached CMS signature string. + * @param cmsPayload base64 encoded CMS message payload string. + */ + protected SignedMessageParser(@NonNull String cmsSignature, @NonNull String cmsPayload) { + raw = cmsSignature.getBytes(StandardCharsets.UTF_8); + rawPayload = cmsPayload.getBytes(StandardCharsets.UTF_8); + afterPropertiesSet(); + } + + /** + * Create a new instance of {@link SignedMessageParser} and starts the parsing process. + * The result of parsing process will be immediately available. + * + * @param cmsSignature base64 encoded detached CMS signature bytes. + * @param cmsPayload base64 encoded CMS message payload string. + */ + protected SignedMessageParser(@NonNull byte[] cmsSignature, @NonNull String cmsPayload) { + raw = cmsSignature; + rawPayload = cmsPayload.getBytes(StandardCharsets.UTF_8); + afterPropertiesSet(); + } + + /** + * Create a new instance of {@link SignedMessageParser} and starts the parsing process. + * The result of parsing process will be immediately available. + * + * @param cmsSignature base64 encoded detached CMS signature string. + * @param cmsPayload base64 encoded CMS message payload bytes. + */ + protected SignedMessageParser(@NonNull String cmsSignature, @NonNull byte[] cmsPayload) { + raw = cmsSignature.getBytes(StandardCharsets.UTF_8); + rawPayload = cmsPayload; + afterPropertiesSet(); + } + + private void afterPropertiesSet() { + Security.addProvider(new BouncyCastleProvider()); + + // Parse Base64 + byte[] cmsBytes; + byte[] cmsPayloadBytes = null; + try { + cmsBytes = Base64.getDecoder().decode(raw); + + if (rawPayload != null) { + cmsPayloadBytes = Base64.getDecoder().decode(rawPayload); + } + + } catch (IllegalArgumentException e) { + parserState = ParserState.FAILURE_INVALID_BASE64; + return; + } + + // Parse CMS Message; + CMSSignedData cmsSignedData; + try { + if (rawPayload == null) { + cmsSignedData = new CMSSignedData(cmsBytes); + } else { + CMSProcessableByteArray cmsProcessablePayload = new CMSProcessableByteArray(cmsPayloadBytes); + cmsSignedData = new CMSSignedData(cmsProcessablePayload, cmsBytes); + } + } catch (CMSException e) { + parserState = ParserState.FAILURE_INVALID_CMS; + return; + } + + // Check Payload of CMS Message + if (cmsSignedData.getSignedContent().getContentType() != CMSObjectIdentifiers.data) { + parserState = ParserState.FAILURE_INVALID_CMS_BODY; + return; + } + + // Extract and convert payload + try { + payload = convertFromBytes((byte[]) cmsSignedData.getSignedContent().getContent()); + } catch (Exception e) { + parserState = ParserState.FAILURE_CMS_BODY_PARSING_FAILED; + return; + } + + // Get signer certificate + Collection certificateHolderCollection = + cmsSignedData.getCertificates().getMatches(null); + + if (certificateHolderCollection.size() != 1) { + log.error("Signed Message contains more than 1 certificate"); + parserState = ParserState.FAILURE_CMS_SIGNING_CERT_INVALID; + return; + } + signingCertificate = certificateHolderCollection.iterator().next(); + + // Try to extract detached CMS Signature + try { + signature = Base64.getEncoder().encodeToString(repackToDetachedCms(cmsSignedData).getEncoded()); + } catch (IOException | CMSException e) { + signature = null; + log.error("Failed to repack CMS to get detached signature."); + } + + // Get signer information and verify signature + if (cmsSignedData.getSignerInfos().size() != 1) { + log.error("Signed Message contains more than 1 signer information"); + parserState = ParserState.FAILURE_CMS_SIGNER_INFO; + return; + } + SignerInformation signerInformation = cmsSignedData.getSignerInfos().iterator().next(); + try { + signatureVerified = signerInformation.verify( + new JcaSimpleSignerInfoVerifierBuilder().build(signingCertificate) + ); + } catch (CMSException | OperatorCreationException | CertificateException e) { + log.error("Failed to validate Signature"); + } + + parserState = ParserState.SUCCESS; + } + + /** + * Recreates a CMS without encapsulated Data. + * + * @param input input CMS Message + * @return CMS message without encapsulated data. + * @throws CMSException if repacking fails. + */ + private CMSSignedData repackToDetachedCms(CMSSignedData input) throws CMSException { + CMSSignedDataGenerator cmsGenerator = new CMSSignedDataGenerator(); + cmsGenerator.addCertificates(input.getCertificates()); + cmsGenerator.addSigners(input.getSignerInfos()); + cmsGenerator.addAttributeCertificates(input.getAttributeCertificates()); + cmsGenerator.addCRLs(input.getCRLs()); + + return cmsGenerator.generate(input.getSignedContent(), false); + } + + public enum ParserState { + IDLE, + SUCCESS, + FAILURE_INVALID_BASE64, + FAILURE_INVALID_CMS, + FAILURE_INVALID_CMS_BODY, + + /** + * Deprecated ParserState Property. + * + * @deprecated Replaced by FAILURE_CMS_BODY_PARSING_FAILED + */ + @Deprecated + FAILURE_CMS_BODY_NO_CERTIFICATE, + + FAILURE_CMS_BODY_PARSING_FAILED, + FAILURE_CMS_SIGNER_INFO, + FAILURE_CMS_SIGNING_CERT_INVALID + } +} diff --git a/src/main/java/eu/europa/ec/dgc/signing/SignedStringMessageBuilder.java b/src/main/java/eu/europa/ec/dgc/signing/SignedStringMessageBuilder.java new file mode 100644 index 0000000..d97b0b9 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/signing/SignedStringMessageBuilder.java @@ -0,0 +1,43 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed 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. + * ---license-end + */ + +package eu.europa.ec.dgc.signing; + +import java.nio.charset.StandardCharsets; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Utility to build a CMS signed message containing a {@link String}. + */ +@Slf4j +@NoArgsConstructor +public class SignedStringMessageBuilder extends SignedMessageBuilder { + + @Override + byte[] convertToBytes(String payload) { + return payload.getBytes(StandardCharsets.UTF_8); + } + + @Override + SignedStringMessageBuilder getThis() { + return this; + } +} diff --git a/src/main/java/eu/europa/ec/dgc/signing/SignedStringMessageParser.java b/src/main/java/eu/europa/ec/dgc/signing/SignedStringMessageParser.java new file mode 100644 index 0000000..7ba4f67 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/signing/SignedStringMessageParser.java @@ -0,0 +1,101 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed 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. + * ---license-end + */ + +package eu.europa.ec.dgc.signing; + +import java.nio.charset.StandardCharsets; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +/** + * Utility to parse a CMS signed message containing a String. + */ +@Slf4j +public class SignedStringMessageParser extends SignedMessageParser { + + @Override + String convertFromBytes(byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8); + } + + /** + * Create a new instance of {@link SignedStringMessageParser} and starts the parsing process. + * The result of parsing process will be immediately available. + * + * @param cmsMessage base64 encoded CMS message bytes. + */ + public SignedStringMessageParser(@NonNull byte[] cmsMessage) { + super(cmsMessage); + } + + /** + * Create a new instance of {@link SignedStringMessageParser} and starts the parsing process. + * The result of parsing process will be immediately available. + * + * @param cmsSignature base64 encoded detached CMS signature bytes. + * @param cmsPayload base64 encoded CMS message payload. + */ + public SignedStringMessageParser(@NonNull byte[] cmsSignature, @NonNull byte[] cmsPayload) { + super(cmsSignature, cmsPayload); + } + + /** + * Create a new instance of {@link SignedStringMessageParser} and starts the parsing process. + * The result of parsing process will be immediately available. + * + * @param cmsMessage base64 encoded CMS message string. + */ + public SignedStringMessageParser(@NonNull String cmsMessage) { + super(cmsMessage); + } + + /** + * Create a new instance of {@link SignedStringMessageParser} and starts the parsing process. + * The result of parsing process will be immediately available. + * + * @param cmsSignature base64 encoded detached CMS signature string. + * @param cmsPayload base64 encoded CMS message payload string. + */ + public SignedStringMessageParser(@NonNull String cmsSignature, @NonNull String cmsPayload) { + super(cmsSignature, cmsPayload); + } + + /** + * Create a new instance of {@link SignedStringMessageParser} and starts the parsing process. + * The result of parsing process will be immediately available. + * + * @param cmsSignature base64 encoded detached CMS signature bytes. + * @param cmsPayload base64 encoded CMS message payload string. + */ + public SignedStringMessageParser(@NonNull byte[] cmsSignature, @NonNull String cmsPayload) { + super(cmsSignature, cmsPayload); + } + + /** + * Create a new instance of {@link SignedStringMessageParser} and starts the parsing process. + * The result of parsing process will be immediately available. + * + * @param cmsSignature base64 encoded detached CMS signature string. + * @param cmsPayload base64 encoded CMS message payload bytes. + */ + public SignedStringMessageParser(@NonNull String cmsSignature, @NonNull byte[] cmsPayload) { + super(cmsSignature, cmsPayload); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/connector/DownloadConnectorTest.java b/src/test/java/eu/europa/ec/dgc/gateway/connector/DownloadConnectorTest.java index fe729b4..4e75b5d 100644 --- a/src/test/java/eu/europa/ec/dgc/gateway/connector/DownloadConnectorTest.java +++ b/src/test/java/eu/europa/ec/dgc/gateway/connector/DownloadConnectorTest.java @@ -74,7 +74,7 @@ void testDownloadOfCertificates() throws Exception { String cscaSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(csca)) + .withPayload(certificateUtils.convertCertificate(csca)) .buildAsString(true); TrustListItemDto cscaTrustListItem = new TrustListItemDto(); @@ -91,7 +91,7 @@ void testDownloadOfCertificates() throws Exception { String uploadSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(upload)) + .withPayload(certificateUtils.convertCertificate(upload)) .buildAsString(true); TrustListItemDto uploadTrustListItem = new TrustListItemDto(); @@ -108,7 +108,7 @@ void testDownloadOfCertificates() throws Exception { String dscSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) - .withPayloadCertificate(certificateUtils.convertCertificate(dsc)) + .withPayload(certificateUtils.convertCertificate(dsc)) .buildAsString(true); TrustListItemDto dscTrustListItem = new TrustListItemDto(); @@ -148,7 +148,7 @@ void testThumbprintIntegrityCheck() throws Exception { String cscaSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(csca)) + .withPayload(certificateUtils.convertCertificate(csca)) .buildAsString(true); TrustListItemDto cscaTrustListItem = new TrustListItemDto(); @@ -165,7 +165,7 @@ void testThumbprintIntegrityCheck() throws Exception { String uploadSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(upload)) + .withPayload(certificateUtils.convertCertificate(upload)) .buildAsString(true); TrustListItemDto uploadTrustListItem = new TrustListItemDto(); @@ -183,7 +183,7 @@ void testThumbprintIntegrityCheck() throws Exception { String dscSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) - .withPayloadCertificate(certificateUtils.convertCertificate(dsc)) + .withPayload(certificateUtils.convertCertificate(dsc)) .buildAsString(true); TrustListItemDto dscTrustListItem = new TrustListItemDto(); @@ -221,7 +221,7 @@ void testThumbprintIntegrityCheckInvalidRawData() throws Exception { String cscaSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(csca)) + .withPayload(certificateUtils.convertCertificate(csca)) .buildAsString(true); TrustListItemDto cscaTrustListItem = new TrustListItemDto(); @@ -238,7 +238,7 @@ void testThumbprintIntegrityCheckInvalidRawData() throws Exception { String uploadSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(upload)) + .withPayload(certificateUtils.convertCertificate(upload)) .buildAsString(true); TrustListItemDto uploadTrustListItem = new TrustListItemDto(); @@ -255,7 +255,7 @@ void testThumbprintIntegrityCheckInvalidRawData() throws Exception { String dscSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) - .withPayloadCertificate(certificateUtils.convertCertificate(dsc)) + .withPayload(certificateUtils.convertCertificate(dsc)) .buildAsString(true); TrustListItemDto dscTrustListItem = new TrustListItemDto(); @@ -295,7 +295,7 @@ void testDownloadOfCertificatesShouldFailWrongTrustAnchorSignatureForCsca() thro String cscaSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(fakeTrustAnchor), fakeTrustAnchorKeyPair.getPrivate()) - .withPayloadCertificate(certificateUtils.convertCertificate(csca)) + .withPayload(certificateUtils.convertCertificate(csca)) .buildAsString(true); TrustListItemDto cscaTrustListItem = new TrustListItemDto(); @@ -312,7 +312,7 @@ void testDownloadOfCertificatesShouldFailWrongTrustAnchorSignatureForCsca() thro String uploadSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(upload)) + .withPayload(certificateUtils.convertCertificate(upload)) .buildAsString(true); TrustListItemDto uploadTrustListItem = new TrustListItemDto(); @@ -329,7 +329,7 @@ void testDownloadOfCertificatesShouldFailWrongTrustAnchorSignatureForCsca() thro String dscSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(dsc)) + .withPayload(certificateUtils.convertCertificate(dsc)) .buildAsString(true); TrustListItemDto dscTrustListItem = new TrustListItemDto(); @@ -362,7 +362,7 @@ void testDownloadOfCertificatesShouldFailWrongTrustAnchorSignatureForUpload() th String cscaSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(csca)) + .withPayload(certificateUtils.convertCertificate(csca)) .buildAsString(true); TrustListItemDto cscaTrustListItem = new TrustListItemDto(); @@ -382,7 +382,7 @@ void testDownloadOfCertificatesShouldFailWrongTrustAnchorSignatureForUpload() th String uploadSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(fakeTrustAnchor), fakeTrustAnchorKeyPair.getPrivate()) - .withPayloadCertificate(certificateUtils.convertCertificate(upload)) + .withPayload(certificateUtils.convertCertificate(upload)) .buildAsString(true); TrustListItemDto uploadTrustListItem = new TrustListItemDto(); @@ -399,7 +399,7 @@ void testDownloadOfCertificatesShouldFailWrongTrustAnchorSignatureForUpload() th String dscSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(dsc)) + .withPayload(certificateUtils.convertCertificate(dsc)) .buildAsString(true); TrustListItemDto dscTrustListItem = new TrustListItemDto(); @@ -518,7 +518,7 @@ void testCscaCheckWithInvalidDscRawData() throws Exception { String cscaSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(csca)) + .withPayload(certificateUtils.convertCertificate(csca)) .buildAsString(true); TrustListItemDto cscaTrustListItem = new TrustListItemDto(); @@ -535,7 +535,7 @@ void testCscaCheckWithInvalidDscRawData() throws Exception { String uploadSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(upload)) + .withPayload(certificateUtils.convertCertificate(upload)) .buildAsString(true); TrustListItemDto uploadTrustListItem = new TrustListItemDto(); @@ -552,7 +552,7 @@ void testCscaCheckWithInvalidDscRawData() throws Exception { String dscSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) - .withPayloadCertificate(certificateUtils.convertCertificate(dsc)) + .withPayload(certificateUtils.convertCertificate(dsc)) .buildAsString(true); TrustListItemDto dscTrustListItem = new TrustListItemDto(); @@ -601,7 +601,7 @@ void testTrustAnchorCheckInvalidSignatureFormat() throws Exception { String uploadSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(upload)) + .withPayload(certificateUtils.convertCertificate(upload)) .buildAsString(true); TrustListItemDto uploadTrustListItem = new TrustListItemDto(); @@ -618,7 +618,7 @@ void testTrustAnchorCheckInvalidSignatureFormat() throws Exception { String dscSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(dsc)) + .withPayload(certificateUtils.convertCertificate(dsc)) .buildAsString(true); TrustListItemDto dscTrustListItem = new TrustListItemDto(); @@ -654,7 +654,7 @@ void testTrustAnchorCheckWrongSignature() throws Exception { String uploadSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(upload)) + .withPayload(certificateUtils.convertCertificate(upload)) .buildAsString(true); TrustListItemDto cscaTrustListItem = new TrustListItemDto(); @@ -680,7 +680,7 @@ void testTrustAnchorCheckWrongSignature() throws Exception { String dscSignature = new SignedCertificateMessageBuilder() .withSigningCertificate(certificateUtils.convertCertificate(testKeyStore.getTrustAnchor()), testKeyStore.getTrustAnchorPrivateKey()) - .withPayloadCertificate(certificateUtils.convertCertificate(dsc)) + .withPayload(certificateUtils.convertCertificate(dsc)) .buildAsString(true); TrustListItemDto dscTrustListItem = new TrustListItemDto(); diff --git a/src/test/java/eu/europa/ec/dgc/gateway/connector/UploadConnectorTest.java b/src/test/java/eu/europa/ec/dgc/gateway/connector/UploadConnectorTest.java index 53e30b9..0def304 100644 --- a/src/test/java/eu/europa/ec/dgc/gateway/connector/UploadConnectorTest.java +++ b/src/test/java/eu/europa/ec/dgc/gateway/connector/UploadConnectorTest.java @@ -85,7 +85,7 @@ void testDownloadOfCertificates() throws Exception { SignedCertificateMessageParser parser = new SignedCertificateMessageParser(argumentCaptor.getValue()); - Assertions.assertEquals(certificateUtils.convertCertificate(dsc), parser.getPayloadCertificate()); + Assertions.assertEquals(certificateUtils.convertCertificate(dsc), parser.getPayload()); Assertions.assertEquals(certificateUtils.convertCertificate(testKeyStore.getUpload()), parser.getSigningCertificate()); } @@ -107,7 +107,7 @@ void testDownloadOfCertificatesHolder() throws Exception { SignedCertificateMessageParser parser = new SignedCertificateMessageParser(argumentCaptor.getValue()); - Assertions.assertEquals(dsc, parser.getPayloadCertificate()); + Assertions.assertEquals(dsc, parser.getPayload()); Assertions.assertEquals(certificateUtils.convertCertificate(testKeyStore.getUpload()), parser.getSigningCertificate()); } @@ -127,7 +127,7 @@ void testDeleteCertificates() throws Exception { SignedCertificateMessageParser parser = new SignedCertificateMessageParser(argumentCaptor.getValue()); - Assertions.assertEquals(certificateUtils.convertCertificate(dsc), parser.getPayloadCertificate()); + Assertions.assertEquals(certificateUtils.convertCertificate(dsc), parser.getPayload()); Assertions.assertEquals(certificateUtils.convertCertificate(testKeyStore.getUpload()), parser.getSigningCertificate()); } @@ -148,7 +148,7 @@ void testDeleteCertificatesHolder() throws Exception { SignedCertificateMessageParser parser = new SignedCertificateMessageParser(argumentCaptor.getValue()); - Assertions.assertEquals(dsc, parser.getPayloadCertificate()); + Assertions.assertEquals(dsc, parser.getPayload()); Assertions.assertEquals(certificateUtils.convertCertificate(testKeyStore.getUpload()), parser.getSigningCertificate()); } diff --git a/src/test/java/eu/europa/ec/dgc/signing/DeprecatedSignedCertificateMessageBuilderTest.java b/src/test/java/eu/europa/ec/dgc/signing/DeprecatedSignedCertificateMessageBuilderTest.java new file mode 100644 index 0000000..0801947 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/signing/DeprecatedSignedCertificateMessageBuilderTest.java @@ -0,0 +1,161 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed 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. + * ---license-end + */ + +package eu.europa.ec.dgc.signing; + +import eu.europa.ec.dgc.testdata.CertificateTestUtils; +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Collection; +import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class DeprecatedSignedCertificateMessageBuilderTest { + + KeyPair payloadKeyPair, signingKeyPair; + X509Certificate payloadCertificate, signingCertificate; + + SignedCertificateMessageBuilder builder; + + @BeforeEach + void setupTestData() throws Exception { + payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + payloadCertificate = CertificateTestUtils.generateCertificate(payloadKeyPair, "DE", "PayloadCertificate"); + + signingKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + signingCertificate = CertificateTestUtils.generateCertificate(signingKeyPair, "DE", "SigningCertificate"); + + builder = new SignedCertificateMessageBuilder() + .withPayloadCertificate(new X509CertificateHolder(payloadCertificate.getEncoded())) + .withSigningCertificate(new X509CertificateHolder(signingCertificate.getEncoded()), signingKeyPair.getPrivate()); + } + + @Test + void testUnreadyBuilder() { + builder = new SignedCertificateMessageBuilder(); + Assertions.assertThrows(RuntimeException.class, builder::buildAsString); + } + + @Test + void testCatchExceptionDuringBuild() throws IOException { + X509CertificateHolder certMock = mock(X509CertificateHolder.class); + when(certMock.getEncoded()).thenThrow(new IOException()); + + builder.withPayloadCertificate(certMock); + + Assertions.assertThrows(RuntimeException.class, builder::build); + } + + @Test + void testSignedMessage() throws Exception { + CMSSignedData cmsSignedData = new CMSSignedData(builder.build()); + + Assertions.assertEquals(CMSObjectIdentifiers.data, cmsSignedData.getSignedContent().getContentType()); + Assertions.assertArrayEquals(payloadCertificate.getEncoded(), (byte[]) cmsSignedData.getSignedContent().getContent()); + + X509CertificateHolder readPayloadCertificate = new X509CertificateHolder((byte[]) cmsSignedData.getSignedContent().getContent()); + Assertions.assertNotNull(readPayloadCertificate); + + Collection certificateHolderCollection = cmsSignedData.getCertificates().getMatches(null); + Assertions.assertEquals(1, certificateHolderCollection.size()); + X509CertificateHolder readSigningCertificate = certificateHolderCollection.iterator().next(); + Assertions.assertNotNull(readSigningCertificate); + + Assertions.assertEquals(1, cmsSignedData.getSignerInfos().size()); + SignerInformation signerInformation = cmsSignedData.getSignerInfos().iterator().next(); + + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(signingCertificate))); + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(readSigningCertificate))); + } + + @Test + void testSignedMessageDetached() throws Exception { + CMSSignedData cmsSignedData = new CMSSignedData(builder.build(true)); + + Assertions.assertNull(cmsSignedData.getSignedContent()); + + Collection certificateHolderCollection = cmsSignedData.getCertificates().getMatches(null); + Assertions.assertEquals(1, certificateHolderCollection.size()); + X509CertificateHolder readSigningCertificate = certificateHolderCollection.iterator().next(); + Assertions.assertNotNull(readSigningCertificate); + + cmsSignedData = new CMSSignedData(new CMSProcessableByteArray(payloadCertificate.getEncoded()), builder.build(true)); + + Assertions.assertEquals(1, cmsSignedData.getSignerInfos().size()); + SignerInformation signerInformation = cmsSignedData.getSignerInfos().iterator().next(); + + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(signingCertificate))); + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(readSigningCertificate))); + } + + @Test + void testSignedMessageBase64() throws Exception { + CMSSignedData cmsSignedData = new CMSSignedData(Base64.getDecoder().decode(builder.buildAsString())); + + Assertions.assertEquals(CMSObjectIdentifiers.data, cmsSignedData.getSignedContent().getContentType()); + Assertions.assertArrayEquals(payloadCertificate.getEncoded(), (byte[]) cmsSignedData.getSignedContent().getContent()); + + X509CertificateHolder readPayloadCertificate = new X509CertificateHolder((byte[]) cmsSignedData.getSignedContent().getContent()); + Assertions.assertNotNull(readPayloadCertificate); + + Collection certificateHolderCollection = cmsSignedData.getCertificates().getMatches(null); + Assertions.assertEquals(1, certificateHolderCollection.size()); + X509CertificateHolder readSigningCertificate = certificateHolderCollection.iterator().next(); + Assertions.assertNotNull(readSigningCertificate); + + Assertions.assertEquals(1, cmsSignedData.getSignerInfos().size()); + SignerInformation signerInformation = cmsSignedData.getSignerInfos().iterator().next(); + + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(signingCertificate))); + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(readSigningCertificate))); + } + + @Test + void testSignedMessageDetachedBase64() throws Exception { + CMSSignedData cmsSignedData = new CMSSignedData(Base64.getDecoder().decode(builder.buildAsString(true))); + + Assertions.assertNull(cmsSignedData.getSignedContent()); + + Collection certificateHolderCollection = cmsSignedData.getCertificates().getMatches(null); + Assertions.assertEquals(1, certificateHolderCollection.size()); + X509CertificateHolder readSigningCertificate = certificateHolderCollection.iterator().next(); + Assertions.assertNotNull(readSigningCertificate); + + cmsSignedData = new CMSSignedData(new CMSProcessableByteArray(payloadCertificate.getEncoded()), Base64.getDecoder().decode(builder.buildAsString(true))); + + Assertions.assertEquals(1, cmsSignedData.getSignerInfos().size()); + SignerInformation signerInformation = cmsSignedData.getSignerInfos().iterator().next(); + + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(signingCertificate))); + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(readSigningCertificate))); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/signing/SignedCertificateMessageBuilderTest.java b/src/test/java/eu/europa/ec/dgc/signing/SignedCertificateMessageBuilderTest.java index 1a1e011..96a8d7c 100644 --- a/src/test/java/eu/europa/ec/dgc/signing/SignedCertificateMessageBuilderTest.java +++ b/src/test/java/eu/europa/ec/dgc/signing/SignedCertificateMessageBuilderTest.java @@ -34,7 +34,6 @@ import org.bouncycastle.cms.SignerInformation; import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; import org.junit.jupiter.api.Assertions; -import static org.junit.jupiter.api.Assertions.assertNotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.mockito.Mockito.mock; @@ -56,14 +55,9 @@ void setupTestData() throws Exception { signingCertificate = CertificateTestUtils.generateCertificate(signingKeyPair, "DE", "SigningCertificate"); builder = new SignedCertificateMessageBuilder() - .withPayloadCertificate(new X509CertificateHolder(payloadCertificate.getEncoded())) + .withPayload(new X509CertificateHolder(payloadCertificate.getEncoded())) .withSigningCertificate(new X509CertificateHolder(signingCertificate.getEncoded()), signingKeyPair.getPrivate()); } - - @Test - void testDefineConstructor() { - assertNotNull(new SignedCertificateMessageBuilder()); - } @Test void testUnreadyBuilder() { @@ -76,7 +70,7 @@ void testCatchExceptionDuringBuild() throws IOException { X509CertificateHolder certMock = mock(X509CertificateHolder.class); when(certMock.getEncoded()).thenThrow(new IOException()); - builder.withPayloadCertificate(certMock); + builder.withPayload(certMock); Assertions.assertThrows(RuntimeException.class, builder::build); } diff --git a/src/test/java/eu/europa/ec/dgc/signing/SignedCertificateMessageParserTest.java b/src/test/java/eu/europa/ec/dgc/signing/SignedCertificateMessageParserTest.java index 98c1458..7069434 100644 --- a/src/test/java/eu/europa/ec/dgc/signing/SignedCertificateMessageParserTest.java +++ b/src/test/java/eu/europa/ec/dgc/signing/SignedCertificateMessageParserTest.java @@ -41,7 +41,6 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; import org.junit.jupiter.api.Assertions; -import static org.junit.jupiter.api.Assertions.assertNotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.mockito.Mockito.spy; @@ -54,11 +53,6 @@ class SignedCertificateMessageParserTest { SignedCertificateMessageBuilder builder; - @Test - void testDefineConstructor() { - assertNotNull(new SignedCertificateMessageParser("Hello World".getBytes())); - } - @BeforeEach void setupTestData() throws Exception { payloadKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); @@ -215,7 +209,7 @@ void parserShouldDetectInvalidCmsContent() throws Exception { SignedCertificateMessageParser parser = new SignedCertificateMessageParser(Base64.getEncoder().encode(signedData.getEncoded())); - Assertions.assertEquals(SignedCertificateMessageParser.ParserState.FAILURE_CMS_BODY_NO_CERTIFICATE, parser.getParserState()); + Assertions.assertEquals(SignedCertificateMessageParser.ParserState.FAILURE_CMS_BODY_PARSING_FAILED, parser.getParserState()); Assertions.assertFalse(parser.isSignatureVerified()); } diff --git a/src/test/java/eu/europa/ec/dgc/signing/SignedStringMessageBuilderTest.java b/src/test/java/eu/europa/ec/dgc/signing/SignedStringMessageBuilderTest.java new file mode 100644 index 0000000..8d94319 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/signing/SignedStringMessageBuilderTest.java @@ -0,0 +1,145 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed 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. + * ---license-end + */ + +package eu.europa.ec.dgc.signing; + +import eu.europa.ec.dgc.testdata.CertificateTestUtils; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Collection; +import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SignedStringMessageBuilderTest { + + KeyPair signingKeyPair; + X509Certificate signingCertificate; + String payloadString = "{ \"key\": \"HalloWeltABC\" }"; + + SignedStringMessageBuilder builder; + + @BeforeEach + void setupTestData() throws Exception { + signingKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + signingCertificate = CertificateTestUtils.generateCertificate(signingKeyPair, "DE", "SigningCertificate"); + + builder = new SignedStringMessageBuilder() + .withPayload(payloadString) + .withSigningCertificate(new X509CertificateHolder(signingCertificate.getEncoded()), signingKeyPair.getPrivate()); + } + + @Test + void testUnreadyBuilder() { + builder = new SignedStringMessageBuilder(); + Assertions.assertThrows(RuntimeException.class, builder::buildAsString); + } + + @Test + void testSignedMessage() throws Exception { + CMSSignedData cmsSignedData = new CMSSignedData(builder.build()); + + Assertions.assertEquals(CMSObjectIdentifiers.data, cmsSignedData.getSignedContent().getContentType()); + Assertions.assertArrayEquals(payloadString.getBytes(StandardCharsets.UTF_8), (byte[]) cmsSignedData.getSignedContent().getContent()); + + Assertions.assertEquals(payloadString, new String((byte[]) cmsSignedData.getSignedContent().getContent(), StandardCharsets.UTF_8)); + + Collection certificateHolderCollection = cmsSignedData.getCertificates().getMatches(null); + Assertions.assertEquals(1, certificateHolderCollection.size()); + X509CertificateHolder readSigningCertificate = certificateHolderCollection.iterator().next(); + Assertions.assertNotNull(readSigningCertificate); + + Assertions.assertEquals(1, cmsSignedData.getSignerInfos().size()); + SignerInformation signerInformation = cmsSignedData.getSignerInfos().iterator().next(); + + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(signingCertificate))); + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(readSigningCertificate))); + } + + @Test + void testSignedMessageDetached() throws Exception { + CMSSignedData cmsSignedData = new CMSSignedData(builder.build(true)); + + Assertions.assertNull(cmsSignedData.getSignedContent()); + + Collection certificateHolderCollection = cmsSignedData.getCertificates().getMatches(null); + Assertions.assertEquals(1, certificateHolderCollection.size()); + X509CertificateHolder readSigningCertificate = certificateHolderCollection.iterator().next(); + Assertions.assertNotNull(readSigningCertificate); + + cmsSignedData = new CMSSignedData(new CMSProcessableByteArray(payloadString.getBytes(StandardCharsets.UTF_8)), builder.build(true)); + + Assertions.assertEquals(1, cmsSignedData.getSignerInfos().size()); + SignerInformation signerInformation = cmsSignedData.getSignerInfos().iterator().next(); + + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(signingCertificate))); + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(readSigningCertificate))); + } + + @Test + void testSignedMessageBase64() throws Exception { + CMSSignedData cmsSignedData = new CMSSignedData(Base64.getDecoder().decode(builder.buildAsString())); + + Assertions.assertEquals(CMSObjectIdentifiers.data, cmsSignedData.getSignedContent().getContentType()); + Assertions.assertArrayEquals(payloadString.getBytes(StandardCharsets.UTF_8), (byte[]) cmsSignedData.getSignedContent().getContent()); + + Assertions.assertEquals(payloadString, new String((byte[]) cmsSignedData.getSignedContent().getContent(), StandardCharsets.UTF_8)); + + Collection certificateHolderCollection = cmsSignedData.getCertificates().getMatches(null); + Assertions.assertEquals(1, certificateHolderCollection.size()); + X509CertificateHolder readSigningCertificate = certificateHolderCollection.iterator().next(); + Assertions.assertNotNull(readSigningCertificate); + + Assertions.assertEquals(1, cmsSignedData.getSignerInfos().size()); + SignerInformation signerInformation = cmsSignedData.getSignerInfos().iterator().next(); + + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(signingCertificate))); + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(readSigningCertificate))); + } + + @Test + void testSignedMessageDetachedBase64() throws Exception { + CMSSignedData cmsSignedData = new CMSSignedData(Base64.getDecoder().decode(builder.buildAsString(true))); + + Assertions.assertNull(cmsSignedData.getSignedContent()); + + Collection certificateHolderCollection = cmsSignedData.getCertificates().getMatches(null); + Assertions.assertEquals(1, certificateHolderCollection.size()); + X509CertificateHolder readSigningCertificate = certificateHolderCollection.iterator().next(); + Assertions.assertNotNull(readSigningCertificate); + + cmsSignedData = new CMSSignedData(new CMSProcessableByteArray(payloadString.getBytes(StandardCharsets.UTF_8)), Base64.getDecoder().decode(builder.buildAsString(true))); + + Assertions.assertEquals(1, cmsSignedData.getSignerInfos().size()); + SignerInformation signerInformation = cmsSignedData.getSignerInfos().iterator().next(); + + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(signingCertificate))); + Assertions.assertTrue(signerInformation.verify(new JcaSimpleSignerInfoVerifierBuilder().build(readSigningCertificate))); + } +} diff --git a/src/test/java/eu/europa/ec/dgc/signing/SignedStringMessageParserTest.java b/src/test/java/eu/europa/ec/dgc/signing/SignedStringMessageParserTest.java new file mode 100644 index 0000000..7d34c8f --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/signing/SignedStringMessageParserTest.java @@ -0,0 +1,256 @@ +/*- + * ---license-start + * EU Digital Green Certificate Gateway Service / dgc-lib + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed 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. + * ---license-end + */ + +package eu.europa.ec.dgc.signing; + +import eu.europa.ec.dgc.testdata.CertificateTestUtils; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.SignerInfoGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DefaultAlgorithmNameFinder; +import org.bouncycastle.operator.DigestCalculatorProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +class SignedStringMessageParserTest { + + KeyPair signingKeyPair; + X509Certificate signingCertificate; + + SignedStringMessageBuilder builder; + String payloadString = "{ \"key\": \"HalloWeltABC\" }"; + + @BeforeEach + void setupTestData() throws Exception { + signingKeyPair = KeyPairGenerator.getInstance("ec").generateKeyPair(); + signingCertificate = CertificateTestUtils.generateCertificate(signingKeyPair, "DE", "SigningCertificate"); + + builder = new SignedStringMessageBuilder() + .withPayload(payloadString) + .withSigningCertificate(new X509CertificateHolder(signingCertificate.getEncoded()), signingKeyPair.getPrivate()); + } + + @Test + void parserShouldParseByteArray() throws IOException, CertificateEncodingException { + SignedStringMessageParser parser = new SignedStringMessageParser( + Base64.getEncoder().encode(builder.build())); + + Assertions.assertEquals(SignedStringMessageParser.ParserState.SUCCESS, parser.getParserState()); + Assertions.assertEquals(payloadString, parser.getPayload()); + Assertions.assertArrayEquals(signingCertificate.getEncoded(), parser.getSigningCertificate().getEncoded()); + Assertions.assertTrue(parser.isSignatureVerified()); + checkSignatureFromParser(parser.getSignature()); + } + + @Test + void parserShouldParseByteArrayWithDetachedPayload() throws IOException, CertificateEncodingException { + byte[] cms = Base64.getEncoder().encode(builder.build(true)); + + SignedStringMessageParser parser = new SignedStringMessageParser( + cms, + Base64.getEncoder().encode(payloadString.getBytes(StandardCharsets.UTF_8))); + + Assertions.assertEquals(SignedStringMessageParser.ParserState.SUCCESS, parser.getParserState()); + Assertions.assertEquals(payloadString, parser.getPayload()); + Assertions.assertArrayEquals(signingCertificate.getEncoded(), parser.getSigningCertificate().getEncoded()); + Assertions.assertTrue(parser.isSignatureVerified()); + Assertions.assertEquals(new String(cms), parser.getSignature()); + } + + @Test + void parserShouldParseByteArrayWithDetachedPayloadAsString() throws IOException, CertificateEncodingException { + byte[] cms = Base64.getEncoder().encode(builder.build(true)); + + SignedStringMessageParser parser = new SignedStringMessageParser( + cms, + Base64.getEncoder().encodeToString(payloadString.getBytes(StandardCharsets.UTF_8))); + + Assertions.assertEquals(SignedStringMessageParser.ParserState.SUCCESS, parser.getParserState()); + Assertions.assertEquals(payloadString, parser.getPayload()); + Assertions.assertArrayEquals(signingCertificate.getEncoded(), parser.getSigningCertificate().getEncoded()); + Assertions.assertTrue(parser.isSignatureVerified()); + checkSignatureFromParser(parser.getSignature()); + } + + @Test + void parserShouldParseString() throws IOException, CertificateEncodingException { + SignedStringMessageParser parser = new SignedStringMessageParser( + builder.buildAsString()); + + Assertions.assertEquals(SignedStringMessageParser.ParserState.SUCCESS, parser.getParserState()); + Assertions.assertEquals(payloadString, parser.getPayload()); + Assertions.assertArrayEquals(signingCertificate.getEncoded(), parser.getSigningCertificate().getEncoded()); + Assertions.assertTrue(parser.isSignatureVerified()); + checkSignatureFromParser(parser.getSignature()); + } + + @Test + void parserShouldParseStringWithDetachedPayload() throws IOException, CertificateEncodingException { + SignedStringMessageParser parser = new SignedStringMessageParser( + builder.buildAsString(true), + Base64.getEncoder().encode(payloadString.getBytes(StandardCharsets.UTF_8))); + + Assertions.assertEquals(SignedStringMessageParser.ParserState.SUCCESS, parser.getParserState()); + Assertions.assertEquals(payloadString, parser.getPayload()); + Assertions.assertArrayEquals(signingCertificate.getEncoded(), parser.getSigningCertificate().getEncoded()); + Assertions.assertTrue(parser.isSignatureVerified()); + checkSignatureFromParser(parser.getSignature()); + } + + @Test + void parserShouldDetectBrokenBase64() { + SignedStringMessageParser parser = new SignedStringMessageParser("randomBadBase64String"); + + Assertions.assertEquals(SignedStringMessageParser.ParserState.FAILURE_INVALID_BASE64, parser.getParserState()); + Assertions.assertFalse(parser.isSignatureVerified()); + } + + @Test + void parserShouldDetectBrokenCms() { + SignedStringMessageParser parser = new SignedStringMessageParser(Base64.getEncoder().encode("randomString".getBytes(StandardCharsets.UTF_8))); + + Assertions.assertEquals(SignedStringMessageParser.ParserState.FAILURE_INVALID_CMS, parser.getParserState()); + Assertions.assertFalse(parser.isSignatureVerified()); + } + + @Test + void parserShouldDetectInvalidCmsContentType() throws Exception { + DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build(); + + X509CertificateHolder signingCertificateHolder = new X509CertificateHolder(signingCertificate.getEncoded()); + + CMSSignedDataGenerator signedDataGenerator = new CMSSignedDataGenerator(); + + String signingAlgorithmName = + new DefaultAlgorithmNameFinder().getAlgorithmName(signingCertificateHolder.getSignatureAlgorithm()); + + ContentSigner contentSigner = + new JcaContentSignerBuilder(signingAlgorithmName).build(signingKeyPair.getPrivate()); + + SignerInfoGenerator signerInfoGenerator = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider) + .build(contentSigner, signingCertificate); + + signedDataGenerator.addSignerInfoGenerator(signerInfoGenerator); + + signedDataGenerator.addCertificate(signingCertificateHolder); + + + CMSProcessableByteArray cmsByteArrayMock = spy(new CMSProcessableByteArray(new byte[0])); + when(cmsByteArrayMock.getContentType()).thenReturn(CMSObjectIdentifiers.encryptedData); + + CMSSignedData signedData = signedDataGenerator.generate(cmsByteArrayMock, true); + + SignedStringMessageParser parser = new SignedStringMessageParser(Base64.getEncoder().encode(signedData.getEncoded())); + + Assertions.assertEquals(SignedStringMessageParser.ParserState.FAILURE_INVALID_CMS_BODY, parser.getParserState()); + Assertions.assertFalse(parser.isSignatureVerified()); + } + + @Test + void parserShouldDetectInvalidCertificateAmount() throws Exception { + DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build(); + + X509CertificateHolder signingCertificateHolder = new X509CertificateHolder(signingCertificate.getEncoded()); + + CMSSignedDataGenerator signedDataGenerator = new CMSSignedDataGenerator(); + + String signingAlgorithmName = + new DefaultAlgorithmNameFinder().getAlgorithmName(signingCertificateHolder.getSignatureAlgorithm()); + + ContentSigner contentSigner = + new JcaContentSignerBuilder(signingAlgorithmName).build(signingKeyPair.getPrivate()); + + SignerInfoGenerator signerInfoGenerator = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider) + .build(contentSigner, signingCertificate); + + signedDataGenerator.addSignerInfoGenerator(signerInfoGenerator); + + signedDataGenerator.addCertificate(signingCertificateHolder); + signedDataGenerator.addCertificate(signingCertificateHolder); + + CMSProcessableByteArray cmsByteArray = new CMSProcessableByteArray(payloadString.getBytes(StandardCharsets.UTF_8)); + CMSSignedData signedData = signedDataGenerator.generate(cmsByteArray, true); + + SignedStringMessageParser parser = new SignedStringMessageParser(Base64.getEncoder().encode(signedData.getEncoded())); + + Assertions.assertEquals(SignedStringMessageParser.ParserState.FAILURE_CMS_SIGNING_CERT_INVALID, parser.getParserState()); + Assertions.assertFalse(parser.isSignatureVerified()); + } + + @Test + void parserShouldDetectInvalidSignerInfoAmount() throws Exception { + DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build(); + + X509CertificateHolder signingCertificateHolder = new X509CertificateHolder(signingCertificate.getEncoded()); + + CMSSignedDataGenerator signedDataGenerator = new CMSSignedDataGenerator(); + + String signingAlgorithmName = + new DefaultAlgorithmNameFinder().getAlgorithmName(signingCertificateHolder.getSignatureAlgorithm()); + + ContentSigner contentSigner = + new JcaContentSignerBuilder(signingAlgorithmName).build(signingKeyPair.getPrivate()); + + SignerInfoGenerator signerInfoGenerator = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider) + .build(contentSigner, signingCertificate); + + signedDataGenerator.addSignerInfoGenerator(signerInfoGenerator); + signedDataGenerator.addSignerInfoGenerator(signerInfoGenerator); + + signedDataGenerator.addCertificate(signingCertificateHolder); + + CMSProcessableByteArray cmsByteArray = new CMSProcessableByteArray(payloadString.getBytes(StandardCharsets.UTF_8)); + CMSSignedData signedData = signedDataGenerator.generate(cmsByteArray, true); + + SignedStringMessageParser parser = new SignedStringMessageParser(Base64.getEncoder().encode(signedData.getEncoded())); + + Assertions.assertEquals(SignedStringMessageParser.ParserState.FAILURE_CMS_SIGNER_INFO, parser.getParserState()); + Assertions.assertFalse(parser.isSignatureVerified()); + } + + private void checkSignatureFromParser(String signature) throws CertificateEncodingException, IOException { + SignedStringMessageParser parser = new SignedStringMessageParser( + signature, Base64.getEncoder().encodeToString(payloadString.getBytes(StandardCharsets.UTF_8))); + + Assertions.assertEquals(SignedStringMessageParser.ParserState.SUCCESS, parser.getParserState()); + Assertions.assertEquals(payloadString, parser.getPayload()); + Assertions.assertEquals(new X509CertificateHolder(signingCertificate.getEncoded()), parser.getSigningCertificate()); + Assertions.assertTrue(parser.isSignatureVerified()); + Assertions.assertEquals(signature, parser.getSignature()); + } +} +