Skip to content

Commit 4ff74af

Browse files
committed
Support timestamp signature verification
DEVSIX-8369
1 parent 39b964f commit 4ff74af

File tree

9 files changed

+255
-45
lines changed

9 files changed

+255
-45
lines changed

sign/src/main/java/com/itextpdf/signatures/PdfPKCS7.java

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,9 @@ public PdfPKCS7(byte[] contentsKey, PdfName filterSubtype, String provider) {
449449
IASN1Set attributeValues = ts.getAttrValues();
450450
IASN1Sequence tokenSequence =
451451
BOUNCY_CASTLE_FACTORY.createASN1SequenceInstance(attributeValues.getObjectAt(0));
452+
this.timestampSignatureContainer = new PdfPKCS7(tokenSequence.getEncoded(),
453+
PdfName.ETSI_RFC3161, BOUNCY_CASTLE_FACTORY.getProviderName());
454+
this.timestampSignatureContainer.update(signatureValue, 0, signatureValue.length);
452455
this.timestampCerts = SignUtils.readAllCerts(tokenSequence.getEncoded());
453456
IContentInfo contentInfo = BOUNCY_CASTLE_FACTORY.createContentInfo(tokenSequence);
454457
this.timeStampTokenInfo = BOUNCY_CASTLE_FACTORY.createTSTInfo(contentInfo);
@@ -461,6 +464,7 @@ public PdfPKCS7(byte[] contentsKey, PdfName filterSubtype, String provider) {
461464
this.timestampCerts = this.certs;
462465
String algOID = timeStampTokenInfo.getMessageImprint().getHashAlgorithm().getAlgorithm().getId();
463466
messageDigest = DigestAlgorithms.getMessageDigestFromOid(algOID, null);
467+
encContDigest = DigestAlgorithms.getMessageDigest(getDigestAlgorithmName(), provider);
464468
} else {
465469
if (this.encapMessageContent != null || digestAttr != null) {
466470
if (PdfName.Adbe_pkcs7_sha1.equals(getFilterSubtype())) {
@@ -1323,32 +1327,39 @@ public boolean verifySignatureIntegrityAndAuthenticity() throws GeneralSecurityE
13231327
if (verified) {
13241328
return verifyResult;
13251329
}
1326-
if (isTsp) {
1327-
IMessageImprint imprint = timeStampTokenInfo.getMessageImprint();
1328-
byte[] md = messageDigest.digest();
1329-
byte[] imphashed = imprint.getHashedMessage();
1330-
verifyResult = Arrays.equals(md, imphashed);
1331-
} else {
1332-
if (sigAttr != null || sigAttrDer != null) {
1333-
final byte[] msgDigestBytes = messageDigest.digest();
1334-
boolean verifySignedMessageContent = true;
1335-
// Stefan Santesson fixed a bug, keeping the code backward compatible
1336-
boolean encContDigestCompare = false;
1337-
if (encapMessageContent != null) {
1330+
if (sigAttr != null || sigAttrDer != null) {
1331+
final byte[] msgDigestBytes = messageDigest.digest();
1332+
boolean verifySignedMessageContent = true;
1333+
// Stefan Santesson fixed a bug, keeping the code backward compatible
1334+
boolean encContDigestCompare = false;
1335+
if (encapMessageContent != null) {
1336+
if (isTsp) {
1337+
byte[] tstInfo = new byte[0];
1338+
try {
1339+
tstInfo = timeStampTokenInfo.toASN1Primitive().getEncoded();
1340+
} catch (IOException e) {
1341+
// Ignore.
1342+
}
1343+
// Check that encapMessageContent is TSTInfo
1344+
boolean isTSTInfo = Arrays.equals(tstInfo, encapMessageContent);
1345+
IMessageImprint imprint = timeStampTokenInfo.getMessageImprint();
1346+
byte[] imphashed = imprint.getHashedMessage();
1347+
verifySignedMessageContent = isTSTInfo && Arrays.equals(msgDigestBytes, imphashed);
1348+
} else {
13381349
verifySignedMessageContent = Arrays.equals(msgDigestBytes, encapMessageContent);
1339-
encContDigest.update(encapMessageContent);
1340-
encContDigestCompare = Arrays.equals(encContDigest.digest(), digestAttr);
1341-
}
1342-
boolean absentEncContDigestCompare = Arrays.equals(msgDigestBytes, digestAttr);
1343-
boolean concludingDigestCompare = absentEncContDigestCompare || encContDigestCompare;
1344-
boolean sigVerify = verifySigAttributes(sigAttr) || verifySigAttributes(sigAttrDer);
1345-
verifyResult = concludingDigestCompare && sigVerify && verifySignedMessageContent;
1346-
} else {
1347-
if (encapMessageContent != null) {
1348-
SignUtils.updateVerifier(sig, messageDigest.digest());
13491350
}
1350-
verifyResult = sig.verify(signatureValue);
1351+
encContDigest.update(encapMessageContent);
1352+
encContDigestCompare = Arrays.equals(encContDigest.digest(), digestAttr);
1353+
}
1354+
boolean absentEncContDigestCompare = Arrays.equals(msgDigestBytes, digestAttr);
1355+
boolean concludingDigestCompare = absentEncContDigestCompare || encContDigestCompare;
1356+
boolean sigVerify = verifySigAttributes(sigAttr) || verifySigAttributes(sigAttrDer);
1357+
verifyResult = concludingDigestCompare && sigVerify && verifySignedMessageContent;
1358+
} else {
1359+
if (encapMessageContent != null) {
1360+
SignUtils.updateVerifier(sig, messageDigest.digest());
13511361
}
1362+
verifyResult = sig.verify(signatureValue);
13521363
}
13531364
verified = true;
13541365
return verifyResult;
@@ -1623,11 +1634,15 @@ private void findOcsp(IASN1Sequence seq) throws IOException {
16231634
*/
16241635
private boolean isCades;
16251636

1637+
/**
1638+
* Inner timestamp signature container.
1639+
*/
1640+
private PdfPKCS7 timestampSignatureContainer;
1641+
16261642
/**
16271643
* BouncyCastle TSTInfo.
16281644
*/
16291645
private ITSTInfo timeStampTokenInfo;
1630-
16311646
/**
16321647
* Check if it's a PAdES-LTV time stamp.
16331648
*
@@ -1637,6 +1652,15 @@ public boolean isTsp() {
16371652
return isTsp;
16381653
}
16391654

1655+
/**
1656+
* Retrieves inner timestamp signature container if there is one.
1657+
*
1658+
* @return timestamp signature container or null.
1659+
*/
1660+
public PdfPKCS7 getTimestampSignatureContainer() {
1661+
return timestampSignatureContainer;
1662+
}
1663+
16401664
/**
16411665
* Gets the timestamp token info if there is one.
16421666
*

sign/src/main/java/com/itextpdf/signatures/validation/v1/SignatureValidator.java

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@ class SignatureValidator {
6767
"Certificate {0} stored in DSS dictionary cannot be parsed.";
6868
static final String CANNOT_VERIFY_SIGNATURE = "Signature {0} cannot be mathematically verified.";
6969
static final String DOCUMENT_IS_NOT_COVERED = "Signature {0} doesn't cover entire document.";
70-
static final String CANNOT_VERIFY_TIMESTAMP = "Signature timestamp attribute cannot be verified";
70+
static final String CANNOT_VERIFY_TIMESTAMP = "Signature timestamp attribute cannot be verified.";
7171
static final String REVISIONS_RETRIEVAL_FAILED = "Wasn't possible to retrieve document revisions.";
7272
private static final String TIMESTAMP_EXTRACTION_FAILED = "Unable to extract timestamp from timestamp signature";
7373
private final ValidationContext baseValidationContext;
7474
private final CertificateChainValidator certificateChainValidator;
7575
private final IssuingCertificateRetriever certificateRetriever;
7676
private final SignatureValidationProperties properties;
77-
private Date lastKnownPoE = (Date) TimestampConstants.UNDEFINED_TIMESTAMP_DATE;
77+
private Date lastKnownPoE = DateTimeUtil.getCurrentTimeDate();
7878

7979
/**
8080
* Create new instance of {@link SignatureValidator}.
@@ -135,7 +135,6 @@ public ValidationReport validateLatestSignature(PdfDocument document) {
135135
pkcs7.getSigningCertificate());
136136
}
137137

138-
Date signingDate = DateTimeUtil.getCurrentTimeDate();
139138
if (pkcs7.getTimeStampTokenInfo() != null) {
140139
try {
141140
if (!pkcs7.verifyTimestampImprint()) {
@@ -149,13 +148,27 @@ public ValidationReport validateLatestSignature(PdfDocument document) {
149148
if (stopValidation(validationReport, baseValidationContext)) {
150149
return validationReport;
151150
}
152-
Certificate[] timestampCertificates = pkcs7.getTimestampCertificates();
151+
152+
PdfPKCS7 timestampSignatureContainer = pkcs7.getTimestampSignatureContainer();
153+
try {
154+
if (!timestampSignatureContainer.verifySignatureIntegrityAndAuthenticity()) {
155+
validationReport.addReportItem(new ReportItem(TIMESTAMP_VERIFICATION,
156+
CANNOT_VERIFY_TIMESTAMP, ReportItemStatus.INVALID));
157+
}
158+
} catch (GeneralSecurityException e) {
159+
validationReport.addReportItem(new ReportItem(TIMESTAMP_VERIFICATION,
160+
CANNOT_VERIFY_TIMESTAMP, e, ReportItemStatus.INVALID));
161+
}
162+
if (stopValidation(validationReport, baseValidationContext)) {
163+
return validationReport;
164+
}
165+
166+
Certificate[] timestampCertificates = timestampSignatureContainer.getCertificates();
153167
validateTimestampChain(validationReport, pkcs7.getTimeStampTokenInfo(), timestampCertificates,
154-
(X509Certificate) timestampCertificates[0]);
168+
timestampSignatureContainer.getSigningCertificate());
155169
if (stopValidation(validationReport, baseValidationContext)) {
156170
return validationReport;
157171
}
158-
signingDate = pkcs7.getTimeStampDate().getTime();
159172
}
160173

161174
Certificate[] certificates = pkcs7.getCertificates();
@@ -164,7 +177,7 @@ public ValidationReport validateLatestSignature(PdfDocument document) {
164177

165178
return certificateChainValidator.validate(validationReport,
166179
baseValidationContext,
167-
signingCertificate, signingDate);
180+
signingCertificate, lastKnownPoE);
168181
}
169182

170183
private PdfPKCS7 mathematicallyVerifySignature(ValidationReport validationReport, PdfDocument document) {
@@ -194,16 +207,12 @@ private PdfPKCS7 mathematicallyVerifySignature(ValidationReport validationReport
194207
private ValidationReport validateTimestampChain(ValidationReport validationReport, ITSTInfo timeStampTokenInfo,
195208
Certificate[] knownCerts, X509Certificate signingCert) {
196209
certificateRetriever.addKnownCertificates(Arrays.asList(knownCerts));
197-
Date signingDate = lastKnownPoE;
198-
if (signingDate == TimestampConstants.UNDEFINED_TIMESTAMP_DATE) {
199-
signingDate = DateTimeUtil.getCurrentTimeDate();
200-
}
201210

202211
ValidationReport tsValidationReport = new ValidationReport();
203212

204213
certificateChainValidator.validate(tsValidationReport,
205214
baseValidationContext.setCertificateSource(CertificateSource.TIMESTAMP),
206-
signingCert, signingDate);
215+
signingCert, lastKnownPoE);
207216
validationReport.merge(tsValidationReport);
208217
if (tsValidationReport.getValidationResult() == ValidationReport.ValidationResult.VALID) {
209218
try {

sign/src/test/java/com/itextpdf/signatures/validation/v1/CertificateChainValidatorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public void severalFailuresWithProceedAfterFailTest() throws CertificateExceptio
146146
certificateRetriever.addKnownCertificates(Collections.singletonList(intermediateCert));
147147
certificateRetriever.setTrustedCertificates(Collections.singletonList(rootCert));
148148

149-
properties.setContinueAfterFailure(ValidatorContexts.all() , CertificateSources.all(),true);
149+
properties.setContinueAfterFailure(ValidatorContexts.all() , CertificateSources.all(), true);
150150
// Set random extension as a required one to force the test to fail.
151151
properties.setRequiredExtensions(CertificateSources.of(CertificateSource.CERT_ISSUER),
152152
Collections.<CertificateExtension>singletonList(new KeyUsageExtension(KeyUsage.DECIPHER_ONLY)));

sign/src/test/java/com/itextpdf/signatures/validation/v1/SignatureValidatorIntegrationTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ This file is part of the iText (R) project.
5454
import java.security.cert.CertificateException;
5555
import java.security.cert.X509Certificate;
5656
import java.time.Duration;
57+
import java.util.Arrays;
5758
import java.util.Collections;
5859
import java.util.Date;
5960

@@ -246,6 +247,52 @@ public void rootIsNotTrustedInLatestSignatureTest() throws GeneralSecurityExcept
246247
);
247248
}
248249

250+
@Test
251+
public void validateMultipleSignaturesUsingLastKnownPoETest() throws Exception {
252+
String trustedCertsFileName = CERTS_SRC + "trustedCerts.pem";
253+
Certificate[] trustedCerts = PemFileHelper.readFirstChain(trustedCertsFileName);
254+
255+
try (PdfDocument document = new PdfDocument(new PdfReader(SOURCE_FOLDER + "signatureSigningCertExpired.pdf"))) {
256+
SignatureValidator signatureValidator = new ValidatorChainBuilder()
257+
.withTrustedCertificates(Arrays.asList(trustedCerts))
258+
.withRevocationDataValidator(new MockRevocationDataValidator()).buildSignatureValidator();
259+
ValidationReport report = signatureValidator.validateSignatures(document);
260+
261+
AssertValidationReport.assertThat(report, r -> r
262+
.hasStatus(ValidationResult.VALID)
263+
.hasNumberOfLogs(4).hasNumberOfFailures(0)
264+
.hasLogItem(l -> l
265+
.withCheckName(SignatureValidator.SIGNATURE_VERIFICATION)
266+
.withMessage(SignatureValidator.VALIDATING_SIGNATURE_NAME, p -> "timestampSig1"))
267+
.hasLogItem(l -> l
268+
.withCheckName(SignatureValidator.SIGNATURE_VERIFICATION)
269+
.withMessage(SignatureValidator.VALIDATING_SIGNATURE_NAME, p -> "Signature1"))
270+
);
271+
}
272+
}
273+
274+
@Test
275+
public void stopAfterTimestampChainValidationFailureTest() throws Exception {
276+
try (PdfDocument document = new PdfDocument(new PdfReader(SOURCE_FOLDER + "validDocWithTimestamp.pdf"))) {
277+
SignatureValidator signatureValidator = new ValidatorChainBuilder()
278+
.withSignatureValidationProperties(new SignatureValidationProperties()
279+
.setContinueAfterFailure(ValidatorContexts.all(), CertificateSources.all(), false))
280+
.withRevocationDataValidator(new MockRevocationDataValidator()).buildSignatureValidator();
281+
ValidationReport report = signatureValidator.validateSignatures(document);
282+
283+
AssertValidationReport.assertThat(report, r -> r
284+
.hasStatus(ValidationResult.INDETERMINATE)
285+
.hasNumberOfLogs(2).hasNumberOfFailures(1)
286+
.hasLogItem(l -> l
287+
.withCheckName(SignatureValidator.SIGNATURE_VERIFICATION)
288+
.withMessage(SignatureValidator.VALIDATING_SIGNATURE_NAME, p -> "Signature1"))
289+
.hasLogItem(l -> l
290+
.withCheckName(CertificateChainValidator.CERTIFICATE_CHECK)
291+
.withStatus(ReportItem.ReportItemStatus.INDETERMINATE))
292+
);
293+
}
294+
}
295+
249296
private void addRevDataClients()
250297
throws AbstractOperatorCreationException, IOException, AbstractPKCSException, CertificateException {
251298
String chainName = CERTS_SRC + "validCertsChain.pem";

0 commit comments

Comments
 (0)