pdf-lite 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/EXAMPLES.md CHANGED
@@ -1510,3 +1510,357 @@ for (const obj of preservingDecoder) {
1510
1510
  console.log('Match:', simpleDict === reconstructed)
1511
1511
  }
1512
1512
  ```
1513
+
1514
+ ## PDF verify signatures example
1515
+
1516
+ ```typescript
1517
+ import { PdfArray } from 'pdf-lite/core/objects/pdf-array'
1518
+ import { PdfDictionary } from 'pdf-lite/core/objects/pdf-dictionary'
1519
+ import { PdfIndirectObject } from 'pdf-lite/core/objects/pdf-indirect-object'
1520
+ import { PdfName } from 'pdf-lite/core/objects/pdf-name'
1521
+ import { PdfNumber } from 'pdf-lite/core/objects/pdf-number'
1522
+ import { PdfObjectReference } from 'pdf-lite/core/objects/pdf-object-reference'
1523
+ import { PdfStream } from 'pdf-lite/core/objects/pdf-stream'
1524
+ import { PdfDocument } from 'pdf-lite/pdf/pdf-document'
1525
+ import { PdfString } from 'pdf-lite/core/objects/pdf-string'
1526
+ import {
1527
+ PdfAdbePkcsX509RsaSha1SignatureObject,
1528
+ PdfAdbePkcs7DetachedSignatureObject,
1529
+ PdfAdbePkcs7Sha1SignatureObject,
1530
+ PdfEtsiCadesDetachedSignatureObject,
1531
+ PdfEtsiRfc3161SignatureObject,
1532
+ } from 'pdf-lite'
1533
+ import { rsaSigningKeys } from '../packages/pdf-lite/test/unit/fixtures/rsa-2048/index'
1534
+
1535
+ function createPage(
1536
+ contentStreamRef: PdfObjectReference,
1537
+ ): PdfIndirectObject<PdfDictionary> {
1538
+ const pageDict = new PdfDictionary()
1539
+ pageDict.set('Type', new PdfName('Page'))
1540
+ pageDict.set(
1541
+ 'MediaBox',
1542
+ new PdfArray([
1543
+ new PdfNumber(0),
1544
+ new PdfNumber(0),
1545
+ new PdfNumber(612),
1546
+ new PdfNumber(792),
1547
+ ]),
1548
+ )
1549
+ pageDict.set('Contents', contentStreamRef)
1550
+ return new PdfIndirectObject({ content: pageDict })
1551
+ }
1552
+
1553
+ function createPages(
1554
+ pages: PdfIndirectObject<PdfDictionary>[],
1555
+ ): PdfIndirectObject<PdfDictionary> {
1556
+ const pagesDict = new PdfDictionary()
1557
+ pagesDict.set('Type', new PdfName('Pages'))
1558
+ pagesDict.set('Kids', new PdfArray(pages.map((x) => x.reference)))
1559
+ pagesDict.set('Count', new PdfNumber(pages.length))
1560
+ return new PdfIndirectObject({ content: pagesDict })
1561
+ }
1562
+
1563
+ function createCatalog(
1564
+ pagesRef: PdfObjectReference,
1565
+ ): PdfIndirectObject<PdfDictionary> {
1566
+ const catalogDict = new PdfDictionary()
1567
+ catalogDict.set('Type', new PdfName('Catalog'))
1568
+ catalogDict.set('Pages', pagesRef)
1569
+ return new PdfIndirectObject({ content: catalogDict })
1570
+ }
1571
+
1572
+ function createFont(): PdfIndirectObject<PdfDictionary> {
1573
+ const fontDict = new PdfDictionary()
1574
+ fontDict.set('Type', new PdfName('Font'))
1575
+ fontDict.set('Subtype', new PdfName('Type1'))
1576
+ fontDict.set('BaseFont', new PdfName('Helvetica'))
1577
+ return new PdfIndirectObject({ content: fontDict })
1578
+ }
1579
+
1580
+ function createResources(
1581
+ fontRef: PdfObjectReference,
1582
+ ): PdfIndirectObject<PdfDictionary> {
1583
+ const resourcesDict = new PdfDictionary()
1584
+ const fontDict = new PdfDictionary()
1585
+ fontDict.set('F1', fontRef)
1586
+ resourcesDict.set('Font', fontDict)
1587
+ return new PdfIndirectObject({ content: resourcesDict })
1588
+ }
1589
+
1590
+ function createPageWithSignatureField(
1591
+ contentStreamRef: PdfObjectReference,
1592
+ signatureAnnotRef: PdfObjectReference,
1593
+ ): PdfIndirectObject<PdfDictionary> {
1594
+ const pageDict = new PdfDictionary()
1595
+ pageDict.set('Type', new PdfName('Page'))
1596
+ pageDict.set(
1597
+ 'MediaBox',
1598
+ new PdfArray([
1599
+ new PdfNumber(0),
1600
+ new PdfNumber(0),
1601
+ new PdfNumber(612),
1602
+ new PdfNumber(792),
1603
+ ]),
1604
+ )
1605
+ pageDict.set('Contents', contentStreamRef)
1606
+ pageDict.set('Annots', new PdfArray([signatureAnnotRef]))
1607
+
1608
+ return new PdfIndirectObject({ content: pageDict })
1609
+ }
1610
+
1611
+ function createSignatureAnnotation(
1612
+ signatureRef: PdfObjectReference,
1613
+ appearanceStreamRef: PdfObjectReference,
1614
+ pageRef: PdfObjectReference,
1615
+ signatureName: string,
1616
+ ): PdfIndirectObject<PdfDictionary> {
1617
+ const signatureAnnotation = new PdfDictionary()
1618
+ signatureAnnotation.set('Type', new PdfName('Annot'))
1619
+ signatureAnnotation.set('Subtype', new PdfName('Widget'))
1620
+ signatureAnnotation.set('FT', new PdfName('Sig'))
1621
+ signatureAnnotation.set('T', new PdfString(signatureName))
1622
+ signatureAnnotation.set(
1623
+ 'Rect',
1624
+ new PdfArray([
1625
+ new PdfNumber(135), // x1: Start after "Signature: " text (~72 + 63)
1626
+ new PdfNumber(640), // y1: Bottom of signature area (652 - 12)
1627
+ new PdfNumber(400), // x2: End of signature line
1628
+ new PdfNumber(665), // y2: Top of signature area (652 + 13)
1629
+ ]),
1630
+ )
1631
+ signatureAnnotation.set('F', new PdfNumber(4))
1632
+ signatureAnnotation.set('P', pageRef) // Reference to parent page
1633
+ signatureAnnotation.set('V', signatureRef)
1634
+
1635
+ // Add appearance dictionary
1636
+ const appearanceDict = new PdfDictionary()
1637
+ appearanceDict.set('N', appearanceStreamRef)
1638
+ signatureAnnotation.set('AP', appearanceDict)
1639
+
1640
+ return new PdfIndirectObject({ content: signatureAnnotation })
1641
+ }
1642
+
1643
+ function createSignatureAppearance(): PdfIndirectObject<PdfStream> {
1644
+ // Create font for appearance
1645
+ const appearanceFont = new PdfDictionary()
1646
+ appearanceFont.set('Type', new PdfName('Font'))
1647
+ appearanceFont.set('Subtype', new PdfName('Type1'))
1648
+ appearanceFont.set('BaseFont', new PdfName('Helvetica'))
1649
+
1650
+ const fontDict = new PdfDictionary()
1651
+ fontDict.set('F1', appearanceFont)
1652
+
1653
+ const resourcesDict = new PdfDictionary()
1654
+ resourcesDict.set('Font', fontDict)
1655
+
1656
+ // Create appearance stream header
1657
+ const appearanceHeader = new PdfDictionary()
1658
+ appearanceHeader.set('Type', new PdfName('XObject'))
1659
+ appearanceHeader.set('Subtype', new PdfName('Form'))
1660
+ appearanceHeader.set(
1661
+ 'BBox',
1662
+ new PdfArray([
1663
+ new PdfNumber(0),
1664
+ new PdfNumber(0),
1665
+ new PdfNumber(265), // Width: 400 - 135
1666
+ new PdfNumber(25), // Height: 665 - 640
1667
+ ]),
1668
+ )
1669
+ appearanceHeader.set('Resources', resourcesDict)
1670
+
1671
+ // Create appearance stream for the signature
1672
+ return new PdfIndirectObject({
1673
+ content: new PdfStream({
1674
+ header: appearanceHeader,
1675
+ original:
1676
+ 'BT /F1 10 Tf 5 14 Td (Digitally signed by: Jake Shirley) Tj ET',
1677
+ }),
1678
+ })
1679
+ }
1680
+
1681
+ // Create the document
1682
+ const document = new PdfDocument()
1683
+
1684
+ // Create font
1685
+ const font = createFont()
1686
+ document.add(font)
1687
+
1688
+ // Create resources with the font
1689
+ const resources = createResources(font.reference)
1690
+ document.add(resources)
1691
+
1692
+ // Create content stream for first page
1693
+ const contentStream = new PdfIndirectObject({
1694
+ content: new PdfStream({
1695
+ header: new PdfDictionary(),
1696
+ original: 'BT /F1 24 Tf 100 700 Td (Hello, PDF-Lite!) Tj ET',
1697
+ }),
1698
+ })
1699
+ document.add(contentStream)
1700
+
1701
+ // Create first page
1702
+ const page1 = createPage(contentStream.reference)
1703
+ page1.content.set('Resources', resources.reference)
1704
+ document.add(page1)
1705
+
1706
+ // Array to hold all pages and signature objects
1707
+ const allPages: PdfIndirectObject<PdfDictionary>[] = [page1]
1708
+ const allSignatures: any[] = []
1709
+ const signatureFields: PdfObjectReference[] = []
1710
+
1711
+ // Helper function to create a signature page
1712
+ function createSignaturePage(
1713
+ signatureType: string,
1714
+ signatureObj: any,
1715
+ pageNumber: number,
1716
+ ) {
1717
+ const content = new PdfIndirectObject({
1718
+ content: new PdfStream({
1719
+ header: new PdfDictionary(),
1720
+ original: `BT /F1 12 Tf 72 712 Td (Signature Type: ${signatureType}) Tj 0 -60 Td (Signature: ________________________________) Tj ET`,
1721
+ }),
1722
+ })
1723
+ document.add(content)
1724
+
1725
+ const appearance = createSignatureAppearance()
1726
+ document.add(appearance)
1727
+
1728
+ // Create page first to get its reference
1729
+ const page = createPageWithSignatureField(
1730
+ content.reference,
1731
+ new PdfObjectReference(0, 0), // Temporary placeholder
1732
+ )
1733
+ page.content.set('Resources', resources.reference)
1734
+ document.add(page)
1735
+
1736
+ // Now create annotation with page reference
1737
+ const annotation = createSignatureAnnotation(
1738
+ signatureObj.reference,
1739
+ appearance.reference,
1740
+ page.reference,
1741
+ `Signature${pageNumber}`,
1742
+ )
1743
+ document.add(annotation)
1744
+
1745
+ // Update page's Annots array with actual annotation reference
1746
+ page.content.set('Annots', new PdfArray([annotation.reference]))
1747
+
1748
+ signatureFields.push(annotation.reference)
1749
+ return page
1750
+ }
1751
+
1752
+ // Page 2: Adobe PKCS7 Detached
1753
+ const pkcs7DetachedSig = new PdfAdbePkcs7DetachedSignatureObject({
1754
+ privateKey: rsaSigningKeys.privateKey,
1755
+ certificate: rsaSigningKeys.cert,
1756
+ issuerCertificate: rsaSigningKeys.caCert,
1757
+ name: 'Jake Shirley',
1758
+ location: 'Earth',
1759
+ reason: 'PKCS7 Detached Signature',
1760
+ contactInfo: 'test@test.com',
1761
+ revocationInfo: {
1762
+ crls: [rsaSigningKeys.caCrl],
1763
+ ocsps: [rsaSigningKeys.ocspResponse],
1764
+ },
1765
+ })
1766
+ allSignatures.push(pkcs7DetachedSig)
1767
+ allPages.push(createSignaturePage('Adobe PKCS7 Detached', pkcs7DetachedSig, 2))
1768
+
1769
+ // Page 3: Adobe PKCS7 SHA1
1770
+ const pkcs7Sha1Sig = new PdfAdbePkcs7Sha1SignatureObject({
1771
+ privateKey: rsaSigningKeys.privateKey,
1772
+ certificate: rsaSigningKeys.cert,
1773
+ issuerCertificate: rsaSigningKeys.caCert,
1774
+ name: 'Jake Shirley',
1775
+ location: 'Earth',
1776
+ reason: 'PKCS7 SHA1 Signature',
1777
+ contactInfo: 'test@test.com',
1778
+ })
1779
+ allSignatures.push(pkcs7Sha1Sig)
1780
+ allPages.push(createSignaturePage('Adobe PKCS7 SHA1', pkcs7Sha1Sig, 3))
1781
+
1782
+ // Page 4: Adobe X509 RSA SHA1
1783
+ const x509RsaSha1Sig = new PdfAdbePkcsX509RsaSha1SignatureObject({
1784
+ privateKey: rsaSigningKeys.privateKey,
1785
+ certificate: rsaSigningKeys.cert,
1786
+ additionalCertificates: [rsaSigningKeys.caCert],
1787
+ name: 'Jake Shirley',
1788
+ location: 'Earth',
1789
+ reason: 'X509 RSA SHA1 Signature',
1790
+ contactInfo: 'test@test.com',
1791
+ revocationInfo: {
1792
+ crls: [rsaSigningKeys.caCrl],
1793
+ ocsps: [rsaSigningKeys.ocspResponse],
1794
+ },
1795
+ })
1796
+ allSignatures.push(x509RsaSha1Sig)
1797
+ allPages.push(createSignaturePage('Adobe X509 RSA SHA1', x509RsaSha1Sig, 4))
1798
+
1799
+ // Page 5: ETSI CAdES Detached
1800
+ const cadesDetachedSig = new PdfEtsiCadesDetachedSignatureObject({
1801
+ privateKey: rsaSigningKeys.privateKey,
1802
+ certificate: rsaSigningKeys.cert,
1803
+ issuerCertificate: rsaSigningKeys.caCert,
1804
+ name: 'Jake Shirley',
1805
+ location: 'Earth',
1806
+ reason: 'CAdES Detached Signature',
1807
+ contactInfo: 'test@test.com',
1808
+ revocationInfo: {
1809
+ crls: [rsaSigningKeys.caCrl],
1810
+ ocsps: [rsaSigningKeys.ocspResponse],
1811
+ },
1812
+ })
1813
+ allSignatures.push(cadesDetachedSig)
1814
+ allPages.push(createSignaturePage('ETSI CAdES Detached', cadesDetachedSig, 5))
1815
+
1816
+ // Page 6: ETSI RFC3161 (Timestamp)
1817
+ const rfc3161Sig = new PdfEtsiRfc3161SignatureObject({
1818
+ timeStampAuthority: {
1819
+ url: 'https://freetsa.org/tsr',
1820
+ },
1821
+ })
1822
+ allSignatures.push(rfc3161Sig)
1823
+ allPages.push(createSignaturePage('ETSI RFC3161 Timestamp', rfc3161Sig, 6))
1824
+
1825
+ // Create pages collection with all pages
1826
+ const pages = createPages(allPages)
1827
+ // Set parent reference for all pages
1828
+ allPages.forEach((page) => {
1829
+ page.content.set('Parent', pages.reference)
1830
+ })
1831
+ document.add(pages)
1832
+
1833
+ // Create catalog with AcroForm
1834
+ const catalog = createCatalog(pages.reference)
1835
+
1836
+ // Add AcroForm to catalog with all signature fields
1837
+ const acroForm = new PdfDictionary()
1838
+ acroForm.set('Fields', new PdfArray(signatureFields))
1839
+ acroForm.set('SigFlags', new PdfNumber(3))
1840
+ const acroFormObj = new PdfIndirectObject({ content: acroForm })
1841
+ document.add(acroFormObj)
1842
+ catalog.content.set('AcroForm', acroFormObj.reference)
1843
+
1844
+ document.add(catalog)
1845
+
1846
+ // Set the catalog as the root
1847
+ document.trailerDict.set('Root', catalog.reference)
1848
+
1849
+ // IMPORTANT: Add all signatures LAST - after all other objects
1850
+ // This ensures the ByteRange is calculated correctly for each signature
1851
+ allSignatures.forEach((sig) => {
1852
+ document.startNewRevision()
1853
+ document.add(sig)
1854
+ })
1855
+
1856
+ await document.commit()
1857
+
1858
+ const documentBytes = document.toBytes()
1859
+ const newDocument = await PdfDocument.fromBytes([documentBytes])
1860
+
1861
+ const validationResult = await newDocument.verifySignatures()
1862
+ console.log(
1863
+ 'Signature validation result:',
1864
+ JSON.stringify(validationResult, null, 2),
1865
+ )
1866
+ ```
package/README.md CHANGED
@@ -8,10 +8,6 @@ A low-level, minimal-dependency, type-safe PDF library that works in the browser
8
8
 
9
9
  PRs and issues are welcome!
10
10
 
11
- ## License
12
-
13
- MIT License - see [LICENSE](LICENSE) for details.
14
-
15
11
  ## Features
16
12
 
17
13
  - **Zero dependencies**: No external libraries are required, making it lightweight and easy to integrate.
@@ -99,6 +95,7 @@ Long-Term Validation (LTV) support ensures that digital signatures remain valid
99
95
  **Other features:**
100
96
 
101
97
  - [x] Timestamping
98
+ - [x] Verification of existing signatures
102
99
 
103
100
  ## Future Plans
104
101
 
@@ -283,3 +280,7 @@ import {
283
280
  PdfEtsiRfc3161SignatureObject,
284
281
  } from 'pdf-lite'
285
282
  ```
