@awacloud/pdf 0.0.0-stage → 1.0.0

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.
Files changed (345) hide show
  1. package/CHANGELOG.md +609 -0
  2. package/LICENSE +661 -0
  3. package/NOTICE +77 -0
  4. package/README.md +363 -2
  5. package/dist/build/index.js +21 -0
  6. package/dist/build/pdf-full-rw.js +10972 -0
  7. package/dist/build/pdf-full-rw.meta.json +105 -0
  8. package/dist/build/pdf-full-rw.min.js +53 -0
  9. package/dist/build/pdf-full.js +6078 -0
  10. package/dist/build/pdf-full.meta.json +90 -0
  11. package/dist/build/pdf-full.min.js +32 -0
  12. package/dist/build/pdf-large-rw.js +10367 -0
  13. package/dist/build/pdf-large-rw.meta.json +99 -0
  14. package/dist/build/pdf-large-rw.min.js +53 -0
  15. package/dist/build/pdf-large.js +5473 -0
  16. package/dist/build/pdf-large.meta.json +84 -0
  17. package/dist/build/pdf-large.min.js +32 -0
  18. package/dist/build/pdf-legacy-rw.js +12402 -0
  19. package/dist/build/pdf-legacy-rw.meta.json +110 -0
  20. package/dist/build/pdf-legacy-rw.min.js +53 -0
  21. package/dist/build/pdf-legacy.js +7508 -0
  22. package/dist/build/pdf-legacy.meta.json +95 -0
  23. package/dist/build/pdf-legacy.min.js +32 -0
  24. package/dist/build/pdf-rw.js +7578 -0
  25. package/dist/build/pdf-rw.meta.json +77 -0
  26. package/dist/build/pdf-rw.min.js +53 -0
  27. package/dist/build/pdf.js +2684 -0
  28. package/dist/build/pdf.meta.json +62 -0
  29. package/dist/build/pdf.min.js +32 -0
  30. package/dist/standalone/pdf-full-rw.js +16798 -0
  31. package/dist/standalone/pdf-full-rw.meta.json +78 -0
  32. package/dist/standalone/pdf-full-rw.min.js +56 -0
  33. package/dist/standalone/pdf-full.js +11904 -0
  34. package/dist/standalone/pdf-full.meta.json +63 -0
  35. package/dist/standalone/pdf-full.min.js +35 -0
  36. package/dist/standalone/pdf-large-rw.js +16193 -0
  37. package/dist/standalone/pdf-large-rw.meta.json +72 -0
  38. package/dist/standalone/pdf-large-rw.min.js +56 -0
  39. package/dist/standalone/pdf-large.js +11299 -0
  40. package/dist/standalone/pdf-large.meta.json +57 -0
  41. package/dist/standalone/pdf-large.min.js +35 -0
  42. package/dist/standalone/pdf-legacy-rw.js +18228 -0
  43. package/dist/standalone/pdf-legacy-rw.meta.json +83 -0
  44. package/dist/standalone/pdf-legacy-rw.min.js +56 -0
  45. package/dist/standalone/pdf-legacy.js +13334 -0
  46. package/dist/standalone/pdf-legacy.meta.json +68 -0
  47. package/dist/standalone/pdf-legacy.min.js +35 -0
  48. package/dist/standalone/pdf-rw.js +13404 -0
  49. package/dist/standalone/pdf-rw.meta.json +50 -0
  50. package/dist/standalone/pdf-rw.min.js +56 -0
  51. package/dist/standalone/pdf.js +8510 -0
  52. package/dist/standalone/pdf.meta.json +35 -0
  53. package/dist/standalone/pdf.min.js +35 -0
  54. package/docs/README.md +53 -0
  55. package/docs/api/README.md +38 -0
  56. package/docs/api/_shared/README.md +91 -0
  57. package/docs/api/action/README.md +29 -0
  58. package/docs/api/action/action.md +81 -0
  59. package/docs/api/action/goTo.md +66 -0
  60. package/docs/api/action/launch.md +58 -0
  61. package/docs/api/action/named.md +55 -0
  62. package/docs/api/action/uri.md +54 -0
  63. package/docs/api/annot/README.md +53 -0
  64. package/docs/api/annot/annot.md +114 -0
  65. package/docs/api/annot/fileAttach.md +53 -0
  66. package/docs/api/annot/freeText.md +68 -0
  67. package/docs/api/annot/ink.md +69 -0
  68. package/docs/api/annot/link.md +74 -0
  69. package/docs/api/annot/markup.md +83 -0
  70. package/docs/api/annot/popup.md +52 -0
  71. package/docs/api/annot/projection.md +56 -0
  72. package/docs/api/annot/redact.md +67 -0
  73. package/docs/api/annot/square.md +87 -0
  74. package/docs/api/annot/stamp.md +54 -0
  75. package/docs/api/annot/text.md +69 -0
  76. package/docs/api/annot/widget.md +69 -0
  77. package/docs/api/associatedFiles/README.md +9 -0
  78. package/docs/api/associatedFiles/associatedFiles.md +78 -0
  79. package/docs/api/bundles/README.md +68 -0
  80. package/docs/api/bundles/dist-matrix.md +165 -0
  81. package/docs/api/bundles/pdf-full.md +148 -0
  82. package/docs/api/bundles/pdf-large.md +144 -0
  83. package/docs/api/bundles/pdf-legacy.md +169 -0
  84. package/docs/api/content/README.md +29 -0
  85. package/docs/api/content/color.md +99 -0
  86. package/docs/api/content/graphics.md +114 -0
  87. package/docs/api/content/images.md +124 -0
  88. package/docs/api/content/ops.md +100 -0
  89. package/docs/api/content/stream.md +107 -0
  90. package/docs/api/content/text.md +98 -0
  91. package/docs/api/crypto/README.md +29 -0
  92. package/docs/api/crypto/aesGcm.md +72 -0
  93. package/docs/api/crypto/permissions.md +79 -0
  94. package/docs/api/crypto/security.md +98 -0
  95. package/docs/api/crypto/standardV4.md +104 -0
  96. package/docs/api/crypto/standardV5.md +84 -0
  97. package/docs/api/crypto/standardV6.md +93 -0
  98. package/docs/api/destination/README.md +9 -0
  99. package/docs/api/destination/destination.md +79 -0
  100. package/docs/api/document/README.md +29 -0
  101. package/docs/api/document/builder.md +281 -0
  102. package/docs/api/document/catalog.md +98 -0
  103. package/docs/api/document/document.md +187 -0
  104. package/docs/api/document/encryptedWriter.md +149 -0
  105. package/docs/api/document/incrementalWriter.md +148 -0
  106. package/docs/api/document/page.md +99 -0
  107. package/docs/api/document/pages.md +82 -0
  108. package/docs/api/document/resources.md +102 -0
  109. package/docs/api/document/writer.md +157 -0
  110. package/docs/api/document/xrefStreamWriter.md +122 -0
  111. package/docs/api/embedded/README.md +13 -0
  112. package/docs/api/embedded/collection.md +80 -0
  113. package/docs/api/embedded/embeddedFile.md +86 -0
  114. package/docs/api/embedded/fileSpec.md +87 -0
  115. package/docs/api/errors.md +110 -0
  116. package/docs/api/extra/3d-richmedia.md +76 -0
  117. package/docs/api/extra/README.md +99 -0
  118. package/docs/api/extra/annot-extended.md +71 -0
  119. package/docs/api/extra/associated-files.md +70 -0
  120. package/docs/api/extra/ccitt-fax-decoder.md +74 -0
  121. package/docs/api/extra/color-spaces-extended.md +72 -0
  122. package/docs/api/extra/content-ops-extended.md +82 -0
  123. package/docs/api/extra/document-parts.md +69 -0
  124. package/docs/api/extra/embedded-files-portfolio.md +87 -0
  125. package/docs/api/extra/font-cid-typed.md +77 -0
  126. package/docs/api/extra/font-color-tagging.md +76 -0
  127. package/docs/api/extra/form-actions-extended.md +75 -0
  128. package/docs/api/extra/info-dict-deprecated.md +72 -0
  129. package/docs/api/extra/jbig2-read.md +80 -0
  130. package/docs/api/extra/legacy-deprecated-annots.md +89 -0
  131. package/docs/api/extra/legacy-deprecated-filters.md +78 -0
  132. package/docs/api/extra/legacy-rc4-read.md +74 -0
  133. package/docs/api/extra/legacy-xfa-read.md +65 -0
  134. package/docs/api/extra/linearization-write.md +71 -0
  135. package/docs/api/extra/misc.md +93 -0
  136. package/docs/api/extra/optional-content-extended.md +83 -0
  137. package/docs/api/extra/pdf-a-output-intent.md +65 -0
  138. package/docs/api/extra/pdf-sandbox.md +76 -0
  139. package/docs/api/extra/pdf-ua-tagged.md +63 -0
  140. package/docs/api/extra/pdf-x-prepress.md +65 -0
  141. package/docs/api/extra/redaction-iso32005.md +65 -0
  142. package/docs/api/extra/shading-typed.md +73 -0
  143. package/docs/api/extra/sig-aes-gcm.md +69 -0
  144. package/docs/api/extra/sig-pades.md +103 -0
  145. package/docs/api/extra/tagged-pdf-typed.md +78 -0
  146. package/docs/api/extra/transparency-typed.md +74 -0
  147. package/docs/api/extra/well-tagged-pdf.md +61 -0
  148. package/docs/api/extra/xmp-extended.md +65 -0
  149. package/docs/api/font/README.md +25 -0
  150. package/docs/api/font/embed.md +157 -0
  151. package/docs/api/font/encoding.md +95 -0
  152. package/docs/api/font/font.md +97 -0
  153. package/docs/api/font/type3.md +89 -0
  154. package/docs/api/form/README.md +35 -0
  155. package/docs/api/form/acroform.md +88 -0
  156. package/docs/api/form/appearance.md +87 -0
  157. package/docs/api/form/button.md +97 -0
  158. package/docs/api/form/choice.md +96 -0
  159. package/docs/api/form/fieldTree.md +93 -0
  160. package/docs/api/form/signature.md +90 -0
  161. package/docs/api/form/text.md +88 -0
  162. package/docs/api/linearization/README.md +11 -0
  163. package/docs/api/linearization/linearization.md +81 -0
  164. package/docs/api/main.md +116 -0
  165. package/docs/api/metadata/README.md +10 -0
  166. package/docs/api/metadata/info.md +70 -0
  167. package/docs/api/metadata/xmp.md +62 -0
  168. package/docs/api/ocg/README.md +23 -0
  169. package/docs/api/ocg/config.md +95 -0
  170. package/docs/api/ocg/ocg.md +77 -0
  171. package/docs/api/outline/README.md +11 -0
  172. package/docs/api/outline/outline.md +107 -0
  173. package/docs/api/pdf.md +152 -0
  174. package/docs/api/prepress/README.md +10 -0
  175. package/docs/api/prepress/outputIntent.md +79 -0
  176. package/docs/api/prepress/pageBoundary.md +75 -0
  177. package/docs/api/sig/README.md +32 -0
  178. package/docs/api/sig/byteRange.md +120 -0
  179. package/docs/api/sig/certChain.md +84 -0
  180. package/docs/api/sig/dss.md +111 -0
  181. package/docs/api/sig/oids.md +76 -0
  182. package/docs/api/sig/sha1.md +72 -0
  183. package/docs/api/sig/sign.md +317 -0
  184. package/docs/api/sig/signature.md +178 -0
  185. package/docs/api/sig/timestamp.md +84 -0
  186. package/docs/api/syntax/README.md +29 -0
  187. package/docs/api/syntax/crossRefStream.md +115 -0
  188. package/docs/api/syntax/filters/README.md +50 -0
  189. package/docs/api/syntax/filters/ascii85.md +76 -0
  190. package/docs/api/syntax/filters/asciiHex.md +73 -0
  191. package/docs/api/syntax/filters/dispatch.md +125 -0
  192. package/docs/api/syntax/filters/flate.md +134 -0
  193. package/docs/api/syntax/filters/runLength.md +78 -0
  194. package/docs/api/syntax/objStream.md +88 -0
  195. package/docs/api/syntax/parser-obj.md +97 -0
  196. package/docs/api/syntax/parser.md +151 -0
  197. package/docs/api/syntax/serializer.md +109 -0
  198. package/docs/api/syntax/tokenizer.md +104 -0
  199. package/docs/api/syntax/trailer.md +85 -0
  200. package/docs/api/syntax/xref.md +139 -0
  201. package/docs/api/tagged/README.md +25 -0
  202. package/docs/api/tagged/classMap.md +67 -0
  203. package/docs/api/tagged/markedContent.md +62 -0
  204. package/docs/api/tagged/parentTree.md +67 -0
  205. package/docs/api/tagged/roleMap.md +67 -0
  206. package/docs/api/tagged/structElement.md +76 -0
  207. package/docs/api/tagged/structTree.md +75 -0
  208. package/docs/guide/coverage.md +113 -0
  209. package/docs/guide/crypto.md +121 -0
  210. package/docs/guide/extending.md +76 -0
  211. package/docs/guide/getting-started.md +75 -0
  212. package/docs/guide/legacy-1.7.md +42 -0
  213. package/docs/guide/pades-integration.md +579 -0
  214. package/docs/guide/read-pdf.md +89 -0
  215. package/package.json +97 -4
  216. package/src/_shared/index.js +179 -0
  217. package/src/action/action.js +119 -0
  218. package/src/action/goTo.js +89 -0
  219. package/src/action/launch.js +61 -0
  220. package/src/action/named.js +54 -0
  221. package/src/action/uri.js +51 -0
  222. package/src/annot/annot.js +212 -0
  223. package/src/annot/fileAttach.js +55 -0
  224. package/src/annot/freeText.js +82 -0
  225. package/src/annot/ink.js +77 -0
  226. package/src/annot/link.js +77 -0
  227. package/src/annot/markup.js +91 -0
  228. package/src/annot/popup.js +53 -0
  229. package/src/annot/projection.js +52 -0
  230. package/src/annot/redact.js +87 -0
  231. package/src/annot/square.js +132 -0
  232. package/src/annot/stamp.js +48 -0
  233. package/src/annot/text.js +54 -0
  234. package/src/annot/widget.js +61 -0
  235. package/src/associatedFiles/associatedFiles.js +86 -0
  236. package/src/bundles/pdf-full.js +91 -0
  237. package/src/bundles/pdf-large.js +81 -0
  238. package/src/bundles/pdf-legacy.js +107 -0
  239. package/src/content/color.js +114 -0
  240. package/src/content/graphics.js +192 -0
  241. package/src/content/images.js +160 -0
  242. package/src/content/ops.js +137 -0
  243. package/src/content/stream.js +154 -0
  244. package/src/content/text.js +125 -0
  245. package/src/crypto/aesGcm.js +123 -0
  246. package/src/crypto/permissions.js +112 -0
  247. package/src/crypto/security.js +327 -0
  248. package/src/crypto/standardV4.js +443 -0
  249. package/src/crypto/standardV5.js +306 -0
  250. package/src/crypto/standardV6.js +334 -0
  251. package/src/destination/destination.js +183 -0
  252. package/src/document/builder.js +618 -0
  253. package/src/document/catalog.js +100 -0
  254. package/src/document/document.js +472 -0
  255. package/src/document/encryptedWriter.js +554 -0
  256. package/src/document/incrementalWriter.js +514 -0
  257. package/src/document/page.js +131 -0
  258. package/src/document/pages.js +103 -0
  259. package/src/document/resources.js +146 -0
  260. package/src/document/writer.js +211 -0
  261. package/src/document/xrefStreamWriter.js +353 -0
  262. package/src/embedded/collection.js +102 -0
  263. package/src/embedded/embeddedFile.js +99 -0
  264. package/src/embedded/fileSpec.js +137 -0
  265. package/src/errors.js +78 -0
  266. package/src/extra/3d-richmedia.js +171 -0
  267. package/src/extra/annot-extended.js +200 -0
  268. package/src/extra/associated-files.js +131 -0
  269. package/src/extra/ccitt-fax-decoder.js +776 -0
  270. package/src/extra/color-spaces-extended.js +196 -0
  271. package/src/extra/content-ops-extended.js +153 -0
  272. package/src/extra/document-parts.js +149 -0
  273. package/src/extra/embedded-files-portfolio.js +234 -0
  274. package/src/extra/font-cid-typed.js +185 -0
  275. package/src/extra/font-color-tagging.js +144 -0
  276. package/src/extra/form-actions-extended.js +196 -0
  277. package/src/extra/info-dict-deprecated.js +137 -0
  278. package/src/extra/jbig2-read.js +169 -0
  279. package/src/extra/legacy-deprecated-annots.js +198 -0
  280. package/src/extra/legacy-deprecated-filters.js +167 -0
  281. package/src/extra/legacy-rc4-read.js +235 -0
  282. package/src/extra/legacy-xfa-read.js +104 -0
  283. package/src/extra/linearization-write.js +97 -0
  284. package/src/extra/misc.js +217 -0
  285. package/src/extra/optional-content-extended.js +142 -0
  286. package/src/extra/pdf-a-output-intent.js +112 -0
  287. package/src/extra/pdf-sandbox.js +88 -0
  288. package/src/extra/pdf-ua-tagged.js +116 -0
  289. package/src/extra/pdf-x-prepress.js +114 -0
  290. package/src/extra/redaction-iso32005.js +136 -0
  291. package/src/extra/shading-typed.js +222 -0
  292. package/src/extra/sig-aes-gcm.js +135 -0
  293. package/src/extra/sig-pades.js +242 -0
  294. package/src/extra/tagged-pdf-typed.js +203 -0
  295. package/src/extra/transparency-typed.js +135 -0
  296. package/src/extra/well-tagged-pdf.js +138 -0
  297. package/src/extra/xmp-extended.js +190 -0
  298. package/src/font/embed.js +480 -0
  299. package/src/font/encoding.js +92 -0
  300. package/src/font/font.js +101 -0
  301. package/src/font/type3.js +75 -0
  302. package/src/form/acroform.js +94 -0
  303. package/src/form/appearance.js +90 -0
  304. package/src/form/button.js +105 -0
  305. package/src/form/choice.js +152 -0
  306. package/src/form/fieldTree.js +120 -0
  307. package/src/form/signature.js +100 -0
  308. package/src/form/text.js +101 -0
  309. package/src/linearization/linearization.js +107 -0
  310. package/src/main.js +411 -0
  311. package/src/metadata/info.js +87 -0
  312. package/src/metadata/xmp.js +62 -0
  313. package/src/ocg/config.js +156 -0
  314. package/src/ocg/ocg.js +124 -0
  315. package/src/outline/outline.js +157 -0
  316. package/src/pdf.js +133 -0
  317. package/src/prepress/outputIntent.js +118 -0
  318. package/src/prepress/pageBoundary.js +108 -0
  319. package/src/sig/byteRange.js +306 -0
  320. package/src/sig/certChain.js +247 -0
  321. package/src/sig/dss.js +317 -0
  322. package/src/sig/oids.js +157 -0
  323. package/src/sig/sha1.js +142 -0
  324. package/src/sig/sign.js +1899 -0
  325. package/src/sig/signature.js +1441 -0
  326. package/src/sig/timestamp.js +236 -0
  327. package/src/syntax/crossRefStream.js +133 -0
  328. package/src/syntax/filters/ascii85.js +122 -0
  329. package/src/syntax/filters/asciiHex.js +83 -0
  330. package/src/syntax/filters/dispatch.js +176 -0
  331. package/src/syntax/filters/flate.js +316 -0
  332. package/src/syntax/filters/runLength.js +96 -0
  333. package/src/syntax/objStream.js +99 -0
  334. package/src/syntax/parser-obj.js +52 -0
  335. package/src/syntax/parser.js +321 -0
  336. package/src/syntax/serializer.js +221 -0
  337. package/src/syntax/tokenizer.js +290 -0
  338. package/src/syntax/trailer.js +76 -0
  339. package/src/syntax/xref.js +341 -0
  340. package/src/tagged/classMap.js +81 -0
  341. package/src/tagged/markedContent.js +123 -0
  342. package/src/tagged/parentTree.js +126 -0
  343. package/src/tagged/roleMap.js +107 -0
  344. package/src/tagged/structElement.js +138 -0
  345. package/src/tagged/structTree.js +94 -0
