@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.
- package/CHANGELOG.md +609 -0
- package/LICENSE +661 -0
- package/NOTICE +77 -0
- package/README.md +363 -2
- package/dist/build/index.js +21 -0
- package/dist/build/pdf-full-rw.js +10972 -0
- package/dist/build/pdf-full-rw.meta.json +105 -0
- package/dist/build/pdf-full-rw.min.js +53 -0
- package/dist/build/pdf-full.js +6078 -0
- package/dist/build/pdf-full.meta.json +90 -0
- package/dist/build/pdf-full.min.js +32 -0
- package/dist/build/pdf-large-rw.js +10367 -0
- package/dist/build/pdf-large-rw.meta.json +99 -0
- package/dist/build/pdf-large-rw.min.js +53 -0
- package/dist/build/pdf-large.js +5473 -0
- package/dist/build/pdf-large.meta.json +84 -0
- package/dist/build/pdf-large.min.js +32 -0
- package/dist/build/pdf-legacy-rw.js +12402 -0
- package/dist/build/pdf-legacy-rw.meta.json +110 -0
- package/dist/build/pdf-legacy-rw.min.js +53 -0
- package/dist/build/pdf-legacy.js +7508 -0
- package/dist/build/pdf-legacy.meta.json +95 -0
- package/dist/build/pdf-legacy.min.js +32 -0
- package/dist/build/pdf-rw.js +7578 -0
- package/dist/build/pdf-rw.meta.json +77 -0
- package/dist/build/pdf-rw.min.js +53 -0
- package/dist/build/pdf.js +2684 -0
- package/dist/build/pdf.meta.json +62 -0
- package/dist/build/pdf.min.js +32 -0
- package/dist/standalone/pdf-full-rw.js +16798 -0
- package/dist/standalone/pdf-full-rw.meta.json +78 -0
- package/dist/standalone/pdf-full-rw.min.js +56 -0
- package/dist/standalone/pdf-full.js +11904 -0
- package/dist/standalone/pdf-full.meta.json +63 -0
- package/dist/standalone/pdf-full.min.js +35 -0
- package/dist/standalone/pdf-large-rw.js +16193 -0
- package/dist/standalone/pdf-large-rw.meta.json +72 -0
- package/dist/standalone/pdf-large-rw.min.js +56 -0
- package/dist/standalone/pdf-large.js +11299 -0
- package/dist/standalone/pdf-large.meta.json +57 -0
- package/dist/standalone/pdf-large.min.js +35 -0
- package/dist/standalone/pdf-legacy-rw.js +18228 -0
- package/dist/standalone/pdf-legacy-rw.meta.json +83 -0
- package/dist/standalone/pdf-legacy-rw.min.js +56 -0
- package/dist/standalone/pdf-legacy.js +13334 -0
- package/dist/standalone/pdf-legacy.meta.json +68 -0
- package/dist/standalone/pdf-legacy.min.js +35 -0
- package/dist/standalone/pdf-rw.js +13404 -0
- package/dist/standalone/pdf-rw.meta.json +50 -0
- package/dist/standalone/pdf-rw.min.js +56 -0
- package/dist/standalone/pdf.js +8510 -0
- package/dist/standalone/pdf.meta.json +35 -0
- package/dist/standalone/pdf.min.js +35 -0
- package/docs/README.md +53 -0
- package/docs/api/README.md +38 -0
- package/docs/api/_shared/README.md +91 -0
- package/docs/api/action/README.md +29 -0
- package/docs/api/action/action.md +81 -0
- package/docs/api/action/goTo.md +66 -0
- package/docs/api/action/launch.md +58 -0
- package/docs/api/action/named.md +55 -0
- package/docs/api/action/uri.md +54 -0
- package/docs/api/annot/README.md +53 -0
- package/docs/api/annot/annot.md +114 -0
- package/docs/api/annot/fileAttach.md +53 -0
- package/docs/api/annot/freeText.md +68 -0
- package/docs/api/annot/ink.md +69 -0
- package/docs/api/annot/link.md +74 -0
- package/docs/api/annot/markup.md +83 -0
- package/docs/api/annot/popup.md +52 -0
- package/docs/api/annot/projection.md +56 -0
- package/docs/api/annot/redact.md +67 -0
- package/docs/api/annot/square.md +87 -0
- package/docs/api/annot/stamp.md +54 -0
- package/docs/api/annot/text.md +69 -0
- package/docs/api/annot/widget.md +69 -0
- package/docs/api/associatedFiles/README.md +9 -0
- package/docs/api/associatedFiles/associatedFiles.md +78 -0
- package/docs/api/bundles/README.md +68 -0
- package/docs/api/bundles/dist-matrix.md +165 -0
- package/docs/api/bundles/pdf-full.md +148 -0
- package/docs/api/bundles/pdf-large.md +144 -0
- package/docs/api/bundles/pdf-legacy.md +169 -0
- package/docs/api/content/README.md +29 -0
- package/docs/api/content/color.md +99 -0
- package/docs/api/content/graphics.md +114 -0
- package/docs/api/content/images.md +124 -0
- package/docs/api/content/ops.md +100 -0
- package/docs/api/content/stream.md +107 -0
- package/docs/api/content/text.md +98 -0
- package/docs/api/crypto/README.md +29 -0
- package/docs/api/crypto/aesGcm.md +72 -0
- package/docs/api/crypto/permissions.md +79 -0
- package/docs/api/crypto/security.md +98 -0
- package/docs/api/crypto/standardV4.md +104 -0
- package/docs/api/crypto/standardV5.md +84 -0
- package/docs/api/crypto/standardV6.md +93 -0
- package/docs/api/destination/README.md +9 -0
- package/docs/api/destination/destination.md +79 -0
- package/docs/api/document/README.md +29 -0
- package/docs/api/document/builder.md +281 -0
- package/docs/api/document/catalog.md +98 -0
- package/docs/api/document/document.md +187 -0
- package/docs/api/document/encryptedWriter.md +149 -0
- package/docs/api/document/incrementalWriter.md +148 -0
- package/docs/api/document/page.md +99 -0
- package/docs/api/document/pages.md +82 -0
- package/docs/api/document/resources.md +102 -0
- package/docs/api/document/writer.md +157 -0
- package/docs/api/document/xrefStreamWriter.md +122 -0
- package/docs/api/embedded/README.md +13 -0
- package/docs/api/embedded/collection.md +80 -0
- package/docs/api/embedded/embeddedFile.md +86 -0
- package/docs/api/embedded/fileSpec.md +87 -0
- package/docs/api/errors.md +110 -0
- package/docs/api/extra/3d-richmedia.md +76 -0
- package/docs/api/extra/README.md +99 -0
- package/docs/api/extra/annot-extended.md +71 -0
- package/docs/api/extra/associated-files.md +70 -0
- package/docs/api/extra/ccitt-fax-decoder.md +74 -0
- package/docs/api/extra/color-spaces-extended.md +72 -0
- package/docs/api/extra/content-ops-extended.md +82 -0
- package/docs/api/extra/document-parts.md +69 -0
- package/docs/api/extra/embedded-files-portfolio.md +87 -0
- package/docs/api/extra/font-cid-typed.md +77 -0
- package/docs/api/extra/font-color-tagging.md +76 -0
- package/docs/api/extra/form-actions-extended.md +75 -0
- package/docs/api/extra/info-dict-deprecated.md +72 -0
- package/docs/api/extra/jbig2-read.md +80 -0
- package/docs/api/extra/legacy-deprecated-annots.md +89 -0
- package/docs/api/extra/legacy-deprecated-filters.md +78 -0
- package/docs/api/extra/legacy-rc4-read.md +74 -0
- package/docs/api/extra/legacy-xfa-read.md +65 -0
- package/docs/api/extra/linearization-write.md +71 -0
- package/docs/api/extra/misc.md +93 -0
- package/docs/api/extra/optional-content-extended.md +83 -0
- package/docs/api/extra/pdf-a-output-intent.md +65 -0
- package/docs/api/extra/pdf-sandbox.md +76 -0
- package/docs/api/extra/pdf-ua-tagged.md +63 -0
- package/docs/api/extra/pdf-x-prepress.md +65 -0
- package/docs/api/extra/redaction-iso32005.md +65 -0
- package/docs/api/extra/shading-typed.md +73 -0
- package/docs/api/extra/sig-aes-gcm.md +69 -0
- package/docs/api/extra/sig-pades.md +103 -0
- package/docs/api/extra/tagged-pdf-typed.md +78 -0
- package/docs/api/extra/transparency-typed.md +74 -0
- package/docs/api/extra/well-tagged-pdf.md +61 -0
- package/docs/api/extra/xmp-extended.md +65 -0
- package/docs/api/font/README.md +25 -0
- package/docs/api/font/embed.md +157 -0
- package/docs/api/font/encoding.md +95 -0
- package/docs/api/font/font.md +97 -0
- package/docs/api/font/type3.md +89 -0
- package/docs/api/form/README.md +35 -0
- package/docs/api/form/acroform.md +88 -0
- package/docs/api/form/appearance.md +87 -0
- package/docs/api/form/button.md +97 -0
- package/docs/api/form/choice.md +96 -0
- package/docs/api/form/fieldTree.md +93 -0
- package/docs/api/form/signature.md +90 -0
- package/docs/api/form/text.md +88 -0
- package/docs/api/linearization/README.md +11 -0
- package/docs/api/linearization/linearization.md +81 -0
- package/docs/api/main.md +116 -0
- package/docs/api/metadata/README.md +10 -0
- package/docs/api/metadata/info.md +70 -0
- package/docs/api/metadata/xmp.md +62 -0
- package/docs/api/ocg/README.md +23 -0
- package/docs/api/ocg/config.md +95 -0
- package/docs/api/ocg/ocg.md +77 -0
- package/docs/api/outline/README.md +11 -0
- package/docs/api/outline/outline.md +107 -0
- package/docs/api/pdf.md +152 -0
- package/docs/api/prepress/README.md +10 -0
- package/docs/api/prepress/outputIntent.md +79 -0
- package/docs/api/prepress/pageBoundary.md +75 -0
- package/docs/api/sig/README.md +32 -0
- package/docs/api/sig/byteRange.md +120 -0
- package/docs/api/sig/certChain.md +84 -0
- package/docs/api/sig/dss.md +111 -0
- package/docs/api/sig/oids.md +76 -0
- package/docs/api/sig/sha1.md +72 -0
- package/docs/api/sig/sign.md +317 -0
- package/docs/api/sig/signature.md +178 -0
- package/docs/api/sig/timestamp.md +84 -0
- package/docs/api/syntax/README.md +29 -0
- package/docs/api/syntax/crossRefStream.md +115 -0
- package/docs/api/syntax/filters/README.md +50 -0
- package/docs/api/syntax/filters/ascii85.md +76 -0
- package/docs/api/syntax/filters/asciiHex.md +73 -0
- package/docs/api/syntax/filters/dispatch.md +125 -0
- package/docs/api/syntax/filters/flate.md +134 -0
- package/docs/api/syntax/filters/runLength.md +78 -0
- package/docs/api/syntax/objStream.md +88 -0
- package/docs/api/syntax/parser-obj.md +97 -0
- package/docs/api/syntax/parser.md +151 -0
- package/docs/api/syntax/serializer.md +109 -0
- package/docs/api/syntax/tokenizer.md +104 -0
- package/docs/api/syntax/trailer.md +85 -0
- package/docs/api/syntax/xref.md +139 -0
- package/docs/api/tagged/README.md +25 -0
- package/docs/api/tagged/classMap.md +67 -0
- package/docs/api/tagged/markedContent.md +62 -0
- package/docs/api/tagged/parentTree.md +67 -0
- package/docs/api/tagged/roleMap.md +67 -0
- package/docs/api/tagged/structElement.md +76 -0
- package/docs/api/tagged/structTree.md +75 -0
- package/docs/guide/coverage.md +113 -0
- package/docs/guide/crypto.md +121 -0
- package/docs/guide/extending.md +76 -0
- package/docs/guide/getting-started.md +75 -0
- package/docs/guide/legacy-1.7.md +42 -0
- package/docs/guide/pades-integration.md +579 -0
- package/docs/guide/read-pdf.md +89 -0
- package/package.json +97 -4
- package/src/_shared/index.js +179 -0
- package/src/action/action.js +119 -0
- package/src/action/goTo.js +89 -0
- package/src/action/launch.js +61 -0
- package/src/action/named.js +54 -0
- package/src/action/uri.js +51 -0
- package/src/annot/annot.js +212 -0
- package/src/annot/fileAttach.js +55 -0
- package/src/annot/freeText.js +82 -0
- package/src/annot/ink.js +77 -0
- package/src/annot/link.js +77 -0
- package/src/annot/markup.js +91 -0
- package/src/annot/popup.js +53 -0
- package/src/annot/projection.js +52 -0
- package/src/annot/redact.js +87 -0
- package/src/annot/square.js +132 -0
- package/src/annot/stamp.js +48 -0
- package/src/annot/text.js +54 -0
- package/src/annot/widget.js +61 -0
- package/src/associatedFiles/associatedFiles.js +86 -0
- package/src/bundles/pdf-full.js +91 -0
- package/src/bundles/pdf-large.js +81 -0
- package/src/bundles/pdf-legacy.js +107 -0
- package/src/content/color.js +114 -0
- package/src/content/graphics.js +192 -0
- package/src/content/images.js +160 -0
- package/src/content/ops.js +137 -0
- package/src/content/stream.js +154 -0
- package/src/content/text.js +125 -0
- package/src/crypto/aesGcm.js +123 -0
- package/src/crypto/permissions.js +112 -0
- package/src/crypto/security.js +327 -0
- package/src/crypto/standardV4.js +443 -0
- package/src/crypto/standardV5.js +306 -0
- package/src/crypto/standardV6.js +334 -0
- package/src/destination/destination.js +183 -0
- package/src/document/builder.js +618 -0
- package/src/document/catalog.js +100 -0
- package/src/document/document.js +472 -0
- package/src/document/encryptedWriter.js +554 -0
- package/src/document/incrementalWriter.js +514 -0
- package/src/document/page.js +131 -0
- package/src/document/pages.js +103 -0
- package/src/document/resources.js +146 -0
- package/src/document/writer.js +211 -0
- package/src/document/xrefStreamWriter.js +353 -0
- package/src/embedded/collection.js +102 -0
- package/src/embedded/embeddedFile.js +99 -0
- package/src/embedded/fileSpec.js +137 -0
- package/src/errors.js +78 -0
- package/src/extra/3d-richmedia.js +171 -0
- package/src/extra/annot-extended.js +200 -0
- package/src/extra/associated-files.js +131 -0
- package/src/extra/ccitt-fax-decoder.js +776 -0
- package/src/extra/color-spaces-extended.js +196 -0
- package/src/extra/content-ops-extended.js +153 -0
- package/src/extra/document-parts.js +149 -0
- package/src/extra/embedded-files-portfolio.js +234 -0
- package/src/extra/font-cid-typed.js +185 -0
- package/src/extra/font-color-tagging.js +144 -0
- package/src/extra/form-actions-extended.js +196 -0
- package/src/extra/info-dict-deprecated.js +137 -0
- package/src/extra/jbig2-read.js +169 -0
- package/src/extra/legacy-deprecated-annots.js +198 -0
- package/src/extra/legacy-deprecated-filters.js +167 -0
- package/src/extra/legacy-rc4-read.js +235 -0
- package/src/extra/legacy-xfa-read.js +104 -0
- package/src/extra/linearization-write.js +97 -0
- package/src/extra/misc.js +217 -0
- package/src/extra/optional-content-extended.js +142 -0
- package/src/extra/pdf-a-output-intent.js +112 -0
- package/src/extra/pdf-sandbox.js +88 -0
- package/src/extra/pdf-ua-tagged.js +116 -0
- package/src/extra/pdf-x-prepress.js +114 -0
- package/src/extra/redaction-iso32005.js +136 -0
- package/src/extra/shading-typed.js +222 -0
- package/src/extra/sig-aes-gcm.js +135 -0
- package/src/extra/sig-pades.js +242 -0
- package/src/extra/tagged-pdf-typed.js +203 -0
- package/src/extra/transparency-typed.js +135 -0
- package/src/extra/well-tagged-pdf.js +138 -0
- package/src/extra/xmp-extended.js +190 -0
- package/src/font/embed.js +480 -0
- package/src/font/encoding.js +92 -0
- package/src/font/font.js +101 -0
- package/src/font/type3.js +75 -0
- package/src/form/acroform.js +94 -0
- package/src/form/appearance.js +90 -0
- package/src/form/button.js +105 -0
- package/src/form/choice.js +152 -0
- package/src/form/fieldTree.js +120 -0
- package/src/form/signature.js +100 -0
- package/src/form/text.js +101 -0
- package/src/linearization/linearization.js +107 -0
- package/src/main.js +411 -0
- package/src/metadata/info.js +87 -0
- package/src/metadata/xmp.js +62 -0
- package/src/ocg/config.js +156 -0
- package/src/ocg/ocg.js +124 -0
- package/src/outline/outline.js +157 -0
- package/src/pdf.js +133 -0
- package/src/prepress/outputIntent.js +118 -0
- package/src/prepress/pageBoundary.js +108 -0
- package/src/sig/byteRange.js +306 -0
- package/src/sig/certChain.js +247 -0
- package/src/sig/dss.js +317 -0
- package/src/sig/oids.js +157 -0
- package/src/sig/sha1.js +142 -0
- package/src/sig/sign.js +1899 -0
- package/src/sig/signature.js +1441 -0
- package/src/sig/timestamp.js +236 -0
- package/src/syntax/crossRefStream.js +133 -0
- package/src/syntax/filters/ascii85.js +122 -0
- package/src/syntax/filters/asciiHex.js +83 -0
- package/src/syntax/filters/dispatch.js +176 -0
- package/src/syntax/filters/flate.js +316 -0
- package/src/syntax/filters/runLength.js +96 -0
- package/src/syntax/objStream.js +99 -0
- package/src/syntax/parser-obj.js +52 -0
- package/src/syntax/parser.js +321 -0
- package/src/syntax/serializer.js +221 -0
- package/src/syntax/tokenizer.js +290 -0
- package/src/syntax/trailer.js +76 -0
- package/src/syntax/xref.js +341 -0
- package/src/tagged/classMap.js +81 -0
- package/src/tagged/markedContent.js +123 -0
- package/src/tagged/parentTree.js +126 -0
- package/src/tagged/roleMap.js +107 -0
- package/src/tagged/structElement.js +138 -0
- package/src/tagged/structTree.js +94 -0
package/src/sig/sign.js
ADDED
|
@@ -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
|
+
};
|