283
+
284
+ ## License
285
+
286
+ MIT License - see [LICENSE](LICENSE) for details.
@@ -13,13 +13,14 @@ export class PdfNumberToken extends PdfToken {
13
13
  options instanceof Ref ||
14
14
  options instanceof PdfNumberToken) {
15
15
  this.#value = PdfNumberToken.getValue(options);
16
- this.padTo = padTo ?? 0;
17
- this.decimalPlaces = decimalPlaces ?? 0;
16
+ this.padTo = padTo ?? PdfNumberToken.getPadding(options);
17
+ this.decimalPlaces =
18
+ decimalPlaces ?? PdfNumberToken.getDecimalPlaces(options);
18
19
  this.isByteToken = false;
19
20
  return;
20
21
  }
21
22
  this.#value = PdfNumberToken.getValue(options.value);
22
- this.padTo = options.padTo ?? 0;
23
+ this.padTo = options.padTo ?? PdfNumberToken.getPadding(options.value);
23
24
  this.decimalPlaces =
24
25
  options.decimalPlaces ??
25
26
  PdfNumberToken.getDecimalPlaces(options.value);
@@ -63,12 +64,16 @@ export class PdfNumberToken extends PdfToken {
63
64
  if (typeof bytes === 'number') {
64
65
  bytes = PdfNumberToken.toBytes(bytes);
65
66
  }
66
- let padding = 0;
67
+ // Count leading zeros
68
+ let leadingZeros = 0;
69
+ const originalLength = bytes.length;
67
70
  while (bytes.length && bytes[0] === 0x30) {
68
71
  bytes = bytes.slice(1);
69
- padding++;
72
+ leadingZeros++;
70
73
  }
71
- return padding;
74
+ // If all characters were zeros (value is 0), padding should be total length
75
+ // Otherwise, padding should be total length (to maintain original formatting)
76
+ return originalLength;
72
77
  }
