@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,1899 @@
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 PAdES signature generation — API `pdfSign.sign()`.
8
+ *
9
+ * Companion of `pdfSignature` (verify side). Produces a PKCS#7/CMS
10
+ * detached `SignedData` blob and embeds it inside a signature dictionary
11
+ * `(/Contents <…hex…>)` whose `/ByteRange` covers the entire document
12
+ * minus the whole `<…>` token of `/Contents`, delimiters included
13
+ * (ISO 32000-2 §12.8.3.3.1).
14
+ *
15
+ * Scope:
16
+ * - PAdES baseline levels **B**, **T**, **LT** and **LTA**, selected by
17
+ * `opts.level` (default `'B'`; any other value throws
18
+ * `pdf/sign/level-not-implemented`):
19
+ * - B — a single embedded signature.
20
+ * - T — the signature plus an RFC 3161 signature timestamp: the
21
+ * caller's `opts.tsaSign({ digest, hashAlg })` callback returns
22
+ * the TimeStampToken (the TSA exchange itself lives outside this
23
+ * module), embedded as the `id-aa-timeStampToken` unsigned
24
+ * attribute. Required for T, LT and LTA.
25
+ * - LT — T, then a second incremental update carrying the DSS
26
+ * dictionary (`pdfDssBuilder`, fed from `opts.dss`) and a
27
+ * Catalog re-definition with `/DSS`.
28
+ * - LTA — LT, then a third incremental update carrying a
29
+ * `/DocTimeStamp` signature (`ETSI.RFC3161`) over the LT bytes.
30
+ * - Algorithms : RSA-PSS, ECDSA (P-256/P-384/P-521), Ed25519.
31
+ * PKCS#1 v1.5 is deliberately refused (fw policy — NIST SP
32
+ * 800-131A Rev.2 deprecation).
33
+ * - Detached PKCS#7 with `eContentInfo` ABSENT (canonical PAdES /
34
+ * `adbe.pkcs7.detached`).
35
+ * - `signedAttrs` (content-type, message-digest, signing-time and the
36
+ * ESS `signing-certificate-v2` attribute, RFC 5035) are always
37
+ * emitted for T / LT / LTA, whose timestamp token rides in
38
+ * `unsignedAttrs`; level B omits them unless `opts.useSignedAttrs`
39
+ * is set. When they are omitted the signature value covers the
40
+ * message digest directly per RFC 5652 §5.4 "the result is the
41
+ * message digest of the content".
42
+ *
43
+ * The signature object is appended as an **incremental update** through
44
+ * `pdfIncrementalWriter` (the base bytes are kept verbatim): the update
45
+ * section takes the form of the base's newest cross-reference section
46
+ * (classical table or xref stream), its `/Root` / `/Info` / `/ID` come
47
+ * from the newest-first merged trailer, and the signature takes the first
48
+ * object number at or past the merged `/Size`.
49
+ *
50
+ * The SAME update registers the signature dictionary in a signature field
51
+ * (ISO 32000-2 §12.7.5.5): an invisible field dictionary merged
52
+ * with its widget annotation (`/FT /Sig`, `/T (Signature<n>)`, `/V` → the
53
+ * signature dictionary, `/Subtype /Widget`, `/Rect [0 0 0 0]`, `/F 132`,
54
+ * `/P` → page 1), page 1's `/Annots` extended, and the Catalog's
55
+ * `/AcroForm` created or extended (`/Fields` appended, `/SigFlags 3`).
56
+ * Existing `/AcroForm` (direct or indirect), `/Fields` and `/Annots` are
57
+ * preserved and appended to. The LTA document timestamp gets its own
58
+ * field the same way (§12.8.5.2). The Catalog and the page are resolved
59
+ * through `pdfDocument.readDocument`, so every level needs that dep.
60
+ *
61
+ * An Ed25519 signature is an ISO/TS 32002 enhancement: its signing update
62
+ * also re-emits the Catalog with `/Extensions` declaring the `ISO_`
63
+ * developer extension at `/ExtensionLevel 32002` (merged with any existing
64
+ * extensions dictionary) and, below PDF 2.0, `/Version /2.0` (ISO/TS 32002
65
+ * §4, ISO 32000-2 §7.7.2 and §7.12). The new strings are encrypted on an
66
+ * encrypted base. ECDSA and RSA-PSS updates are unchanged.
67
+ *
68
+ * Encrypted base (trailer `/Encrypt`): the document key is derived from
69
+ * `opts.password` through the standard security handler (`pdfSecurity`,
70
+ * `pdfStandardV4/V5/V6`), once per call; the new field's `/T` is encrypted
71
+ * with it and every update trailer repeats the base's `/Encrypt`
72
+ * (ISO 32000-2 §7.5.6). Levels LT and LTA encrypt the DSS streams and
73
+ * strings and the DocTimeStamp field's `/T` with the same key; the
74
+ * hexadecimal `/Contents` of the `/Sig` and `/DocTimeStamp` dictionaries
75
+ * stays clear (§7.6.2). AES only (V=4 R=4 `AESV2`, V=5 R=5/R=6
76
+ * `AESV3`); RC4, AES-GCM, a non-standard handler, a missing or wrong
77
+ * password and insufficient permissions are refused with typed errors.
78
+ *
79
+ * @module pdf/sig/sign
80
+ */
81
+
82
+ /**
83
+ * Module factory.
84
+ */
85
+ import { pdfErrors } from '../errors.js';
86
+ import { pdfSigOids } from './oids.js';
87
+ import { pdfByteRange } from './byteRange.js';
88
+ import { pdfDssBuilder } from './dss.js';
89
+ import { pdfIncrementalWriter } from '../document/incrementalWriter.js';
90
+ import { pdfParser } from '../syntax/parser.js';
91
+ import { pdfDocument } from '../document/document.js';
92
+ import { pdfSecurity } from '../crypto/security.js';
93
+ import { pdfStandardV4 } from '../crypto/standardV4.js';
94
+ import { pdfStandardV5 } from '../crypto/standardV5.js';
95
+ import { pdfStandardV6 } from '../crypto/standardV6.js';
96
+ import { asn1 } from '@awacloud/fw/crypto/utils/asn1.js';
97
+ import { rsa } from '@awacloud/fw/crypto/pkc/rsa.js';
98
+ import { ecc } from '@awacloud/fw/crypto/pkc/ecc.js';
99
+ import { ed25519 } from '@awacloud/fw/crypto/pkc/ed25519.js';
100
+ import { sha256 } from '@awacloud/fw/crypto/hash/sha256.js';
101
+ import { sha384 } from '@awacloud/fw/crypto/hash/sha384.js';
102
+ import { sha512 } from '@awacloud/fw/crypto/hash/sha512.js';
103
+ import { bitArray } from '@awacloud/fw/crypto/utils/bitArray.js';
104
+
105
+ export const pdfSign = {
106
+ name: 'pdfSign',
107
+ dependencies: ['pdfErrors', 'pdfSigOids', 'pdfByteRange',
108
+ 'pdfDssBuilder', 'pdfIncrementalWriter', 'pdfParser',
109
+ 'asn1', 'rsa', 'ecc', 'ed25519',
110
+ 'sha256', 'sha384', 'sha512', 'bitArray',
111
+ 'pdfDocument', 'pdfSecurity', 'pdfStandardV4',
112
+ 'pdfStandardV5', 'pdfStandardV6'],
113
+ deps: [pdfErrors, pdfSigOids, pdfByteRange, pdfDssBuilder, pdfIncrementalWriter, pdfParser, asn1, rsa, ecc, ed25519, sha256, sha384, sha512, bitArray, pdfDocument, pdfSecurity, pdfStandardV4, pdfStandardV5, pdfStandardV6],
114
+ // `_parserMod` is no longer read (the Catalog is resolved through
115
+ // `pdfDocument`); it keeps its slot so positional factory
116
+ // callers stay valid. `documentMod` is appended LAST for the same
117
+ // reason — every level needs it (the signature field
118
+ // update resolves the Catalog and page 1 through it). The standard
119
+ // security handler modules follow, also appended last: they are read
120
+ // only when the base is encrypted (`pdf/sign/no-security-handler`
121
+ // otherwise), so a hand-wired caller signing clear bases may pass
122
+ // `null` for them.
123
+ factory(errors, sigOids, byteRange,
124
+ dssBuilderMod, incrementalWriterMod, _parserMod,
125
+ asn1, rsa, ecc, ed25519,
126
+ sha256, sha384, sha512, bitArray,
127
+ documentMod, securityMod, v4Mod, v5Mod, v6Mod) {
128
+ const { EncryptionError, ContractError } = errors;
129
+ const { computeByteRange } = byteRange;
130
+
131
+ // ── OID constants ─────────────────────────────────────────────
132
+ const OID_CONTENT_TYPE_DATA = '1.2.840.113549.1.7.1';
133
+ const OID_SIGNED_DATA = '1.2.840.113549.1.7.2';
134
+ const OID_SHA256 = '2.16.840.1.101.3.4.2.1';
135
+ const OID_SHA384 = '2.16.840.1.101.3.4.2.2';
136
+ const OID_SHA512 = '2.16.840.1.101.3.4.2.3';
137
+ const OID_RSASSA_PSS = '1.2.840.113549.1.1.10';
138
+ const OID_ECDSA_WITH_SHA256 = '1.2.840.10045.4.3.2';
139
+ const OID_ECDSA_WITH_SHA384 = '1.2.840.10045.4.3.3';
140
+ const OID_ECDSA_WITH_SHA512 = '1.2.840.10045.4.3.4';
141
+ const OID_ED25519 = '1.3.101.112';
142
+ const OID_MGF1 = '1.2.840.113549.1.1.8';
143
+ // CMS signed attribute OIDs (RFC 5652 + RFC 5035).
144
+ const OID_AA_CONTENT_TYPE = '1.2.840.113549.1.9.3';
145
+ const OID_AA_MESSAGE_DIGEST = '1.2.840.113549.1.9.4';
146
+ const OID_AA_SIGNING_TIME = '1.2.840.113549.1.9.5';
147
+ const OID_AA_SIGNING_CERT_V2 = '1.2.840.113549.1.9.16.2.47';
148
+ // RFC 3161 timestamp token attribute (unsignedAttrs).
149
+ const OID_AA_TIMESTAMP_TOKEN = '1.2.840.113549.1.9.16.2.14';
150
+
151
+ const HASH_TABLE = {
152
+ sha256: { mod: sha256, oid: OID_SHA256, len: 32 },
153
+ sha384: { mod: sha384, oid: OID_SHA384, len: 48 },
154
+ sha512: { mod: sha512, oid: OID_SHA512, len: 64 }
155
+ };
156
+
157
+ // ── Hex/byte helpers ─────────────────────────────────────────
158
+ function _hexToBytes(hex) {
159
+ const clean = hex.replace(/[^0-9a-fA-F]/g, '');
160
+ const out = new Uint8Array(clean.length >>> 1);
161
+ for (let i = 0; i < out.length; i++) {
162
+ out[i] = parseInt(clean.substr(i * 2, 2), 16);
163
+ }
164
+ return out;
165
+ }
166
+
167
+ function _bytesToHex(bytes) {
168
+ let s = '';
169
+ for (let i = 0; i < bytes.length; i++) {
170
+ s += bytes[i].toString(16).padStart(2, '0').toUpperCase();
171
+ }
172
+ return s;
173
+ }
174
+
175
+ function _hashBytes(hashMod, bytes) {
176
+ // fw hash modules expect input as a bitArray (or a UTF-8
177
+ // string). Always go through the streaming API to keep the
178
+ // input type unambiguous, and normalise the output to
179
+ // Uint8Array.
180
+ const ctx = new hashMod.fn();
181
+ ctx.update(bitArray.ui8_to_ba(bytes));
182
+ const out = ctx.finalize();
183
+ if (out instanceof Uint8Array) return out;
184
+ return bitArray.ba_to_ui8(out);
185
+ }
186
+
187
+ // ── Cert / key parsing ───────────────────────────────────────
188
+ /**
189
+ * Strip PEM armor and decode the base64 payload. Accepts either a
190
+ * `Uint8Array` (assumed DER) or a PEM string.
191
+ */
192
+ function _toDer(input) {
193
+ if (input instanceof Uint8Array) return input;
194
+ if (typeof input !== 'string') {
195
+ throw new ContractError('pdf/sign/bad-input',
196
+ 'cert/privateKey must be Uint8Array (DER) or PEM string');
197
+ }
198
+ const stripped = input.replace(/-----BEGIN [^-]+-----/g, '')
199
+ .replace(/-----END [^-]+-----/g, '')
200
+ .replace(/\s+/g, '');
201
+ // Decode base64. `atob` covers the PRIMARY targets (browser,
202
+ // worker, Bun); the `Buffer` branch is the fallback for Node, a
203
+ // supported SECONDARY deployment target. `Buffer` is declared readonly for
204
+ // THIS FILE ONLY by the office ESLint preset (delta 8) — it is
205
+ // deliberately absent from the package-wide browser+worker
206
+ // globals, which must not assert Node's API surface everywhere.
207
+ const bin = (typeof atob === 'function')
208
+ ? atob(stripped)
209
+ : Buffer.from(stripped, 'base64').toString('binary');
210
+ const out = new Uint8Array(bin.length);
211
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
212
+ return out;
213
+ }
214
+
215
+ /**
216
+ * Extract issuer DN bytes + serialNumber bytes from a parsed X.509
217
+ * cert DER. Used to populate the SignerInfo `IssuerAndSerialNumber`.
218
+ * Returns `{ issuerDer, serialDer }` where each is the raw TLV.
219
+ */
220
+ function _extractIssuerSerial(certDer) {
221
+ const top = asn1.parseOne(certDer, 0);
222
+ if (!top) {
223
+ throw new ContractError('pdf/sign/cert-parse',
224
+ 'failed to parse cert top-level SEQUENCE');
225
+ }
226
+ // Certificate ::= SEQUENCE { tbsCertificate, sigAlg, sig }
227
+ const tbsChildren = asn1.parseChildren(top.value);
228
+ if (!tbsChildren || tbsChildren.length < 1) {
229
+ throw new ContractError('pdf/sign/cert-parse',
230
+ 'cert lacks tbsCertificate');
231
+ }
232
+ const tbsInner = asn1.parseChildren(tbsChildren[0].value);
233
+ if (!tbsInner) {
234
+ throw new ContractError('pdf/sign/cert-parse',
235
+ 'failed to parse TBSCertificate fields');
236
+ }
237
+ // TBSCertificate ::= SEQUENCE {
238
+ // version [0] EXPLICIT Version DEFAULT v1, (optional)
239
+ // serialNumber CertificateSerialNumber,
240
+ // signature AlgorithmIdentifier,
241
+ // issuer Name,
242
+ // validity Validity,
243
+ // subject Name,
244
+ // ... }
245
+ let idx = 0;
246
+ // Skip version if present (context-specific [0]).
247
+ if (tbsInner[idx] && tbsInner[idx].tag === 0xA0) idx++;
248
+ const serialNode = tbsInner[idx]; // INTEGER
249
+ const sigAlg = tbsInner[idx + 1]; // SEQUENCE
250
+ const issuerNode = tbsInner[idx + 2]; // SEQUENCE (RDN)
251
+ void sigAlg;
252
+ if (!serialNode || !issuerNode) {
253
+ throw new ContractError('pdf/sign/cert-parse',
254
+ 'failed to locate serialNumber/issuer');
255
+ }
256
+ // Reconstruct the raw TLV for each (we have value bytes only; we
257
+ // need the whole TLV — re-encode via parseOne's offsets).
258
+ // Easier path : re-encode INTEGER and SEQUENCE manually from
259
+ // value bytes.
260
+ const serialDer = _reencode(0x02, serialNode.value);
261
+ const issuerDer = _reencode(0x30, issuerNode.value);
262
+ // Also expose the SubjectPublicKeyInfo for the consumer to
263
+ // double-check it matches the privateKey (best-effort).
264
+ return { serialDer, issuerDer };
265
+ }
266
+
267
+ function _reencode(tag, valueBytes) {
268
+ const lenBytes = _encLen(valueBytes.length);
269
+ const out = new Uint8Array(1 + lenBytes.length + valueBytes.length);
270
+ out[0] = tag;
271
+ out.set(lenBytes, 1);
272
+ out.set(valueBytes, 1 + lenBytes.length);
273
+ return out;
274
+ }
275
+
276
+ function _encLen(n) {
277
+ if (n < 0x80) return Uint8Array.of(n);
278
+ if (n <= 0xff) return Uint8Array.of(0x81, n);
279
+ if (n <= 0xffff) return Uint8Array.of(0x82, (n >>> 8) & 0xff, n & 0xff);
280
+ if (n <= 0xffffff) {
281
+ return Uint8Array.of(0x83,
282
+ (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff);
283
+ }
284
+ return Uint8Array.of(0x84,
285
+ (n >>> 24) & 0xff, (n >>> 16) & 0xff,
286
+ (n >>> 8) & 0xff, n & 0xff);
287
+ }
288
+
289
+ // ── PKCS#7 SignedData construction ───────────────────────────
290
+ /**
291
+ * Build a minimal detached SignedData ContentInfo around a
292
+ * pre-computed `messageDigest` over the document bytes.
293
+ *
294
+ * Layout (RFC 5652 §5) :
295
+ *
296
+ * ContentInfo ::= SEQUENCE {
297
+ * contentType OBJECT IDENTIFIER (id-signedData),
298
+ * content [0] EXPLICIT SignedData
299
+ * }
300
+ * SignedData ::= SEQUENCE {
301
+ * version INTEGER (1),
302
+ * digestAlgorithms SET OF AlgorithmIdentifier,
303
+ * encapContentInfo SEQUENCE {
304
+ * eContentType OBJECT IDENTIFIER (id-data),
305
+ * eContent [0] EXPLICIT OCTET STRING OPTIONAL -- ABSENT
306
+ * },
307
+ * certificates [0] IMPLICIT SET OF Certificate,
308
+ * signerInfos SET OF SignerInfo
309
+ * }
310
+ * SignerInfo ::= SEQUENCE {
311
+ * version INTEGER (1),
312
+ * sid IssuerAndSerialNumber,
313
+ * digestAlgorithm AlgorithmIdentifier,
314
+ * -- signedAttrs OMITTED
315
+ * signatureAlgorithm AlgorithmIdentifier,
316
+ * signature OCTET STRING
317
+ * }
318
+ */
319
+ function _buildPkcs7({ certDer, sigBytes, hashAlg, signatureAlg,
320
+ signedAttrsTlv, unsignedAttrsTlv }) {
321
+ const ih = HASH_TABLE[hashAlg];
322
+ if (!ih) {
323
+ throw new ContractError('pdf/sign/unknown-hash',
324
+ 'unknown hashAlg: ' + hashAlg);
325
+ }
326
+ const { issuerDer, serialDer } = _extractIssuerSerial(certDer);
327
+
328
+ // digestAlgorithm = SEQUENCE { OID, NULL }
329
+ const digestAlg = asn1.encodeSequence([
330
+ asn1.encodeOid(ih.oid),
331
+ asn1.encodeNull()
332
+ ]);
333
+ const digestAlgorithms = asn1.encodeSet([digestAlg]);
334
+
335
+ // encapContentInfo : eContent ABSENT (detached)
336
+ const encapContentInfo = asn1.encodeSequence([
337
+ asn1.encodeOid(OID_CONTENT_TYPE_DATA)
338
+ ]);
339
+
340
+ // certificates [0] IMPLICIT SET OF Certificate
341
+ // Use context-specific [0] IMPLICIT — set the tag class bits
342
+ // directly. We have the cert DER (a full SEQUENCE TLV).
343
+ // [0] IMPLICIT on a SET-of-certs is encoded as 0xA0 (constructed,
344
+ // class=context, tag=0). The body is the concatenation of cert
345
+ // DERs (since [0] IMPLICIT replaces the SET tag).
346
+ const certsImplicit = _wrapImplicit(0xA0, certDer);
347
+
348
+ // SignerInfo
349
+ const issuerAndSerial = asn1.encodeSequence([issuerDer, serialDer]);
350
+
351
+ // signatureAlgorithm
352
+ let sigAlgEncoded;
353
+ if (signatureAlg === 'rsa-pss') {
354
+ // RSASSA-PSS-params per RFC 8017 §A.2.3.
355
+ // hashAlgorithm [0] AlgorithmIdentifier DEFAULT sha1
356
+ // maskGenAlgo [1] AlgorithmIdentifier DEFAULT mgf1Sha1
357
+ // saltLength [2] INTEGER DEFAULT 20
358
+ // trailerField [3] INTEGER DEFAULT 1
359
+ const hashAlgId = asn1.encodeSequence([
360
+ asn1.encodeOid(ih.oid),
361
+ asn1.encodeNull()
362
+ ]);
363
+ const mgfHashAlgId = asn1.encodeSequence([
364
+ asn1.encodeOid(ih.oid),
365
+ asn1.encodeNull()
366
+ ]);
367
+ const mgfAlgId = asn1.encodeSequence([
368
+ asn1.encodeOid(OID_MGF1),
369
+ mgfHashAlgId
370
+ ]);
371
+ const saltLenInt = asn1.encodeInteger(ih.len);
372
+ const params = asn1.encodeSequence([
373
+ asn1.encodeExplicit(0, hashAlgId),
374
+ asn1.encodeExplicit(1, mgfAlgId),
375
+ asn1.encodeExplicit(2, saltLenInt)
376
+ ]);
377
+ sigAlgEncoded = asn1.encodeSequence([
378
+ asn1.encodeOid(OID_RSASSA_PSS),
379
+ params
380
+ ]);
381
+ } else if (signatureAlg === 'ecdsa') {
382
+ const ecdsaOid = hashAlg === 'sha256' ? OID_ECDSA_WITH_SHA256
383
+ : hashAlg === 'sha384' ? OID_ECDSA_WITH_SHA384
384
+ : OID_ECDSA_WITH_SHA512;
385
+ sigAlgEncoded = asn1.encodeSequence([asn1.encodeOid(ecdsaOid)]);
386
+ } else if (signatureAlg === 'ed25519') {
387
+ sigAlgEncoded = asn1.encodeSequence([asn1.encodeOid(OID_ED25519)]);
388
+ } else {
389
+ throw new ContractError('pdf/sign/unknown-sigalg',
390
+ 'unsupported signatureAlg: ' + signatureAlg);
391
+ }
392
+
393
+ const siParts = [
394
+ asn1.encodeInteger(1), // version
395
+ issuerAndSerial, // sid
396
+ digestAlg // digestAlgorithm
397
+ ];
398
+ if (signedAttrsTlv) {
399
+ // signedAttrs [0] IMPLICIT — re-tag the SET TLV.
400
+ const sa = new Uint8Array(signedAttrsTlv.length);
401
+ sa.set(signedAttrsTlv);
402
+ sa[0] = 0xA0;
403
+ siParts.push(sa);
404
+ }
405
+ siParts.push(sigAlgEncoded);
406
+ siParts.push(asn1.encodeOctetString(sigBytes));
407
+ if (unsignedAttrsTlv) {
408
+ const ua = new Uint8Array(unsignedAttrsTlv.length);
409
+ ua.set(unsignedAttrsTlv);
410
+ ua[0] = 0xA1; // [1] IMPLICIT
411
+ siParts.push(ua);
412
+ }
413
+ const signerInfo = asn1.encodeSequence(siParts);
414
+ const signerInfos = asn1.encodeSet([signerInfo]);
415
+
416
+ const signedData = asn1.encodeSequence([
417
+ asn1.encodeInteger(1), // version
418
+ digestAlgorithms,
419
+ encapContentInfo,
420
+ certsImplicit,
421
+ signerInfos
422
+ ]);
423
+
424
+ const contentInfo = asn1.encodeSequence([
425
+ asn1.encodeOid(OID_SIGNED_DATA),
426
+ asn1.encodeExplicit(0, signedData)
427
+ ]);
428
+ return contentInfo;
429
+ }
430
+
431
+ function _wrapImplicit(tag, body) {
432
+ const lenBytes = _encLen(body.length);
433
+ const out = new Uint8Array(1 + lenBytes.length + body.length);
434
+ out[0] = tag;
435
+ out.set(lenBytes, 1);
436
+ out.set(body, 1 + lenBytes.length);
437
+ return out;
438
+ }
439
+
440
+ // ── Document patching ────────────────────────────────────────
441
+ // A 10-digit integer the serializer writes verbatim (it emits ints
442
+ // as `value | 0`, so the int32 maximum is the widest it can carry).
443
+ // Four of them give the fixed-width `/ByteRange` placeholder
444
+ // `[2147483647 2147483647 2147483647 2147483647]` (45 bytes) the
445
+ // real range is patched into, space-padded, without moving a byte.
446
+ const BR_PLACEHOLDER_INT = 2147483647;
447
+ const BR_PLACEHOLDER = '[' + Array(4).fill(BR_PLACEHOLDER_INT).join(' ') + ']';
448
+
449
+ /**
450
+ * The base's trailer as an update over it starts from: the
451
+ * newest-first merge of every cross-reference section's dict,
452
+ * read by `pdfIncrementalWriter.readBaseTrailer` (which also
453
+ * refuses a hybrid-reference base, `pdf/incremental/hybrid-base`).
454
+ *
455
+ * Maps the writer's `startxref` failures onto this module's
456
+ * historical codes (`pdf/sign/no-startxref` / `bad-startxref`);
457
+ * every other refusal propagates unchanged.
458
+ */
459
+ function _readBaseTrailer(baseBytes) {
460
+ let base;
461
+ try {
462
+ base = incrementalWriterMod.readBaseTrailer(baseBytes);
463
+ } catch (e) {
464
+ const code = e && e.code;
465
+ if (code === 'pdf/incremental/no-startxref'
466
+ || code === 'pdf/xref/no-startxref') {
467
+ throw new ContractError('pdf/sign/no-startxref',
468
+ 'base PDF has no startxref', { context: { cause: code } });
469
+ }
470
+ if (code === 'pdf/xref/bad-startxref') {
471
+ throw new ContractError('pdf/sign/bad-startxref',
472
+ 'startxref does not parse', { context: { cause: code } });
473
+ }
474
+ throw e;
475
+ }
476
+ if (!base.trailer || !Number.isInteger(base.trailer.size)) {
477
+ throw new ContractError('pdf/sign/no-trailer',
478
+ 'no cross-reference section of the base supplies a usable '
479
+ + '/Size and /Root — cannot allocate or chain an update');
480
+ }
481
+ return base.trailer;
482
+ }
483
+
484
+ /**
485
+ * First object number free in `bytes`: the merged trailer's
486
+ * `/Size` (ISO 32000-2 §7.5.5 — one greater than the highest
487
+ * object number across the whole document, every section).
488
+ *
489
+ * TRAP, do not "simplify" this back to a byte scan: the highest
490
+ * `N 0 obj` header misses every object compressed in an object
491
+ * stream (the signature once overwrote a live compressed
492
+ * page), and a search for the literal `xref\n` lands inside the
493
+ * trailing `startxref\n` keyword.
494
+ */
495
+ function _firstFreeObjNum(trailer) {
496
+ return Math.max(trailer.size, 1);
497
+ }
498
+
499
+ // ── Encrypted base ───────────────────────────────────────────
500
+ /**
501
+ * A typed PDF object as the loose shape `pdfSecurity.typeEncryptDict`
502
+ * reads: numbers and booleans unwrapped, strings as their bytes,
503
+ * names kept as `{ type: 'name' }` (its `nameValue` accepts them),
504
+ * dictionaries (`/CF` and its crypt filters) as plain objects.
505
+ */
506
+ function _plainOf(node) {
507
+ if (!node || typeof node !== 'object') return node;
508
+ switch (node.type) {
509
+ case 'int': case 'real': case 'bool': case 'string':
510
+ return node.value;
511
+ case 'dict': {
512
+ const out = {};
513
+ for (const k of Object.keys(node.entries)) {
514
+ out[k] = _plainOf(node.entries[k]);
515
+ }
516
+ return out;
517
+ }
518
+ case 'array':
519
+ return node.items.map(_plainOf);
520
+ default:
521
+ return node;
522
+ }
523
+ }
524
+
525
+ function _unsupported(message, context) {
526
+ return new ContractError('pdf/sign/encrypted-unsupported',
527
+ message, { context });
528
+ }
529
+
530
+ /**
531
+ * Open an encrypted base for signing: derive the file key from
532
+ * `opts.password` through the standard security handler and check
533
+ * the permissions. Run once per `sign()` call, before anything is
534
+ * emitted; the returned context is threaded through every update
535
+ * the call writes.
536
+ *
537
+ * Accepted: `/Filter /Standard` with V=4 R=4 (`AESV2`) or V=5
538
+ * R=5 / R=6 (`AESV3`), on BOTH the string and the stream crypt
539
+ * filter (`Identity` allowed). Refused, all `ContractError`:
540
+ * - `pdf/sign/no-security-handler` — a handler module is not wired;
541
+ * - `pdf/sign/encrypted-password-required` — no `opts.password`;
542
+ * - `pdf/sign/encrypted-unsupported` — `context.filter` (not
543
+ * `Standard`), `context.reason` `'rc4'` (V < 4 or a `V2` crypt
544
+ * filter), `'aes-gcm'` (`AESV4`, ISO/TS 32003) or `'no-id'` (V=4
545
+ * without `/ID`), `context.cause` (an unreadable `/Encrypt`);
546
+ * - `pdf/sign/encrypted-bad-password` — neither the owner nor the
547
+ * user password (no context: the password is never echoed);
548
+ * - `pdf/sign/encrypted-permission-denied` — user password and
549
+ * `/P` lacks bit 4 (modify) or bit 6 (annotations / forms).
550
+ * And `pdf/sign/no-random` (`EncryptionError`) when no IV source
551
+ * exists (`opts.randomBytes`, else `crypto.getRandomValues`).
552
+ *
553
+ * @returns {{typedForPw: Object, handler: Object, strMethod: string,
554
+ * stmMethod: string, fek: Uint8Array,
555
+ * randomBytes: function(number): Uint8Array}}
556
+ */
557
+ function _openEncrypted(baseBytes, trailer, opts) {
558
+ opts = opts || {};
559
+ if (!securityMod || !v4Mod || !v5Mod || !v6Mod) {
560
+ throw new ContractError('pdf/sign/no-security-handler',
561
+ 'signing an encrypted base requires the pdfSecurity, '
562
+ + 'pdfStandardV4, pdfStandardV5 and pdfStandardV6 deps');
563
+ }
564
+ if (opts.password === undefined || opts.password === null) {
565
+ throw new ContractError('pdf/sign/encrypted-password-required',
566
+ "the base is encrypted; pass opts.password (use '' for an "
567
+ + 'empty user password)');
568
+ }
569
+ const doc = documentMod.readDocument(baseBytes, { allowEncrypted: true });
570
+ const encEntry = doc.trailer.encrypt;
571
+ const encNode = encEntry && encEntry.type === 'dict'
572
+ ? encEntry
573
+ : doc._raw.resolve(_ref(encEntry.num, encEntry.gen));
574
+ let typed;
575
+ try {
576
+ typed = securityMod.typeEncryptDict(
577
+ encNode && encNode.type === 'dict' ? _plainOf(encNode) : null);
578
+ } catch (e) {
579
+ throw _unsupported('the base /Encrypt dictionary cannot be read',
580
+ { cause: e && e.code });
581
+ }
582
+ const filter = typed.Filter && typed.Filter.type === 'name'
583
+ ? typed.Filter.value : typed.Filter;
584
+ if (filter !== 'Standard') {
585
+ throw _unsupported('only the standard security handler '
586
+ + '(/Filter /Standard) is supported', { filter });
587
+ }
588
+ const { V, R } = typed;
589
+ if (V < 4) {
590
+ throw _unsupported('RC4 encryption (V < 4) is not supported; '
591
+ + 'only AES bases can be signed', { V, R, reason: 'rc4' });
592
+ }
593
+ let sel;
594
+ try {
595
+ sel = securityMod.selectHandler(typed,
596
+ { v4: v4Mod, v5: v5Mod, v6: v6Mod });
597
+ } catch (e) {
598
+ throw _unsupported('the base encryption is not supported',
599
+ { V, R, cause: e && e.code });
600
+ }
601
+ // Both crypt filters: the strings this module writes go through
602
+ // /StrF, the streams of later updates through /StmF.
603
+ for (const m of [sel.strMethod, sel.method]) {
604
+ if (m === 'V2') {
605
+ throw _unsupported('an RC4 crypt filter (CFM /V2) is not '
606
+ + 'supported; only AES bases can be signed',
607
+ { V, R, reason: 'rc4' });
608
+ }
609
+ if (m === 'AESV4') {
610
+ throw _unsupported('AES-GCM encryption (CFM /AESV4, ISO/TS '
611
+ + '32003) is not supported', { V, R, reason: 'aes-gcm' });
612
+ }
613
+ }
614
+ const idFirst = trailer.id ? trailer.id[0] : undefined;
615
+ if (V === 4 && !(idFirst instanceof Uint8Array)) {
616
+ throw _unsupported('a V=4 base needs the trailer /ID to derive '
617
+ + 'its key', { V, R, reason: 'no-id' });
618
+ }
619
+ const typedForPw = Object.assign({}, typed,
620
+ { idFirst, method: sel.strMethod });
621
+ let r = sel.handler.tryPassword(typedForPw, opts.password, true);
622
+ const isOwner = !!r.fileEncryptionKey;
623
+ if (!isOwner) r = sel.handler.tryPassword(typedForPw, opts.password, false);
624
+ if (!r.fileEncryptionKey) {
625
+ throw new ContractError('pdf/sign/encrypted-bad-password',
626
+ 'opts.password is neither the owner nor the user password '
627
+ + 'of the base');
628
+ }
629
+ if (!isOwner) {
630
+ // Creating a signature field needs ISO 32000-2 Table 22
631
+ // bit 4 (modify, 0x08) AND bit 6 (annotations / forms,
632
+ // 0x20) — the bits `crypto/permissions.js:60-63` decodes
633
+ // as `modify` and `annot`. The owner password lifts them.
634
+ const P = typed.P | 0;
635
+ if (!((P & 0x20) && (P & 0x08))) {
636
+ throw new ContractError('pdf/sign/encrypted-permission-denied',
637
+ 'the user password does not grant the permissions a '
638
+ + 'signature field needs (modify + annotations/forms); '
639
+ + 'pass the owner password',
640
+ { context: { P, required: ['modify', 'annot'] } });
641
+ }
642
+ }
643
+ const randomBytes = opts.randomBytes
644
+ || (globalThis.crypto && typeof globalThis.crypto.getRandomValues === 'function'
645
+ ? (n) => globalThis.crypto.getRandomValues(new Uint8Array(n))
646
+ : null);
647
+ if (!randomBytes) {
648
+ throw new EncryptionError('pdf/sign/no-random',
649
+ 'no random source for the string IV: pass opts.randomBytes '
650
+ + '(crypto.getRandomValues is unavailable)');
651
+ }
652
+ return {
653
+ typedForPw,
654
+ handler: sel.handler,
655
+ strMethod: sel.strMethod,
656
+ stmMethod: sel.method,
657
+ fek: r.fileEncryptionKey,
658
+ randomBytes
659
+ };
660
+ }
661
+
662
+ // ── Signature field ──────────────────────────────────────────
663
+ // Annotation flags Print (bit 3, 4) + Locked (bit 8, 128) —
664
+ // ISO 32000-2 Table 167.
665
+ const WIDGET_FLAGS = 132;
666
+ // SignaturesExist (bit 1) + AppendOnly (bit 2) — Table 225.
667
+ const SIG_FLAGS = 3;
668
+ const FIELD_NAME_PREFIX = 'Signature';
669
+ // Page-tree walk bound (same order as pdfPages' own depth guard).
670
+ const MAX_PAGE_TREE_DEPTH = 64;
671
+
672
+ const _ref = (num, gen) => ({ type: 'ref', num, gen: gen | 0 });
673
+ const _name = (value) => ({ type: 'name', value });
674
+
675
+ /** Decode a PDF text string (UTF-16BE with BOM, else 8-bit). */
676
+ function _textOf(str) {
677
+ if (!str || str.type !== 'string' || !(str.value instanceof Uint8Array)) return null;
678
+ const b = str.value;
679
+ if (b.length >= 2 && b[0] === 0xFE && b[1] === 0xFF) {
680
+ let s = '';
681
+ for (let i = 2; i + 1 < b.length; i += 2) {
682
+ s += String.fromCharCode((b[i] << 8) | b[i + 1]);
683
+ }
684
+ return s;
685
+ }
686
+ return _bytesToString(b);
687
+ }
688
+
689
+ /**
690
+ * Page 1's reference: depth-first, first leaf of the page tree
691
+ * rooted at the Catalog's `/Pages` (the order `pdfPages` walks).
692
+ * `null` when the tree holds no page.
693
+ */
694
+ function _firstPageRef(catalog, resolve) {
695
+ const seen = new Set();
696
+ function walk(ref, depth) {
697
+ if (!ref || ref.type !== 'ref' || depth > MAX_PAGE_TREE_DEPTH) return null;
698
+ const key = ref.num + ':' + ref.gen;
699
+ if (seen.has(key)) return null;
700
+ seen.add(key);
701
+ const node = resolve(ref);
702
+ if (!node || node.type !== 'dict') return null;
703
+ const t = node.entries.Type;
704
+ const kids = node.entries.Kids;
705
+ if ((t && t.type === 'name' && t.value === 'Page')
706
+ || (!kids && !t)) {
707
+ return _ref(ref.num, ref.gen);
708
+ }
709
+ const items = kids && kids.type === 'array' ? kids.items : [];
710
+ for (const k of items) {
711
+ const leaf = walk(k, depth + 1);
712
+ if (leaf) return leaf;
713
+ }
714
+ return null;
715
+ }
716
+ return walk(catalog.entries.Pages, 0);
717
+ }
718
+
719
+ /**
720
+ * Append `itemRef` to the array held by `holder.entries[key]`.
721
+ *
722
+ * - direct array → a copy with the item appended is written back
723
+ * into the holder (the caller re-emits the holder);
724
+ * - indirect array → that array object is re-emitted in the update
725
+ * with the item appended (`updates`), the holder keeps its ref;
726
+ * - absent, or anything that is not an array → `[itemRef]`.
727
+ *
728
+ * Returns the existing items (resolved array contents).
729
+ */
730
+ function _appendToArrayEntry(holderEntries, key, itemRef, resolve, updates) {
731
+ const cur = holderEntries[key];
732
+ if (cur && cur.type === 'ref') {
733
+ const arr = resolve(cur);
734
+ if (arr && arr.type === 'array') {
735
+ updates.set(cur.num, { num: cur.num, gen: cur.gen | 0,
736
+ value: { type: 'array', items: arr.items.concat([itemRef]) } });
737
+ return arr.items;
738
+ }
739
+ } else if (cur && cur.type === 'array') {
740
+ holderEntries[key] = { type: 'array', items: cur.items.concat([itemRef]) };
741
+ return cur.items;
742
+ }
743
+ holderEntries[key] = { type: 'array', items: [itemRef] };
744
+ return [];
745
+ }
746
+
747
+ // ── ISO/TS 32002 declaration (Ed25519) ───────────────────────
748
+ /**
749
+ * The ISO/TS 32002 developer extensions dictionary. ISO/TS
750
+ * 32002:2022 §4: "PDF documents using enhancements described in this
751
+ * document shall include in their document catalogue dictionary
752
+ * (see ISO 32000-2:2020, 7.7.2) an extensions dictionary (see ISO
753
+ * 32000-2:2020, 7.12) with a prefix name of ISO_", holding a
754
+ * developer extensions dictionary (ISO 32000-2 §7.12.3) with the
755
+ * values of its Table 1. EdDSA signatures are one of those
756
+ * enhancements (§5.1). A fresh object on every call.
757
+ */
758
+ function _iso32002Extension() {
759
+ return { type: 'dict', entries: {
760
+ Type: { type: 'name', value: 'DeveloperExtensions' },
761
+ BaseVersion: { type: 'name', value: '2.0' },
762
+ ExtensionLevel: { type: 'int', value: 32002 },
763
+ ExtensionRevision: { type: 'string',
764
+ value: Uint8Array.from(':2022', (c) => c.charCodeAt(0)) },
765
+ URL: { type: 'string',
766
+ value: Uint8Array.from('https://www.iso.org/standard/45875.html',
767
+ (c) => c.charCodeAt(0)) }
768
+ } };
769
+ }
770
+
771
+ /** `node` is a developer extensions dictionary at level 32002. */
772
+ function _isIso32002(node) {
773
+ const level = node.entries.ExtensionLevel;
774
+ return !!level && level.type === 'int' && level.value === 32002;
775
+ }
776
+
777
+ /**
778
+ * A copy of `value` — read as a DIRECT value of indirect object
779
+ * `from` — re-keyed for being written directly inside object `to`:
780
+ * with the V=4 handler a string's key depends on the number and
781
+ * generation of the object that holds it (ISO 32000-2 §7.6.3.3), so
782
+ * each string is decrypted under `from` and encrypted again under
783
+ * `to` with a fresh IV (string crypt filter). A clear base, an
784
+ * `Identity` string method or `from === to` returns `value` as is;
785
+ * a string that does not decrypt under `from` is kept unchanged.
786
+ */
787
+ function _rekeyStrings(value, from, to, encCtx) {
788
+ if (!encCtx || encCtx.strMethod === 'Identity' || !from
789
+ || (from.num === to.num && (from.gen | 0) === (to.gen | 0))) {
790
+ return value;
791
+ }
792
+ const { handler, typedForPw, fek, randomBytes } = encCtx;
793
+ function walk(v) {
794
+ if (!v || typeof v !== 'object') return v;
795
+ switch (v.type) {
796
+ case 'string': {
797
+ if (!(v.value instanceof Uint8Array)) return v;
798
+ let plain;
799
+ try {
800
+ plain = handler.decryptString(typedForPw, fek,
801
+ from.num, from.gen | 0, v.value);
802
+ } catch (_) {
803
+ return v;
804
+ }
805
+ return { type: 'string', syntax: 'hex',
806
+ value: handler.encryptString(typedForPw, fek,
807
+ to.num, to.gen | 0, plain, randomBytes(16)) };
808
+ }
809
+ case 'array':
810
+ return { type: 'array', items: v.items.map(walk) };
811
+ case 'dict': {
812
+ const entries = {};
813
+ for (const k of Object.keys(v.entries)) entries[k] = walk(v.entries[k]);
814
+ return { type: 'dict', entries };
815
+ }
816
+ default:
817
+ return v;
818
+ }
819
+ }
820
+ return walk(value);
821
+ }
822
+
823
+ /**
824
+ * Declare ISO/TS 32002 in the Catalog entries `catalogEntries` (a
825
+ * copy the caller re-emits): add `ISO_` → the developer extensions
826
+ * dictionary of `_iso32002Extension` to `/Extensions`, merged with
827
+ * what is there (ISO 32000-2 §7.12.2: the value of a prefix entry
828
+ * is a developer extensions dictionary or an array of them):
829
+ *
830
+ * - no `/Extensions` (absent or null) → `<< /ISO_ ext >>`;
831
+ * - `/Extensions` without `ISO_` → `ISO_` added, other prefixes kept;
832
+ * - `ISO_` a dictionary at `/ExtensionLevel 32002`, or an array
833
+ * holding one → unchanged (re-signing declares nothing twice);
834
+ * - `ISO_` a dictionary at another level → `[existing ext]`;
835
+ * - `ISO_` an array without level 32002 → `ext` appended.
836
+ *
837
+ * An indirect `/Extensions`, `ISO_` or array element is resolved and
838
+ * the merged result is written DIRECT in the Catalog (the referenced
839
+ * objects stay in place, no longer referenced by it). Encrypted base
840
+ * (`encCtx`): the new dictionary's strings are encrypted under the
841
+ * Catalog's number and generation (`catalogRef`, through
842
+ * `_encryptNewObject`), the strings of an inlined indirect value are
843
+ * re-keyed to it (`_rekeyStrings`); entries of the Catalog's own
844
+ * keep their ciphertext. Any other shape throws
845
+ * `pdf/sign/bad-extensions` (`ContractError`, `context.shape` = the
846
+ * offending value's type) before anything is written.
847
+ *
848
+ * @returns {boolean} whether `catalogEntries` changed.
849
+ */
850
+ function _mergeExtensions(catalogEntries, resolve, encCtx, catalogRef) {
851
+ const fresh = () => (encCtx
852
+ ? _encryptNewObject(_iso32002Extension(), catalogRef.num,
853
+ catalogRef.gen | 0, encCtx)
854
+ : _iso32002Extension());
855
+ // `node` with a reference followed; `holder` is the indirect
856
+ // object its direct content is keyed by (null: the Catalog).
857
+ const deref = (node, holder) => (node && node.type === 'ref'
858
+ ? { value: resolve(node), holder: node }
859
+ : { value: node, holder });
860
+ const isAbsent = (v) => v === null || v === undefined || v.type === 'null';
861
+ const direct = (d) => _rekeyStrings(d.value, d.holder, catalogRef, encCtx);
862
+ const refuse = (v) => new ContractError('pdf/sign/bad-extensions',
863
+ 'the Catalog /Extensions is not an extensions dictionary whose '
864
+ + 'ISO_ entry is a developer extensions dictionary or an array '
865
+ + 'of them (ISO 32000-2 section 7.12)',
866
+ { context: { shape: isAbsent(v) ? 'null' : v.type } });
867
+
868
+ const top = deref(catalogEntries.Extensions, null);
869
+ if (isAbsent(top.value)) {
870
+ catalogEntries.Extensions = { type: 'dict', entries: { ISO_: fresh() } };
871
+ return true;
872
+ }
873
+ if (top.value.type !== 'dict') throw refuse(top.value);
874
+ const iso = deref(top.value.entries.ISO_, top.holder);
875
+ let isoOut;
876
+ if (isAbsent(iso.value)) {
877
+ isoOut = fresh();
878
+ } else if (iso.value.type === 'dict') {
879
+ if (_isIso32002(iso.value)) return false;
880
+ isoOut = { type: 'array', items: [direct(iso), fresh()] };
881
+ } else if (iso.value.type === 'array') {
882
+ const items = [];
883
+ let declared = false;
884
+ for (const item of iso.value.items) {
885
+ const el = deref(item, iso.holder);
886
+ if (isAbsent(el.value) || el.value.type !== 'dict') throw refuse(el.value);
887
+ if (_isIso32002(el.value)) declared = true;
888
+ items.push(direct(el));
889
+ }
890
+ if (declared) return false;
891
+ items.push(fresh());
892
+ isoOut = { type: 'array', items };
893
+ } else {
894
+ throw refuse(iso.value);
895
+ }
896
+ const entries = {};
897
+ for (const k of Object.keys(top.value.entries)) {
898
+ if (k !== 'ISO_') {
899
+ entries[k] = direct({ value: top.value.entries[k], holder: top.holder });
900
+ }
901
+ }
902
+ entries.ISO_ = isoOut;
903
+ catalogEntries.Extensions = { type: 'dict', entries };
904
+ return true;
905
+ }
906
+
907
+ /**
908
+ * Set `/Version /2.0` in `catalogEntries` when the document's
909
+ * effective version is below 2.0: the Catalog `/Version` name when
910
+ * present (ISO 32000-2 §7.7.2 — the entry exists so an incremental
911
+ * update can raise the version), else the header version
912
+ * `headerVersion`. A `/Version` that is not a name counts as absent.
913
+ * ISO/TS 32002 Table 2 marks EdDSA as PDF 2.x.
914
+ *
915
+ * @returns {boolean} whether `catalogEntries` changed.
916
+ */
917
+ function _raiseVersionTo20(catalogEntries, resolve, headerVersion) {
918
+ const parse = (s) => {
919
+ const m = typeof s === 'string' ? /^\s*(\d+)\.(\d+)/.exec(s) : null;
920
+ return m ? [parseInt(m[1], 10), parseInt(m[2], 10)] : null;
921
+ };
922
+ let entry = catalogEntries.Version;
923
+ if (entry && entry.type === 'ref') entry = resolve(entry);
924
+ const v = (entry && entry.type === 'name' ? parse(entry.value) : null)
925
+ || parse(headerVersion);
926
+ if (v && v[0] >= 2) return false;
927
+ catalogEntries.Version = { type: 'name', value: '2.0' };
928
+ return true;
929
+ }
930
+
931
+ /**
932
+ * The objects that register signature object `sigObjNum` in a
933
+ * signature field, all for the SAME incremental update as the
934
+ * signature dictionary (ISO 32000-2 §12.7.5.5, §12.7.3, §12.5.6.19):
935
+ *
936
+ * - the field dictionary merged with its widget annotation,
937
+ * object `fieldNum` (`/FT /Sig`, `/T (Signature<n>)` — `n` the
938
+ * lowest index no root field already uses — `/V`, `/Subtype
939
+ * /Widget`, `/Rect [0 0 0 0]` (invisible), `/F 132`, `/P`);
940
+ * - page 1 re-emitted with `/Annots` extended (or, when its
941
+ * `/Annots` is an indirect array, that array re-emitted);
942
+ * - the `/AcroForm` extended with `/Fields` + `/SigFlags 3`: an
943
+ * indirect AcroForm is re-emitted under its own number, a direct
944
+ * or absent one through a Catalog re-emission (an indirect
945
+ * `/Fields` array is re-emitted under its own number);
946
+ * - Ed25519 (`signOpts.algorithm`): the Catalog declares the ISO/TS
947
+ * 32002 developer extension (`_mergeExtensions`) and, on a
948
+ * document below PDF 2.0, `/Version /2.0` (`_raiseVersionTo20`);
949
+ * the Catalog is re-emitted whenever that changes it, an
950
+ * indirect AcroForm included. Other algorithms leave it as is.
951
+ *
952
+ * Every existing entry is copied verbatim. Returns
953
+ * `{ updates, fieldNum, fieldName }`.
954
+ *
955
+ * Encrypted base (`encCtx`, from `_openEncrypted`; derived here from
956
+ * `signOpts` when not supplied): existing root field names are
957
+ * decrypted before the uniqueness scan, and the new `/T` text string
958
+ * is encrypted with the document key (string crypt filter, object
959
+ * `fieldNum` generation 0, a fresh 16-byte IV) — in clear only when
960
+ * the string method is `Identity`. Re-emitted objects keep their
961
+ * number and generation, so their copied (encrypted) strings keep
962
+ * their key. The update adds no other string: the signature
963
+ * dictionary's `/Contents` is excluded from encryption (ISO 32000-2
964
+ * §7.6.2) and stays the hex placeholder, `/ByteRange` holds
965
+ * integers.
966
+ */
967
+ function _buildSignatureField(baseBytes, trailer, sigObjNum, fieldNum,
968
+ signOpts, encCtx) {
969
+ if (!(documentMod && typeof documentMod.readDocument === 'function')) {
970
+ throw new ContractError('pdf/sign/no-document-reader',
971
+ 'signing requires the pdfDocument dep (the Catalog and '
972
+ + 'page 1 are resolved through readDocument)');
973
+ }
974
+ const enc = trailer.encrypt
975
+ ? (encCtx || _openEncrypted(baseBytes, trailer, signOpts))
976
+ : null;
977
+ const doc = documentMod.readDocument(baseBytes,
978
+ enc ? { allowEncrypted: true } : undefined);
979
+ const resolve = doc._raw.resolve;
980
+ const rootRef = doc.trailer.root;
981
+ const catalog = resolve(_ref(rootRef.num, rootRef.gen));
982
+ if (!catalog || catalog.type !== 'dict') {
983
+ throw new ContractError('pdf/sign/catalog-not-found',
984
+ `the trailer /Root (object ${rootRef.num} ${rootRef.gen}) `
985
+ + 'does not resolve to a Catalog dictionary',
986
+ { context: { root: rootRef } });
987
+ }
988
+ /** @type {Map<number, {num:number, gen:number, value:Object}>} */
989
+ const updates = new Map();
990
+ const fieldRef = _ref(fieldNum, 0);
991
+
992
+ const catalogEntries = Object.assign({}, catalog.entries);
993
+ // ── Ed25519: the Catalog declares ISO/TS 32002 (and PDF 2.0).
994
+ let catalogChanged = false;
995
+ if (signOpts && signOpts.algorithm === 'ed25519') {
996
+ const catalogRef = { num: rootRef.num, gen: rootRef.gen | 0 };
997
+ catalogChanged = _mergeExtensions(catalogEntries, resolve, enc, catalogRef);
998
+ if (_raiseVersionTo20(catalogEntries, resolve, doc.version)) {
999
+ catalogChanged = true;
1000
+ }
1001
+ }
1002
+
1003
+ // ── AcroForm: indirect → re-emit it; direct / absent → re-emit
1004
+ // the Catalog carrying the extended dict.
1005
+ const acroEntry = catalogEntries.AcroForm;
1006
+ let acroIndirect = null;
1007
+ let acroSrc = null;
1008
+ if (acroEntry && acroEntry.type === 'ref') {
1009
+ const r = resolve(acroEntry);
1010
+ if (r && r.type === 'dict') { acroIndirect = acroEntry; acroSrc = r; }
1011
+ } else if (acroEntry && acroEntry.type === 'dict') {
1012
+ acroSrc = acroEntry;
1013
+ }
1014
+ const acroEntries = Object.assign({}, acroSrc ? acroSrc.entries : {});
1015
+ // The indirect object holding the /Fields array — the one whose
1016
+ // number keys a DIRECT field dictionary's strings: the indirect
1017
+ // /Fields array, else the indirect AcroForm, else the Catalog.
1018
+ const fieldsEntry = acroEntries.Fields;
1019
+ const fieldsHolder = fieldsEntry && fieldsEntry.type === 'ref'
1020
+ && (resolve(fieldsEntry) || {}).type === 'array'
1021
+ ? fieldsEntry
1022
+ : (acroIndirect || rootRef);
1023
+ const rootFields = _appendToArrayEntry(acroEntries, 'Fields', fieldRef,
1024
+ resolve, updates);
1025
+ acroEntries.SigFlags = { type: 'int', value: SIG_FLAGS };
1026
+ const acroDict = { type: 'dict', entries: acroEntries };
1027
+ if (acroIndirect) {
1028
+ updates.set(acroIndirect.num, { num: acroIndirect.num,
1029
+ gen: acroIndirect.gen | 0, value: acroDict });
1030
+ if (catalogChanged) {
1031
+ updates.set(rootRef.num, { num: rootRef.num, gen: rootRef.gen | 0,
1032
+ value: { type: 'dict', entries: catalogEntries } });
1033
+ }
1034
+ } else {
1035
+ catalogEntries.AcroForm = acroDict;
1036
+ updates.set(rootRef.num, { num: rootRef.num, gen: rootRef.gen | 0,
1037
+ value: { type: 'dict', entries: catalogEntries } });
1038
+ }
1039
+
1040
+ // ── Unique partial name among the root fields.
1041
+ const taken = new Set();
1042
+ for (const it of rootFields) {
1043
+ const f = it && it.type === 'ref' ? resolve(it) : it;
1044
+ let tStr = f && f.type === 'dict' ? f.entries.T : null;
1045
+ if (enc && enc.strMethod !== 'Identity' && tStr
1046
+ && tStr.type === 'string' && tStr.value instanceof Uint8Array) {
1047
+ const holder = it.type === 'ref' ? it : fieldsHolder;
1048
+ try {
1049
+ tStr = { type: 'string', value: enc.handler.decryptString(
1050
+ enc.typedForPw, enc.fek, holder.num, holder.gen | 0,
1051
+ tStr.value) };
1052
+ } catch (_) {
1053
+ // Not a string under this key: it names no field
1054
+ // the new one could collide with.
1055
+ tStr = null;
1056
+ }
1057
+ }
1058
+ const t = _textOf(tStr);
1059
+ if (t !== null) taken.add(t);
1060
+ }
1061
+ let n = 1;
1062
+ while (taken.has(FIELD_NAME_PREFIX + n)) n++;
1063
+ const fieldName = FIELD_NAME_PREFIX + n;
1064
+
1065
+ // ── Page 1: /Annots extended.
1066
+ const pageRef = _firstPageRef(catalog, resolve);
1067
+ if (pageRef) {
1068
+ const page = resolve(pageRef);
1069
+ const pageEntries = Object.assign({}, page.entries);
1070
+ const annotsWasIndirect = pageEntries.Annots
1071
+ && pageEntries.Annots.type === 'ref'
1072
+ && (resolve(pageEntries.Annots) || {}).type === 'array';
1073
+ _appendToArrayEntry(pageEntries, 'Annots', fieldRef, resolve, updates);
1074
+ if (!annotsWasIndirect) {
1075
+ updates.set(pageRef.num, { num: pageRef.num, gen: pageRef.gen,
1076
+ value: { type: 'dict', entries: pageEntries } });
1077
+ }
1078
+ }
1079
+
1080
+ // ── The field dictionary merged with its widget annotation.
1081
+ const plainName = new TextEncoder().encode(fieldName);
1082
+ let tValue = { type: 'string', value: plainName };
1083
+ if (enc && enc.strMethod !== 'Identity') {
1084
+ const iv = enc.randomBytes(16);
1085
+ tValue = { type: 'string', syntax: 'hex',
1086
+ value: enc.handler.encryptString(enc.typedForPw, enc.fek,
1087
+ fieldNum, 0, plainName, iv) };
1088
+ }
1089
+ const widget = { type: 'dict', entries: {
1090
+ Type: _name('Annot'),
1091
+ Subtype: _name('Widget'),
1092
+ FT: _name('Sig'),
1093
+ T: tValue,
1094
+ V: _ref(sigObjNum, 0),
1095
+ Rect: { type: 'array', items: [0, 0, 0, 0].map(
1096
+ (v) => ({ type: 'int', value: v })) },
1097
+ F: { type: 'int', value: WIDGET_FLAGS }
1098
+ } };
1099
+ if (pageRef) widget.entries.P = pageRef;
1100
+ updates.set(fieldNum, { num: fieldNum, gen: 0, value: widget });
1101
+
1102
+ return { updates: Array.from(updates.values()), fieldNum, fieldName };
1103
+ }
1104
+
1105
+ /**
1106
+ * Take an existing PDF (`baseBytes`) and append a signature
1107
+ * dictionary as a new indirect object through
1108
+ * `pdfIncrementalWriter` — so the update section takes the form of
1109
+ * the base's newest section (classical table or cross-reference
1110
+ * stream) and carries `/Root`, `/Info`, `/ID` from the
1111
+ * merged trailer. The SAME update carries the signature field that
1112
+ * holds it (`_buildSignatureField`). The signature dict has the
1113
+ * fixed-width placeholders `/Contents <00…00>` and `/ByteRange
1114
+ * [2147483647 …]`; both are located inside the signature object's
1115
+ * own byte span (from its offset to the next object's) and patched
1116
+ * in place — the byte length never changes, so every recorded
1117
+ * cross-reference offset stays valid, and the field objects are
1118
+ * covered by the `/ByteRange` like every other byte.
1119
+ *
1120
+ * Over an encrypted base the update's cross-reference section
1121
+ * repeats the base's `/Encrypt` (ISO 32000-2 §7.5.6) and the field's
1122
+ * `/T` is encrypted (`_buildSignatureField`). `encCtx` is the
1123
+ * context `sign()` derived once; when it is `undefined` and the base
1124
+ * is encrypted, it is derived here from `signOpts`
1125
+ * (`_openEncrypted`).
1126
+ *
1127
+ * Returns `{ bytes, contentsOffset, contentsLength, byteRange,
1128
+ * sigObjNum, fieldObjNum, fieldName, encrypted }` — `contentsOffset`
1129
+ * / `contentsLength` are the hex-digit span (where the blob is
1130
+ * patched in); `byteRange`'s gap is that span plus its `<` and `>`;
1131
+ * `encrypted` tells whether the base carries `/Encrypt`.
1132
+ */
1133
+ function _emitWithPlaceholder(baseBytes, sigPayloadLen,
1134
+ subFilter, isDocTimeStamp,
1135
+ signOpts, encCtx) {
1136
+ subFilter = subFilter || 'adbe.pkcs7.detached';
1137
+ if (!incrementalWriterMod
1138
+ || typeof incrementalWriterMod.appendIncrementalWithOffsets !== 'function'
1139
+ || typeof incrementalWriterMod.readBaseTrailer !== 'function') {
1140
+ throw new ContractError('pdf/sign/no-incremental-writer',
1141
+ 'signing requires the pdfIncrementalWriter dep');
1142
+ }
1143
+ const trailer = _readBaseTrailer(baseBytes);
1144
+ const sigObjNum = _firstFreeObjNum(trailer);
1145
+ if (trailer.encrypt && encCtx === undefined) {
1146
+ encCtx = _openEncrypted(baseBytes, trailer, signOpts);
1147
+ }
1148
+ const field = _buildSignatureField(baseBytes, trailer,
1149
+ sigObjNum, sigObjNum + 1, signOpts, encCtx);
1150
+
1151
+ const typeName = isDocTimeStamp ? 'DocTimeStamp' : 'Sig';
1152
+ const name = (value) => ({ type: 'name', value });
1153
+ const sigDict = { type: 'dict', entries: {
1154
+ Type: name(typeName),
1155
+ Filter: name('Adobe.PPKLite'),
1156
+ SubFilter: name(subFilter),
1157
+ ByteRange: { type: 'array', items: [0, 1, 2, 3].map(
1158
+ () => ({ type: 'int', value: BR_PLACEHOLDER_INT })) },
1159
+ // sigPayloadLen is in BYTES — the hex literal is 2x.
1160
+ Contents: { type: 'string', syntax: 'hex',
1161
+ value: new Uint8Array(sigPayloadLen) }
1162
+ } };
1163
+
1164
+ const updates = [{ num: sigObjNum, gen: 0, value: sigDict }];
1165
+ updates.push(...field.updates);
1166
+ const emitted = incrementalWriterMod.appendIncrementalWithOffsets(
1167
+ baseBytes, {
1168
+ updates,
1169
+ root: trailer.root,
1170
+ info: trailer.info,
1171
+ id: trailer.id,
1172
+ encrypt: trailer.encrypt
1173
+ });
1174
+ const out = emitted.bytes;
1175
+ const sigObjStart = emitted.offsets.get(sigObjNum);
1176
+ // The signature object spans from its own offset to the next
1177
+ // appended object's (or, when it is the last one, to the
1178
+ // start of the update's xref section).
1179
+ let sigObjEnd = emitted.xrefOffset;
1180
+ for (const off of emitted.offsets.values()) {
1181
+ if (off > sigObjStart && off < sigObjEnd) sigObjEnd = off;
1182
+ }
1183
+ const sigObjStr = _bytesToString(out.subarray(sigObjStart, sigObjEnd));
1184
+ const brKey = '/ByteRange ' + BR_PLACEHOLDER;
1185
+ const contentsKey = '/Contents <';
1186
+ const brLocal = sigObjStr.indexOf(brKey);
1187
+ const contentsLocal = sigObjStr.indexOf(contentsKey);
1188
+ const contentsLength = sigPayloadLen * 2;
1189
+ if (!sigObjStr.startsWith(`${sigObjNum} 0 obj`)
1190
+ || brLocal < 0 || contentsLocal < 0
1191
+ || sigObjStr[contentsLocal + contentsKey.length + contentsLength] !== '>') {
1192
+ throw new ContractError('pdf/sign/br-placeholder-missing',
1193
+ 'ByteRange / Contents placeholder not found in the '
1194
+ + 'signature object', { context: { sigObjNum, sigObjStart } });
1195
+ }
1196
+ const contentsOffset = sigObjStart + contentsLocal + contentsKey.length;
1197
+
1198
+ // ByteRange excludes the whole `<…>` token of /Contents, its
1199
+ // delimiters included (ISO 32000-2 §12.8.3.3.1: the string
1200
+ // "shall fit precisely in the space between the ranges" —
1201
+ // form b). `contentsOffset` stays the first hex digit:
1202
+ // it is where the PKCS#7 hex is patched in.
1203
+ const br = computeByteRange(out, contentsOffset, contentsLength, {
1204
+ token: { offset: contentsOffset - 1, length: contentsLength + 2 }
1205
+ });
1206
+
1207
+ // Patch the /ByteRange placeholder with the real values,
1208
+ // space-padded inside the brackets to the same width.
1209
+ const realBr = `[${br[0]} ${br[1]} ${br[2]} ${br[3]}]`;
1210
+ let padded;
1211
+ if (realBr.length < BR_PLACEHOLDER.length) {
1212
+ padded = realBr.slice(0, realBr.length - 1)
1213
+ + ' '.repeat(BR_PLACEHOLDER.length - realBr.length) + ']';
1214
+ } else if (realBr.length === BR_PLACEHOLDER.length) {
1215
+ padded = realBr;
1216
+ } else {
1217
+ throw new ContractError('pdf/sign/br-overflow',
1218
+ 'real ByteRange does not fit in 10-digit placeholder');
1219
+ }
1220
+ out.set(new TextEncoder().encode(padded),
1221
+ sigObjStart + brLocal + '/ByteRange '.length);
1222
+ return {
1223
+ bytes: out,
1224
+ contentsOffset, contentsLength,
1225
+ byteRange: br,
1226
+ sigObjNum,
1227
+ fieldObjNum: field.fieldNum,
1228
+ fieldName: field.fieldName,
1229
+ encrypted: !!trailer.encrypt
1230
+ };
1231
+ }
1232
+
1233
+ function _bytesToString(bytes) {
1234
+ // Latin-1 / 8-bit clean string view of bytes — fine since the
1235
+ // PDF body around our markers is ASCII.
1236
+ let s = '';
1237
+ const CHUNK = 0x8000;
1238
+ for (let i = 0; i < bytes.length; i += CHUNK) {
1239
+ s += String.fromCharCode.apply(null,
1240
+ bytes.subarray(i, Math.min(i + CHUNK, bytes.length)));
1241
+ }
1242
+ return s;
1243
+ }
1244
+
1245
+ // ── Crypto dispatch ──────────────────────────────────────────
1246
+ function _signDigest({ algorithm, privateKey, message, hashMod }) {
1247
+ if (algorithm === 'rsa-pss') {
1248
+ if (!rsa || typeof rsa.pssSign !== 'function') {
1249
+ throw new EncryptionError('pdf/sign/no-rsa',
1250
+ 'fw rsa.pssSign unavailable');
1251
+ }
1252
+ if (!privateKey || !privateKey.n || !privateKey.e
1253
+ || !privateKey.d) {
1254
+ throw new ContractError('pdf/sign/bad-rsa-key',
1255
+ 'RSA privateKey requires { n, e, d }');
1256
+ }
1257
+ const sig = rsa.pssSign(privateKey, message, hashMod);
1258
+ if (!sig) {
1259
+ throw new EncryptionError('pdf/sign/rsa-failed',
1260
+ 'rsa.pssSign returned false');
1261
+ }
1262
+ return sig;
1263
+ }
1264
+ if (algorithm === 'ecdsa') {
1265
+ if (!ecc || !ecc.ecdsa) {
1266
+ throw new EncryptionError('pdf/sign/no-ecc',
1267
+ 'fw ecc.ecdsa unavailable');
1268
+ }
1269
+ if (!privateKey || !privateKey.curve
1270
+ || !privateKey.secretKey) {
1271
+ throw new ContractError('pdf/sign/bad-ecdsa-key',
1272
+ 'ECDSA privateKey requires '
1273
+ + '{ curve, secretKey: <ecc.ecdsa.secretKey> }');
1274
+ }
1275
+ // fw hashMod.hash expects a bitArray and returns a bitArray.
1276
+ const digestBits = hashMod.hash(bitArray.ui8_to_ba(message));
1277
+ // RFC 3279 §2.2.3 ECDSA-Sig-Value — CMS carries the DER
1278
+ // SEQUENCE, not the fixed-width r||s the fw primitive
1279
+ // returns.
1280
+ const rsBytes = bitArray.ba_to_ui8(privateKey.secretKey.sign(digestBits));
1281
+ const half = rsBytes.length >>> 1;
1282
+ return asn1.encodeSequence([
1283
+ asn1.encodeInteger(rsBytes.subarray(0, half)),
1284
+ asn1.encodeInteger(rsBytes.subarray(half))
1285
+ ]);
1286
+ }
1287
+ if (algorithm === 'ed25519') {
1288
+ if (!ed25519 || typeof ed25519.sign !== 'function') {
1289
+ throw new EncryptionError('pdf/sign/no-ed25519',
1290
+ 'fw ed25519.sign unavailable');
1291
+ }
1292
+ if (!(privateKey instanceof Uint8Array)
1293
+ || privateKey.length !== 64) {
1294
+ throw new ContractError('pdf/sign/bad-ed25519-key',
1295
+ 'Ed25519 privateKey must be 64 bytes (seed || pub)');
1296
+ }
1297
+ const sig = ed25519.sign(privateKey, message);
1298
+ if (!sig) {
1299
+ throw new EncryptionError('pdf/sign/ed25519-failed',
1300
+ 'ed25519.sign returned false');
1301
+ }
1302
+ return sig;
1303
+ }
1304
+ throw new ContractError('pdf/sign/unknown-algorithm',
1305
+ 'unsupported algorithm: ' + algorithm);
1306
+ }
1307
+
1308
+ // ── signedAttrs / ESS / TSA helpers ──────────────────────────
1309
+ /**
1310
+ * Build a signedAttrs SET (DER TLV) containing the mandatory
1311
+ * attributes per PAdES baseline B :
1312
+ * - id-contentType = id-data
1313
+ * - id-messageDigest = digest(ByteRange)
1314
+ * - id-signingTime = current UTCTime
1315
+ * - id-aa-signingCertV2 = ESSCertIDv2 (cert hash, SHA-256)
1316
+ *
1317
+ * `certDigestSha256` is the SHA-256 of the signer cert DER. The
1318
+ * caller passes it explicitly to keep this helper hash-impl-agnostic.
1319
+ *
1320
+ * The attributes are written in DER `SET OF` order
1321
+ * (`_sortDerSetOf`): RFC 5652 §5.4 computes the signature over "the
1322
+ * DER encoding of the SignedAttributes value", and X.690 §11.6 orders
1323
+ * the components of a DER set-of "in ascending order of their
1324
+ * encodings". A verifier that re-encodes the attributes before it
1325
+ * checks the signature therefore checks these exact bytes.
1326
+ *
1327
+ * @returns {Uint8Array} The DER-encoded SET (tag 0x31). Callers
1328
+ * that want the [0] IMPLICIT form for embedding in SignerInfo
1329
+ * should re-tag the first byte to 0xA0 (see `_buildPkcs7`).
1330
+ */
1331
+ function _buildSignedAttrs({ messageDigest, certDigestSha256,
1332
+ signingTime }) {
1333
+ // Attribute ::= SEQUENCE { OID, SET OF AttributeValue }
1334
+ const attrs = [];
1335
+ // contentType = id-data
1336
+ attrs.push(asn1.encodeSequence([
1337
+ asn1.encodeOid(OID_AA_CONTENT_TYPE),
1338
+ asn1.encodeSet([asn1.encodeOid(OID_CONTENT_TYPE_DATA)])
1339
+ ]));
1340
+ // messageDigest
1341
+ attrs.push(asn1.encodeSequence([
1342
+ asn1.encodeOid(OID_AA_MESSAGE_DIGEST),
1343
+ asn1.encodeSet([asn1.encodeOctetString(messageDigest)])
1344
+ ]));
1345
+ // signingTime (UTCTime)
1346
+ const t = signingTime instanceof Date ? signingTime : new Date();
1347
+ const utc = _encodeUtcTime(t);
1348
+ attrs.push(asn1.encodeSequence([
1349
+ asn1.encodeOid(OID_AA_SIGNING_TIME),
1350
+ asn1.encodeSet([utc])
1351
+ ]));
1352
+ // signing-certificate-v2 (RFC 5035)
1353
+ // SigningCertificateV2 ::= SEQUENCE {
1354
+ // certs SEQUENCE OF ESSCertIDv2 }
1355
+ // ESSCertIDv2 ::= SEQUENCE {
1356
+ // hashAlgorithm AlgorithmIdentifier DEFAULT sha256,
1357
+ // certHash OCTET STRING }
1358
+ // We omit hashAlgorithm (SHA-256 is the default).
1359
+ const essCertIdV2 = asn1.encodeSequence([
1360
+ asn1.encodeOctetString(certDigestSha256)
1361
+ ]);
1362
+ const signingCertV2 = asn1.encodeSequence([
1363
+ asn1.encodeSequence([essCertIdV2])
1364
+ ]);
1365
+ attrs.push(asn1.encodeSequence([
1366
+ asn1.encodeOid(OID_AA_SIGNING_CERT_V2),
1367
+ asn1.encodeSet([signingCertV2])
1368
+ ]));
1369
+ return asn1.encodeSet(_sortDerSetOf(attrs));
1370
+ }
1371
+
1372
+ /**
1373
+ * Sort encoded `SET OF` components into DER order (X.690 §11.6:
1374
+ * "the encodings of the component values of a set-of value shall
1375
+ * appear in ascending order, the encodings being compared as octet
1376
+ * strings with the shorter components being padded at their trailing
1377
+ * end with 0-octets"). Compares unsigned bytes left to right; when one
1378
+ * encoding is a prefix of the other, the shorter one sorts first.
1379
+ * Returns a new array; the input is left as is.
1380
+ *
1381
+ * @param {Uint8Array[]} tlvs Complete component encodings (TLVs).
1382
+ * @returns {Uint8Array[]}
1383
+ */
1384
+ function _sortDerSetOf(tlvs) {
1385
+ return tlvs.slice().sort((a, b) => {
1386
+ const n = Math.min(a.length, b.length);
1387
+ for (let i = 0; i < n; i++) {
1388
+ if (a[i] !== b[i]) return a[i] - b[i];
1389
+ }
1390
+ return a.length - b.length;
1391
+ });
1392
+ }
1393
+
1394
+ function _encodeUtcTime(date) {
1395
+ const yy = String(date.getUTCFullYear() % 100).padStart(2, '0');
1396
+ const mo = String(date.getUTCMonth() + 1).padStart(2, '0');
1397
+ const dd = String(date.getUTCDate()).padStart(2, '0');
1398
+ const hh = String(date.getUTCHours()).padStart(2, '0');
1399
+ const mi = String(date.getUTCMinutes()).padStart(2, '0');
1400
+ const se = String(date.getUTCSeconds()).padStart(2, '0');
1401
+ const s = yy + mo + dd + hh + mi + se + 'Z';
1402
+ const v = new TextEncoder().encode(s);
1403
+ const out = new Uint8Array(2 + v.length);
1404
+ out[0] = 0x17; // UTCTime
1405
+ out[1] = v.length;
1406
+ out.set(v, 2);
1407
+ return out;
1408
+ }
1409
+
1410
+ /**
1411
+ * Build the unsignedAttrs SET (for level T) containing the
1412
+ * RFC 3161 timestamp-token attribute (id-aa-timeStampToken).
1413
+ */
1414
+ function _buildUnsignedAttrsTsa(tstToken) {
1415
+ const attr = asn1.encodeSequence([
1416
+ asn1.encodeOid(OID_AA_TIMESTAMP_TOKEN),
1417
+ asn1.encodeSet([tstToken])
1418
+ ]);
1419
+ return asn1.encodeSet([attr]);
1420
+ }
1421
+
1422
+ // ── Public API ───────────────────────────────────────────────
1423
+ /**
1424
+ * Sign a PDF byte buffer (PAdES baseline B, T, LT or LTA).
1425
+ *
1426
+ * @param {Uint8Array} pdfBytes Existing PDF bytes (e.g. as
1427
+ * produced by `pdf.write(...)` or any external generator).
1428
+ * @param {Object} opts
1429
+ * @param {Uint8Array|string} opts.cert X.509 certificate DER bytes
1430
+ * or PEM string.
1431
+ * @param {Object|Uint8Array} opts.privateKey Algorithm-specific
1432
+ * private key :
1433
+ * - RSA-PSS : `{ n, e, d }` (Uint8Array each).
1434
+ * - ECDSA : `{ curve: ecc.curves.c256|c384|c521,
1435
+ * secretKey: new ecc.ecdsa.secretKey(curve, k) }`.
1436
+ * - Ed25519 : 64-byte `Uint8Array` (seed || pub).
1437
+ * @param {string} opts.algorithm 'rsa-pss' | 'ecdsa' | 'ed25519'.
1438
+ * Algorithm notes: the ECDSA CMS signature value is the DER
1439
+ * `ECDSA-Sig-Value` (SEQUENCE { r INTEGER, s INTEGER }, RFC 3279
1440
+ * §2.2.3); the Ed25519 digest is always SHA-512 (RFC 8419
1441
+ * section 3.1). An Ed25519 signing update also re-emits the
1442
+ * Catalog declaring the ISO/TS 32002 developer extension
1443
+ * (`/Extensions` → `/ISO_` with `/ExtensionLevel 32002`, merged
1444
+ * with any existing entry) and sets `/Version /2.0` when the
1445
+ * document is below PDF 2.0 (ISO/TS 32002 §4, ISO 32000-2
1446
+ * §7.7.2); a malformed `/Extensions` throws
1447
+ * `pdf/sign/bad-extensions`. The later LT and LTA updates copy
1448
+ * the Catalog, declaration included. Adobe Acrobat Reader does
1449
+ * not validate EdDSA signatures; use ECDSA P-256 where Acrobat
1450
+ * interoperability matters.
1451
+ * @param {string} [opts.hashAlg='sha256'] 'sha256' | 'sha384' |
1452
+ * 'sha512' for RSA-PSS and ECDSA (default 'sha256'). Ed25519:
1453
+ * 'sha512' only, which is also its default — any other value
1454
+ * throws `pdf/sign/ed25519-requires-sha512`; an unknown value
1455
+ * throws `pdf/sign/bad-hash-alg`. The resolved value is the
1456
+ * `hashAlg` handed to `opts.tsaSign`.
1457
+ * @param {string} [opts.level='B'] PAdES baseline level:
1458
+ * 'B' | 'T' | 'LT' | 'LTA'. T, LT and LTA require
1459
+ * `opts.tsaSign`, a callback `({ digest, hashAlg })` returning
1460
+ * the RFC 3161 TimeStampToken DER bytes (a `Uint8Array`); LT
1461
+ * also appends the DSS built from `opts.dss`, and LTA then a
1462
+ * DocTimeStamp. Any other value throws
1463
+ * `pdf/sign/level-not-implemented`.
1464
+ * @param {function({digest: Uint8Array, hashAlg: string}): Uint8Array}
1465
+ * [opts.tsaSign] RFC 3161 TimeStampToken callback (required for
1466
+ * T, LT and LTA): `digest` is the `hashAlg` digest of the
1467
+ * signature value (T) or of the DocTimeStamp's covered bytes
1468
+ * (LTA).
1469
+ * @param {boolean} [opts.useSignedAttrs=false] Emit CMS signed
1470
+ * attributes (content-type, message-digest, signing-time, ESS
1471
+ * signing-certificate-v2) at level B; forced `true` for T, LT
1472
+ * and LTA.
1473
+ * @param {number} [opts.placeholderBytes=8192] Size in bytes
1474
+ * reserved for the PKCS#7 blob in the `/Contents` placeholder.
1475
+ * @param {string} [opts.subFilter='adbe.pkcs7.detached'] The
1476
+ * signature dictionary `/SubFilter`; a PAdES signature needs
1477
+ * `'ETSI.CAdES.detached'`.
1478
+ * @param {Date} [opts.signingTime] The signing-time signed
1479
+ * attribute (default: now); read only when signed attributes
1480
+ * are emitted.
1481
+ * @param {boolean} [opts.docTimeStamp] Internal: emit the
1482
+ * placeholder object as a `/DocTimeStamp` instead of a `/Sig`.
1483
+ * @param {{certs?: Array<Uint8Array|string>, ocsps?: Uint8Array[],
1484
+ * crls?: Uint8Array[], vri?: Object, autoVri?: boolean}}
1485
+ * [opts.dss] LT / LTA: the DSS content (certificates as DER or
1486
+ * PEM, OCSP responses, CRLs, VRI entries) appended after the
1487
+ * signature.
1488
+ * @param {number} [opts.docTimeStampPlaceholder] LTA: size in
1489
+ * bytes reserved for the DocTimeStamp token (default:
1490
+ * `opts.placeholderBytes`, else 8192).
1491
+ * @param {string|Uint8Array} [opts.password] Encrypted base: the
1492
+ * owner or user password (`''` is a valid empty user password).
1493
+ * The document key is derived from it through the standard
1494
+ * security handler (V=4 R=4 `AESV2`, V=5 R=5/R=6 `AESV3`); a user
1495
+ * password must grant the modify and annotations/forms
1496
+ * permissions. Without it an encrypted base throws
1497
+ * `pdf/sign/encrypted-password-required`. Ignored on an
1498
+ * unencrypted base.
1499
+ * @param {function(number): Uint8Array} [opts.randomBytes]
1500
+ * Encrypted base: IV source (called with 16) for each encrypted
1501
+ * field name and, at LT / LTA, for each DSS string and stream.
1502
+ * Default `crypto.getRandomValues`; when neither exists
1503
+ * `pdf/sign/no-random` is thrown.
1504
+ * @returns {Uint8Array} the signed PDF bytes. Each signature
1505
+ * dictionary (and the LTA DocTimeStamp) is the `/V` of an
1506
+ * invisible signature field `Signature<n>` written in the same
1507
+ * incremental update (see `_buildSignatureField`).
1508
+ */
1509
+ function sign(pdfBytes, opts) {
1510
+ if (!(pdfBytes instanceof Uint8Array)) {
1511
+ throw new ContractError('pdf/sign/bad-input',
1512
+ 'pdfBytes must be a Uint8Array');
1513
+ }
1514
+ if (!opts || typeof opts !== 'object') {
1515
+ throw new ContractError('pdf/sign/bad-opts',
1516
+ 'opts object required');
1517
+ }
1518
+ const level = opts.level || 'B';
1519
+ if (level !== 'B' && level !== 'T'
1520
+ && level !== 'LT' && level !== 'LTA') {
1521
+ throw new ContractError('pdf/sign/level-not-implemented',
1522
+ 'unknown PAdES level: ' + level,
1523
+ { context: { level } });
1524
+ }
1525
+ if ((level === 'LT' || level === 'LTA')
1526
+ && !dssBuilderMod) {
1527
+ throw new ContractError('pdf/sign/no-dss-builder',
1528
+ 'levels LT/LTA require the pdfDssBuilder dep',
1529
+ { context: { level } });
1530
+ }
1531
+ if (!incrementalWriterMod) {
1532
+ // Every level appends its signature through the writer.
1533
+ throw new ContractError('pdf/sign/no-incremental-writer',
1534
+ 'signing requires the pdfIncrementalWriter dep',
1535
+ { context: { level } });
1536
+ }
1537
+ if (!(documentMod && typeof documentMod.readDocument === 'function')) {
1538
+ // Every level registers its signature in a signature field:
1539
+ // the Catalog and page 1 are resolved through
1540
+ // readDocument.
1541
+ throw new ContractError('pdf/sign/no-document-reader',
1542
+ 'signing requires the pdfDocument dep (the Catalog and '
1543
+ + 'page 1 are resolved through readDocument)',
1544
+ { context: { level } });
1545
+ }
1546
+ const algorithm = opts.algorithm;
1547
+ if (!algorithm) {
1548
+ throw new ContractError('pdf/sign/no-algorithm',
1549
+ 'opts.algorithm required (rsa-pss|ecdsa|ed25519)');
1550
+ }
1551
+ // Ed25519 with CMS uses SHA-512 as its digestAlgorithm (RFC 8419
1552
+ // section 3.1), with or without signed attributes: it is the
1553
+ // default for Ed25519 and any other value is refused.
1554
+ const hashAlg = opts.hashAlg
1555
+ || (algorithm === 'ed25519' ? 'sha512' : 'sha256');
1556
+ const ih = HASH_TABLE[hashAlg];
1557
+ if (!ih) {
1558
+ throw new ContractError('pdf/sign/bad-hash-alg',
1559
+ 'unknown hashAlg: ' + hashAlg);
1560
+ }
1561
+ if (algorithm === 'ed25519' && hashAlg !== 'sha512') {
1562
+ throw new ContractError('pdf/sign/ed25519-requires-sha512',
1563
+ 'Ed25519 CMS signatures use SHA-512 (RFC 8419 section 3.1); '
1564
+ + 'omit opts.hashAlg or pass "sha512"',
1565
+ { context: { hashAlg } });
1566
+ }
1567
+ // TSA injection — required for level T (and recursively for
1568
+ // LT/LTA). The callback signs a digest and returns the RFC
1569
+ // 3161 TimeStampToken bytes.
1570
+ if ((level === 'T' || level === 'LT' || level === 'LTA')
1571
+ && typeof opts.tsaSign !== 'function') {
1572
+ throw new ContractError('pdf/sign/tsa-required-for-level-T',
1573
+ 'levels T/LT/LTA require opts.tsaSign({digest,hashAlg})'
1574
+ + ' callback returning the RFC 3161 TimeStampToken bytes',
1575
+ { context: { level } });
1576
+ }
1577
+ // Whether to include signedAttrs (ESS signing-certificate-v2,
1578
+ // etc.). Required for levels T/LT/LTA (the timestamp-token
1579
+ // lives in unsignedAttrs which is only emitted when the
1580
+ // SignerInfo also carries signedAttrs per the CMS schema).
1581
+ // For level B the default is `false` for backward compatibility
1582
+ // with the no-signedAttrs PAdES-B emit; callers can opt in via
1583
+ // `opts.useSignedAttrs: true` to get an ESS signingCertificateV2
1584
+ // attribute (PAdES baseline B).
1585
+ const useSignedAttrs = (level !== 'B') || !!opts.useSignedAttrs;
1586
+
1587
+ // Normalise the cert to DER.
1588
+ const certDer = _toDer(opts.cert);
1589
+ const placeholderBytes = opts.placeholderBytes || 8192;
1590
+
1591
+ // An encrypted base: derive the document key ONCE, before
1592
+ // anything is emitted, and thread it through the update.
1593
+ let encCtx;
1594
+ if (typeof incrementalWriterMod.readBaseTrailer === 'function') {
1595
+ const baseTrailer = _readBaseTrailer(pdfBytes);
1596
+ encCtx = baseTrailer.encrypt
1597
+ ? _openEncrypted(pdfBytes, baseTrailer, opts) : null;
1598
+ }
1599
+
1600
+ // Emit the document with a placeholder.
1601
+ const emitted = _emitWithPlaceholder(pdfBytes, placeholderBytes,
1602
+ opts.subFilter || 'adbe.pkcs7.detached',
1603
+ opts.docTimeStamp, opts, encCtx);
1604
+ const { bytes, contentsOffset, contentsLength, byteRange } = emitted;
1605
+
1606
+ // Hash the ByteRange-covered bytes.
1607
+ const signedBytes = new Uint8Array(byteRange[1] + byteRange[3]);
1608
+ signedBytes.set(
1609
+ bytes.subarray(byteRange[0], byteRange[0] + byteRange[1]), 0);
1610
+ signedBytes.set(
1611
+ bytes.subarray(byteRange[2], byteRange[2] + byteRange[3]),
1612
+ byteRange[1]);
1613
+ // messageDigest of the ByteRange-covered bytes.
1614
+ const messageDigest = _hashBytes(ih.mod, signedBytes);
1615
+
1616
+ // Build signedAttrs if needed.
1617
+ let signedAttrsTlv = null;
1618
+ let messageToSign = signedBytes;
1619
+ if (useSignedAttrs) {
1620
+ const certDigestSha256 = _hashBytes(sha256, certDer);
1621
+ signedAttrsTlv = _buildSignedAttrs({
1622
+ messageDigest, certDigestSha256,
1623
+ signingTime: opts.signingTime
1624
+ });
1625
+ // Sign the SET-tagged form (0x31), not the [0] IMPLICIT form.
1626
+ messageToSign = signedAttrsTlv;
1627
+ }
1628
+
1629
+ // Sign.
1630
+ const sigBytes = _signDigest({
1631
+ algorithm, privateKey: opts.privateKey,
1632
+ message: messageToSign, hashMod: ih.mod
1633
+ });
1634
+
1635
+ // Build unsignedAttrs for level T+ (timestamp token).
1636
+ let unsignedAttrsTlv = null;
1637
+ if (level === 'T' || level === 'LT' || level === 'LTA') {
1638
+ // Hash the signature value (RFC 3161 §2.4.1 messageImprint
1639
+ // of the signature-time-stamp is over the signature OCTET
1640
+ // STRING value).
1641
+ const sigDigest = _hashBytes(ih.mod, sigBytes);
1642
+ const tstToken = opts.tsaSign({
1643
+ digest: sigDigest, hashAlg
1644
+ });
1645
+ if (!(tstToken instanceof Uint8Array)) {
1646
+ throw new ContractError('pdf/sign/tsa-bad-result',
1647
+ 'opts.tsaSign must return a Uint8Array '
1648
+ + '(RFC 3161 TimeStampToken DER bytes)');
1649
+ }
1650
+ unsignedAttrsTlv = _buildUnsignedAttrsTsa(tstToken);
1651
+ }
1652
+
1653
+ // Build the PKCS#7 SignedData.
1654
+ const pkcs7 = _buildPkcs7({
1655
+ certDer, sigBytes, hashAlg,
1656
+ signatureAlg: algorithm,
1657
+ signedAttrsTlv, unsignedAttrsTlv
1658
+ });
1659
+ if (pkcs7.length > placeholderBytes) {
1660
+ throw new ContractError('pdf/sign/pkcs7-too-large',
1661
+ 'PKCS#7 blob exceeds placeholder',
1662
+ { context: { pkcs7Length: pkcs7.length,
1663
+ placeholder: placeholderBytes } });
1664
+ }
1665
+
1666
+ // Patch /Contents hex literal with the PKCS#7 hex.
1667
+ const hex = _bytesToHex(pkcs7);
1668
+ const TE = new TextEncoder();
1669
+ const hexBytes = TE.encode(hex);
1670
+ const pad = contentsLength - hexBytes.length;
1671
+ if (pad < 0) {
1672
+ throw new ContractError('pdf/sign/hex-overflow',
1673
+ 'PKCS#7 hex exceeds /Contents placeholder');
1674
+ }
1675
+ bytes.set(hexBytes, contentsOffset);
1676
+ for (let i = 0; i < pad; i++) {
1677
+ bytes[contentsOffset + hexBytes.length + i] = 0x30; // '0'
1678
+ }
1679
+ if (level !== 'LT' && level !== 'LTA') {
1680
+ return bytes;
1681
+ }
1682
+ // ── LT : append DSS via incremental writer ──
1683
+ // An encrypted base: the same document key (`encCtx`) encrypts
1684
+ // the DSS objects and the DocTimeStamp field.
1685
+ const ltBytes = _appendDss(bytes, opts, encCtx);
1686
+ if (level === 'LT') return ltBytes;
1687
+ // ── LTA : append DocTimeStamp on top of the LT bytes ──
1688
+ return _appendDocTimeStamp(ltBytes, opts, hashAlg, encCtx);
1689
+ }
1690
+
1691
+ // ── LT/LTA helpers ──────────────────────────────────────────────
1692
+ /**
1693
+ * Encrypt every string and stream payload of a FRESH typed object the
1694
+ * LT/LTA update introduces (ISO 32000-2 §7.6.2: encryption applies to
1695
+ * "all strings and streams in the document's file", the strings
1696
+ * through the string crypt filter, the streams through the stream
1697
+ * crypt filter): strings with `encCtx.strMethod`, stream payloads with
1698
+ * `encCtx.stmMethod`, each with a fresh 16-byte IV from
1699
+ * `encCtx.randomBytes`. A crypt filter whose method is `Identity`
1700
+ * leaves its values as they are. Names, numbers, booleans,
1701
+ * references and stream dictionary keys pass through (§7.6.2 encrypts
1702
+ * none of them); the serializer recomputes a stream's `/Length` from
1703
+ * the encrypted payload. `(num, gen)` is the object the value is
1704
+ * written under — the per-object key of the V=4 handler. Returns a
1705
+ * new object; `value` is not modified.
1706
+ *
1707
+ * Only objects the update CREATES go through here. Objects it copies
1708
+ * (the Catalog, a page, an AcroForm) keep their existing ciphertext
1709
+ * under their own number and generation, hence under their own key.
1710
+ * The one string §7.6.2 exempts — the hexadecimal `/Contents` of a
1711
+ * signature dictionary — is never passed here.
1712
+ */
1713
+ function _encryptNewObject(value, num, gen, encCtx) {
1714
+ const { handler, typedForPw, fek, randomBytes } = encCtx;
1715
+ const strTyped = Object.assign({}, typedForPw,
1716
+ { method: encCtx.strMethod });
1717
+ const stmTyped = Object.assign({}, typedForPw,
1718
+ { method: encCtx.stmMethod });
1719
+ function walk(v) {
1720
+ if (!v || typeof v !== 'object') return v;
1721
+ switch (v.type) {
1722
+ case 'string': {
1723
+ if (encCtx.strMethod === 'Identity'
1724
+ || !(v.value instanceof Uint8Array)) return v;
1725
+ return { type: 'string', syntax: 'hex',
1726
+ value: handler.encryptString(strTyped, fek,
1727
+ num, gen, v.value, randomBytes(16)) };
1728
+ }
1729
+ case 'array':
1730
+ return { type: 'array', items: v.items.map(walk) };
1731
+ case 'dict': {
1732
+ const entries = {};
1733
+ for (const k of Object.keys(v.entries)) {
1734
+ entries[k] = walk(v.entries[k]);
1735
+ }
1736
+ return { type: 'dict', entries };
1737
+ }
1738
+ case 'stream': {
1739
+ const dict = walk(v.dict);
1740
+ const raw = encCtx.stmMethod === 'Identity'
1741
+ || !(v.raw instanceof Uint8Array)
1742
+ ? v.raw
1743
+ : handler.encryptStream(stmTyped, fek, num, gen,
1744
+ v.raw, randomBytes(16));
1745
+ return { type: 'stream', dict, raw };
1746
+ }
1747
+ default:
1748
+ return v;
1749
+ }
1750
+ }
1751
+ return walk(value);
1752
+ }
1753
+
1754
+ /**
1755
+ * Append the DSS dictionary (+ supporting cert/OCSP/CRL streams)
1756
+ * and an updated Catalog (carrying `/DSS dssNum 0 R`) as an
1757
+ * incremental update over `signedBytes`.
1758
+ *
1759
+ * Encrypted base (`encCtx`, the context `sign()` derived): the DSS
1760
+ * objects `pdfDssBuilder.buildDss` returns in plaintext are encrypted
1761
+ * with the document key before they are written
1762
+ * (`_encryptNewObject`), and the update trailer repeats the base's
1763
+ * `/Encrypt` (ISO 32000-2 §7.5.6). `encCtx === null` (a clear base)
1764
+ * writes them as built.
1765
+ */
1766
+ function _appendDss(signedBytes, opts, encCtx) {
1767
+ const dssOpts = opts.dss || {};
1768
+ const certs = Array.isArray(dssOpts.certs) ? dssOpts.certs.map(_toDer) : [];
1769
+ const ocsps = Array.isArray(dssOpts.ocsps) ? dssOpts.ocsps.slice() : [];
1770
+ const crls = Array.isArray(dssOpts.crls) ? dssOpts.crls.slice() : [];
1771
+ // Fresh object numbers start at the merged trailer's /Size of
1772
+ // the signed bytes (every section, newest first).
1773
+ const trailer = _readBaseTrailer(signedBytes);
1774
+ const nextNum = _firstFreeObjNum(trailer);
1775
+ const built = dssBuilderMod.buildDss({
1776
+ certs, ocsps, crls, vri: dssOpts.vri,
1777
+ autoVri: !!dssOpts.autoVri,
1778
+ parentBytes: signedBytes,
1779
+ startNum: nextNum
1780
+ });
1781
+ // Re-define the Catalog — whatever its number or container —
1782
+ // with its entries copied and /DSS added.
1783
+ const catalogUpdate = _buildUpdatedCatalog(signedBytes, built.dssNum);
1784
+ const updates = encCtx
1785
+ ? built.updates.map((u) => ({ num: u.num, gen: u.gen | 0,
1786
+ value: _encryptNewObject(u.value, u.num, u.gen | 0, encCtx) }))
1787
+ : built.updates.slice();
1788
+ // The Catalog is NOT re-encrypted: its entries are the base
1789
+ // Catalog's own (ciphertext strings, if any), copied under the
1790
+ // same object number and generation, so their key is unchanged.
1791
+ updates.push(catalogUpdate);
1792
+ return incrementalWriterMod.appendIncremental(signedBytes, {
1793
+ updates,
1794
+ root: { num: catalogUpdate.num, gen: catalogUpdate.gen },
1795
+ info: trailer.info,
1796
+ id: trailer.id,
1797
+ // An update over an encrypted base repeats its /Encrypt
1798
+ // (ISO 32000-2 §7.5.6).
1799
+ encrypt: trailer.encrypt
1800
+ });
1801
+ }
1802
+
1803
+ /**
1804
+ * Append a DocTimeStamp signature (RFC 3161) as a second
1805
+ * incremental update. Reuses `_emitWithPlaceholder` (the same
1806
+ * `pdfIncrementalWriter` emitter as the /Sig) with
1807
+ * `subFilter='ETSI.RFC3161'` and `isDocTimeStamp=true`.
1808
+ *
1809
+ * Encrypted base: `encCtx` encrypts the DocTimeStamp field's `/T`
1810
+ * (`_buildSignatureField`); the `/DocTimeStamp` dictionary's
1811
+ * hexadecimal `/Contents` is written and patched in clear — ISO
1812
+ * 32000-2 §7.6.2 excludes the Contents value of a signature
1813
+ * dictionary from encryption.
1814
+ */
1815
+ function _appendDocTimeStamp(ltBytes, opts, hashAlg, encCtx) {
1816
+ const ih = HASH_TABLE[hashAlg];
1817
+ const placeholderBytes = opts.docTimeStampPlaceholder
1818
+ || opts.placeholderBytes || 8192;
1819
+ const emitted = _emitWithPlaceholder(ltBytes, placeholderBytes,
1820
+ 'ETSI.RFC3161', /* isDocTimeStamp */ true, opts, encCtx);
1821
+ const { bytes, contentsOffset, contentsLength, byteRange } = emitted;
1822
+ const signedRange = new Uint8Array(byteRange[1] + byteRange[3]);
1823
+ signedRange.set(
1824
+ bytes.subarray(byteRange[0], byteRange[0] + byteRange[1]), 0);
1825
+ signedRange.set(
1826
+ bytes.subarray(byteRange[2], byteRange[2] + byteRange[3]),
1827
+ byteRange[1]);
1828
+ const digest = _hashBytes(ih.mod, signedRange);
1829
+ const tstToken = opts.tsaSign({ digest, hashAlg });
1830
+ if (!(tstToken instanceof Uint8Array)) {
1831
+ throw new ContractError('pdf/sign/tsa-bad-result-lta',
1832
+ 'opts.tsaSign must return a Uint8Array (RFC 3161 '
1833
+ + 'TimeStampToken DER) for the LTA DocTimeStamp');
1834
+ }
1835
+ if (tstToken.length > placeholderBytes) {
1836
+ throw new ContractError('pdf/sign/tst-too-large',
1837
+ 'TimeStampToken exceeds DocTimeStamp /Contents placeholder',
1838
+ { context: { tstLength: tstToken.length,
1839
+ placeholder: placeholderBytes } });
1840
+ }
1841
+ const hex = _bytesToHex(tstToken);
1842
+ const TE = new TextEncoder();
1843
+ const hexBytes = TE.encode(hex);
1844
+ const pad = contentsLength - hexBytes.length;
1845
+ if (pad < 0) {
1846
+ throw new ContractError('pdf/sign/dts-hex-overflow',
1847
+ 'TimeStampToken hex exceeds /Contents placeholder');
1848
+ }
1849
+ bytes.set(hexBytes, contentsOffset);
1850
+ for (let i = 0; i < pad; i++) {
1851
+ bytes[contentsOffset + hexBytes.length + i] = 0x30;
1852
+ }
1853
+ return bytes;
1854
+ }
1855
+
1856
+ /**
1857
+ * The Catalog re-definition carrying `/DSS dssNum 0 R`.
1858
+ *
1859
+ * The Catalog is the object the document's merged trailer names as
1860
+ * `/Root`, resolved through `pdfDocument.readDocument` — whatever its
1861
+ * object number, and whether it sits at a byte offset or inside an
1862
+ * object stream. Its entries are copied verbatim (any prior `/DSS`
1863
+ * is replaced) and the update keeps the Catalog's own number and
1864
+ * generation, so the newest cross-reference section redefines it.
1865
+ * An encrypted document is read with `allowEncrypted` — the copied
1866
+ * strings keep their object number and generation, hence their key.
1867
+ */
1868
+ function _buildUpdatedCatalog(bytes, dssNum) {
1869
+ const doc = documentMod.readDocument(bytes, { allowEncrypted: true });
1870
+ const rootRef = doc.trailer.root;
1871
+ const catalog = doc._raw.resolve(
1872
+ { type: 'ref', num: rootRef.num, gen: rootRef.gen });
1873
+ if (!catalog || catalog.type !== 'dict') {
1874
+ throw new ContractError('pdf/sign/catalog-not-found',
1875
+ `the trailer /Root (object ${rootRef.num} ${rootRef.gen}) `
1876
+ + 'does not resolve to a Catalog dictionary',
1877
+ { context: { root: rootRef } });
1878
+ }
1879
+ const entries = Object.assign({}, catalog.entries);
1880
+ entries.DSS = { type: 'ref', num: dssNum | 0, gen: 0 };
1881
+ return { num: rootRef.num, gen: rootRef.gen | 0,
1882
+ value: { type: 'dict', entries } };
1883
+ }
1884
+
1885
+ return {
1886
+ sign,
1887
+ // Exposed for white-box testing :
1888
+ _buildPkcs7,
1889
+ _buildSignedAttrs,
1890
+ _sortDerSetOf,
1891
+ _buildUnsignedAttrsTsa,
1892
+ _extractIssuerSerial,
1893
+ _emitWithPlaceholder,
1894
+ _hashBytes,
1895
+ _toDer,
1896
+ HASH_TABLE
1897
+ };
1898
+ }
1899
+ };