Merge pull request #956 from AB527/fix/952-validate-doc-timestamps

(Bug) Detect and verify document timestamps in Validate Signature
This commit is contained in:
Alam authored and GitHub committed 2026-11-12 13:50:09 +05:30
commit 4dd34a659a
6 files changed
+422 -38

No files matched your search

+19 -13
View File
@@ -367,7 +367,10 @@ function createSignatureCard(
let statusColor = 'text-green-400';
let statusIcon = 'check-circle';
let statusText = trText('Valid Signature');
const isTimestamp = result.isTimestamp === true;
let statusText = isTimestamp
? trText('Valid Timestamp')
: trText('Valid Signature');
if (!result.isValid) {
if (result.cryptoVerificationStatus === 'unsupported') {
@@ -380,19 +383,19 @@ function createSignatureCard(
) {
statusColor = 'text-yellow-400';
statusIcon = 'alert-triangle';
statusText = trText('Modified After Signing (signed part intact)');
statusText = isTimestamp
? trText('Modified After Timestamping (timestamped part intact)')
: trText('Modified After Signing (signed part intact)');
} else {
statusColor = 'text-red-400';
statusIcon = 'x-circle';
statusText =
result.cryptoVerified === false
? trText('Invalid: Cryptographic Verification Failed')
: trText('Invalid Signature');
: result.usesInsecureDigest
? trText('Insecure Digest (MD5 / SHA-1)')
: trText('Invalid Signature');
}
} else if (result.usesInsecureDigest) {
statusColor = 'text-red-400';
statusIcon = 'x-circle';
statusText = trText('Insecure Digest (MD5 / SHA-1)');
} else if (result.isExpired) {
statusColor = 'text-yellow-400';
statusIcon = 'alert-triangle';
@@ -400,9 +403,11 @@ function createSignatureCard(
} else if (!result.isTrusted) {
statusColor = 'text-yellow-400';
statusIcon = 'alert-triangle';
statusText = result.isSelfSigned
? trText('Self-Signed: Signer Identity Not Verified')
: trText('Signature Intact: Signer Identity Not Verified');
statusText = isTimestamp
? trText('Timestamp Intact: Timestamp Authority Not Verified')
: result.isSelfSigned
? trText('Self-Signed: Signer Identity Not Verified')
: trText('Signature Intact: Signer Identity Not Verified');
}
const formatDate = (date: Date) => {
@@ -430,7 +435,7 @@ function createSignatureCard(
<div class="flex items-center gap-3">
<i data-lucide="${statusIcon}" class="w-6 h-6 ${statusColor}"></i>
<div>
<h3 class="font-semibold text-white">${trHtml('Signature {{n}}', { n: index + 1 })}</h3>
<h3 class="font-semibold text-white">${isTimestamp ? trHtml('Document Timestamp {{n}}', { n: index + 1 }) : trHtml('Signature {{n}}', { n: index + 1 })}</h3>
<p class="text-sm ${statusColor}">${statusText}</p>
</div>
</div>
@@ -448,7 +453,7 @@ function createSignatureCard(
<div class="space-y-3 text-sm">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p class="text-gray-400">${trHtml('Signed By')}</p>
<p class="text-gray-400">${isTimestamp ? trHtml('Timestamped By') : trHtml('Signed By')}</p>
<p class="text-white font-medium">${escapeHtml(result.signerName)}</p>
${result.signerOrg ? `<p class="text-gray-400 text-xs">${escapeHtml(result.signerOrg)}</p>` : ''}
${result.signerEmail ? `<p class="text-gray-400 text-xs">${escapeHtml(result.signerEmail)}</p>` : ''}
@@ -464,7 +469,7 @@ function createSignatureCard(
result.signatureDate
? `
<div>
<p class="text-gray-400">${trHtml('Signed On')}</p>
<p class="text-gray-400">${isTimestamp ? trHtml('Timestamped On') : trHtml('Signed On')}</p>
<p class="text-white">${formatDate(result.signatureDate)}</p>
</div>
`
@@ -512,6 +517,7 @@ function createSignatureCard(
<p><span class="text-gray-400">${trHtml('Serial Number:')}</span> <span class="text-gray-300 font-mono">${escapeHtml(result.serialNumber)}</span></p>
<p><span class="text-gray-400">${trHtml('Digest Algorithm:')}</span> <span class="text-gray-300">${escapeHtml(result.algorithms.digest)}</span></p>
<p><span class="text-gray-400">${trHtml('Signature Algorithm:')}</span> <span class="text-gray-300">${escapeHtml(result.algorithms.signature)}</span></p>
${result.algorithms.imprint ? `<p><span class="text-gray-400">${trHtml('Timestamp Imprint Algorithm:')}</span> <span class="text-gray-300">${escapeHtml(result.algorithms.imprint)}</span></p>` : ''}
${result.errorMessage ? `<p class="text-red-400">${trHtml('Error: {{error}}', { error: result.errorMessage })}</p>` : ''}
${result.unsupportedAlgorithmReason ? `<p class="text-yellow-300">${escapeHtml(result.unsupportedAlgorithmReason)}</p>` : ''}
</div>
+148 -22
View File
@@ -11,6 +11,8 @@ const INSECURE_DIGEST_OIDS = new Set<string>([
const MIN_RSA_EXPONENT = new forge.jsbn.BigInteger('65537');
const TST_INFO_OID = '1.2.840.113549.1.9.16.1.4';
const PDF_WHITESPACE = new Set('\0\t\n\f\r ');
const PDF_TOKEN_TERMINATORS = new Set('\0\t\n\f\r ()<>[]{}/%');
const PDF_STRING_ESCAPES: Record<string, string> = {
@@ -52,13 +54,15 @@ export function extractSignatures(
if (end < 0) continue;
for (const entries of dictionaries) {
if (isSignatureDictionary(pdfString, entries)) {
const kind = getSignatureKind(pdfString, entries);
if (kind) {
signatures.push(
readSignatureDictionary(
pdfString,
pdfBytes,
entries,
signatures.length
signatures.length,
kind === 'timestamp'
)
);
}
@@ -72,33 +76,36 @@ export function extractSignatures(
* /Type is optional in a signature dictionary (PDF 32000-1, 12.8.1, Table
* 252), and some signers leave it out, so a dictionary without /Type that
* carries both /ByteRange and /Contents is a signature too. Document
* timestamps (/Type /DocTimeStamp, /SubFilter /ETSI.RFC3161) hold an RFC 3161
* token rather than a CMS signature over the document and stay out.
* timestamps (/Type /DocTimeStamp, /SubFilter /ETSI.RFC3161, ISO 32000-2,
* 12.8.5) hold an RFC 3161 token instead of a CMS signature over the
* document, so they are reported separately and verified differently.
*/
function isSignatureDictionary(
function getSignatureKind(
pdfString: string,
entries: Map<string, PdfValueSpan>
): boolean {
): 'signature' | 'timestamp' | null {
const name = (key: string) => {
const span = entries.get(key);
return span ? pdfString.slice(span.start, span.end) : undefined;
};
const type = name('Type');
if (type !== undefined) return type === '/Sig';
return (
entries.has('ByteRange') &&
entries.has('Contents') &&
name('SubFilter') !== '/ETSI.RFC3161'
);
if (type === '/DocTimeStamp') return 'timestamp';
if (type !== undefined) return type === '/Sig' ? 'signature' : null;
if (!entries.has('ByteRange') || !entries.has('Contents')) return null;
return name('SubFilter') === '/ETSI.RFC3161' ? 'timestamp' : 'signature';
}
function readSignatureDictionary(
pdfString: string,
pdfBytes: Uint8Array,
entries: Map<string, PdfValueSpan>,
index: number
index: number,
isTimestamp: boolean
): ExtractedSignatureWithError {
const text = (key: string) => decodePdfText(pdfBytes, entries.get(key));
// A PDF processor should ignore these on ETSI.RFC3161 dictionaries (ISO
// 32000-2, Table 255)
const signerText = (key: string) => (isTimestamp ? undefined : text(key));
const timeMatch = text('M')?.match(
/^D:(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/
);
@@ -106,13 +113,14 @@ function readSignatureDictionary(
index,
contents: new Uint8Array(0),
byteRange: [],
reason: text('Reason'),
location: text('Location'),
contactInfo: text('ContactInfo'),
name: text('Name'),
reason: signerText('Reason'),
location: signerText('Location'),
contactInfo: signerText('ContactInfo'),
name: signerText('Name'),
signingTime: timeMatch
? `${timeMatch[1]}-${timeMatch[2]}-${timeMatch[3]}T${timeMatch[4]}:${timeMatch[5]}:${timeMatch[6]}`
: undefined,
isTimestamp,
};
const rangeSpan = entries.get('ByteRange');
@@ -342,6 +350,7 @@ export async function validateSignature(
reason: signature.reason,
location: signature.location,
contactInfo: signature.contactInfo,
isTimestamp: signature.isTimestamp,
};
if (signature.extractionError) {
@@ -357,6 +366,13 @@ export async function validateSignature(
Array.from(signature.contents)
);
const asn1 = forge.asn1.fromDer(binaryString);
const timestamp = signature.isTimestamp ? unwrapTimestampToken(asn1) : null;
if (signature.isTimestamp && !timestamp) {
result.errorMessage = trText('Could not read the timestamp token');
result.cryptoVerified = false;
result.cryptoVerificationStatus = 'failed';
return result;
}
const p7 = forge.pkcs7.messageFromAsn1(
asn1
) as forge.pkcs7.PkcsSignedData & {
@@ -429,14 +445,22 @@ export async function validateSignature(
(digestOid && getDigestAlgorithmName(digestOid)) ||
getDigestAlgorithmName(signerCert.siginfo?.algorithmOid || ''),
signature: getSignatureAlgorithmName(signerCert.signatureOid || ''),
imprint: timestamp
? getDigestAlgorithmName(timestamp.imprintOid)
: undefined,
};
if (digestOid && INSECURE_DIGEST_OIDS.has(digestOid)) {
result.usesInsecureDigest = true;
}
if (timestamp && INSECURE_DIGEST_OIDS.has(timestamp.imprintOid)) {
result.usesInsecureDigest = true;
}
// Parse signing time if available in signature
if (signature.signingTime) {
if (timestamp?.genTime) {
result.signatureDate = timestamp.genTime;
} else if (signature.signingTime) {
result.signatureDate = new Date(signature.signingTime);
} else {
try {
@@ -490,7 +514,8 @@ export async function validateSignature(
pdfBytes,
signature.byteRange,
signerCert,
signerInfoFields
signerInfoFields,
timestamp
);
result.cryptoVerified = verification.status === 'verified';
@@ -640,6 +665,80 @@ interface SignerInfoFields {
signatureBytes: string;
}
interface TimestampToken {
tstInfo: Uint8Array;
imprintOid: string;
imprint: string;
genTime?: Date;
}
/**
* Reads the TSTInfo out of an RFC 3161 timestamp token and relabels its
* encapsulated content as id-data, because node-forge only parses SignedData
* that wraps plain data.
*/
function unwrapTimestampToken(
contentInfo: forge.asn1.Asn1
): TimestampToken | null {
try {
const signedData = asn1Children(asn1Children(contentInfo)?.[1])?.[0];
const encapContentInfo = asn1Children(signedData)?.[2];
const [typeNode, explicitContent] = asn1Children(encapContentInfo) ?? [];
if (
typeNode?.type !== forge.asn1.Type.OID ||
forge.asn1.derToOid(typeNode.value as string) !== TST_INFO_OID
) {
return null;
}
const octets = asn1Children(explicitContent)?.[0];
if (!octets || octets.type !== forge.asn1.Type.OCTETSTRING) return null;
const tstInfoDer =
typeof octets.value === 'string'
? octets.value
: (octets.value as forge.asn1.Asn1[])
.map((chunk) => chunk.value as string)
.join('');
const tstInfo = asn1Children(forge.asn1.fromDer(tstInfoDer));
const [hashAlgorithm, hashedMessage] = asn1Children(tstInfo?.[2]) ?? [];
const imprintOidNode = asn1Children(hashAlgorithm)?.[0];
if (
imprintOidNode?.type !== forge.asn1.Type.OID ||
hashedMessage?.type !== forge.asn1.Type.OCTETSTRING ||
typeof hashedMessage.value !== 'string'
) {
return null;
}
let genTime: Date | undefined;
const genTimeNode = tstInfo?.[4];
if (
genTimeNode?.type === forge.asn1.Type.GENERALIZEDTIME &&
typeof genTimeNode.value === 'string'
) {
genTime = forge.asn1.generalizedTimeToDate(genTimeNode.value);
}
typeNode.value = forge.asn1.oidToDer(forge.pki.oids.data).getBytes();
return {
tstInfo: latin1ToUint8(tstInfoDer),
imprintOid: forge.asn1.derToOid(imprintOidNode.value as string),
imprint: hashedMessage.value,
genTime,
};
} catch {
return null;
}
}
function asn1Children(
node: forge.asn1.Asn1 | undefined
): forge.asn1.Asn1[] | null {
return node && Array.isArray(node.value) ? node.value : null;
}
function extractSignerInfoFields(
p7: forge.pkcs7.PkcsSignedData & {
rawCapture?: {
@@ -1019,7 +1118,8 @@ async function performCryptoVerification(
pdfBytes: Uint8Array,
byteRange: number[],
signerCert: forge.pki.Certificate,
fields: SignerInfoFields | null
fields: SignerInfoFields | null,
timestamp: TimestampToken | null = null
): Promise<CryptoVerificationResult> {
if (!fields) {
return { status: 'failed', reason: trText('Could not parse signer info') };
@@ -1044,7 +1144,33 @@ async function performCryptoVerification(
signedContent.set(pdfBytes.subarray(start1, start1 + len1), 0);
signedContent.set(pdfBytes.subarray(start2, start2 + len2), len1);
md.update(uint8ToLatin1(signedContent));
// A timestamp token signs its TSTInfo, which in turn holds the hash of the
// document bytes (RFC 3161, 2.4.2), so the document is checked against that
// imprint and the CMS signature against the TSTInfo.
let content: Uint8Array = signedContent;
if (timestamp) {
const imprintMd = createMd(timestamp.imprintOid);
if (!imprintMd) {
return {
status: 'unsupported',
reason: trText('Unsupported digest OID {{digestOid}}', {
digestOid: timestamp.imprintOid,
}),
};
}
imprintMd.update(uint8ToLatin1(signedContent));
if (imprintMd.digest().bytes() !== timestamp.imprint) {
return {
status: 'failed',
reason: trText(
'Document hash does not match the timestamp: PDF was modified after timestamping'
),
};
}
content = timestamp.tstInfo;
}
md.update(uint8ToLatin1(content));
const contentHashBytes = md.digest().bytes();
const authAttrs = fields.authAttrs;
@@ -1107,7 +1233,7 @@ async function performCryptoVerification(
);
signedBytesForVerify = latin1ToUint8(forge.asn1.toDer(asSet).getBytes());
} else {
signedBytesForVerify = signedContent;
signedBytesForVerify = content;
}
if (scheme.kind === 'rsa-pkcs1' && !hasLowRsaExponent(signerCert.publicKey)) {
+3
View File
@@ -17,6 +17,7 @@ export interface SignatureValidationResult {
algorithms: {
digest: string;
signature: string;
imprint?: string;
};
serialNumber: string;
reason?: string;
@@ -29,6 +30,7 @@ export interface SignatureValidationResult {
cryptoVerificationStatus?: 'verified' | 'failed' | 'unsupported';
unsupportedAlgorithmReason?: string;
usesInsecureDigest?: boolean;
isTimestamp?: boolean;
}
export interface ExtractedSignature {
@@ -40,6 +42,7 @@ export interface ExtractedSignature {
contactInfo?: string;
name?: string;
signingTime?: string;
isTimestamp?: boolean;
}
export interface ValidateSignatureState {
@@ -167,15 +167,43 @@ describe('validatePdfSignatures extraction and coverage', () => {
const pdfBytes = new TextEncoder().encode(
'%PDF-1.7\n' +
'1 0 obj\n<< /Subtype /Text /Contents (A note) >>\nendobj\n' +
'2 0 obj\n<< /Filter /Adobe.PPKLite /SubFilter /ETSI.RFC3161 ' +
'/ByteRange [0 10 20 10] /Contents <00> >>\nendobj\n' +
'3 0 obj\n<< /Type /DocTimeStamp /ByteRange [0 10 20 10] ' +
'2 0 obj\n<< /Type /Annot /ByteRange [0 10 20 10] ' +
'/Contents <00> >>\nendobj\n%%EOF\n'
);
expect(countSignatures(pdfBytes)).toBe(0);
});
it('reports document timestamps as timestamps, typed or not', () => {
const pdfBytes = new TextEncoder().encode(
'%PDF-1.7\n' +
'1 0 obj\n<< /Filter /Adobe.PPKLite /SubFilter /ETSI.RFC3161 ' +
'/ByteRange [0 10 20 10] /Contents <00> >>\nendobj\n' +
'2 0 obj\n<< /Type /DocTimeStamp /ByteRange [0 10 20 10] ' +
'/Contents <00> >>\nendobj\n%%EOF\n'
);
const signatures = extractSignatures(pdfBytes);
expect(signatures).toHaveLength(2);
expect(signatures.every((sig) => sig.isTimestamp)).toBe(true);
});
it('ignores Reason, Location, ContactInfo and Name on document timestamps', () => {
const pdfBytes = new TextEncoder().encode(
'%PDF-1.7\n' +
'1 0 obj\n<< /Type /DocTimeStamp /SubFilter /ETSI.RFC3161 ' +
'/Reason (r) /Location (l) /ContactInfo (c) /Name (n) ' +
'/ByteRange [0 10 20 10] /Contents <00> >>\nendobj\n%%EOF\n'
);
const [signature] = extractSignatures(pdfBytes);
expect(signature.isTimestamp).toBe(true);
expect(signature.reason).toBeUndefined();
expect(signature.location).toBeUndefined();
expect(signature.contactInfo).toBeUndefined();
expect(signature.name).toBeUndefined();
});
it('reports an intact signature as partial when an incremental update follows', async () => {
const signed = await signPdf(samplePdf, certificateData, {
signatureInfo: { reason: 'Original' },
@@ -0,0 +1,167 @@
import { describe, it, expect } from 'vitest';
import forge from 'node-forge';
import { validatePdfSignatures } from '@/js/logic/validate-signature-pdf';
const TST_INFO_OID = '1.2.840.113549.1.9.16.1.4';
const RANGE_PLACEHOLDER = '{{RANGE}}';
const RANGE_WIDTH = 64;
const HEX_LENGTH = 8192;
const HEAD =
'%PDF-1.7\n1 0 obj\n<< /Type /DocTimeStamp /Filter /Adobe.PPKLite ' +
`/SubFilter /ETSI.RFC3161 ${RANGE_PLACEHOLDER} /Contents `;
const TAIL = ' >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF\n';
const keys = forge.pki.rsa.generateKeyPair(1024);
const tsaCert = forge.pki.createCertificate();
tsaCert.publicKey = keys.publicKey;
tsaCert.serialNumber = '02';
tsaCert.validity.notBefore = new Date(Date.now() - 86400000);
tsaCert.validity.notAfter = new Date(Date.now() + 86400000 * 365);
tsaCert.setSubject([{ name: 'commonName', value: 'Test TSA' }]);
tsaCert.setIssuer([{ name: 'commonName', value: 'Test TSA' }]);
tsaCert.sign(keys.privateKey, forge.md.sha256.create());
const { asn1 } = forge;
const node = (type: number, value: string | forge.asn1.Asn1[]) =>
asn1.create(asn1.Class.UNIVERSAL, type, Array.isArray(value), value);
const oid = (id: string) => node(asn1.Type.OID, asn1.oidToDer(id).getBytes());
function tstInfoDer(imprint: string, imprintOid: string): string {
return asn1
.toDer(
node(asn1.Type.SEQUENCE, [
node(asn1.Type.INTEGER, asn1.integerToDer(1).getBytes()),
oid('1.2.3.4.1'),
node(asn1.Type.SEQUENCE, [
node(asn1.Type.SEQUENCE, [oid(imprintOid), node(asn1.Type.NULL, '')]),
node(asn1.Type.OCTETSTRING, imprint),
]),
node(asn1.Type.INTEGER, asn1.integerToDer(42).getBytes()),
node(asn1.Type.GENERALIZEDTIME, '20260102030405Z'),
])
)
.getBytes();
}
/** An RFC 3161 token whose TSTInfo carries the given document hash. */
function timestampTokenHex(imprint: string, imprintOid: string): string {
const p7 = forge.pkcs7.createSignedData();
p7.content = forge.util.createBuffer(tstInfoDer(imprint, imprintOid));
p7.addCertificate(tsaCert);
p7.addSigner({
key: keys.privateKey,
certificate: tsaCert,
digestAlgorithm: forge.pki.oids.sha256,
authenticatedAttributes: [
{ type: forge.pki.oids.contentType, value: TST_INFO_OID },
{ type: forge.pki.oids.messageDigest },
],
});
p7.sign();
const contentInfo = p7.toAsn1();
// node-forge always labels the content as id-data; a TSA labels it TSTInfo
const signedData = (contentInfo.value as forge.asn1.Asn1[])[1]
.value as forge.asn1.Asn1[];
const encap = (signedData[0].value as forge.asn1.Asn1[])[2]
.value as forge.asn1.Asn1[];
encap[0] = oid(TST_INFO_OID);
return forge.util.bytesToHex(asn1.toDer(contentInfo).getBytes());
}
function sha256(content: string): string {
const md = forge.md.sha256.create();
md.update(content);
return md.digest().bytes();
}
function buildTimestampedPdf(
imprintFor: (signedContent: string) => string = sha256,
imprintOid: string = forge.pki.oids.sha256
): Uint8Array {
const headLength = HEAD.length - RANGE_PLACEHOLDER.length + RANGE_WIDTH;
const gapLength = HEX_LENGTH + 2;
const byteRange = [0, headLength, headLength + gapLength, TAIL.length];
const head = HEAD.replace(
RANGE_PLACEHOLDER,
`/ByteRange [${byteRange.join(' ')}]`.padEnd(RANGE_WIDTH)
);
const hex = timestampTokenHex(imprintFor(head + TAIL), imprintOid).padEnd(
HEX_LENGTH,
'0'
);
return new TextEncoder().encode(`${head}<${hex}>${TAIL}`);
}
describe('document timestamps (RFC 3161)', () => {
it('verifies an intact document timestamp', async () => {
const [result] = await validatePdfSignatures(buildTimestampedPdf());
expect(result.isTimestamp).toBe(true);
expect(result.cryptoVerificationStatus).toBe('verified');
expect(result.coverageStatus).toBe('full');
expect(result.isValid).toBe(true);
expect(result.usesInsecureDigest).toBeFalsy();
expect(result.algorithms.imprint).toBe('SHA-256');
expect(result.signerName).toBe('Test TSA');
expect(result.signatureDate?.toISOString()).toBe(
'2026-01-02T03:04:05.000Z'
);
});
it.each([
['SHA-1', forge.md.sha1, forge.pki.oids.sha1],
['MD5', forge.md.md5, forge.pki.oids.md5],
])('treats a %s messageImprint as insecure', async (name, algorithm, id) => {
const imprintFor = (content: string) => {
const md = algorithm.create();
md.update(content);
return md.digest().bytes();
};
const [result] = await validatePdfSignatures(
buildTimestampedPdf(imprintFor, id)
);
expect(result.cryptoVerificationStatus).toBe('verified');
expect(result.usesInsecureDigest).toBe(true);
expect(result.isValid).toBe(false);
expect(result.algorithms.imprint).toBe(name);
});
it('fails when the document no longer matches the timestamped hash', async () => {
const pdfBytes = buildTimestampedPdf();
// Rename /Adobe.PPKLite to /Adobe.QPKLite: still a valid PDF, new bytes
const at = new TextDecoder('latin1').decode(pdfBytes).indexOf('PPKLite');
pdfBytes[at] = 'Q'.charCodeAt(0);
const [result] = await validatePdfSignatures(pdfBytes);
expect(result.isValid).toBe(false);
expect(result.cryptoVerificationStatus).toBe('failed');
expect(result.errorMessage).toMatch(/modified after timestamping/);
});
it('fails when the token holds the hash of a different document', async () => {
const [result] = await validatePdfSignatures(
buildTimestampedPdf(() => sha256('some other document'))
);
expect(result.isValid).toBe(false);
expect(result.cryptoVerificationStatus).toBe('failed');
});
it('fails when the TSTInfo was altered after the TSA signed it', async () => {
const pdfBytes = buildTimestampedPdf();
const text = new TextDecoder('latin1').decode(pdfBytes);
// GeneralizedTime 20260102030405Z, hex-encoded inside /Contents
const timeHex = forge.util.bytesToHex('20260102030405Z');
const at = text.indexOf(timeHex);
expect(at).toBeGreaterThan(0);
pdfBytes[at + 1] = pdfBytes[at + 1] === 0x31 ? 0x32 : 0x31;
const [result] = await validatePdfSignatures(pdfBytes);
expect(result.isValid).toBe(false);
expect(result.cryptoVerificationStatus).toBe('failed');
});
});
+54
View File
@@ -2155,6 +2155,12 @@
],
"fileCount": 1
},
"Could not read the timestamp token": {
"files": [
"logic/validate-signature-pdf.ts"
],
"fileCount": 1
},
"Could not reduce file size further. Original: {{originalSize}}, New: {{compressedSize}}.": {
"files": [
"logic/compress-pdf-page.ts"
@@ -2580,12 +2586,24 @@
],
"fileCount": 1
},
"Document Timestamp {{n}}": {
"files": [
"logic/validate-signature-pdf-page.ts"
],
"fileCount": 1
},
"Document Type": {
"files": [
"logic/page-dimensions-page.ts"
],
"fileCount": 1
},
"Document hash does not match the timestamp: PDF was modified after timestamping": {
"files": [
"logic/validate-signature-pdf.ts"
],
"fileCount": 1
},
"Document properties": {
"files": [
"logic/edit-pdf-text-dock.ts"
@@ -5222,6 +5240,12 @@
],
"fileCount": 1
},
"Modified After Timestamping (timestamped part intact)": {
"files": [
"logic/validate-signature-pdf-page.ts"
],
"fileCount": 1
},
"More": {
"files": [
"logic/edit-pdf-text-dock.ts"
@@ -8903,6 +8927,30 @@
],
"fileCount": 1
},
"Timestamp Imprint Algorithm:": {
"files": [
"logic/validate-signature-pdf-page.ts"
],
"fileCount": 1
},
"Timestamp Intact: Timestamp Authority Not Verified": {
"files": [
"logic/validate-signature-pdf-page.ts"
],
"fileCount": 1
},
"Timestamped By": {
"files": [
"logic/validate-signature-pdf-page.ts"
],
"fileCount": 1
},
"Timestamped On": {
"files": [
"logic/validate-signature-pdf-page.ts"
],
"fileCount": 1
},
"Tint": {
"files": [
"workflow/nodes/adjust-colors-node.ts"
@@ -9440,6 +9488,12 @@
],
"fileCount": 1
},
"Valid Timestamp": {
"files": [
"logic/validate-signature-pdf-page.ts"
],
"fileCount": 1
},
"Valid Until": {
"files": [
"logic/validate-signature-pdf-page.ts"