73
78
  static getDecimalPlaces(bytes) {
74
79
  if (bytes instanceof PdfNumberToken) {
@@ -11,7 +11,7 @@ import { PdfEncryptionDictionaryObject } from '../security/types';
11
11
  import { PdfTrailerEntries } from '../core/objects/pdf-trailer';
12
12
  import { PdfDocumentSecurityStoreObject } from '../signing/document-security-store';
13
13
  import { ByteArray } from '../types';
14
- import { PdfSigner } from '../signing/signer';
14
+ import { PdfDocumentVerificationResult, PdfSigner } from '../signing/signer';
15
15
  /**
16
16
  * Represents a PDF document with support for reading, writing, and modifying PDF files.
17
17
  * Handles document structure, revisions, encryption, and digital signatures.
@@ -214,9 +214,9 @@ export declare class PdfDocument extends PdfObject {
214
214
  * Sets whether the document should use incremental updates.
215
215
  * When true, locks all existing revisions to preserve original content.
216
216
  *
217
- * @param value - True to enable incremental mode, false to disable
217
+ * @param value - True to enable incremental mode, false to disable. Defaults to true.
218
218
  */
219
- setIncremental(value: boolean): void;
219
+ setIncremental(value?: boolean): void;
220
220
  /**
221
221
  * Checks if the document is in incremental mode.
222
222
  *
@@ -274,4 +274,10 @@ export declare class PdfDocument extends PdfObject {
274
274
  */
275
275
  static fromBytes(input: AsyncIterable<ByteArray> | Iterable<ByteArray>): Promise<PdfDocument>;
276
276
  isModified(): boolean;
277
+ /**
278
+ * Verifies all digital signatures in the document.
279
+ *
280
+ * @returns A promise that resolves to the verification result
281
+ */
282
+ verifySignatures(): Promise<PdfDocumentVerificationResult>;
277
283
  }
@@ -148,7 +148,11 @@ export class PdfDocument extends PdfObject {
148
148
  const revision = this.revisions.find((rev) => rev.xref.offset === lastStartXRef.offset.ref) ??
149
149
  this.revisions.find((rev) => rev.xref.offset.equals(lastStartXRef.offset.value));
150
150
  if (!revision) {
151
- throw new Error('Cannot find revision for last StartXRef');
151
+ throw new Error('Cannot find revision for last StartXRef with offset ' +
152
+ `${lastStartXRef.offset.value}. Options are: ` +
153
+ this.revisions
154
+ .map((rev) => rev.xref.offset.value)
155
+ .join(', '));
152
156
  }
153
157
  return revision;
154
158
  }
@@ -497,9 +501,9 @@ export class PdfDocument extends PdfObject {
497
501
  * Sets whether the document should use incremental updates.
498
502
  * When true, locks all existing revisions to preserve original content.
499
503
  *
500
- * @param value - True to enable incremental mode, false to disable
504
+ * @param value - True to enable incremental mode, false to disable. Defaults to true.
501
505
  */
502
- setIncremental(value) {
506
+ setIncremental(value = true) {
503
507
  for (const revision of this.revisions) {
504
508
  revision.locked = value;
505
509
  }
@@ -685,4 +689,12 @@ export class PdfDocument extends PdfObject {
685
689
  isModified() {
686
690
  return (super.isModified() || this.revisions.some((rev) => rev.isModified()));
687
691
  }
692
+ /**
693
+ * Verifies all digital signatures in the document.
694
+ *
695
+ * @returns A promise that resolves to the verification result
696
+ */
697
+ async verifySignatures() {
698
+ return await this.signer.verify(this);
699
+ }
688
700
  }
@@ -1,3 +1,4 @@
1
1
  export * from './signatures';
2
2
  export * from './document-security-store';
3
3
  export * from './types';
4
+ export * from './signer';
@@ -1,3 +1,4 @@
1
1
  export * from './signatures';
2
2
  export * from './document-security-store';
3
3
  export * from './types';
4
+ export * from './signer';
@@ -54,4 +54,11 @@ export declare class PdfAdbePkcs7DetachedSignatureObject extends PdfSignatureObj
54
54
  * @returns The CMS SignedData and revocation information.
55
55
  */
56
56
  sign: PdfSignatureObject['sign'];
57
+ /**
58
+ * Verifies the signature against the provided document bytes.
59
+ *
60
+ * @param options - Verification options including the signed bytes.
61
+ * @returns The verification result.
62
+ */
63
+ verify: PdfSignatureObject['verify'];
57
64
  }
@@ -114,4 +114,40 @@ export class PdfAdbePkcs7DetachedSignatureObject extends PdfSignatureObject {
114
114
  revocationInfo,
115
115
  };
116
116
  };
117
+ /**
118
+ * Verifies the signature against the provided document bytes.
119
+ *
120
+ * @param options - Verification options including the signed bytes.
121
+ * @returns The verification result.
122
+ */
123
+ verify = async (options) => {
124
+ const { bytes, certificateValidation } = options;
125
+ try {
126
+ const signedData = SignedData.fromCms(this.signedBytes);
127
+ const certValidationOptions = certificateValidation === true
128
+ ? {}
129
+ : certificateValidation || undefined;
130
+ const result = await signedData.verify({
131
+ data: bytes,
132
+ certificateValidation: certValidationOptions,
133
+ });
134
+ if (result.valid) {
135
+ return { valid: true };
136
+ }
137
+ else {
138
+ return {
139
+ valid: false,
140
+ reasons: result.reasons,
141
+ };
142
+ }
143
+ }
144
+ catch (error) {
145
+ return {
146
+ valid: false,
147
+ reasons: [
148
+ `Failed to verify signature: ${error instanceof Error ? error.message : String(error)}`,
149
+ ],
150
+ };
151
+ }
152
+ };
117
153
  }
@@ -52,4 +52,11 @@ export declare class PdfAdbePkcs7Sha1SignatureObject extends PdfSignatureObject
52
52
  * @returns The CMS SignedData and revocation information.
53
53
  */
54
54
  sign: PdfSignatureObject['sign'];
55
+ /**
56
+ * Verifies the signature against the provided document bytes.
57
+ *
58
+ * @param options - Verification options including the signed bytes.
59
+ * @returns The verification result.
60
+ */
61
+ verify: PdfSignatureObject['verify'];
55
62
  }