@@ -0,0 +1,1441 @@
1
+ // Copyright (c) 2026 AwaCloud SAS
2
+ // Author: Matthieu Bouilloux
3
+ // SPDX-License-Identifier: AGPL-3.0-only
4
+ // Dual-licensed; see the NOTICE file for licensing and any additional terms.
5
+
6
+ /**
7
+ * @fileoverview Digital signature dict typing + PKCS#7 detached blob
8
+ * verification (ISO 32000-2:2020 §12.8 + ISO/TS 32001 / 32002).
9
+ *
10
+ * - `typeSignature(dict)` → typed record from a `/Type /Sig` or
11
+ * `/Type /DocTimeStamp` dictionary.
12
+ * - `verifySignature(typedSig, documentBytes, fwBundle)` → digests the
13
+ * ByteRange-covered bytes and dispatches to the fw verifier matching
14
+ * the PKCS#7 SignerInfo's digest + signature OIDs. A `/Sig` also passes
15
+ * only when its ByteRange gap is exactly the `/Contents` value: the
16
+ * whole `<…>` token or its hex digits. `verifyAllSignatures`
17
+ * applies the same rule to each `/DocTimeStamp` and reports the
18
+ * accepted form as `gapForm` on its `timestamps[]` entries.
19
+ *
20
+ * Chain validation, revocation and network I/O are out of scope.
21
+ *
22
+ * @module pdf/sig/signature
23
+ */
24
+
25
+ /**
26
+ * Module factory.
27
+ */
28
+ import { pdfErrors } from '../errors.js';
29
+ import { pdfParser } from '../syntax/parser.js';
30
+ import { pdfSigOids } from './oids.js';
31
+ import { asn1 } from '@awacloud/fw/crypto/utils/asn1.js';
32
+ import { rsa } from '@awacloud/fw/crypto/pkc/rsa.js';
33
+ import { ecc } from '@awacloud/fw/crypto/pkc/ecc.js';
34
+ import { ed25519 } from '@awacloud/fw/crypto/pkc/ed25519.js';
35
+ import { sha256 } from '@awacloud/fw/crypto/hash/sha256.js';
36
+ import { sha384 } from '@awacloud/fw/crypto/hash/sha384.js';
37
+ import { sha512 } from '@awacloud/fw/crypto/hash/sha512.js';
38
+ import { bitArray } from '@awacloud/fw/crypto/utils/bitArray.js';
39
+
40
+ export const pdfSignature = {
41
+ name: 'pdfSignature',
42
+ dependencies: ['pdfErrors', 'pdfParser', 'pdfSigOids',
43
+ 'asn1', 'rsa', 'ecc', 'ed25519',
44
+ 'sha256', 'sha384', 'sha512', 'bitArray'],
45
+ deps: [pdfErrors, pdfParser, pdfSigOids, asn1, rsa, ecc, ed25519, sha256, sha384, sha512, bitArray],
46
+ factory(errors, parser, sigOids, asn1, rsa, ecc, ed25519,
47
+ sha256, sha384, sha512, bitArray) {
48
+ const { EncryptionError, ParseError } = errors;
49
+ const { isType } = parser;
50
+ const {
51
+ DIGEST_OIDS,
52
+ SIG_DISPATCH_OIDS: SIG_OIDS
53
+ } = sigOids;
54
+ const bundle = { asn1, rsa, ecc, ed25519,
55
+ sha256, sha384, sha512, bitArray };
56
+
57
+ const KNOWN_SUBFILTERS = new Set([
58
+ 'adbe.x509.rsa_sha1', 'adbe.pkcs7.detached', 'adbe.pkcs7.sha1',
59
+ 'ETSI.CAdES.detached', 'ETSI.RFC3161'
60
+ ]);
61
+
62
+ function typeSignature(dict, refInfo) {
63
+ const ctxBase = refInfo
64
+ ? { objNum: refInfo.objNum, objGen: refInfo.objGen }
65
+ : null;
66
+ function ctx(extra) {
67
+ if (!ctxBase && !extra) return undefined;
68
+ return Object.assign({}, ctxBase, extra);
69
+ }
70
+ if (!isType(dict, 'dict')) {
71
+ throw new ParseError('pdf/sig/not-dict',
72
+ 'signature must be a dictionary',
73
+ { context: ctx({ type: dict && dict.type }) });
74
+ }
75
+ const e = dict.entries;
76
+ const typeName = e.Type && e.Type.type === 'name' ? e.Type.value : null;
77
+ if (typeName && typeName !== 'Sig' && typeName !== 'DocTimeStamp') {
78
+ throw new ParseError('pdf/sig/bad-type',
79
+ '/Type must be /Sig or /DocTimeStamp',
80
+ { context: ctx({ actual: typeName }) });
81
+ }
82
+ if (!e.Filter || e.Filter.type !== 'name') {
83
+ throw new ParseError('pdf/sig/missing-filter',
84
+ 'signature missing /Filter',
85
+ ctxBase ? { context: ctxBase } : undefined);
86
+ }
87
+ if (!e.SubFilter || e.SubFilter.type !== 'name') {
88
+ throw new ParseError('pdf/sig/missing-subfilter',
89
+ 'signature missing /SubFilter',
90
+ ctxBase ? { context: ctxBase } : undefined);
91
+ }
92
+ if (!KNOWN_SUBFILTERS.has(e.SubFilter.value)) {
93
+ throw new ParseError('pdf/sig/unknown-subfilter',
94
+ 'unknown /SubFilter',
95
+ { context: ctx({ subFilter: e.SubFilter.value }) });
96
+ }
97
+ if (!e.Contents || e.Contents.type !== 'string') {
98
+ throw new ParseError('pdf/sig/missing-contents',
99
+ '/Contents (hex PKCS#7 blob) missing',
100
+ ctxBase ? { context: ctxBase } : undefined);
101
+ }
102
+ if (!e.ByteRange || e.ByteRange.type !== 'array') {
103
+ throw new ParseError('pdf/sig/missing-byterange',
104
+ '/ByteRange missing',
105
+ ctxBase ? { context: ctxBase } : undefined);
106
+ }
107
+ const br = e.ByteRange.items.map((n) => (n && typeof n.value === 'number')
108
+ ? n.value : 0);
109
+ if (br.length !== 4) {
110
+ throw new ParseError('pdf/sig/bad-byterange',
111
+ '/ByteRange must have exactly 4 integers',
112
+ { context: ctx({ length: br.length }) });
113
+ }
114
+ function str(k) {
115
+ const v = e[k];
116
+ if (!v) return undefined;
117
+ if (v.type === 'string') return v.value;
118
+ if (v.type === 'name') return v.value;
119
+ return undefined;
120
+ }
121
+ return {
122
+ kind: typeName || 'Sig',
123
+ filter: e.Filter.value,
124
+ subFilter: e.SubFilter.value,
125
+ contents: e.Contents.value,
126
+ byteRange: br,
127
+ reference: e.Reference && e.Reference.type === 'array'
128
+ ? e.Reference.items : undefined,
129
+ cert: e.Cert,
130
+ name: str('Name'),
131
+ m: str('M'),
132
+ location: str('Location'),
133
+ reason: str('Reason'),
134
+ contactInfo: str('ContactInfo'),
135
+ v: e.V && e.V.type === 'int' ? e.V.value : undefined,
136
+ propBuild: e.Prop_Build,
137
+ propAuthTime: e.Prop_AuthTime && e.Prop_AuthTime.type === 'int'
138
+ ? e.Prop_AuthTime.value : undefined,
139
+ propAuthType: str('Prop_AuthType'),
140
+ raw: dict
141
+ };
142
+ }
143
+
144
+ function locatePkcs7(blob, asn1Mod) {
145
+ const a1 = asn1Mod || asn1;
146
+ if (!(blob instanceof Uint8Array)) return false;
147
+ const top = a1.parseOne(blob, 0);
148
+ if (!top) return false;
149
+ const outer = a1.parseChildren(top.value);
150
+ if (!outer || outer.length < 2) return false;
151
+ const explicit = outer[1];
152
+ const inner = a1.parseOne(explicit.value, 0);
153
+ if (!inner) return false;
154
+ return a1.parseChildren(inner.value);
155
+ }
156
+
157
+ function _concatRange(bytes, br) {
158
+ const [a, b, c, d] = br;
159
+ const total = bytes.length;
160
+ if (a < 0 || b < 0 || c < 0 || d < 0
161
+ || a + b > total
162
+ || c + d > total
163
+ || c < a + b) {
164
+ throw new ParseError('pdf/sig/byterange/inconsistent',
165
+ '_concatRange refusing inconsistent ByteRange',
166
+ { context: { a, b, c, d, total } });
167
+ }
168
+ const out = new Uint8Array(b + d);
169
+ out.set(bytes.subarray(a, a + b), 0);
170
+ out.set(bytes.subarray(c, c + d), b);
171
+ return out;
172
+ }
173
+
174
+ function _hashByteRange(bytes, br, hashMod, bitArrayMod) {
175
+ const ba = bitArrayMod || bitArray;
176
+ if (hashMod && typeof hashMod.fn === 'function'
177
+ && ba && typeof ba.ui8_to_ba === 'function') {
178
+ const a = br[0] | 0, b = br[1] | 0;
179
+ const c = br[2] | 0, d = br[3] | 0;
180
+ const total = bytes.length;
181
+ if (a < 0 || b < 0 || c < 0 || d < 0
182
+ || a + b > total || c + d > total || c < a + b) {
183
+ throw new ParseError('pdf/sig/byterange/inconsistent',
184
+ '_hashByteRange refusing inconsistent ByteRange',
185
+ { context: { a, b, c, d, total } });
186
+ }
187
+ const ctx = new hashMod.fn();
188
+ ctx.update(ba.ui8_to_ba(bytes.subarray(a, a + b)));
189
+ ctx.update(ba.ui8_to_ba(bytes.subarray(c, c + d)));
190
+ const out = ctx.finalize();
191
+ return out instanceof Uint8Array ? out : ba.ba_to_ui8(out);
192
+ }
193
+ if (!ba || typeof ba.ui8_to_ba !== 'function'
194
+ || typeof ba.ba_to_ui8 !== 'function') {
195
+ throw new ParseError('pdf/sig/byterange/no-bitarray',
196
+ '_hashByteRange fallback requires a live bitArray API '
197
+ + '(ui8_to_ba/ba_to_ui8) to hash identical bit '
198
+ + 'content to the streaming path',
199
+ { context: { hasBitArrayApi: !!ba } });
200
+ }
201
+ const raw = hashMod.hash(ba.ui8_to_ba(_concatRange(bytes, br)));
202
+ return raw instanceof Uint8Array ? raw : ba.ba_to_ui8(raw);
203
+ }
204
+
205
+ // ── /ByteRange gap check ─────────────────────────────────────
206
+ //
207
+ // The gap `[a+b, c)` must be exactly the `/Contents` value: the
208
+ // whole `<…>` token (form b — ISO 32000-2 §12.8.3.3.1, what
209
+ // pdfSign emits) or its hex digits only (form a — what earlier
210
+ // pdfSign releases emitted). Any other gap is refused with the codes
211
+ // pdfByteRange.auditByteRange uses, measured against the token
212
+ // bounds; the rule is restated here because pdfSignature does not
213
+ // depend on pdfByteRange. The digest is always taken over the
214
+ // ranges the file declares.
215
+ const GAP_START = 'pdf/sig/byterange/gap-start-mismatch';
216
+ const GAP_END = 'pdf/sig/byterange/gap-end-mismatch';
217
+
218
+ function _isPdfWs(b) {
219
+ return b === 0x00 || b === 0x09 || b === 0x0A || b === 0x0C
220
+ || b === 0x0D || b === 0x20;
221
+ }
222
+
223
+ function _isHexOrWs(b) {
224
+ return (b >= 0x30 && b <= 0x39) || (b >= 0x41 && b <= 0x46)
225
+ || (b >= 0x61 && b <= 0x66) || _isPdfWs(b);
226
+ }
227
+
228
+ // Hex-digit span of the `/Contents <…>` token the gap sits on,
229
+ // found from the bytes alone (a typed dict carries no file
230
+ // offsets), or null when the gap sits on no `/Contents` value.
231
+ function _contentsSpanFromGap(bytes, gapStart, gapEnd) {
232
+ const n = bytes.length;
233
+ if (!(gapStart >= 0 && gapEnd > gapStart && gapEnd <= n)) return null;
234
+ let i = gapStart + ((gapEnd - gapStart) >> 1);
235
+ if (bytes[i] === 0x3E) i--; // on '>'
236
+ while (i >= 0 && _isHexOrWs(bytes[i])) i--;
237
+ if (i < 0 || bytes[i] !== 0x3C) return null; // '<'
238
+ const lt = i;
239
+ let j = lt + 1;
240
+ while (j < n && _isHexOrWs(bytes[j])) j++;
241
+ if (j >= n || bytes[j] !== 0x3E) return null; // '>'
242
+ let k = lt - 1;
243
+ while (k >= 0 && _isPdfWs(bytes[k])) k--;
244
+ const KEY = '/Contents';
245
+ const keyAt = k - KEY.length + 1;
246
+ if (keyAt < 0) return null;
247
+ for (let q = 0; q < KEY.length; q++) {
248
+ if (bytes[keyAt + q] !== KEY.charCodeAt(q)) return null;
249
+ }
250
+ return { offset: lt + 1, length: j - lt - 1 };
251
+ }
252
+
253
+ // Gap check for `byteRange` against the `/Contents` digit span
254
+ // `span`: `issues` is `[]` and `gapForm` names the form when the
255
+ // gap is exactly form (b) (`'token'`) or form (a) (`'digits'`);
256
+ // otherwise `gapForm` is null and `issues` carries the gap codes.
257
+ // One comparison serves both verify paths (/Sig and
258
+ // /DocTimeStamp); `gapForm` uses pdfByteRange's
259
+ // vocabulary.
260
+ function _gapCheck(byteRange, span) {
261
+ const gapStart = (byteRange[0] | 0) + (byteRange[1] | 0);
262
+ const gapEnd = byteRange[2] | 0;
263
+ const tokenStart = span ? span.offset - 1 : null;
264
+ const tokenEnd = span ? span.offset + span.length + 1 : null;
265
+ if (span && gapStart === tokenStart && gapEnd === tokenEnd) {
266
+ return { issues: [], gapForm: 'token' };
267
+ }
268
+ if (span && gapStart === span.offset
269
+ && gapEnd === span.offset + span.length) {
270
+ return { issues: [], gapForm: 'digits' };
271
+ }
272
+ const issues = [];
273
+ if (gapStart !== tokenStart) {
274
+ issues.push({ code: GAP_START,
275
+ message: 'first range end does not align with /Contents '
276
+ + 'literal start',
277
+ context: { firstEnd: gapStart, expected: tokenStart } });
278
+ }
279
+ if (gapEnd !== tokenEnd) {
280
+ issues.push({ code: GAP_END,
281
+ message: 'second range start does not align with /Contents '
282
+ + 'literal end',
283
+ context: { secondStart: gapEnd, expected: tokenEnd } });
284
+ }
285
+ return { issues, gapForm: null };
286
+ }
287
+
288
+ function _result(verified, errs, signerCerts, hashAlg, signatureAlg,
289
+ pkVerified, computedDigest) {
290
+ return {
291
+ verified: !!verified,
292
+ valid: !!verified, // deprecated alias of `verified`, removed in a future major version
293
+ pkVerified: !!pkVerified,
294
+ errors: errs, signerCerts, hashAlg, signatureAlg,
295
+ computedDigest: computedDigest || null
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Verify one typed `/Sig` dictionary against the document bytes.
301
+ *
302
+ * @param {object} typedSig A record from `typeSignature`.
303
+ * @param {Uint8Array} documentBytes The whole document.
304
+ * @param {object} [fwBundle] Optional fw crypto bundle (the module
305
+ * carries its own default).
306
+ * @returns {{
307
+ * verified: boolean,
308
+ * valid: boolean,
309
+ * pkVerified: boolean,
310
+ * errors: Array<{code: string, message: string}>,
311
+ * signerCerts: Array<{der: Uint8Array}>,
312
+ * hashAlg: (string|null),
313
+ * signatureAlg: (string|null),
314
+ * computedDigest: (Uint8Array|null)
315
+ * }} The verification result. `valid` is deprecated: same value as
316
+ * `verified`; kept for compatibility, will be removed in a future
317
+ * major version; read `verified`.
318
+ */
319
+ function verifySignature(typedSig, documentBytes, fwBundle) {
320
+ return _verifySig(typedSig, documentBytes, fwBundle, null);
321
+ }
322
+
323
+ // `contentsSpan`: the `/Contents` hex-digit span when the caller
324
+ // located it in the file (verifyAllSignatures); null → found from
325
+ // the gap itself.
326
+ function _verifySig(typedSig, documentBytes, fwBundle, contentsSpan) {
327
+ const fb = fwBundle || bundle;
328
+ if (!fb || !fb.asn1) {
329
+ throw new EncryptionError('pdf/sig/missing-fw',
330
+ 'verifySignature requires fwBundle.asn1');
331
+ }
332
+ const errs = [];
333
+ const sd = locatePkcs7(typedSig.contents, fb.asn1);
334
+ if (!sd) {
335
+ errs.push({ code: 'pdf/sig/pkcs7-malformed',
336
+ message: 'PKCS#7 SignedData parse failed' });
337
+ return _result(false, errs, [], null, null, false);
338
+ }
339
+ // Detached PKCS#7 (PDF /Sig) : there is no eContent inside the
340
+ // PKCS#7 — the "content" that was hashed for signedAttrs.
341
+ // messageDigest is the ByteRange-covered bytes of the PDF. So
342
+ // we pass eContentBytes=null and supply the precomputed digest
343
+ // + the raw ByteRange bytes (used when signedAttrs is absent).
344
+ const r = _verifyPkcs7Signature(sd, null, fb, 'pdf/sig', {
345
+ byteRangeDocument: documentBytes,
346
+ byteRange: typedSig.byteRange
347
+ });
348
+ for (const er of r.errors) errs.push(er);
349
+ const br = typedSig.byteRange;
350
+ const span = contentsSpan
351
+ || (documentBytes instanceof Uint8Array && Array.isArray(br)
352
+ ? _contentsSpanFromGap(documentBytes,
353
+ (br[0] | 0) + (br[1] | 0), br[2] | 0)
354
+ : null);
355
+ const gap = _gapCheck(br || [], span).issues;
356
+ for (const g of gap) errs.push(g);
357
+ return _result(r.verified && gap.length === 0, errs, r.signerCerts,
358
+ r.hashAlg, r.signatureAlg, r.pkVerified, r.computedDigest);
359
+ }
360
+
361
+ // ── SignerInfo dissection helpers ────────────────────────────
362
+ function _dissectSignerInfo(si) {
363
+ // si is the parsed children of the first SignerInfo SEQUENCE.
364
+ // Walk positionally with safeguards.
365
+ if (!si || si.length < 5) {
366
+ return { ok: false,
367
+ code: 'pdf/sig/signer-info-short',
368
+ message: 'SignerInfo has fewer than 5 fields' };
369
+ }
370
+ // [0] version, [1] sid, [2] digestAlg, [3]? signedAttrs,
371
+ // [n-2] sigAlg, [n-1] signature, [n]? unsignedAttrs.
372
+ const version = si[0];
373
+ const sid = si[1];
374
+ const digAlg = si[2];
375
+ void version; void digAlg;
376
+ let idx = 3;
377
+ let signedAttrsNode = null;
378
+ if (si[idx] && si[idx].tag === 0xA0) {
379
+ signedAttrsNode = si[idx];
380
+ idx++;
381
+ }
382
+ const sigAlg = si[idx]; idx++;
383
+ const sigVal = si[idx]; idx++;
384
+ void sigAlg;
385
+ if (!sigVal || sigVal.tag !== 0x04) {
386
+ return { ok: false,
387
+ code: 'pdf/sig/no-encrypted-digest',
388
+ message: 'SignerInfo signature OCTET STRING missing' };
389
+ }
390
+ let unsignedAttrsNode = null;
391
+ if (si[idx] && si[idx].tag === 0xA1) {
392
+ unsignedAttrsNode = si[idx];
393
+ }
394
+ void unsignedAttrsNode;
395
+ // Reconstruct the raw signedAttrs TLV (needed for hashing).
396
+ let signedAttrsRaw = null;
397
+ let signedAttrsValueBytes = null;
398
+ if (signedAttrsNode) {
399
+ signedAttrsValueBytes = signedAttrsNode.value;
400
+ // Re-emit the TLV.
401
+ signedAttrsRaw = _reencodeTlv(
402
+ signedAttrsNode.tag, signedAttrsNode.value);
403
+ }
404
+ return {
405
+ ok: true,
406
+ sid,
407
+ encryptedDigest: new Uint8Array(sigVal.value),
408
+ signedAttrsRaw,
409
+ signedAttrsValueBytes
410
+ };
411
+ }
412
+
413
+ function _reencodeTlv(tag, value) {
414
+ const lenBytes = _encLen(value.length);
415
+ const out = new Uint8Array(1 + lenBytes.length + value.length);
416
+ out[0] = tag;
417
+ out.set(lenBytes, 1);
418
+ out.set(value, 1 + lenBytes.length);
419
+ return out;
420
+ }
421
+
422
+ function _encLen(n) {
423
+ if (n < 0x80) return Uint8Array.of(n);
424
+ if (n <= 0xff) return Uint8Array.of(0x81, n);
425
+ if (n <= 0xffff) return Uint8Array.of(0x82,
426
+ (n >>> 8) & 0xff, n & 0xff);
427
+ if (n <= 0xffffff) return Uint8Array.of(0x83,
428
+ (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff);
429
+ return Uint8Array.of(0x84,
430
+ (n >>> 24) & 0xff, (n >>> 16) & 0xff,
431
+ (n >>> 8) & 0xff, n & 0xff);
432
+ }
433
+
434
+ function _checkSignedAttrsDigest(saValueBytes, expectedDigest, a1) {
435
+ // saValueBytes is the inner value of the signedAttrs SET (no
436
+ // outer tag). Walk attributes; find OID 1.2.840.113549.1.9.4
437
+ // (id-messageDigest) → SET OF { OCTET STRING }.
438
+ const attrs = a1.parseChildren(saValueBytes);
439
+ if (!attrs) {
440
+ return { ok: false,
441
+ code: 'pdf/sig/signed-attrs-parse-failed',
442
+ message: 'failed to parse signedAttrs SET' };
443
+ }
444
+ const OID_MD = '1.2.840.113549.1.9.4';
445
+ for (let i = 0; i < attrs.length; i++) {
446
+ const a = a1.parseChildren(attrs[i].value);
447
+ if (!a || a.length < 2) continue;
448
+ const oid = a1.readOid(a[0]);
449
+ if (oid !== OID_MD) continue;
450
+ const vals = a1.parseChildren(a[1].value);
451
+ if (!vals || !vals.length || vals[0].tag !== 0x04) continue;
452
+ const md = vals[0].value;
453
+ if (!expectedDigest) {
454
+ return { ok: false,
455
+ code: 'pdf/sig/digest-not-computed',
456
+ message: 'recomputed digest unavailable for '
457
+ + 'signedAttrs comparison' };
458
+ }
459
+ if (md.length !== expectedDigest.length) {
460
+ return { ok: false,
461
+ code: 'pdf/sig/digest-length-mismatch',
462
+ message: 'messageDigest length mismatch' };
463
+ }
464
+ for (let j = 0; j < md.length; j++) {
465
+ if (md[j] !== expectedDigest[j]) {
466
+ return { ok: false,
467
+ code: 'pdf/sig/digest-mismatch',
468
+ message: 'signedAttrs.messageDigest does '
469
+ + 'not match ByteRange digest' };
470
+ }
471
+ }
472
+ return { ok: true };
473
+ }
474
+ return { ok: false,
475
+ code: 'pdf/sig/no-message-digest-attr',
476
+ message: 'signedAttrs lacks messageDigest attribute' };
477
+ }
478
+
479
+ function _findSignerCert(sd, sidNode, a1) {
480
+ // Find certificates [0] IMPLICIT field in the SignedData
481
+ // children. Each child cert is a SEQUENCE — reconstruct its
482
+ // DER and parse its IssuerAndSerial to match sid.
483
+ let certsField = null;
484
+ for (let i = 0; i < sd.length; i++) {
485
+ if (sd[i].tag === 0xA0) { certsField = sd[i]; break; }
486
+ }
487
+ if (!certsField) return null;
488
+ const certs = a1.parseChildren(certsField.value);
489
+ if (!certs) return null;
490
+ // Parse sid (IssuerAndSerialNumber ::= SEQUENCE { Name, INTEGER }).
491
+ const sidKids = sidNode && sidNode.tag === 0x30
492
+ ? a1.parseChildren(sidNode.value) : null;
493
+ const wantIssuerVal = sidKids && sidKids[0]
494
+ ? sidKids[0].value : null;
495
+ const wantSerialVal = sidKids && sidKids[1]
496
+ ? sidKids[1].value : null;
497
+ for (let i = 0; i < certs.length; i++) {
498
+ if (certs[i].tag !== 0x30) continue;
499
+ const certDer = _reconstructDer(certs[i], certsField.value);
500
+ if (!wantIssuerVal || !wantSerialVal) {
501
+ // No sid match info — return the first cert.
502
+ return { der: certDer };
503
+ }
504
+ const match = _certMatchesIssuerSerial(
505
+ certDer, wantIssuerVal, wantSerialVal, a1);
506
+ if (match) return { der: certDer };
507
+ }
508
+ // Fallback : if any cert exists, return the first.
509
+ for (let i = 0; i < certs.length; i++) {
510
+ if (certs[i].tag === 0x30) {
511
+ return { der: _reconstructDer(certs[i], certsField.value) };
512
+ }
513
+ }
514
+ return null;
515
+ }
516
+
517
+ function _reconstructDer(node, parentBuf) {
518
+ const len = node.length;
519
+ let lenLen;
520
+ if (len < 0x80) lenLen = 1;
521
+ else if (len < 0x100) lenLen = 2;
522
+ else if (len < 0x10000) lenLen = 3;
523
+ else if (len < 0x1000000) lenLen = 4;
524
+ else lenLen = 5;
525
+ return parentBuf.subarray(node.valueOff - 1 - lenLen, node.next);
526
+ }
527
+
528
+ function _certMatchesIssuerSerial(certDer, wantIssuerVal,
529
+ wantSerialVal, a1) {
530
+ const top = a1.parseOne(certDer, 0);
531
+ if (!top) return false;
532
+ const certKids = a1.parseChildren(top.value);
533
+ if (!certKids || !certKids[0]) return false;
534
+ const tbs = a1.parseChildren(certKids[0].value);
535
+ if (!tbs) return false;
536
+ let idx = 0;
537
+ if (tbs[idx] && tbs[idx].tag === 0xA0) idx++; // version
538
+ const serialNode = tbs[idx]; idx++;
539
+ idx++; // sigAlg
540
+ const issuerNode = tbs[idx];
541
+ if (!serialNode || !issuerNode) return false;
542
+ // Compare serial values byte-by-byte.
543
+ if (serialNode.value.length !== wantSerialVal.length) return false;
544
+ for (let i = 0; i < serialNode.value.length; i++) {
545
+ if (serialNode.value[i] !== wantSerialVal[i]) return false;
546
+ }
547
+ // Compare issuer value bytes.
548
+ if (issuerNode.value.length !== wantIssuerVal.length) return false;
549
+ for (let i = 0; i < issuerNode.value.length; i++) {
550
+ if (issuerNode.value[i] !== wantIssuerVal[i]) return false;
551
+ }
552
+ return true;
553
+ }
554
+
555
+ function _extractSpki(certDer, signatureAlg, a1) {
556
+ // Walk into TBSCertificate to extract the SPKI bytes, then
557
+ // shape into the verifyPk pubKey for the algorithm.
558
+ const top = a1.parseOne(certDer, 0);
559
+ if (!top) {
560
+ return { ok: false, code: 'pdf/sig/spki-cert-parse',
561
+ message: 'cert parse failed' };
562
+ }
563
+ const certKids = a1.parseChildren(top.value);
564
+ if (!certKids || !certKids[0]) {
565
+ return { ok: false, code: 'pdf/sig/spki-cert-parse',
566
+ message: 'cert lacks tbsCertificate' };
567
+ }
568
+ const tbs = a1.parseChildren(certKids[0].value);
569
+ if (!tbs) {
570
+ return { ok: false, code: 'pdf/sig/spki-cert-parse',
571
+ message: 'tbsCertificate parse failed' };
572
+ }
573
+ let idx = 0;
574
+ if (tbs[idx] && tbs[idx].tag === 0xA0) idx++; // version
575
+ // serial, sigAlg, issuer, validity, subject, spki
576
+ idx += 5;
577
+ const spkiNode = tbs[idx];
578
+ if (!spkiNode || spkiNode.tag !== 0x30) {
579
+ return { ok: false, code: 'pdf/sig/spki-not-found',
580
+ message: 'SubjectPublicKeyInfo missing' };
581
+ }
582
+ const spkiKids = a1.parseChildren(spkiNode.value);
583
+ if (!spkiKids || spkiKids.length < 2) {
584
+ return { ok: false, code: 'pdf/sig/spki-malformed',
585
+ message: 'SPKI structure malformed' };
586
+ }
587
+ const algKids = a1.parseChildren(spkiKids[0].value);
588
+ const keyAlgOid = algKids && algKids[0]
589
+ ? a1.readOid(algKids[0]) : null;
590
+ // bit string : first byte = unused-bits count.
591
+ const bsVal = spkiKids[1].value;
592
+ if (bsVal.length < 1) {
593
+ return { ok: false, code: 'pdf/sig/spki-bitstring-empty',
594
+ message: 'SPKI BIT STRING empty' };
595
+ }
596
+ const keyBytes = bsVal.subarray(1);
597
+ // Shape the public key per `signatureAlg`.
598
+ if (signatureAlg === 'rsa-pss' || signatureAlg === 'rsa') {
599
+ // RSAPublicKey ::= SEQUENCE { n INTEGER, e INTEGER }
600
+ const inner = a1.parseOne(keyBytes, 0);
601
+ if (!inner) {
602
+ return { ok: false, code: 'pdf/sig/spki-rsa-parse',
603
+ message: 'RSAPublicKey parse failed' };
604
+ }
605
+ const rsaKids = a1.parseChildren(inner.value);
606
+ if (!rsaKids || rsaKids.length < 2) {
607
+ return { ok: false, code: 'pdf/sig/spki-rsa-fields',
608
+ message: 'RSAPublicKey lacks n/e' };
609
+ }
610
+ return { ok: true,
611
+ pubKey: { n: _trimIntLeadZero(rsaKids[0].value),
612
+ e: _trimIntLeadZero(rsaKids[1].value) } };
613
+ }
614
+ if (signatureAlg === 'ecc') {
615
+ // SEC1 uncompressed point : 0x04 || x || y. Strip the 0x04
616
+ // prefix to match the SEC1 raw form expected by verifyPk
617
+ // (curve.fromBits decodes x||y).
618
+ if (keyBytes.length < 1 || keyBytes[0] !== 0x04) {
619
+ return { ok: false, code: 'pdf/sig/spki-ecc-not-uncompressed',
620
+ message: 'ECC point not uncompressed SEC1' };
621
+ }
622
+ // Determine curve from EC parameters (algorithm parameters).
623
+ let curve = 'c256';
624
+ if (algKids && algKids[1] && algKids[1].tag === 0x06) {
625
+ const curveOid = a1.readOid(algKids[1]);
626
+ if (curveOid === '1.2.840.10045.3.1.7') curve = 'c256';
627
+ else if (curveOid === '1.3.132.0.34') curve = 'c384';
628
+ else if (curveOid === '1.3.132.0.35') curve = 'c521';
629
+ }
630
+ return { ok: true,
631
+ pubKey: { curve,
632
+ point: keyBytes.subarray(1) } };
633
+ }
634
+ if (signatureAlg === 'ed25519') {
635
+ if (keyBytes.length !== 32) {
636
+ return { ok: false, code: 'pdf/sig/spki-ed25519-bad-len',
637
+ message: 'Ed25519 pubkey must be 32 bytes',
638
+ actual: keyBytes.length };
639
+ }
640
+ return { ok: true, pubKey: keyBytes };
641
+ }
642
+ return { ok: false, code: 'pdf/sig/spki-unsupported-alg',
643
+ message: 'unsupported keyAlgorithm: ' + keyAlgOid };
644
+ }
645
+
646
+ function _trimIntLeadZero(bytes) {
647
+ // Strip a single leading 0x00 added by DER when the MSB is set
648
+ // (preserves the positive-integer semantics in raw form).
649
+ if (bytes.length > 1 && bytes[0] === 0x00) {
650
+ return bytes.subarray(1);
651
+ }
652
+ return bytes;
653
+ }
654
+
655
+ /**
656
+ * ECDSA CMS signature value → fixed-width raw r||s (`width` bytes each).
657
+ * DER `ECDSA-Sig-Value` (SEQUENCE { INTEGER r, INTEGER s }) is tried first;
658
+ * a buffer that is not a well-formed SEQUENCE spanning the whole value is
659
+ * taken as the legacy raw r||s form (2*width bytes). Returns
660
+ * `{ ok: true, rs }` or `{ ok: false, code, error }`.
661
+ */
662
+ function _ecdsaSigToRaw(signature, width, asn1Mod) {
663
+ const BAD = 'pdf/sig/verify-pk/bad-ecdsa-sig';
664
+ let kids = null;
665
+ if (signature.length > 0 && signature[0] === 0x30 && asn1Mod) {
666
+ const seq = asn1Mod.parseOne(signature, 0);
667
+ if (seq && seq.next === signature.length) {
668
+ const c = asn1Mod.parseChildren(seq.value);
669
+ if (c && c.length === 2
670
+ && c[0].tag === 0x02 && c[1].tag === 0x02) {
671
+ kids = c;
672
+ }
673
+ }
674
+ }
675
+ if (kids) {
676
+ const rs = new Uint8Array(2 * width);
677
+ for (let i = 0; i < 2; i++) {
678
+ let v = kids[i].value;
679
+ if (v.length === width + 1 && v[0] === 0x00) {
680
+ v = v.subarray(1);
681
+ }
682
+ if (v.length > width) {
683
+ return { ok: false, code: BAD,
684
+ error: 'ECDSA-Sig-Value INTEGER longer than '
685
+ + width + ' bytes' };
686
+ }
687
+ rs.set(v, i * width + (width - v.length));
688
+ }
689
+ return { ok: true, rs };
690
+ }
691
+ if (signature.length !== 2 * width) {
692
+ return { ok: false, code: BAD,
693
+ error: 'ECDSA signature is neither a DER '
694
+ + 'ECDSA-Sig-Value nor a ' + (2 * width)
695
+ + '-byte raw r||s' };
696
+ }
697
+ return { ok: true, rs: signature };
698
+ }
699
+
700
+ /**
701
+ * Public-key verification primitive dispatcher.
702
+ *
703
+ * Item #1 wiring (P0). Given pre-parsed inputs from a PKCS#7
704
+ * SignerInfo + signer certificate SPKI, dispatch to the matching
705
+ * fw primitive:
706
+ * - RSA-PSS → `rsa.pssVerify(pub, msg, sig, hashMod, sLen?)`
707
+ * - RSA v1.5 → fw refuses (NIST SP 800-131A Rev.2 deprecation);
708
+ * returns `{ verified: false,
709
+ * code: 'pdf/sig/rsa-pkcs1v15-deprecated' }`.
710
+ * - ECDSA → `ecc.ecdsa.publicKey(curve, point).verify(hash, rs)`
711
+ * - Ed25519 → `ed25519.verify(pub, msg, sig)`
712
+ *
713
+ * @param {Object} args
714
+ * @param {string} args.algorithm One of 'rsa-pss', 'rsa-v15',
715
+ * 'ecdsa', 'ed25519'.
716
+ * @param {Object} args.pubKey Algorithm-specific public key:
717
+ * - RSA : `{ n: Uint8Array, e: Uint8Array }`
718
+ * - ECDSA : `{ curve: 'c256'|'c384'|'c521', point: Uint8Array }`
719
+ * (`point` is the uncompressed SEC1 encoding without the 0x04
720
+ * prefix — concatenation of x||y).
721
+ * - Ed25519: 32-byte `Uint8Array`.
722
+ * @param {Uint8Array} args.signature Signature bytes (PSS:
723
+ * k-byte; ECDSA: DER ECDSA-Sig-Value, or the legacy fixed-width
724
+ * r||s — DER is tried first, anything else returns
725
+ * `pdf/sig/verify-pk/bad-ecdsa-sig`; Ed25519: 64 bytes).
726
+ * @param {Uint8Array} args.message Bytes whose digest was signed
727
+ * (for PSS/Ed25519). For ECDSA, may either be the message bytes
728
+ * (will be hashed via `hashMod`) or — when `args.digest` is
729
+ * provided — the digest is used directly.
730
+ * @param {Uint8Array} [args.digest] Pre-computed digest of
731
+ * `message` (used by ECDSA which expects a hash on input).
732
+ * @param {Object} [args.hashMod] Hash module (e.g. sha256) for
733
+ * PSS / ECDSA-when-message-is-passed.
734
+ * @param {number} [args.sLen] Optional PSS salt length override
735
+ * (defaults to hashMod's digest length).
736
+ * @returns {{ verified: boolean, code?: string, error?: string }}
737
+ */
738
+ function verifyPk(args) {
739
+ const fb = bundle;
740
+ if (!args || typeof args !== 'object') {
741
+ return { verified: false, code: 'pdf/sig/verify-pk/bad-args',
742
+ error: 'verifyPk requires an args object' };
743
+ }
744
+ const { algorithm, pubKey, signature, message, digest,
745
+ hashMod, sLen } = args;
746
+ if (!algorithm) {
747
+ return { verified: false, code: 'pdf/sig/verify-pk/no-alg',
748
+ error: 'algorithm required' };
749
+ }
750
+ if (!(signature instanceof Uint8Array)) {
751
+ return { verified: false, code: 'pdf/sig/verify-pk/bad-sig',
752
+ error: 'signature must be a Uint8Array' };
753
+ }
754
+ try {
755
+ if (algorithm === 'rsa-pss') {
756
+ if (!fb.rsa || typeof fb.rsa.pssVerify !== 'function') {
757
+ return { verified: false,
758
+ code: 'pdf/sig/verify-pk/no-rsa',
759
+ error: 'fw rsa.pssVerify unavailable' };
760
+ }
761
+ if (!pubKey || !pubKey.n || !pubKey.e) {
762
+ return { verified: false,
763
+ code: 'pdf/sig/verify-pk/bad-rsa-key',
764
+ error: 'RSA pubKey requires { n, e }' };
765
+ }
766
+ if (!(message instanceof Uint8Array)) {
767
+ return { verified: false,
768
+ code: 'pdf/sig/verify-pk/no-message',
769
+ error: 'PSS verify requires message bytes' };
770
+ }
771
+ if (!hashMod || typeof hashMod.hash !== 'function') {
772
+ return { verified: false,
773
+ code: 'pdf/sig/verify-pk/no-hash',
774
+ error: 'PSS verify requires hashMod' };
775
+ }
776
+ const ok = fb.rsa.pssVerify(pubKey, message, signature,
777
+ hashMod, sLen);
778
+ return { verified: !!ok };
779
+ }
780
+ if (algorithm === 'rsa-v15' || algorithm === 'rsa') {
781
+ // PKCS#1 v1.5 — fw deliberately refuses (Bleichenbacher
782
+ // 2006, NIST SP 800-131A Rev.2 deprecation). Surface a
783
+ // precise code instead of a silent false.
784
+ return { verified: false,
785
+ code: 'pdf/sig/rsa-pkcs1v15-deprecated',
786
+ error: 'RSA PKCS#1 v1.5 signature scheme is '
787
+ + 'deprecated by NIST SP 800-131A Rev.2 '
788
+ + '(Table 5) and refused by fw rsa; use '
789
+ + 'RSA-PSS instead' };
790
+ }
791
+ if (algorithm === 'ecdsa' || algorithm === 'ecc') {
792
+ if (!fb.ecc || !fb.ecc.ecdsa || !fb.ecc.curves
793
+ || !fb.bitArray) {
794
+ return { verified: false,
795
+ code: 'pdf/sig/verify-pk/no-ecc',
796
+ error: 'fw ecc unavailable' };
797
+ }
798
+ if (!pubKey || !pubKey.curve || !pubKey.point) {
799
+ return { verified: false,
800
+ code: 'pdf/sig/verify-pk/bad-ecc-key',
801
+ error: 'ECDSA pubKey requires { curve, point }' };
802
+ }
803
+ const curve = fb.ecc.curves[pubKey.curve];
804
+ if (!curve) {
805
+ return { verified: false,
806
+ code: 'pdf/sig/verify-pk/unknown-curve',
807
+ error: 'unknown ECC curve: ' + pubKey.curve };
808
+ }
809
+ // Deserialize the SEC1 raw (x||y) point into a curve
810
+ // point. `curve.fromBits` mirrors what `ecc.deserialize`
811
+ // does internally for ecdsa publicKey reconstitution.
812
+ const ba = fb.bitArray;
813
+ const pointBits = ba.ui8_to_ba(pubKey.point);
814
+ const Q = curve.fromBits
815
+ ? curve.fromBits(pointBits)
816
+ : null;
817
+ if (!Q) {
818
+ return { verified: false,
819
+ code: 'pdf/sig/verify-pk/bad-ecc-point',
820
+ error: 'ECC point reconstruction failed' };
821
+ }
822
+ const pub = new fb.ecc.ecdsa.publicKey(curve, Q);
823
+ // Hash → bits for ECDSA.verify
824
+ let h;
825
+ if (digest instanceof Uint8Array) {
826
+ h = ba.ui8_to_ba(digest);
827
+ } else {
828
+ if (!(message instanceof Uint8Array)
829
+ || !hashMod
830
+ || typeof hashMod.hash !== 'function') {
831
+ return { verified: false,
832
+ code: 'pdf/sig/verify-pk/no-digest',
833
+ error: 'ECDSA verify needs digest '
834
+ + 'or (message, hashMod)' };
835
+ }
836
+ h = hashMod.hash(ba.ui8_to_ba(message));
837
+ }
838
+ const norm = _ecdsaSigToRaw(signature,
839
+ pubKey.point.length / 2, fb.asn1);
840
+ if (!norm.ok) {
841
+ return { verified: false, code: norm.code,
842
+ error: norm.error };
843
+ }
844
+ const rs = ba.ui8_to_ba(norm.rs);
845
+ const ok = pub.verify(h, rs);
846
+ return { verified: !!ok };
847
+ }
848
+ if (algorithm === 'ed25519') {
849
+ if (!fb.ed25519
850
+ || typeof fb.ed25519.verify !== 'function') {
851
+ return { verified: false,
852
+ code: 'pdf/sig/verify-pk/no-ed25519',
853
+ error: 'fw ed25519 unavailable' };
854
+ }
855
+ if (!(pubKey instanceof Uint8Array)
856
+ || pubKey.length !== 32) {
857
+ return { verified: false,
858
+ code: 'pdf/sig/verify-pk/bad-ed25519-key',
859
+ error: 'Ed25519 pubKey must be 32 bytes' };
860
+ }
861
+ if (!(message instanceof Uint8Array)) {
862
+ return { verified: false,
863
+ code: 'pdf/sig/verify-pk/no-message',
864
+ error: 'Ed25519 verify requires message' };
865
+ }
866
+ const ok = fb.ed25519.verify(pubKey, message, signature);
867
+ return { verified: !!ok };
868
+ }
869
+ return { verified: false,
870
+ code: 'pdf/sig/verify-pk/unknown-alg',
871
+ error: 'unsupported algorithm: ' + algorithm };
872
+ } catch (e) {
873
+ return { verified: false,
874
+ code: 'pdf/sig/verify-pk/throw',
875
+ error: 'primitive threw: ' + (e && e.message) };
876
+ }
877
+ }
878
+
879
+ // ── Multi-signature + DocTimeStamp verify ──────────────
880
+ //
881
+ // Walks ALL `/Type /Sig` and `/Type /DocTimeStamp` objects in
882
+ // the raw bytes (regardless of how many incremental updates
883
+ // were appended) and runs the appropriate verifier on each.
884
+ // For `/Sig` objects we delegate to `verifySignature`. For
885
+ // `/DocTimeStamp` we parse the embedded RFC 3161
886
+ // TimeStampToken, verify its messageImprint matches the
887
+ // hash of the DocTimeStamp's ByteRange-covered bytes, and
888
+ // (if a TSA cert + SPKI are extractable) verify the TSA
889
+ // signature on TSTInfo.
890
+ //
891
+ // Returns { signatures: [...], timestamps: [...] }.
892
+ /**
893
+ * Verify every `/Sig` and `/DocTimeStamp` object in a document.
894
+ *
895
+ * @param {Uint8Array} documentBytes The whole document.
896
+ * @param {object} [fwBundle] Optional fw crypto bundle (the module
897
+ * carries its own default).
898
+ * @returns {{
899
+ * signatures: Array<object>,
900
+ * timestamps: Array<object>
901
+ * }} Each `signatures` entry is a `verifySignature` result plus
902
+ * `objNum` / `objGen`; each `timestamps` entry carries `verified`,
903
+ * `valid`, `kind`, `subFilter`, `hashAlg`, `tstInfo`,
904
+ * `imprintVerified`, `tsaVerified`, `gapForm`, `errors`, `signerCerts`,
905
+ * `objNum`, `objGen`.
906
+ * `valid` on both kinds of entry is deprecated: same value as
907
+ * `verified`; kept for compatibility, will be removed in a future
908
+ * major version; read `verified`.
909
+ */
910
+ function verifyAllSignatures(documentBytes, fwBundle) {
911
+ const fb = fwBundle || bundle;
912
+ const sigObjs = _scanSignatureObjects(documentBytes);
913
+ const out = { signatures: [], timestamps: [] };
914
+ for (const obj of sigObjs) {
915
+ if (obj.kind === 'Sig') {
916
+ const typed = {
917
+ kind: 'Sig',
918
+ filter: obj.filter,
919
+ subFilter: obj.subFilter,
920
+ contents: obj.contents,
921
+ byteRange: obj.byteRange
922
+ };
923
+ const r = _verifySig(typed, documentBytes, fb,
924
+ obj.contentsSpan);
925
+ out.signatures.push(Object.assign({
926
+ objNum: obj.num, objGen: obj.gen
927
+ }, r));
928
+ } else {
929
+ const r = _verifyDocTimeStamp(obj, documentBytes, fb);
930
+ out.timestamps.push(Object.assign({
931
+ objNum: obj.num, objGen: obj.gen
932
+ }, r));
933
+ }
934
+ }
935
+ return out;
936
+ }
937
+
938
+ // Scan bytes for `N M obj << … /Type /(Sig|DocTimeStamp) … >>`
939
+ // and extract /ByteRange, /Contents, /SubFilter, /Filter for
940
+ // each. Tolerant : signs that the dict body straddles newlines
941
+ // are normal; the regex anchors are non-greedy.
942
+ function _scanSignatureObjects(bytes) {
943
+ const s = _bytesToLatin1(bytes);
944
+ const out = [];
945
+ const re =
946
+ /(\d+)\s+(\d+)\s+obj\s*<<([\s\S]*?)>>\s*endobj/g;
947
+ let m;
948
+ while ((m = re.exec(s)) !== null) {
949
+ const body = m[3];
950
+ const tm = body.match(/\/Type\s*\/(Sig|DocTimeStamp)\b/);
951
+ if (!tm) continue;
952
+ const kind = tm[1];
953
+ const filterM = body.match(/\/Filter\s*\/([A-Za-z0-9._]+)/);
954
+ const subM = body.match(/\/SubFilter\s*\/([A-Za-z0-9._]+)/);
955
+ const brM = body.match(
956
+ /\/ByteRange\s*\[\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*\]/);
957
+ if (!brM) continue;
958
+ const br = [
959
+ parseInt(brM[1], 10), parseInt(brM[2], 10),
960
+ parseInt(brM[3], 10), parseInt(brM[4], 10)
961
+ ];
962
+ // Locate the /Contents hex literal in the absolute byte
963
+ // stream. The literal start `<` is unique inside the
964
+ // object's slice — find it via absolute-offset search.
965
+ const objStart = m.index;
966
+ const objSliceStr = s.slice(objStart, objStart + m[0].length);
967
+ const conM = objSliceStr.match(/\/Contents\s*<([0-9A-Fa-f]*)>/);
968
+ if (!conM) continue;
969
+ const hex = conM[1];
970
+ const contents = _hexToBytesLocal(hex);
971
+ // Hex-digit span of this object's own /Contents, in file
972
+ // offsets — the anchor of the /ByteRange gap check.
973
+ const ltAt = objStart + conM.index + conM[0].indexOf('<');
974
+ out.push({
975
+ num: parseInt(m[1], 10),
976
+ gen: parseInt(m[2], 10),
977
+ kind,
978
+ filter: filterM ? filterM[1] : null,
979
+ subFilter: subM ? subM[1] : null,
980
+ byteRange: br,
981
+ contents,
982
+ contentsSpan: { offset: ltAt + 1, length: hex.length }
983
+ });
984
+ }
985
+ // Dedup by (num, byteRange[0]+byteRange[1]) — incremental
986
+ // re-writes can produce multiple definitions but we treat
987
+ // each distinct ByteRange as its own signature.
988
+ const seen = new Set();
989
+ const dedup = [];
990
+ for (const it of out) {
991
+ const k = `${it.num}:${it.byteRange.join(',')}`;
992
+ if (seen.has(k)) continue;
993
+ seen.add(k);
994
+ dedup.push(it);
995
+ }
996
+ return dedup;
997
+ }
998
+
999
+ function _bytesToLatin1(bytes) {
1000
+ let s = '';
1001
+ const CHUNK = 0x8000;
1002
+ for (let i = 0; i < bytes.length; i += CHUNK) {
1003
+ s += String.fromCharCode.apply(null,
1004
+ bytes.subarray(i, Math.min(i + CHUNK, bytes.length)));
1005
+ }
1006
+ return s;
1007
+ }
1008
+
1009
+ function _hexToBytesLocal(hex) {
1010
+ const clean = hex.replace(/[^0-9A-Fa-f]/g, '');
1011
+ const out = new Uint8Array(clean.length >>> 1);
1012
+ for (let i = 0; i < out.length; i++) {
1013
+ out[i] = parseInt(clean.substr(i * 2, 2), 16);
1014
+ }
1015
+ return out;
1016
+ }
1017
+
1018
+ function _verifyDocTimeStamp(obj, documentBytes, fb) {
1019
+ const errs = [];
1020
+ function fail(code, msg, c, cause) {
1021
+ const rec = { code, message: msg };
1022
+ if (c) rec.context = c;
1023
+ if (cause) rec.cause = cause;
1024
+ errs.push(rec);
1025
+ }
1026
+ // The /ByteRange gap rule, anchored on this object's own
1027
+ // /Contents span: computed once, `gapForm` reported
1028
+ // on every return path, the issues weighed at the verdict.
1029
+ const gap = _gapCheck(obj.byteRange || [], obj.contentsSpan || null);
1030
+ const result = {
1031
+ verified: false,
1032
+ valid: false, // deprecated alias of `verified`, removed in a future major version
1033
+ kind: 'DocTimeStamp',
1034
+ subFilter: obj.subFilter,
1035
+ hashAlg: null,
1036
+ tstInfo: null,
1037
+ imprintVerified: false,
1038
+ tsaVerified: false,
1039
+ gapForm: gap.gapForm,
1040
+ errors: errs,
1041
+ signerCerts: []
1042
+ };
1043
+ // Parse the TimeStampToken (the /Contents is the bare DER
1044
+ // of the RFC 3161 token — a CMS SignedData wrapping
1045
+ // TSTInfo).
1046
+ const blob = obj.contents;
1047
+ if (!(blob instanceof Uint8Array) || blob.length === 0) {
1048
+ fail('pdf/ts/empty-contents',
1049
+ 'DocTimeStamp /Contents is empty');
1050
+ return result;
1051
+ }
1052
+ // Locate the TSTInfo OCTET STRING + messageImprint.
1053
+ let tstInfo;
1054
+ try {
1055
+ tstInfo = _parseTstInfoMinimal(blob, fb.asn1);
1056
+ } catch (e) {
1057
+ fail('pdf/ts/parse-failed',
1058
+ 'TimeStampToken parse failed: ' + (e && e.message),
1059
+ null, e);
1060
+ return result;
1061
+ }
1062
+ result.tstInfo = tstInfo;
1063
+ const hashAlg = tstInfo.hashAlg;
1064
+ result.hashAlg = hashAlg;
1065
+ const hashMod = hashAlg && fb[hashAlg];
1066
+ if (!hashMod || typeof hashMod.hash !== 'function') {
1067
+ fail('pdf/ts/unknown-hash',
1068
+ 'unsupported messageImprint hashAlg',
1069
+ { hashAlg });
1070
+ return result;
1071
+ }
1072
+ // Compute the ByteRange digest and compare with
1073
+ // messageImprint.hashedMessage.
1074
+ let actual;
1075
+ try {
1076
+ actual = _hashByteRange(documentBytes, obj.byteRange,
1077
+ hashMod, fb.bitArray);
1078
+ } catch (e) {
1079
+ fail('pdf/ts/byterange-failed',
1080
+ 'failed to digest ByteRange: ' + (e && e.message),
1081
+ null, e);
1082
+ return result;
1083
+ }
1084
+ const expected = tstInfo.imprint;
1085
+ if (!expected || expected.length !== actual.length) {
1086
+ fail('pdf/ts/imprint-length-mismatch',
1087
+ 'TSTInfo messageImprint length differs from digest',
1088
+ { expectedLen: expected && expected.length,
1089
+ actualLen: actual.length });
1090
+ return result;
1091
+ }
1092
+ let imprintOk = true;
1093
+ for (let i = 0; i < actual.length; i++) {
1094
+ if (actual[i] !== expected[i]) { imprintOk = false; break; }
1095
+ }
1096
+ if (!imprintOk) {
1097
+ fail('pdf/ts/imprint-mismatch',
1098
+ 'TSTInfo messageImprint does not match '
1099
+ + 'DocTimeStamp ByteRange digest');
1100
+ return result;
1101
+ }
1102
+ result.imprintVerified = true;
1103
+ // Try to verify the TSA signature too. Optional — if the
1104
+ // token has no signerInfos (test fixtures) we still treat
1105
+ // the imprint match as the validity criterion (mirroring
1106
+ // the existing pdfTimestamp.verifyTimestamp policy).
1107
+ // Parse SignedData for signerInfo[0] + certs.
1108
+ const sigResult = _verifyTsaSignature(blob, fb);
1109
+ result.tsaVerified = !!sigResult.verified;
1110
+ if (sigResult.signerCerts) {
1111
+ result.signerCerts = sigResult.signerCerts;
1112
+ }
1113
+ if (sigResult.errors && sigResult.errors.length) {
1114
+ for (const er of sigResult.errors) errs.push(er);
1115
+ }
1116
+ // Overall verified = imprint matches AND the gap is exactly
1117
+ // form (b) or form (a). TSA-sig is informational —
1118
+ // many real TSA tokens use ESSCertID v1/v2 + signerInfos we
1119
+ // partially parse. We keep `verified` lenient on the TSA leg;
1120
+ // `imprintVerified` reports the imprint outcome alone.
1121
+ for (const g of gap.issues) errs.push(g);
1122
+ result.verified = gap.issues.length === 0;
1123
+ result.valid = result.verified; // deprecated alias of `verified`, removed in a future major version
1124
+ return result;
1125
+ }
1126
+
1127
+ function _parseTstInfoMinimal(blob, a1) {
1128
+ const ci = a1.parseOne(blob, 0);
1129
+ if (!ci) throw new ParseError('pdf/ts/no-ci', 'ContentInfo missing');
1130
+ const ciKids = a1.parseChildren(ci.value);
1131
+ if (!ciKids || ciKids.length < 2) {
1132
+ throw new ParseError('pdf/ts/short-ci',
1133
+ 'ContentInfo too short');
1134
+ }
1135
+ const sdNode = a1.parseOne(ciKids[1].value, 0);
1136
+ if (!sdNode) {
1137
+ throw new ParseError('pdf/ts/no-sd',
1138
+ 'SignedData node missing');
1139
+ }
1140
+ const sd = a1.parseChildren(sdNode.value);
1141
+ if (!sd) {
1142
+ throw new ParseError('pdf/ts/short-sd',
1143
+ 'SignedData parse failed');
1144
+ }
1145
+ const eci = sd[2] && a1.parseChildren(sd[2].value);
1146
+ if (!eci || eci.length < 2) {
1147
+ throw new ParseError('pdf/ts/no-eci',
1148
+ 'encapContentInfo missing');
1149
+ }
1150
+ const eContentExp = a1.parseOne(eci[1].value, 0);
1151
+ if (!eContentExp) {
1152
+ throw new ParseError('pdf/ts/no-econtent',
1153
+ 'eContent OCTET STRING missing');
1154
+ }
1155
+ // eContent is [0] EXPLICIT OCTET STRING; inside is the TSTInfo
1156
+ // SEQUENCE (DER-encoded).
1157
+ const tst = a1.parseOne(eContentExp.value, 0);
1158
+ if (!tst) {
1159
+ throw new ParseError('pdf/ts/no-tst',
1160
+ 'TSTInfo SEQUENCE missing');
1161
+ }
1162
+ const tstKids = a1.parseChildren(tst.value);
1163
+ if (!tstKids || tstKids.length < 4) {
1164
+ throw new ParseError('pdf/ts/short-tstinfo',
1165
+ 'TSTInfo too short');
1166
+ }
1167
+ // version, policy, messageImprint, serial, …
1168
+ const miKids = a1.parseChildren(tstKids[2].value);
1169
+ if (!miKids || miKids.length < 2) {
1170
+ throw new ParseError('pdf/ts/no-imprint',
1171
+ 'messageImprint malformed');
1172
+ }
1173
+ const algKids = a1.parseChildren(miKids[0].value);
1174
+ const algOid = algKids && algKids[0]
1175
+ ? a1.readOid(algKids[0]) : null;
1176
+ const hashAlg = DIGEST_OIDS[algOid] || null;
1177
+ return {
1178
+ hashAlg,
1179
+ imprint: miKids[1].value,
1180
+ rawSd: sd
1181
+ };
1182
+ }
1183
+
1184
+ function _verifyTsaSignature(blob, fb) {
1185
+ // Parse ContentInfo → SignedData and locate the eContent
1186
+ // (TSTInfo OCTET STRING value bytes). Then dispatch to the
1187
+ // shared `_verifyPkcs7Signature` helper.
1188
+ try {
1189
+ const ci = fb.asn1.parseOne(blob, 0);
1190
+ const ciKids = ci && fb.asn1.parseChildren(ci.value);
1191
+ const sdNode = ciKids && ciKids[1]
1192
+ && fb.asn1.parseOne(ciKids[1].value, 0);
1193
+ const sd = sdNode && fb.asn1.parseChildren(sdNode.value);
1194
+ if (!sd) {
1195
+ return { verified: false, signerCerts: [],
1196
+ errors: [{ code: 'pdf/ts/tsa-no-sd',
1197
+ message: 'SignedData missing' }] };
1198
+ }
1199
+ // Locate eContent inside encapContentInfo (sd[2]).
1200
+ // encapContentInfo = SEQUENCE { eContentType OID,
1201
+ // eContent [0] EXPLICIT OCTET STRING OPTIONAL }
1202
+ let eContentBytes = null;
1203
+ if (sd[2] && sd[2].tag === 0x30) {
1204
+ const eci = fb.asn1.parseChildren(sd[2].value);
1205
+ if (eci && eci.length >= 2 && eci[1].tag === 0xA0) {
1206
+ const inner = fb.asn1.parseOne(eci[1].value, 0);
1207
+ if (inner && inner.tag === 0x04) {
1208
+ // The eContent OCTET STRING value bytes are
1209
+ // what gets hashed when no signedAttrs OR they
1210
+ // are referenced via messageDigest in signedAttrs.
1211
+ eContentBytes = inner.value;
1212
+ }
1213
+ }
1214
+ }
1215
+ return _verifyPkcs7Signature(sd, eContentBytes, fb,
1216
+ /* codePrefix */ 'pdf/ts');
1217
+ } catch (e) {
1218
+ return { verified: false, signerCerts: [],
1219
+ errors: [{ code: 'pdf/ts/tsa-throw',
1220
+ message: 'TSA verify threw: '
1221
+ + (e && e.message),
1222
+ cause: e }] };
1223
+ }
1224
+ }
1225
+
1226
+ // Shared PKCS#7 SignerInfo verify : given parsed SignedData
1227
+ // children and the eContent bytes (what was hashed for the
1228
+ // messageDigest signed attribute), recompute messageDigest,
1229
+ // optionally re-tag signedAttrs SET and pk-verify the encrypted
1230
+ // digest against the matching signer cert SPKI.
1231
+ //
1232
+ // `opts.byteRangeDocument` + `opts.byteRange` : detached-PKCS#7
1233
+ // mode (PDF /Sig). The helper hashes the ByteRange-covered bytes
1234
+ // itself (used as the messageDigest for signedAttrs; or as the
1235
+ // raw signed payload when signedAttrs is absent).
1236
+ //
1237
+ // Returns an extended result : { verified, signerCerts, errors,
1238
+ // hashAlg, signatureAlg, computedDigest, pkVerified }.
1239
+ function _verifyPkcs7Signature(sd, eContentBytes, fb,
1240
+ codePrefix, opts) {
1241
+ const cp = codePrefix || 'pdf/sig';
1242
+ const o = opts || {};
1243
+ const errors = [];
1244
+ function fail(code, msg) {
1245
+ errors.push({ code, message: msg });
1246
+ }
1247
+ function bail(hashAlg, signatureAlg, computedDigest,
1248
+ signerCerts) {
1249
+ return { verified: false,
1250
+ signerCerts: signerCerts || [],
1251
+ errors,
1252
+ hashAlg: hashAlg || null,
1253
+ signatureAlg: signatureAlg || null,
1254
+ computedDigest: computedDigest || null,
1255
+ pkVerified: false };
1256
+ }
1257
+ // Locate signerInfos SET (last 0x31 child of sd).
1258
+ let siSet = null;
1259
+ for (let i = sd.length - 1; i >= 0; i--) {
1260
+ if (sd[i].tag === 0x31) { siSet = sd[i]; break; }
1261
+ }
1262
+ if (!siSet) {
1263
+ fail(cp + '/no-signer',
1264
+ 'no signerInfos SET in SignedData');
1265
+ return bail();
1266
+ }
1267
+ const signers = fb.asn1.parseChildren(siSet.value);
1268
+ if (!signers || !signers.length) {
1269
+ fail(cp + '/empty-signers',
1270
+ 'signerInfos SET is empty');
1271
+ return bail();
1272
+ }
1273
+ const si = fb.asn1.parseChildren(signers[0].value);
1274
+ if (!si) {
1275
+ fail(cp + '/bad-signer',
1276
+ 'signerInfo[0] parse failed');
1277
+ return bail();
1278
+ }
1279
+ // Read digestAlgorithm from SignerInfo (si[2]).
1280
+ let hashAlg = null;
1281
+ if (si[2] && si[2].tag === 0x30) {
1282
+ const algKids = fb.asn1.parseChildren(si[2].value);
1283
+ if (algKids && algKids[0]) {
1284
+ const oid = fb.asn1.readOid(algKids[0]);
1285
+ hashAlg = DIGEST_OIDS[oid] || null;
1286
+ }
1287
+ }
1288
+ if (!hashAlg) {
1289
+ fail(cp + '/unknown-digest',
1290
+ 'unsupported SignerInfo digestAlgorithm');
1291
+ return bail();
1292
+ }
1293
+ const hashMod = fb[hashAlg];
1294
+ if (!hashMod || typeof hashMod.hash !== 'function') {
1295
+ fail(cp + '/no-hash',
1296
+ 'hash module unavailable: ' + hashAlg);
1297
+ return bail(hashAlg);
1298
+ }
1299
+ // Identify signatureAlgorithm OID (live at si[idx] after the
1300
+ // optional signedAttrs 0xA0 node, before the signature OCTET
1301
+ // STRING).
1302
+ let signatureAlg = null;
1303
+ for (let i = 3; i < si.length - 1; i++) {
1304
+ if (si[i].tag === 0x30 && si[i + 1].tag === 0x04) {
1305
+ const a = fb.asn1.parseChildren(si[i].value);
1306
+ if (a && a[0]) {
1307
+ const oid = fb.asn1.readOid(a[0]);
1308
+ signatureAlg = SIG_OIDS[oid] || null;
1309
+ }
1310
+ break;
1311
+ }
1312
+ }
1313
+ if (!signatureAlg) {
1314
+ fail(cp + '/unknown-sigalg',
1315
+ 'unsupported SignerInfo signatureAlgorithm');
1316
+ return bail(hashAlg);
1317
+ }
1318
+ const dissection = _dissectSignerInfo(si, fb.asn1);
1319
+ if (!dissection.ok) {
1320
+ fail(cp + '/' + dissection.code.replace('pdf/sig/', ''),
1321
+ dissection.message);
1322
+ return bail(hashAlg, signatureAlg);
1323
+ }
1324
+ // Recompute the digest that messageDigest is expected to match.
1325
+ // - eContent mode (TSA) : hash the eContent bytes.
1326
+ // - ByteRange mode (PDF /Sig) : hash the ByteRange-covered
1327
+ // bytes via `_hashByteRange` (avoids materialising a
1328
+ // ByteRange concat just to feed the hash).
1329
+ let computedDigest = null;
1330
+ if (o.byteRangeDocument instanceof Uint8Array
1331
+ && Array.isArray(o.byteRange)) {
1332
+ try {
1333
+ computedDigest = _hashByteRange(
1334
+ o.byteRangeDocument, o.byteRange,
1335
+ hashMod, fb.bitArray);
1336
+ } catch (e) {
1337
+ fail(cp + '/digest-failed',
1338
+ 'failed to recompute ByteRange digest: '
1339
+ + (e && e.message));
1340
+ return bail(hashAlg, signatureAlg);
1341
+ }
1342
+ } else if (eContentBytes && fb.bitArray) {
1343
+ const ba = fb.bitArray;
1344
+ const bits = hashMod.hash(ba.ui8_to_ba(eContentBytes));
1345
+ computedDigest = bits instanceof Uint8Array
1346
+ ? bits : ba.ba_to_ui8(bits);
1347
+ }
1348
+ // Determine the bytes the signer actually signed.
1349
+ // - signedAttrs present : re-tag [0] IMPLICIT (0xA0) → SET
1350
+ // (0x31) per RFC 5652 §5.4 and use that as the signed
1351
+ // payload (with messageDigest cross-check).
1352
+ // - signedAttrs absent : the eContent bytes (TSA mode) or
1353
+ // the ByteRange-covered bytes (PDF /Sig mode).
1354
+ let messageToVerify;
1355
+ if (dissection.signedAttrsRaw) {
1356
+ const mdOk = _checkSignedAttrsDigest(
1357
+ dissection.signedAttrsValueBytes,
1358
+ computedDigest, fb.asn1);
1359
+ if (!mdOk.ok) {
1360
+ fail(cp + '/' + mdOk.code.replace('pdf/sig/', ''),
1361
+ mdOk.message);
1362
+ return bail(hashAlg, signatureAlg, computedDigest);
1363
+ }
1364
+ messageToVerify = new Uint8Array(
1365
+ dissection.signedAttrsRaw.length);
1366
+ messageToVerify.set(dissection.signedAttrsRaw);
1367
+ messageToVerify[0] = 0x31;
1368
+ } else if (eContentBytes) {
1369
+ messageToVerify = eContentBytes;
1370
+ } else if (o.byteRangeDocument instanceof Uint8Array
1371
+ && Array.isArray(o.byteRange)) {
1372
+ try {
1373
+ messageToVerify = _concatRange(
1374
+ o.byteRangeDocument, o.byteRange);
1375
+ } catch (e) {
1376
+ fail(cp + '/byterange-concat-failed',
1377
+ 'failed to extract ByteRange-covered bytes: '
1378
+ + (e && e.message));
1379
+ return bail(hashAlg, signatureAlg, computedDigest);
1380
+ }
1381
+ } else {
1382
+ fail(cp + '/no-econtent',
1383
+ 'no signedAttrs and no signed payload available');
1384
+ return bail(hashAlg, signatureAlg, computedDigest);
1385
+ }
1386
+ const signerCert = _findSignerCert(sd, dissection.sid, fb.asn1);
1387
+ if (!signerCert) {
1388
+ fail(cp + '/signer-cert-not-found',
1389
+ 'unable to match signer cert by IssuerAndSerialNumber');
1390
+ return bail(hashAlg, signatureAlg, computedDigest);
1391
+ }
1392
+ const spki = _extractSpki(signerCert.der, signatureAlg, fb.asn1);
1393
+ if (!spki.ok) {
1394
+ fail(cp + '/' + spki.code.replace('pdf/sig/', ''),
1395
+ spki.message);
1396
+ return bail(hashAlg, signatureAlg, computedDigest,
1397
+ [signerCert]);
1398
+ }
1399
+ let digestForEcdsa = null;
1400
+ if (signatureAlg === 'ecc' && fb.bitArray) {
1401
+ const dBits = hashMod.hash(
1402
+ fb.bitArray.ui8_to_ba(messageToVerify));
1403
+ digestForEcdsa = dBits instanceof Uint8Array
1404
+ ? dBits : fb.bitArray.ba_to_ui8(dBits);
1405
+ }
1406
+ const pkResult = verifyPk({
1407
+ algorithm: signatureAlg === 'rsa' ? 'rsa-v15'
1408
+ : signatureAlg === 'ecc' ? 'ecdsa'
1409
+ : signatureAlg,
1410
+ pubKey: spki.pubKey,
1411
+ signature: dissection.encryptedDigest,
1412
+ message: messageToVerify,
1413
+ digest: digestForEcdsa,
1414
+ hashMod
1415
+ });
1416
+ if (!pkResult.verified) {
1417
+ fail(pkResult.code || (cp + '/pk-verify-failed'),
1418
+ pkResult.error || 'public-key verification failed');
1419
+ return bail(hashAlg, signatureAlg, computedDigest,
1420
+ [signerCert]);
1421
+ }
1422
+ return { verified: true, signerCerts: [signerCert], errors,
1423
+ hashAlg, signatureAlg, computedDigest,
1424
+ pkVerified: true };
1425
+ }
1426
+
1427
+ return {
1428
+ typeSignature,
1429
+ verifySignature,
1430
+ verifyAllSignatures,
1431
+ verifyPk,
1432
+ locatePkcs7,
1433
+ _hashByteRange,
1434
+ _scanSignatureObjects,
1435
+ _verifyPkcs7Signature,
1436
+ _verifyTsaSignature,
1437
+ _ecdsaSigToRaw,
1438
+ DIGEST_OIDS, SIG_OIDS
1439
+ };
1440
+ }
1441
+ };