/* DigiDoc4J library * * This software is released under either the GNU Library General Public * License (see LICENSE.LGPL). * * Note that the only valid version of the LGPL license as far as this * project is concerned is the original GNU Library General Public License * Version 2.1, February 1999 */ package org.digidoc4j.impl.bdoc; import static org.digidoc4j.SignatureProfile.LT; import static org.digidoc4j.testutils.TestDataBuilder.createContainerWithFile; import static org.digidoc4j.testutils.TestDataBuilder.createEmptyBDocContainer; import static org.digidoc4j.testutils.TestDataBuilder.open; import static org.digidoc4j.testutils.TestDataBuilder.signContainer; import static org.digidoc4j.testutils.TestHelpers.containsErrorMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.io.InputStream; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.List; import org.apache.commons.lang.StringUtils; import org.digidoc4j.Configuration; import org.digidoc4j.Container; import org.digidoc4j.ContainerBuilder; import org.digidoc4j.ContainerOpener; import org.digidoc4j.DataToSign; import org.digidoc4j.SignatureBuilder; import org.digidoc4j.ValidationResult; import org.digidoc4j.exceptions.DigiDoc4JException; import org.digidoc4j.exceptions.DuplicateDataFileException; import org.digidoc4j.exceptions.InvalidTimestampException; import org.digidoc4j.exceptions.TimestampAfterOCSPResponseTimeException; import org.digidoc4j.exceptions.UnsupportedFormatException; import org.digidoc4j.exceptions.UntrustedRevocationSourceException; import org.digidoc4j.impl.DigiDoc4JTestHelper; import org.digidoc4j.impl.bdoc.tsl.TSLCertificateSourceImpl; import org.digidoc4j.testutils.TSLHelper; import org.digidoc4j.testutils.TestSigningHelper; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import eu.europa.esig.dss.DSSUtils; public class ValidationTests extends DigiDoc4JTestHelper { public static final Configuration PROD_CONFIGURATION = new Configuration(Configuration.Mode.PROD); public static final Configuration PROD_CONFIGURATION_WITH_TEST_POLICY = new Configuration(Configuration.Mode.PROD); String testContainerPath; @Rule public TemporaryFolder testFolder = new TemporaryFolder(); @Before public void setUp() throws Exception { testContainerPath = testFolder.newFile("container.bdoc").getPath(); } @BeforeClass public static void setUpOnce() throws Exception { PROD_CONFIGURATION_WITH_TEST_POLICY.setValidationPolicy("conf/test_constraint.xml"); } @Test public void testVerifySignedDocument() throws Exception { Container container = createSignedBDocDocument(testContainerPath); ValidationResult result = container.validate(); assertTrue(result.isValid()); } @Test public void testTestVerifyOnInvalidDocument() throws Exception { Container container = open("testFiles/invalid_container.bdoc"); assertFalse(container.validate().isValid()); } @Test public void testValidateEmptyDocument() { Container container = createEmptyBDocContainer(); ValidationResult result = container.validate(); assertTrue(result.isValid()); } @Test public void testValidate() throws Exception { Container container = createContainerWithFile("testFiles/test.txt", "text/plain"); signContainer(container); ValidationResult validationResult = container.validate(); assertEquals(0, validationResult.getErrors().size()); } @Test(expected = UnsupportedFormatException.class) public void notBDocThrowsException() { open("testFiles/notABDoc.bdoc"); } @Test(expected = UnsupportedFormatException.class) public void incorrectMimetypeThrowsException() { open("testFiles/incorrectMimetype.bdoc"); } @Ignore("Unable to test if OCSP responds with unknown, because the signing certificate is expired") @Test(expected = Exception.class) public void testOCSPUnknown() { try { testSigningWithOCSPCheck("testFiles/20167000013.p12"); } catch (Exception e) { assertTrue(e.getMessage().contains("UNKNOWN")); throw e; } } @Test(expected = Exception.class) public void testExpiredCertSign() { try { testSigningWithOCSPCheck("testFiles/expired_signer.p12"); } catch (Exception e) { assertTrue(e.getMessage().contains("not in certificate validity range")); throw e; } } @Test public void signatureFileContainsIncorrectFileName() { Container container = ContainerOpener.open("testFiles/filename_mismatch_signature.asice", PROD_CONFIGURATION); ValidationResult validate = container.validate(); List<DigiDoc4JException> errors = validate.getErrors(); assertEquals(1, errors.size()); assertContainsError("The reference data object(s) is not found!", errors); } @Test public void validateContainer_withChangedDataFileContent_isInvalid() throws Exception { Container container = ContainerOpener.open("testFiles/invalid-data-file.bdoc"); ValidationResult validate = container.validate(); assertEquals(1, validate.getErrors().size()); assertEquals("The reference data object(s) is not intact!", validate.getErrors().get(0).toString()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void secondSignatureFileContainsIncorrectFileName() throws IOException, CertificateException { Configuration configuration = new Configuration(Configuration.Mode.TEST); TSLHelper.addSkTsaCertificateToTsl(configuration); Container container = ContainerOpener.open("testFiles/filename_mismatch_second_signature.asice", configuration); ValidationResult validate = container.validate(); List<DigiDoc4JException> errors = validate.getErrors(); assertEquals(3, errors.size()); assertEquals("The reference data object(s) is not intact!", errors.get(0).toString()); assertEquals("Manifest file has an entry for file test.txt with mimetype text/plain but the signature file for " + "signature S1 does not have an entry for this file", errors.get(1).toString()); assertEquals("Container contains a file named test.txt which is not found in the signature file", errors.get(2).toString()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void manifestFileContainsIncorrectFileName() { Container container = ContainerOpener.open("testFiles/filename_mismatch_manifest.asice", PROD_CONFIGURATION_WITH_TEST_POLICY); ValidationResult validate = container.validate(); assertEquals(2, validate.getErrors().size()); assertEquals("Manifest file has an entry for file incorrect.txt with mimetype text/plain but the signature file " + "for signature S0 does not have an entry for this file", validate.getErrors().get(0).toString()); assertEquals("The signature file for signature S0 has an entry for file RELEASE-NOTES.txt with mimetype " + "text/plain but the manifest file does not have an entry for this file", validate.getErrors().get(1).toString()); } @Test public void container_withChangedDataFileName_shouldBeInvalid() throws Exception { Container container = open("testFiles/invalid-containers/bdoc-tm-with-changed-data-file-name.bdoc"); ValidationResult validate = container.validate(); assertEquals(1, validate.getErrors().size()); } @Test @SuppressWarnings("ThrowableResultOfMethodCallIgnored") public void revocationAndTimeStampDifferenceTooLarge() { Container container = ContainerOpener.open("testFiles/revocation_timestamp_delta_26h.asice", PROD_CONFIGURATION); ValidationResult validate = container.validate(); assertEquals(1, validate.getErrors().size()); assertEquals("The difference between the revocation time and the signature time stamp is too large", validate.getErrors().get(0).toString()); } @Test public void revocationAndTimeStampDifferenceNotTooLarge() { Configuration configuration = new Configuration(Configuration.Mode.PROD); int delta27Hours = 27 * 60; configuration.setRevocationAndTimestampDeltaInMinutes(delta27Hours); Container container = ContainerOpener.open("testFiles/revocation_timestamp_delta_26h.asice", configuration); ValidationResult validate = container.validate(); assertEquals(0, validate.getErrors().size()); } @Test public void signatureFileAndManifestFileContainDifferentMimeTypeForFile() { Container container = ContainerOpener.open("testFiles/mimetype_mismatch.asice", PROD_CONFIGURATION_WITH_TEST_POLICY); ValidationResult validate = container.validate(); assertEquals(1, validate.getErrors().size()); assertEquals("Manifest file has an entry for file RELEASE-NOTES.txt with mimetype application/pdf but the " + "signature file for signature S0 indicates the mimetype is text/plain", validate.getErrors().get(0).toString()); } @Test(expected = DuplicateDataFileException.class) public void duplicateFileThrowsException() { Container container = ContainerOpener.open("testFiles/22902_data_files_with_same_names.bdoc"); container.validate(); } @Test(expected = DigiDoc4JException.class) public void duplicateSignatureFileThrowsException() { Container container = ContainerOpener.open("testFiles/22913_signatures_xml_double.bdoc"); container.validate(); } @Test public void missingManifestFile() { Container container = ContainerOpener.open("testFiles/missing_manifest.asice", PROD_CONFIGURATION); ValidationResult result = container.validate(); assertFalse(result.isValid()); assertEquals("Unsupported format: Container does not contain a manifest file", result.getErrors().get(0).getMessage()); } @Test(expected = DigiDoc4JException.class) public void missingMimeTypeFile() { ContainerOpener.open("testFiles/missing_mimetype_file.asice"); } @Test public void containerHasFileWhichIsNotInManifestAndNotInSignatureFile() { Container container = ContainerOpener.open("testFiles/extra_file_in_container.asice", PROD_CONFIGURATION_WITH_TEST_POLICY); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertEquals(1, errors.size()); assertEquals("Container contains a file named AdditionalFile.txt which is not found in the signature file", errors.get(0).getMessage()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void containerMissesFileWhichIsInManifestAndSignatureFile() { Configuration configuration = new Configuration(Configuration.Mode.TEST); TSLHelper.addSkTsaCertificateToTsl(configuration); Container container = ContainerOpener.open("testFiles/zip_misses_file_which_is_in_manifest.asice"); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertContainsError("The reference data object(s) is not found!", errors); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void containerMissingOCSPData() { Container container = ContainerOpener.open("testFiles/TS-06_23634_TS_missing_OCSP_adjusted.asice"); ValidationResult validate = container.validate(); System.out.println(validate.getReport()); List<DigiDoc4JException> errors = validate.getErrors(); assertEquals(LT, container.getSignatures().get(0).getProfile()); assertContainsError("No revocation data for the certificate", errors); assertContainsError("Manifest file has an entry for file test.txt with mimetype text/plain but the signature file for signature S0 indicates the mimetype is application/octet-stream", errors); } @Ignore("This signature has two OCSP responses: one correct and one is technically corrupted. Opening a container should not throw an exception") @Test(expected = DigiDoc4JException.class) public void corruptedOCSPDataThrowsException() { ContainerOpener.open("testFiles/corrupted_ocsp_data.asice"); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void invalidNoncePolicyOid() { Container container = ContainerOpener.open("testFiles/23608_bdoc21-invalid-nonce-policy-oid.bdoc", PROD_CONFIGURATION); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertEquals(1, errors.size()); assertEquals("Wrong policy identifier: urn:oid:1.3.6.1.4.1.10015.1000.3.4.3", errors.get(0).toString()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void noNoncePolicy() { Container container = ContainerOpener.open("testFiles/23608_bdoc21-no-nonce-policy.bdoc", PROD_CONFIGURATION); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertEquals(1, errors.size()); assertEquals("The signature policy is not available!", errors.get(0).toString()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void badNonceContent() { Container container = ContainerOpener.open("testFiles/bdoc21-bad-nonce-content.bdoc", PROD_CONFIGURATION_WITH_TEST_POLICY); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertEquals(1, errors.size()); assertEquals("Nonce is invalid", errors.get(0).toString()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void noSignedPropRefTM() { Container container = ContainerOpener.open("testFiles/REF-03_bdoc21-TM-no-signedpropref.bdoc", PROD_CONFIGURATION_WITH_TEST_POLICY); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertEquals(2, errors.size()); assertContainsError("Signed properties missing", errors); assertContainsError("The reference data object(s) is not found!", errors); assertEquals(2, container.getSignatures().get(0).validateSignature().getErrors().size()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void noSignedPropRefTS() { Container container = ContainerOpener.open("testFiles/REF-03_bdoc21-TS-no-signedpropref.asice", PROD_CONFIGURATION_WITH_TEST_POLICY); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertEquals(2, errors.size()); assertContainsError("Signed properties missing", errors); assertContainsError("The reference data object(s) is not found!", errors); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void multipleSignedProperties() { Container container = ContainerOpener.open("testFiles/multiple_signed_properties.asice"); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); containsErrorMessage(errors, "Multiple signed properties"); containsErrorMessage(errors, "The signature is not intact!"); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void incorrectSignedPropertiesReference() { Container container = ContainerOpener.open("testFiles/signed_properties_reference_not_found.asice", PROD_CONFIGURATION_WITH_TEST_POLICY); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertEquals(1, errors.size()); assertEquals("The reference data object(s) is not found!", errors.get(0).toString()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void nonceIncorrectContent() { Container container = ContainerOpener.open("testFiles/nonce-vale-sisu.bdoc", PROD_CONFIGURATION_WITH_TEST_POLICY); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertEquals(3, errors.size()); assertEquals("Wrong policy identifier: urn:oid:1.3.6.1.4.1.10015.1000.2.10.10", errors.get(0).toString()); assertEquals("The reference data object(s) is not found!", errors.get(1).toString()); assertEquals("Nonce is invalid", errors.get(2).toString()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void badNoncePolicyOidQualifier() { Container container = ContainerOpener.open("testFiles/SP-03_bdoc21-bad-nonce-policy-oidasuri.bdoc", PROD_CONFIGURATION_WITH_TEST_POLICY); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertEquals(1, errors.size()); assertEquals("Wrong policy identifier qualifier: OIDAsURI", errors.get(0).toString()); assertEquals(1, container.getSignatures().get(0).validateSignature().getErrors().size()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void invalidNonce() { Container container = ContainerOpener.open("testFiles/23200_weakdigest-wrong-nonce.asice"); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertEquals(1, errors.size()); assertEquals("Nonce is invalid", errors.get(0).toString()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void noPolicyURI() { Container container = ContainerOpener.open("testFiles/SP-06_bdoc21-no-uri.bdoc", PROD_CONFIGURATION); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertEquals(1, errors.size()); assertEquals("The signature policy is not available!", errors.get(0).toString()); } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Test public void brokenTS() { Container container = ContainerOpener.open("testFiles/TS_broken_TS.asice"); ValidationResult result = container.validate(); List<DigiDoc4JException> errors = result.getErrors(); assertEquals(2, errors.size()); assertEquals(InvalidTimestampException.MESSAGE, errors.get(0).toString()); assertEquals(TimestampAfterOCSPResponseTimeException.MESSAGE, errors.get(1).toString()); } @Test public void asicValidationShouldFail_ifTimeStampHashDoesntMatchSignature() throws Exception { ValidationResult result = validateContainer("testFiles/TS-02_23634_TS_wrong_SignatureValue.asice"); assertFalse(result.isValid()); assertTrue(containsErrorMessage(result.getErrors(), InvalidTimestampException.MESSAGE)); } @Test public void asicOcspTimeShouldBeAfterTimestamp() throws Exception { ValidationResult result = validateContainer("testFiles/TS-08_23634_TS_OCSP_before_TS.asice"); assertFalse(result.isValid()); assertTrue(result.getErrors().size() >= 1); assertTrue(containsErrorMessage(result.getErrors(), TimestampAfterOCSPResponseTimeException.MESSAGE)); } @Test public void containerWithTMProfile_SignedWithExpiredCertificate_shouldBeInvalid() throws Exception { assertFalse(validateContainer("testFiles/invalid_bdoc_tm_old-sig-sigat-NOK-prodat-NOK.bdoc").isValid()); assertFalse(validateContainer("testFiles/invalid_bdoc_tm_old-sig-sigat-OK-prodat-NOK.bdoc").isValid()); } @Test public void containerWithTSProfile_SignedWithExpiredCertificate_shouldBeInvalid() throws Exception { ValidationResult result = validateContainer("testFiles/invalid_bdoc21-TS-old-cert.bdoc"); assertFalse(result.isValid()); } @Test public void bdocTM_signedWithValidCert_isExpiredByNow_shouldBeValid() throws Exception { String containerPath = "testFiles/valid_bdoc_tm_signed_with_valid_cert_expired_by_now.bdoc"; Configuration configuration = new Configuration(Configuration.Mode.TEST); TSLHelper.addCertificateFromFileToTsl(configuration, "testFiles/certs/ESTEID-SK_2007_prod.pem.crt"); Container container = ContainerBuilder. aContainer("BDOC"). fromExistingFile(containerPath). withConfiguration(configuration). build(); ValidationResult result = container.validate(); assertTrue(result.isValid()); } @Test public void signaturesWithCrlShouldBeInvalid() throws Exception { ValidationResult validationResult = validateContainer("testFiles/invalid-containers/asic-with-crl-and-without-ocsp.asice", PROD_CONFIGURATION); assertFalse(validationResult.isValid()); assertTrue(validationResult.getErrors().get(0) instanceof UntrustedRevocationSourceException); } @Test public void bDoc_withoutOcspResponse_shouldBeInvalid() throws Exception { assertFalse(validateContainer("testFiles/23608-bdoc21-no-ocsp.bdoc", PROD_CONFIGURATION).isValid()); } @Test public void ocspResponseShouldNotBeTakenFromPreviouslyValidatedSignatures_whenOcspResponseIsMissing() throws Exception { Configuration configuration = new Configuration(Configuration.Mode.TEST); assertFalse(validateContainer("testFiles/invalid-containers/bdoc-tm-ocsp-revoked.bdoc", configuration).isValid()); assertTrue(validateContainer("testFiles/valid-containers/valid-bdoc-tm.bdoc", configuration).isValid()); assertFalse(validateContainer("testFiles/invalid-containers/invalid-bdoc-tm-missing-revoked-ocsp.bdoc", configuration).isValid()); } @Test public void validateContainerWithBomSymbolsInMimeType_shouldBeValid() throws Exception { assertTrue(validateContainer("testFiles/valid-containers/IB-4185_bdoc21_TM_mimetype_with_BOM.bdoc", PROD_CONFIGURATION).isValid()); } @Test public void havingOnlyCaCertificateInTSL_shouldNotValidateOCSPResponse() throws Exception { Configuration configuration = new Configuration(Configuration.Mode.TEST); TSLCertificateSourceImpl tsl = new TSLCertificateSourceImpl(); configuration.setTSL(tsl); InputStream inputStream = getClass().getResourceAsStream("/certs/TEST ESTEID-SK 2011.crt"); X509Certificate caCertificate = DSSUtils.loadCertificate(inputStream).getCertificate(); tsl.addTSLCertificate(caCertificate); ValidationResult result = validateContainer("testFiles/valid-containers/valid-bdoc-tm.bdoc", configuration); assertFalse(result.isValid()); assertTrue(containsErrorMessage(result.getErrors(), "The certificate chain for revocation data is not trusted, there is no trusted anchor.")); } private void testSigningWithOCSPCheck(String unknownCert) { Container container = createEmptyBDocContainer(); container.addDataFile("testFiles/test.txt", "text/plain"); X509Certificate signerCert = TestSigningHelper.getSigningCert(unknownCert, "test"); DataToSign dataToSign = SignatureBuilder. aSignature(container). withSigningCertificate(signerCert). buildDataToSign(); byte[] signature = TestSigningHelper.sign(dataToSign.getDigestToSign(), dataToSign.getDigestAlgorithm()); dataToSign.finalize(signature); } private void assertContainsError(String errorMsg, List<DigiDoc4JException> errors) { for (DigiDoc4JException e : errors) { if (StringUtils.equalsIgnoreCase(errorMsg, e.toString())) { return; } } assertFalse("Expected '" + errorMsg + "' was not found", true); } private Container createSignedBDocDocument(String fileName) { Container container = createContainerWithFile("testFiles/test.txt"); signContainer(container); container.saveAsFile(fileName); return container; } private ValidationResult validateContainer(String containerPath) { Container container = openContainerBuilder(containerPath). build(); return container.validate(); } private ValidationResult validateContainer(String containerPath, Configuration configuration) { Container container = openContainerBuilder(containerPath). withConfiguration(configuration). build(); return container.validate(); } private ContainerBuilder openContainerBuilder(String containerPath) { return ContainerBuilder. aContainer("BDOC"). fromExistingFile(containerPath); } }