@@ -118,4 +118,58 @@ export class PdfAdbePkcs7Sha1SignatureObject extends PdfSignatureObject {
118
118
  revocationInfo,
119
119
  };
120
120
  };
121
+ /**
122
+ * Verifies the signature against the provided document bytes.
123
+ *
124
+ * @param options - Verification options including the signed bytes.
125
+ * @returns The verification result.
126
+ */
127
+ verify = async (options) => {
128
+ const { bytes, certificateValidation } = options;
129
+ try {
130
+ const signedData = SignedData.fromCms(this.signedBytes);
131
+ // For adbe.pkcs7.sha1, the signed content is the SHA-1 hash of the document
132
+ // We need to compute the SHA-1 hash of the data and compare with the embedded content
133
+ const expectedHash = await DigestAlgorithmIdentifier.digestAlgorithm('SHA-1').digest(bytes);
134
+ const certValidationOptions = certificateValidation === true
135
+ ? {}
136
+ : certificateValidation || undefined;
137
+ // Verify the signature with the hash as the data (non-detached mode)
138
+ const result = await signedData.verify({
139
+ certificateValidation: certValidationOptions,
140
+ });
141
+ if (!result.valid) {
142
+ return {
143
+ valid: false,
144
+ reasons: result.reasons,
145
+ };
146
+ }
147
+ // Additionally verify that the embedded hash matches the document hash
148
+ const embeddedContent = signedData.encapContentInfo.eContent;
149
+ if (!embeddedContent) {
150
+ return {
151
+ valid: false,
152
+ reasons: ['No embedded content in SignedData'],
153
+ };
154
+ }
155
+ // Compare the hashes
156
+ if (!this.compareArrays(embeddedContent, expectedHash)) {
157
+ return {
158
+ valid: false,
159
+ reasons: [
160
+ 'Document hash does not match embedded signature hash',
161
+ ],
162
+ };
163
+ }
164
+ return { valid: true };
165
+ }
166
+ catch (error) {
167
+ return {
168
+ valid: false,
169
+ reasons: [
170
+ `Failed to verify signature: ${error instanceof Error ? error.message : String(error)}`,
171
+ ],
172
+ };
173
+ }
174
+ };
121
175
  }
@@ -46,4 +46,11 @@ export declare class PdfAdbePkcsX509RsaSha1SignatureObject extends PdfSignatureO
46
46
  * @returns The signature bytes and revocation information.
47
47
  */
48
48
  sign: PdfSignatureObject['sign'];
49
+ /**
50
+ * Verifies the signature against the provided document bytes.
51
+ *
52
+ * @param options - Verification options including the signed bytes.
53
+ * @returns The verification result.
54
+ */
55
+ verify: PdfSignatureObject['verify'];
49
56
  }
@@ -2,7 +2,10 @@ import { PrivateKeyInfo } from 'pki-lite/keys/PrivateKeyInfo';
2
2
  import { PdfSignatureObject } from './base';
3
3
  import { OctetString } from 'pki-lite/asn1/OctetString';
4
4
  import { AlgorithmIdentifier } from 'pki-lite/algorithms/AlgorithmIdentifier';
5
+ import { Certificate } from 'pki-lite/x509/Certificate';
5
6
  import { fetchRevocationInfo } from '../utils';
7
+ import { PdfArray } from '../../core/objects/pdf-array';
8
+ import { PdfHexadecimal } from '../../core/objects/pdf-hexadecimal';
6
9
  /**
7
10
  * X.509 RSA-SHA1 signature object (adbe.x509.rsa_sha1).
8
11
  * Creates a raw RSA-SHA1 signature with certificates in the Cert entry.
@@ -78,4 +81,78 @@ export class PdfAdbePkcsX509RsaSha1SignatureObject extends PdfSignatureObject {
78
81
  revocationInfo,
79
82
  };
80
83
  };
84
+ /**
85
+ * Verifies the signature against the provided document bytes.
86
+ *
87
+ * @param options - Verification options including the signed bytes.
88
+ * @returns The verification result.
89
+ */
90
+ verify = async (options) => {
91
+ const { bytes } = options;
92
+ try {
93
+ const certificates = [];
94
+ const Cert = this.content.get('Cert');
95
+ if (Cert instanceof PdfArray) {
96
+ for (const certObj of Cert.items) {
97
+ const certBytes = certObj instanceof PdfHexadecimal
98
+ ? certObj.bytes
99
+ : certObj.raw;
100
+ const certificate = Certificate.fromDer(certBytes);
101
+ certificates.push(certificate);
102
+ }
103
+ }
104
+ else if (Cert instanceof PdfHexadecimal) {
105
+ const certificate = Certificate.fromDer(Cert.bytes);
106
+ certificates.push(certificate);
107
+ }
108
+ else if (Cert) {
109
+ const certificate = Certificate.fromDer(Cert.raw);
110
+ certificates.push(certificate);
111
+ }
112
+ else {
113
+ throw new Error('No Cert entry found in signature dictionary');
114
+ }
115
+ if (certificates.length === 0) {
116
+ return {
117
+ valid: false,
118
+ reasons: [
119
+ 'No certificates available for adbe.x509.rsa_sha1 verification',
120
+ ],
121
+ };
122
+ }
123
+ // Parse the signature as an OctetString
124
+ const signatureOctetString = OctetString.fromDer(this.signedBytes);
125
+ const signatureValue = signatureOctetString.bytes;
126
+ const signatureAlgorithm = AlgorithmIdentifier.signatureAlgorithm(PdfAdbePkcsX509RsaSha1SignatureObject.ALGORITHM);
127
+ for (const cert of certificates) {
128
+ const isValid = await signatureAlgorithm.verify(bytes, signatureValue, cert.tbsCertificate.subjectPublicKeyInfo);
129
+ if (isValid) {
130
+ if (options.certificateValidation === true) {
131
+ await cert.validate({
132
+ //TODO: implement default validation options
133
+ checkSignature: true,
134
+ validateChain: true,
135
+ otherCertificates: certificates,
136
+ });
137
+ }
138
+ else if (options.certificateValidation) {
139
+ await cert.validate(options.certificateValidation);
140
+ }
141
+ return { valid: true };
142
+ }
143
+ }
144
+ return {
145
+ valid: false,
146
+ reasons: ['Signature verification failed for all certificates'],
147
+ };
148
+ }
149
+ catch (error) {
150
+ return {
151
+ valid: false,
152
+ reasons: [
153
+ `Failed to verify signature: ${error instanceof Error ? error.message : String(error)}`,
154
+ ],
155
+ };
156
+ }
157
+ };
81
158
  }
@@ -1,5 +1,5 @@
1
1
  import { PdfDictionary } from '../../core/objects/pdf-dictionary';
2
- import { PdfSignatureDictionaryEntries, PdfSignatureSubType, RevocationInfo } from '../types';
2
+ import { PdfSignatureDictionaryEntries, PdfSignatureSubType, PdfSignatureVerificationOptions, PdfSignatureVerificationResult, RevocationInfo } from '../types';
3
3
  import { PdfHexadecimal } from '../../core/objects/pdf-hexadecimal';
4
4
  import { PdfIndirectObject } from '../../core/objects/pdf-indirect-object';
5
5
  import { ByteArray } from '../../types';
@@ -71,6 +71,13 @@ export declare abstract class PdfSignatureObject extends PdfIndirectObject<PdfSi
71
71
  signedBytes: ByteArray;
72
72
  revocationInfo?: RevocationInfo;
73
73
  }>;
74
+ /**
75
+ * Verifies the signature against the provided document bytes.
76
+ *
77
+ * @param options - Verification options including the signed bytes.
78
+ * @returns The verification result.
79
+ */
80
+ abstract verify(options: PdfSignatureVerificationOptions): Promise<PdfSignatureVerificationResult>;
74
81
  /**
75
82
  * Gets the signature hexadecimal content.
76
83
  *
@@ -104,4 +111,12 @@ export declare abstract class PdfSignatureObject extends PdfIndirectObject<PdfSi
104
111
  * @returns High order value to place signature near end of document.
105
112
  */
106
113
  order(): number;
114
+ /**
115
+ * Compares two byte arrays for equality.
116
+ *
117
+ * @param a - First byte array.
118
+ * @param b - Second byte array.
119
+ * @returns True if arrays are equal, false otherwise.
120
+ */
121
+ protected compareArrays(a: ByteArray, b: ByteArray): boolean;
107
122
  }
@@ -142,4 +142,22 @@ export class PdfSignatureObject extends PdfIndirectObject {
142
142
  order() {
143
143
  return PdfIndirectObject.MAX_ORDER_INDEX - 10;
144
144
  }
145
+ /**
146
+ * Compares two byte arrays for equality.
147
+ *
148
+ * @param a - First byte array.
149
+ * @param b - Second byte array.
150
+ * @returns True if arrays are equal, false otherwise.
151
+ */
152
+ compareArrays(a, b) {
153
+ if (a.length !== b.length) {
154
+ return false;
155
+ }
156
+ for (let i = 0; i < a.length; i++) {
157
+ if (a[i] !== b[i]) {
158
+ return false;
159
+ }
160
+ }
161
+ return true;
162
+ }
145
163
  }
@@ -61,4 +61,11 @@ export declare class PdfEtsiCadesDetachedSignatureObject extends PdfSignatureObj
61
61
  * @returns The CMS SignedData and revocation information.
62
62
  */
63
63
  sign: PdfSignatureObject['sign'];
64
+ /**
65
+ * Verifies the signature against the provided document bytes.
66
+ *
67
+ * @param options - Verification options including the signed bytes.
68
+ * @returns The verification result.
69
+ */
70
+ verify: PdfSignatureObject['verify'];
64
71
  }
@@ -156,4 +156,40 @@ export class PdfEtsiCadesDetachedSignatureObject extends PdfSignatureObject {
156
156
  revocationInfo,
157
157
  };
158
158
  };
159
+ /**
160
+ * Verifies the signature against the provided document bytes.
161
+ *
162
+ * @param options - Verification options including the signed bytes.
163
+ * @returns The verification result.
164
+ */
165
+ verify = async (options) => {
166
+ const { bytes, certificateValidation } = options;
167
+ try {
168
+ const signedData = SignedData.fromCms(this.signedBytes);
169
+ const certValidationOptions = certificateValidation === true
170
+ ? {}
171
+ : certificateValidation || undefined;
172
+ const result = await signedData.verify({
173
+ data: bytes,
174
+ certificateValidation: certValidationOptions,
175
+ });
176
+ if (result.valid) {
177
+ return { valid: true };
178
+ }
179
+ else {
180
+ return {
181
+ valid: false,
182
+ reasons: result.reasons,
183
+ };
184
+ }
185
+ }
186
+ catch (error) {
187
+ return {
188
+ valid: false,
189
+ reasons: [
190
+ `Failed to verify signature: ${error instanceof Error ? error.message : String(error)}`,
191
+ ],
192
+ };
193
+ }
194
+ };
159
195
  }
@@ -34,4 +34,11 @@ export declare class PdfEtsiRfc3161SignatureObject extends PdfSignatureObject {
34
34
  * @throws Error if no timestamp token is received.
35
35
  */
36
36
  sign: PdfSignatureObject['sign'];
37
+ /**
38
+ * Verifies the timestamp signature against the provided document bytes.
39
+ *
40
+ * @param options - Verification options including the signed bytes.
41
+ * @returns The verification result.
42
+ */
43
+ verify: PdfSignatureObject['verify'];
37
44
  }
@@ -1,9 +1,12 @@
1
1
  import { PdfSignatureObject } from './base';
2
2
  import { DigestAlgorithmIdentifier } from 'pki-lite/algorithms/AlgorithmIdentifier';
3
3
  import { TimeStampReq } from 'pki-lite/timestamp/TimeStampReq';
4
+ import { TSTInfo } from 'pki-lite/timestamp/TSTInfo';
4
5
  import { MessageImprint } from 'pki-lite/timestamp/MessageImprint';
5
6
  import { SignedData } from 'pki-lite/pkcs7/SignedData';
6
7
  import { fetchRevocationInfo } from '../utils';
8
+ import { Certificate } from 'pki-lite/x509/Certificate';
9
+ import { OIDs } from 'pki-lite/core/OIDs';
7
10
  /**
8
11
  * RFC 3161 timestamp signature object (ETSI.RFC3161).
9
12
  * Creates document timestamps using a Time Stamp Authority (TSA).
@@ -67,4 +70,103 @@ export class PdfEtsiRfc3161SignatureObject extends PdfSignatureObject {
67
70
  revocationInfo,
68
71
  };
69
72
  };
73
+ /**
74
+ * Verifies the timestamp signature against the provided document bytes.
75
+ *
76
+ * @param options - Verification options including the signed bytes.
77
+ * @returns The verification result.
78
+ */
79
+ verify = async (options) => {
80
+ const { bytes, certificateValidation } = options;
81
+ const digestAlgorithm = DigestAlgorithmIdentifier.digestAlgorithm('SHA-512');
82
+ const expectedMessageImprint = new MessageImprint({
83
+ hashAlgorithm: digestAlgorithm,
84
+ hashedMessage: await digestAlgorithm.digest(bytes),
85
+ });
86
+ try {
87
+ const signedData = SignedData.fromCms(this.signedBytes);
88
+ // Extract TSTInfo from the signed data's encapsulated content
89
+ // The eContentType should be id-ct-TSTInfo (1.2.840.113549.1.9.16.1.4)
90
+ const encapContentInfo = signedData.encapContentInfo;
91
+ if (encapContentInfo.eContentType.toString() !== OIDs.PKCS7.TST_INFO) {
92
+ return {
93
+ valid: false,
94
+ reasons: [
95
+ `Invalid content type: expected id-ct-TSTInfo (${OIDs.PKCS7.TST_INFO}), got ${encapContentInfo.eContentType.toString()}`,
96
+ ],
97
+ };
98
+ }
99
+ if (!encapContentInfo.eContent) {
100
+ return {
101
+ valid: false,
102
+ reasons: ['No TSTInfo content found in timestamp token'],
103
+ };
104
+ }
105
+ // Parse the TSTInfo from the encapsulated content
106
+ const tstInfo = TSTInfo.fromDer(encapContentInfo.eContent);
107
+ // Verify that the messageImprint in TSTInfo matches what we computed
108
+ const tstMessageImprint = tstInfo.messageImprint;
109
+ if (tstMessageImprint.hashAlgorithm.algorithm.toString() !==
110
+ expectedMessageImprint.hashAlgorithm.algorithm.toString()) {
111
+ return {
112
+ valid: false,
113
+ reasons: [
114
+ `Hash algorithm mismatch: TSTInfo uses ${tstMessageImprint.hashAlgorithm.algorithm.toString()}, expected ${expectedMessageImprint.hashAlgorithm.algorithm.toString()}`,
115
+ ],
116
+ };
117
+ }
118
+ const tstHash = tstMessageImprint.hashedMessage;
119
+ const expectedHash = expectedMessageImprint.hashedMessage;
120
+ if (tstHash.length !== expectedHash.length) {
121
+ return {
122
+ valid: false,
123
+ reasons: [
124
+ `Hash length mismatch: TSTInfo has ${tstHash.length} bytes, expected ${expectedHash.length} bytes`,
125
+ ],
126
+ };
127
+ }
128
+ for (let i = 0; i < tstHash.length; i++) {
129
+ if (tstHash[i] !== expectedHash[i]) {
130
+ return {
131
+ valid: false,
132
+ reasons: [
133
+ 'Message imprint mismatch: the timestamp does not match the document',
134
+ ],
135
+ };
136
+ }
137
+ }
138
+ // Verify the signature on the SignedData
139
+ const certValidationOptions = certificateValidation === true
140
+ ? {}
141
+ : certificateValidation || undefined;
142
+ if (options.certificateValidation) {
143
+ const certificates = signedData.certificates ?? [];
144
+ for (const cert of certificates) {
145
+ if (!(cert instanceof Certificate)) {
146
+ //TODO: support other cert types
147
+ continue;
148
+ }
149
+ if (!(await cert.validate(certValidationOptions))) {
150
+ return {
151
+ valid: false,
152
+ reasons: [
153
+ 'Certificate validation failed for timestamp signer',
154
+ ],
155
+ };
156
+ }
157
+ }
158
+ }
159
+ return {
160
+ valid: true,
161
+ };
162
+ }
163
+ catch (error) {
164
+ return {
165
+ valid: false,
166
+ reasons: [
167
+ `Failed to verify signature: ${error instanceof Error ? error.message : String(error)}`,
168
+ ],
169
+ };
170
+ }
171
+ };
70
172
  }
@@ -1,4 +1,24 @@
1
1
  import { PdfDocument } from '../pdf/pdf-document';
2
+ import { PdfSignatureObject } from './signatures';
3
+ import { PdfSignatureVerificationResult, CertificateValidationOptions, PdfSignatureSubType } from './types';
4
+ /**
5
+ * Result of verifying all signatures in a document.
6
+ */
7
+ export type PdfDocumentVerificationResult = {
8
+ /** Whether all signatures in the document are valid. */
9
+ valid: boolean;
10
+ /** Individual signature verification results. */
11
+ signatures: {
12
+ /** The signature subfilter type. */
13
+ type: PdfSignatureSubType;
14
+ /** Index of the signature in the document. */
15
+ index: number;
16
+ /** The signature object. */
17
+ signature: PdfSignatureObject;
18
+ /** The verification result. */
19
+ result: PdfSignatureVerificationResult;
20
+ }[];
21
+ };
2
22
  /**
3
23
  * Handles digital signing operations for PDF documents.
4
24
  * Processes signature objects and optionally stores revocation information in the DSS.
@@ -20,4 +40,39 @@ export declare class PdfSigner {
20
40
  * @returns The signed document.
21
41
  */
22
42
  sign(document: PdfDocument): Promise<PdfDocument>;
43
+ /**
44
+ * Instantiates the appropriate signature object based on SubFilter type.
45
+ *
46
+ * @param signatureDict - The signature dictionary.
47
+ * @returns A properly typed PdfSignatureObject subclass.
48
+ */
49
+ private instantiateSignatureObject;
50
+ /**
51
+ * Verifies all signatures in the document.
52
+ * First serializes the document to bytes and reloads it to ensure signatures
53
+ * are properly deserialized into the correct classes before verification.
54
+ * Then searches for signature objects, computes their byte ranges, and verifies each one.
55
+ *
56
+ * @param document - The PDF document to verify.
57
+ * @param options - Optional verification options.
58
+ * @returns The verification result for all signatures.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * const signer = new PdfSigner()
63
+ * const result = await signer.verify(document)
64
+ * if (result.valid) {
65
+ * console.log('All signatures are valid')
66
+ * } else {
67
+ * result.signatures.forEach(sig => {
68
+ * if (!sig.result.valid) {
69
+ * console.log(`Signature ${sig.index} invalid:`, sig.result.reasons)
70
+ * }
71
+ * })
72
+ * }
73
+ * ```
74
+ */
75
+ verify(document: PdfDocument, options?: {
76
+ certificateValidation?: CertificateValidationOptions | boolean;
77
+ }): Promise<PdfDocumentVerificationResult>;
23
78
  }
@@ -3,7 +3,11 @@ import { PdfHexadecimalToken } from '../core/tokens/hexadecimal-token';
3
3
  import { PdfNameToken } from '../core/tokens/name-token';
4
4
  import { concatUint8Arrays } from '../utils/concatUint8Arrays';
5
5
  import { PdfDocumentSecurityStoreObject } from './document-security-store';
6
- import { PdfSignatureObject } from './signatures';
6
+ import { PdfSignatureObject, PdfAdbePkcs7DetachedSignatureObject, PdfAdbePkcs7Sha1SignatureObject, PdfAdbePkcsX509RsaSha1SignatureObject, PdfEtsiCadesDetachedSignatureObject, PdfEtsiRfc3161SignatureObject, PdfSignatureDictionary, } from './signatures';
7
+ import { PdfNumber } from '../core/objects/pdf-number';
8
+ import { PdfIndirectObject } from '../core/objects/pdf-indirect-object';
9
+ import { PdfDictionary } from '../core/objects/pdf-dictionary';
10
+ import { PdfArray } from '../core/objects/pdf-array';
7
11
  /**
8
12
  * Handles digital signing operations for PDF documents.
9
13
  * Processes signature objects and optionally stores revocation information in the DSS.
@@ -89,4 +93,195 @@ export class PdfSigner {
89
93
  }
90
94
  return document;
91
95
  }
96
+ /**
97
+ * Instantiates the appropriate signature object based on SubFilter type.
98
+ *
99
+ * @param signatureDict - The signature dictionary.
100
+ * @returns A properly typed PdfSignatureObject subclass.
101
+ */
102
+ instantiateSignatureObject(signatureDict) {
103
+ const content = signatureDict.content;
104
+ const subFilter = content.get('SubFilter').value;
105
+ // Create a PdfSignatureDictionary wrapper
106
+ const sigDict = new PdfSignatureDictionary({
107
+ Type: content.get('Type'),
108
+ Filter: content.get('Filter'),
109
+ SubFilter: content.get('SubFilter'),
110
+ Reason: content.get('Reason'),
111
+ M: content.get('M'),
112
+ Name: content.get('Name'),
113
+ Reference: content.get('Reference'),
114
+ ContactInfo: content.get('ContactInfo'),
115
+ Location: content.get('Location'),
116
+ Cert: content.get('Cert'),
117
+ ByteRange: content.get('ByteRange'),
118
+ Contents: content.get('Contents'),
119
+ });
120
+ // Instantiate the appropriate signature type based on SubFilter
121
+ let signatureObj;
122
+ switch (subFilter) {
123
+ case 'adbe.pkcs7.detached':
124
+ signatureObj = new PdfAdbePkcs7DetachedSignatureObject({
125
+ privateKey: new Uint8Array(),
126
+ certificate: new Uint8Array(),
127
+ });
128
+ break;
129
+ case 'adbe.pkcs7.sha1':
130
+ signatureObj = new PdfAdbePkcs7Sha1SignatureObject({
131
+ privateKey: new Uint8Array(),
132
+ certificate: new Uint8Array(),
133
+ });
134
+ break;
135
+ case 'adbe.x509.rsa_sha1':
136
+ signatureObj = new PdfAdbePkcsX509RsaSha1SignatureObject({
137
+ privateKey: new Uint8Array(),
138
+ certificate: new Uint8Array(),
139
+ });
140
+ break;
141
+ case 'ETSI.CAdES.detached':
142
+ signatureObj = new PdfEtsiCadesDetachedSignatureObject({
143
+ privateKey: new Uint8Array(),
144
+ certificate: new Uint8Array(),
145
+ });
146
+ break;
147
+ case 'ETSI.RFC3161':
148
+ signatureObj = new PdfEtsiRfc3161SignatureObject({
149
+ timeStampAuthority: {
150
+ url: '',
151
+ },
152
+ });
153
+ break;
154
+ default:
155
+ throw new Error(`Unsupported signature SubFilter type: ${subFilter}`);
156
+ }
157
+ // Replace the content with the actual signature dictionary
158
+ signatureObj.content = sigDict;
159
+ signatureObj.objectNumber = signatureDict.objectNumber;
160
+ signatureObj.generationNumber = signatureDict.generationNumber;
161
+ return signatureObj;
162
+ }
163
+ /**
164
+ * Verifies all signatures in the document.
165
+ * First serializes the document to bytes and reloads it to ensure signatures
166
+ * are properly deserialized into the correct classes before verification.
167
+ * Then searches for signature objects, computes their byte ranges, and verifies each one.
168
+ *
169
+ * @param document - The PDF document to verify.
170
+ * @param options - Optional verification options.
171
+ * @returns The verification result for all signatures.
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * const signer = new PdfSigner()
176
+ * const result = await signer.verify(document)
177
+ * if (result.valid) {
178
+ * console.log('All signatures are valid')
179
+ * } else {
180
+ * result.signatures.forEach(sig => {
181
+ * if (!sig.result.valid) {
182
+ * console.log(`Signature ${sig.index} invalid:`, sig.result.reasons)
183
+ * }
184
+ * })
185
+ * }
186
+ * ```
187
+ */
188
+ async verify(document, options) {
189
+ const documentBytes = document.toBytes();
190
+ const results = [];
191
+ let allValid = true;
192
+ const documentObjects = document.objects;
193
+ for (let i = 0; i < documentObjects.length; i++) {
194
+ const obj = documentObjects[i];
195
+ if (!(obj instanceof PdfIndirectObject)) {
196
+ continue;
197
+ }
198
+ if (!(obj.content instanceof PdfDictionary)) {
199
+ continue;
200
+ }
201
+ // Check if this is a signature dictionary by looking for Type = /Sig or SubFilter
202
+ const typeEntry = obj.content.get('Type');
203
+ const subFilterEntry = obj.content.get('SubFilter');
204
+ // A signature can be identified by Type=/Sig or by having a SubFilter entry
205
+ const isSignature = (typeEntry && typeEntry.toString() === '/Sig') ||
206
+ (subFilterEntry &&
207
+ subFilterEntry.toString().startsWith('/adbe.')) ||
208
+ subFilterEntry?.toString().startsWith('/ETSI.');
209
+ if (!isSignature) {
210
+ continue;
211
+ }
212
+ const signatureDict = obj;
213
+ const subFilter = signatureDict.content.get('SubFilter').value;
214
+ // Instantiate the correct signature type
215
+ const signature = this.instantiateSignatureObject(signatureDict);
216
+ // Get the ByteRange from the signature dictionary
217
+ const byteRangeEntry = signatureDict.content.get('ByteRange');
218
+ if (!byteRangeEntry) {
219
+ results.push({
220
+ type: subFilter,
221
+ index: i,
222
+ signature,
223
+ result: {
224
+ valid: false,
225
+ reasons: ['Signature is missing ByteRange entry'],
226
+ },
227
+ });
228
+ allValid = false;
229
+ continue;
230
+ }
231
+ // Extract the byte range values
232
+ const byteRangeArray = byteRangeEntry.as(PdfArray);
233
+ if (!byteRangeArray) {
234
+ results.push({
235
+ type: subFilter,
236
+ index: i,
237
+ signature,
238
+ result: {
239
+ valid: false,
240
+ reasons: ['ByteRange is not an array'],
241
+ },
242
+ });
243
+ allValid = false;
244
+ continue;
245
+ }
246
+ const byteRange = byteRangeArray.items.map((item) => {
247
+ if (item instanceof PdfNumber) {
248
+ return item.value;
249
+ }
250
+ return 0;
251
+ });
252
+ if (byteRange.length !== 4) {
253
+ results.push({
254
+ type: subFilter,
255
+ index: i,
256
+ signature,
257
+ result: {
258
+ valid: false,
259
+ reasons: ['Invalid ByteRange format'],
260
+ },
261
+ });
262
+ allValid = false;
263
+ continue;
264
+ }
265
+ // Compute the bytes that were signed (excluding the signature contents)
266
+ const signedBytes = concatUint8Arrays(documentBytes.slice(byteRange[0], byteRange[0] + byteRange[1]), documentBytes.slice(byteRange[2], byteRange[2] + byteRange[3]));
267
+ // Verify the signature
268
+ const result = await signature.verify({
269
+ bytes: signedBytes,
270
+ certificateValidation: options?.certificateValidation,
271
+ });
272
+ results.push({
273
+ type: subFilter,
274
+ index: i,
275
+ signature,
276
+ result,
277
+ });
278
+ if (!result.valid) {
279
+ allValid = false;
280
+ }
281
+ }
282
+ return {
283
+ valid: allValid,
284
+ signatures: results,
285
+ };
286
+ }
92
287
  }
@@ -1,4 +1,5 @@
1
1
  import { HashAlgorithm } from 'pki-lite/core/crypto/index.js';
2
+ import type { CertificateValidationOptions, CertificateValidationResult, TrustAnchor } from 'pki-lite/core/CertificateValidator.js';
2
3
  import { PdfDictionary } from '../core/objects/pdf-dictionary';
3
4
  import { PdfName } from '../core/objects/pdf-name';
4
5
  import { PdfHexadecimal } from '../core/objects/pdf-hexadecimal';
@@ -6,6 +7,7 @@ import { PdfArray } from '../core/objects/pdf-array';
6
7
  import { PdfNumber } from '../core/objects/pdf-number';
7
8
  import { PdfString } from '../core/objects/pdf-string';
8
9
  import { ByteArray } from '../types';
10
+ export type { CertificateValidationOptions, CertificateValidationResult, TrustAnchor, };
9
11
  /**
10
12
  * PDF signature subfilter types defining the signature format.
11
13
  * - 'adbe.pkcs7.detached': PKCS#7 detached signature
@@ -76,3 +78,27 @@ export type SignaturePolicyDocument = {
76
78
  /** Hash algorithm used for the policy document. */
77
79
  hashAlgorithm: HashAlgorithm;
78
80
  };
81
+ /**
82
+ * Result of a PDF signature verification operation.
83
+ */
84
+ export type PdfSignatureVerificationResult = {
85
+ /** Whether the signature is valid. */
86
+ valid: boolean;
87
+ /** Reasons for verification failure, if any. */
88
+ reasons?: string[];
89
+ /** Certificate validation result, if certificate validation was performed. */
90
+ certificateValidationResult?: CertificateValidationResult;
91
+ };
92
+ /**
93
+ * Options for PDF signature verification.
94
+ */
95
+ export type PdfSignatureVerificationOptions = {
96
+ /** The original document bytes that were signed. */
97
+ bytes: ByteArray;
98
+ /**
99
+ * Certificate validation options.
100
+ * Pass `true` to use default certificate validation, or provide custom options.
101
+ * Pass `undefined` or `false` to skip certificate validation.
102
+ */
103
+ certificateValidation?: CertificateValidationOptions | boolean;
104
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-lite",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -54,8 +54,8 @@
54
54
  ],
55
55
  "dependencies": {
56
56
  "pako": "2.1.0",
57
- "pki-lite": "^1.0.10",
58
- "pki-lite-crypto-extended": "^1.0.10"
57
+ "pki-lite": "^1.0.12",
58
+ "pki-lite-crypto-extended": "^1.0.12"
59
59
  },
60
60
  "scripts": {
61
61
  "test:acceptance": "vitest run test/acceptance",