@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
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
# PAdES integration — sign and verify in the browser
|
|
2
|
+
|
|
3
|
+
> From PDF bytes to a signed document to a machine-readable verification
|
|
4
|
+
> report — the wiring, the trust-model boundaries, and what you may claim.
|
|
5
|
+
|
|
6
|
+
## What this guide covers
|
|
7
|
+
|
|
8
|
+
Bytes in, signed PDF out, then a verification report back: this guide
|
|
9
|
+
walks the whole PAdES (ETSI EN 319 142-1) path through `@awacloud/pdf` —
|
|
10
|
+
wiring the runtime, bringing your own key material, signing at levels
|
|
11
|
+
B/T/LT/LTA, timestamping through the `tsaSign` seam, verifying, and
|
|
12
|
+
reading the trust-model boundaries that gate every claim below.
|
|
13
|
+
|
|
14
|
+
**Prerequisites** — the package `@awacloud/pdf` (root entry
|
|
15
|
+
`@awacloud/pdf`) and its `@awacloud/fw` / `@awacloud/fonts` dependencies,
|
|
16
|
+
registered on one `@awacloud/fw` `ModuleRuntime`; any modern JavaScript
|
|
17
|
+
runtime (browser main thread or Worker, Bun, Node.js 18+). The cryptography
|
|
18
|
+
is synchronous and does not use Web Crypto.
|
|
19
|
+
|
|
20
|
+
The demo `apps/demo/pades` (the source monorepo's PAdES demo application,
|
|
21
|
+
not published) is this guide executed: every snippet below is the wiring, sign and
|
|
22
|
+
verify path the demo actually runs, minus its UI. Where the demo departs
|
|
23
|
+
from a snippet (the mock TSA, the demo certificate emitter), this guide
|
|
24
|
+
says so explicitly.
|
|
25
|
+
|
|
26
|
+
## Wiring
|
|
27
|
+
|
|
28
|
+
`@awacloud/pdf` publishes a declarative 5-array manifest from its `.`
|
|
29
|
+
export — `fw_require`, `pkg_require`, `modules`, `extras`, `bundle` — and
|
|
30
|
+
no runtime bootstrap of its own. An integrator wires it into one
|
|
31
|
+
`@awacloud/fw` `ModuleRuntime`, registering the five arrays in that
|
|
32
|
+
order:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import { ModuleRuntime } from '@awacloud/fw/core/runtime.js';
|
|
36
|
+
import {
|
|
37
|
+
fw_require, pkg_require, modules, extras, bundle
|
|
38
|
+
} from '@awacloud/pdf';
|
|
39
|
+
|
|
40
|
+
const runtime = new ModuleRuntime();
|
|
41
|
+
for (const m of fw_require) runtime.register(m);
|
|
42
|
+
for (const m of pkg_require) runtime.register(m);
|
|
43
|
+
for (const m of modules) runtime.register(m);
|
|
44
|
+
for (const m of extras) runtime.register(m);
|
|
45
|
+
for (const m of bundle) runtime.register(m);
|
|
46
|
+
|
|
47
|
+
const pdfSign = runtime.resolve('pdfSign');
|
|
48
|
+
const pdfSignature = runtime.resolve('pdfSignature');
|
|
49
|
+
const pdfSigPades = runtime.resolve('pdfSigPades');
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`pdfSign` and `pdfSignature` are registered in `src/main.js` `modules`,
|
|
53
|
+
but none of the three source bundles (`pdf-large` / `pdf-full` /
|
|
54
|
+
`pdf-legacy`) wires them: those carry only the read-only `pdfSigPades`
|
|
55
|
+
extra. Among the committed prebuilt files, only the four Read+Write roots
|
|
56
|
+
(`dist/standalone/pdf-rw.js` and the other `-rw` roots) carry both the
|
|
57
|
+
signer and the verifier (see the package README, "Committed dist"). This
|
|
58
|
+
guide — like the demo — uses the manifest + `ModuleRuntime` path above,
|
|
59
|
+
which reaches signing without a prebuilt file.
|
|
60
|
+
|
|
61
|
+
Loading `@awacloud/pdf` on a zero-bundler page uses a bare import map. The
|
|
62
|
+
map below is the one the demo ships:
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<script type="importmap">
|
|
66
|
+
{ "imports": {
|
|
67
|
+
"@awacloud/fw": "/node_modules/@awacloud/fw/src/main.js",
|
|
68
|
+
"@awacloud/fw/": "/node_modules/@awacloud/fw/src/",
|
|
69
|
+
"@awacloud/pdf": "/node_modules/@awacloud/pdf/src/main.js",
|
|
70
|
+
"@awacloud/pdf/": "/node_modules/@awacloud/pdf/src/",
|
|
71
|
+
"@awacloud/fonts": "/node_modules/@awacloud/fonts/src/main.js",
|
|
72
|
+
"@awacloud/fonts/": "/node_modules/@awacloud/fonts/src/"
|
|
73
|
+
}}
|
|
74
|
+
</script>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The trailing-slash keys are required because `@awacloud/pdf`'s `main.js`
|
|
78
|
+
imports `@awacloud/fw/crypto/...` subpaths and `pkg_require` reaches into
|
|
79
|
+
`@awacloud/fonts`.
|
|
80
|
+
|
|
81
|
+
**`fw_require` carries the full crypto closure today.** An earlier
|
|
82
|
+
measurement of this path found `@awacloud/pdf`'s `fw_require` missing four
|
|
83
|
+
of `rsa`/`ecc`'s own transitive dependencies (`bn`, `random`, `hex`,
|
|
84
|
+
`hmac`), which made `runtime.resolve('pdfSign')` throw `Module not
|
|
85
|
+
found: bn` unless a consumer patched the closure itself. That gap is
|
|
86
|
+
closed: `fw_require` (`src/main.js`) now registers `random`, `bn`,
|
|
87
|
+
`hmac` and `hex` alongside `rsa`, `ecc` and `ed25519`, so the loop above
|
|
88
|
+
is complete as written — **do not** add a four-module patch before
|
|
89
|
+
registering `modules`; there is nothing left to patch. The source
|
|
90
|
+
monorepo's PAdES demo pins this integrator path in a test that registers
|
|
91
|
+
the five arrays exactly as above and asserts `pdfSign`/`pdfSignature`
|
|
92
|
+
resolve with no extra registration step.
|
|
93
|
+
|
|
94
|
+
## Key material
|
|
95
|
+
|
|
96
|
+
`@awacloud/pdf` does not generate or manage keys for you:
|
|
97
|
+
|
|
98
|
+
> Bring your own key material. `@awacloud/pdf` accepts a DER or PEM
|
|
99
|
+
> certificate and an algorithm-specific private key; unwrapping a
|
|
100
|
+
> PKCS#12 / `.pfx` container is the integrator's responsibility.
|
|
101
|
+
|
|
102
|
+
— Do not claim otherwise. No PKCS#12 parser exists. Say: "bring your
|
|
103
|
+
own key material; PKCS#12 unwrapping is integrator-side." PKCS#8 / SEC1
|
|
104
|
+
/ SPKI / PEM *key* encoding IS available via
|
|
105
|
+
`@awacloud/fw/crypto/utils/{keyformat,pem}.js`.
|
|
106
|
+
|
|
107
|
+
`opts.privateKey`'s shape depends on the algorithm — the dispatch is
|
|
108
|
+
`_signDigest` in `src/sig/sign.js`:
|
|
109
|
+
|
|
110
|
+
| Algorithm | `opts.privateKey` shape |
|
|
111
|
+
|---|---|
|
|
112
|
+
| RSA-PSS | `{ n, e, d }` (`Uint8Array` each) |
|
|
113
|
+
| ECDSA | `{ curve: ecc.curves.c256\|c384\|c521, secretKey: new ecc.ecdsa.secretKey(curve, k) }` |
|
|
114
|
+
| Ed25519 | 64-byte `Uint8Array` (seed ‖ public key) |
|
|
115
|
+
|
|
116
|
+
If your key material arrives as PKCS#8, SEC1 or SPKI DER, or as PEM,
|
|
117
|
+
`@awacloud/fw` publishes the conversion — both subpaths resolve through
|
|
118
|
+
`@awacloud/fw`'s `exports` map's `./crypto/*.js` wildcard entry, so they
|
|
119
|
+
are reachable as ordinary subpath imports, not only from inside the
|
|
120
|
+
package:
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
import { keyformat } from '@awacloud/fw/crypto/utils/keyformat.js';
|
|
124
|
+
import { pem } from '@awacloud/fw/crypto/utils/pem.js';
|
|
125
|
+
import { b64 } from '@awacloud/fw/io/codec/b64.js';
|
|
126
|
+
|
|
127
|
+
// keyformat needs its own `asn1` dependency when instantiated directly
|
|
128
|
+
// (it is not part of @awacloud/pdf's own fw_require closure — it is an
|
|
129
|
+
// integrator-side helper for key material, not something pdfSign needs).
|
|
130
|
+
const kf = keyformat.factory(asn1Instance);
|
|
131
|
+
const { d, curveOid, publicKey } = kf.decodePkcs8Ec(pkcs8Der);
|
|
132
|
+
|
|
133
|
+
// pem needs its own `b64` dependency when instantiated directly, same
|
|
134
|
+
// reason as keyformat above — and it is not optional here: `pem.js`'s
|
|
135
|
+
// `factory(b64)` parameter shadows the module's own top-level `b64`
|
|
136
|
+
// import, so calling `pem.factory()` with no argument leaves the
|
|
137
|
+
// in-scope `b64` undefined and `decode` throws on `b64.toBytes(...)`.
|
|
138
|
+
const b64Instance = b64.factory();
|
|
139
|
+
const { bytes } = pem.factory(b64Instance).decode(pemText, 'PRIVATE KEY');
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`keyformat.js` covers PKCS#8 / SEC1 encode+decode for EC keys and
|
|
143
|
+
PKCS#8 / SPKI encode+decode for Edwards keys; `pem.js` covers generic
|
|
144
|
+
PEM block encode/decode. Neither handles a password-protected PKCS#12
|
|
145
|
+
container.
|
|
146
|
+
|
|
147
|
+
For a certificate, this guide's own snippets — and the demo — use a
|
|
148
|
+
throwaway self-signed certificate: **the demo generates a throwaway
|
|
149
|
+
demo certificate in your browser**, never "the library issues
|
|
150
|
+
certificates" — `@awacloud/fw` and `@awacloud/pdf` publish no X.509
|
|
151
|
+
emitter. Bring your own CA-issued certificate for anything beyond a
|
|
152
|
+
demo.
|
|
153
|
+
|
|
154
|
+
## Signing
|
|
155
|
+
|
|
156
|
+
A minimal PAdES-B signature, run through this guide's own verification
|
|
157
|
+
script under Bun:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
const signed = pdfSign.sign(pdfBytes, {
|
|
161
|
+
cert, privateKey, algorithm: 'ed25519',
|
|
162
|
+
hashAlg: 'sha512', level: 'B',
|
|
163
|
+
useSignedAttrs: true,
|
|
164
|
+
subFilter: 'ETSI.CAdES.detached'
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**`subFilter: 'ETSI.CAdES.detached'` is not optional.** Quoting the
|
|
169
|
+
measured trap verbatim:
|
|
170
|
+
|
|
171
|
+
> `sign()` emits a non-PAdES SubFilter by default. With no
|
|
172
|
+
> `opts.subFilter`, the emitted `/SubFilter` is `adbe.pkcs7.detached`,
|
|
173
|
+
> which `pdfSigPades.detectPadesProfile` classifies `{ isPades: false,
|
|
174
|
+
> level: null }` — for **all four** levels. Passing `subFilter:
|
|
175
|
+
> 'ETSI.CAdES.detached'` yields `isPades: true` and levels `B-B` /
|
|
176
|
+
> `B-T` / `B-LT` / `B-LTA`, and the signature still verifies. Measured
|
|
177
|
+
> both ways.
|
|
178
|
+
|
|
179
|
+
Ed25519 always signs with SHA-512 (RFC 8419 §3.1): `hashAlg` defaults to
|
|
180
|
+
`'sha512'` for Ed25519, so passing it is optional, and any other value
|
|
181
|
+
throws `pdf/sign/ed25519-requires-sha512`. RSA-PSS and ECDSA take
|
|
182
|
+
`'sha256'` (the default), `'sha384'` or `'sha512'`.
|
|
183
|
+
|
|
184
|
+
### Interoperability: Ed25519 in PDF viewers
|
|
185
|
+
|
|
186
|
+
An Ed25519 signature is an ISO/TS 32002 enhancement of PDF 2.0, and the
|
|
187
|
+
standard asks the document to say so (ISO/TS 32002 §4). `sign()` does it
|
|
188
|
+
in the signing update: the Catalog's `/Extensions` declares the `ISO_`
|
|
189
|
+
developer extension at `/ExtensionLevel 32002`, and a document below PDF
|
|
190
|
+
2.0 gets `/Version /2.0` (see [`pdfSign`](../api/sig/sign.md)). ECDSA and
|
|
191
|
+
RSA-PSS signatures leave the Catalog's `/Extensions` and `/Version` alone.
|
|
192
|
+
|
|
193
|
+
**Adobe Acrobat Reader does not validate Ed25519 (EdDSA) signatures.**
|
|
194
|
+
Measured on 2026-10-05: Acrobat Reader reports an error about the
|
|
195
|
+
formatting of the signature for our Ed25519 output, with or without the
|
|
196
|
+
ISO/TS 32002 declaration, and the same error for an Ed25519 signature
|
|
197
|
+
produced by OpenSSL. In the same run it reports the ECDSA P-256 signature
|
|
198
|
+
as unmodified, with trust and time remarks only (the certificate was
|
|
199
|
+
self-signed). OpenSSL 3.5 and later verify the Ed25519 output with
|
|
200
|
+
`openssl cms -verify`, and so does `pdfSignature.verifyAllSignatures`.
|
|
201
|
+
|
|
202
|
+
Choose the algorithm by where the document will be checked: **ECDSA
|
|
203
|
+
P-256 when Acrobat must validate the signature**; Ed25519 when the
|
|
204
|
+
verifiers are known to support EdDSA.
|
|
205
|
+
|
|
206
|
+
`useSignedAttrs: true` adds the ESS `signing-certificate-v2` attribute
|
|
207
|
+
(RFC 5035); it is required for T/LT/LTA (the timestamp token lives in
|
|
208
|
+
`unsignedAttrs`, only emitted alongside `signedAttrs`) and optional for
|
|
209
|
+
B, where this guide always passes it for a PAdES baseline signature.
|
|
210
|
+
|
|
211
|
+
Levels, in the exact wording every public sentence about them must
|
|
212
|
+
quote:
|
|
213
|
+
|
|
214
|
+
| Level | Claim |
|
|
215
|
+
|---|---|
|
|
216
|
+
| B | "Produces PAdES-B (B-B) signatures in the browser." |
|
|
217
|
+
| T | "Produces PAdES-T signatures; the RFC 3161 timestamp token is supplied by the integrator through a `tsaSign` callback — the library performs no network call." |
|
|
218
|
+
| LT | "Emits the LT structure (DSS with the signer certificates). Revocation material (CRL/OCSP) is supplied by the integrator; the library neither fetches nor validates it." |
|
|
219
|
+
| LTA | "Emits the LTA structure (DSS + document timestamp) and re-verifies the document timestamp on read. The archival *policy* (renewal before algorithm expiry) is the integrator's." |
|
|
220
|
+
|
|
221
|
+
LT and LTA additionally take `opts.dss = { certs: [...] }` — the
|
|
222
|
+
certificates to embed in the Document Security Store:
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
const signedLt = pdfSign.sign(pdfBytes, {
|
|
226
|
+
cert, privateKey, algorithm: 'ed25519',
|
|
227
|
+
hashAlg: 'sha512', level: 'LT',
|
|
228
|
+
useSignedAttrs: true,
|
|
229
|
+
subFilter: 'ETSI.CAdES.detached',
|
|
230
|
+
tsaSign, // see "The tsaSign seam" below
|
|
231
|
+
dss: { certs: [cert] }
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**The signature sits in an invisible signature field.** In the same
|
|
236
|
+
incremental update as the `<< /Type /Sig /Filter /Adobe.PPKLite
|
|
237
|
+
/SubFilter /... /ByteRange [...] /Contents <...> >>` dictionary, `sign()`
|
|
238
|
+
writes a signature field merged with its widget annotation (`/FT /Sig`,
|
|
239
|
+
`/T (Signature1)` — the next free `Signature<n>` when the document
|
|
240
|
+
already has signature fields — `/V` pointing at that dictionary,
|
|
241
|
+
`/Rect [0 0 0 0]`, `/F 132`, `/P` page 1). It also adds the widget to
|
|
242
|
+
page 1's `/Annots` and creates or extends the Catalog's `/AcroForm` with
|
|
243
|
+
`/Fields` and `/SigFlags 3`, as ISO 32000-2 §12.7.5.5 requires. Existing
|
|
244
|
+
form fields and annotations are kept, and the LTA `/DocTimeStamp` gets a
|
|
245
|
+
field of its own. The field has a zero-size rectangle and no `/AP`
|
|
246
|
+
appearance stream. On an encrypted base the field name is encrypted too
|
|
247
|
+
(see [Encrypted documents](#encrypted-documents) below).
|
|
248
|
+
|
|
249
|
+
**The `/ByteRange` leaves out the whole `/Contents <…>` token.** The
|
|
250
|
+
gap between the two signed ranges starts at the `<` and ends after the
|
|
251
|
+
`>`, as ISO 32000-2 §12.8.3.3.1 requires ("it shall fit precisely in the
|
|
252
|
+
space between the ranges") and as PDFBox and pyHanko emit — for the
|
|
253
|
+
`/Sig` and for the LTA `/DocTimeStamp` alike. Signatures `sign()` wrote
|
|
254
|
+
by earlier versions left out the hex digits only; they still verify (see
|
|
255
|
+
[Verifying](#verifying)).
|
|
256
|
+
The signature is invisible; a visible appearance is the integrator's
|
|
257
|
+
document work.
|
|
258
|
+
|
|
259
|
+
### Encrypted documents
|
|
260
|
+
|
|
261
|
+
An encrypted PDF (standard security handler, AES) is signed like any
|
|
262
|
+
other, with its password:
|
|
263
|
+
|
|
264
|
+
```js
|
|
265
|
+
const signedEnc = pdfSign.sign(encryptedBytes, {
|
|
266
|
+
cert, privateKey, algorithm: 'ed25519',
|
|
267
|
+
useSignedAttrs: true,
|
|
268
|
+
subFilter: 'ETSI.CAdES.detached',
|
|
269
|
+
password: 'user-pwd' // owner or user password; '' for an empty user password
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`sign()` derives the document key from `password`, encrypts the new
|
|
274
|
+
field's `/T` with it and repeats the document's `/Encrypt` in the update's
|
|
275
|
+
trailer. With the user password, the document's permissions must allow
|
|
276
|
+
modifying it and adding annotations or form fields; the owner password
|
|
277
|
+
always signs. AES-128 (V=4, `AESV2`) and AES-256 (V=5, R=5 or R=6,
|
|
278
|
+
`AESV3`) are supported. RC4 and AES-GCM documents, handlers other than
|
|
279
|
+
the standard one, a missing or wrong password and missing permissions are
|
|
280
|
+
refused with a `pdf/sign/encrypted-*` error, and nothing is written. The
|
|
281
|
+
IV comes from `crypto.getRandomValues`, or from `opts.randomBytes` when
|
|
282
|
+
you pass one. Levels LT and LTA work on an encrypted document too: the DSS
|
|
283
|
+
streams and strings and the document timestamp's field name are encrypted
|
|
284
|
+
with the same key, and the hexadecimal `/Contents` of the signature and of
|
|
285
|
+
the document timestamp stays clear, as ISO 32000-2 §7.6.2 requires. See
|
|
286
|
+
the [`pdfSign` API page](../api/sig/sign.md#encrypted-base).
|
|
287
|
+
|
|
288
|
+
## Timestamping — the `tsaSign` seam
|
|
289
|
+
|
|
290
|
+
`@awacloud/pdf` never calls a Timestamp Authority itself. The contract:
|
|
291
|
+
|
|
292
|
+
- `pdfSign.sign` never performs a network call and has no TSA client.
|
|
293
|
+
For `level: 'T'|'LT'|'LTA'` it requires a caller-supplied callback, and
|
|
294
|
+
throws `pdf/sign/tsa-required-for-level-T` without one:
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
opts.tsaSign({ digest, hashAlg }) -> Uint8Array // RFC 3161 TimeStampToken DER
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
- `digest` is the hash of the **signature value** (RFC 3161 §2.4.1
|
|
301
|
+
messageImprint over the signature OCTET STRING); the returned token is
|
|
302
|
+
wrapped verbatim into the CMS `unsignedAttrs` id-aa-timeStampToken
|
|
303
|
+
attribute (`_buildUnsignedAttrsTsa`). LTA calls the same callback a
|
|
304
|
+
second time to build the standalone DocTimeStamp.
|
|
305
|
+
|
|
306
|
+
This is the decisive structural fact: **the library's TSA seam is a
|
|
307
|
+
callback, so the network exception is entirely the integrator's**. It is
|
|
308
|
+
never inside `@awacloud/pdf`.
|
|
309
|
+
|
|
310
|
+
This is exactly what row 2 of the claim matrix (§ "What you may claim"
|
|
311
|
+
below) commits to: "Produces PAdES-T signatures; the RFC 3161 timestamp
|
|
312
|
+
token is supplied by the integrator through a `tsaSign` callback — the
|
|
313
|
+
library performs no network call."
|
|
314
|
+
|
|
315
|
+
### Example — an external-TSA `tsaSign`
|
|
316
|
+
|
|
317
|
+
A worked external-TSA `tsaSign` implementation, built with fw's `asn1`
|
|
318
|
+
primitives. The package test suite runs this exact snippet against a
|
|
319
|
+
stubbed TSA reply (no network); a real deployment needs a reachable TSA.
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
import { asn1 as asn1Descriptor } from '@awacloud/fw/crypto/utils/asn1.js';
|
|
323
|
+
const asn1 = asn1Descriptor.factory(); // the descriptor carries no encoders
|
|
324
|
+
|
|
325
|
+
// fw's asn1 has no BOOLEAN encoder (see the guide's Key material
|
|
326
|
+
// section for the same gap on primitive string/time types) — hand-roll
|
|
327
|
+
// the one TLV TimeStampReq needs.
|
|
328
|
+
function encodeBoolean(v) {
|
|
329
|
+
return Uint8Array.of(0x01, 0x01, v ? 0xff : 0x00);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// The messageImprint AlgorithmIdentifier follows the hashAlg sign() passes.
|
|
333
|
+
const HASH_OIDS = {
|
|
334
|
+
sha256: '2.16.840.1.101.3.4.2.1',
|
|
335
|
+
sha384: '2.16.840.1.101.3.4.2.2',
|
|
336
|
+
sha512: '2.16.840.1.101.3.4.2.3'
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
function buildTimeStampReq(digest, hashAlg) {
|
|
340
|
+
const messageImprint = asn1.encodeSequence([
|
|
341
|
+
asn1.encodeSequence([ asn1.encodeOid(HASH_OIDS[hashAlg]), asn1.encodeNull() ]),
|
|
342
|
+
asn1.encodeOctetString(digest)
|
|
343
|
+
]);
|
|
344
|
+
return asn1.encodeSequence([
|
|
345
|
+
asn1.encodeInteger(1), // version
|
|
346
|
+
messageImprint,
|
|
347
|
+
encodeBoolean(true) // certReq
|
|
348
|
+
]);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function tsaSign({ digest, hashAlg }) {
|
|
352
|
+
const body = buildTimeStampReq(digest, hashAlg);
|
|
353
|
+
const res = await fetch(tsaUrl, {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
headers: { 'content-type': 'application/timestamp-query' },
|
|
356
|
+
body
|
|
357
|
+
});
|
|
358
|
+
if (!res.ok || res.headers.get('content-type') !== 'application/timestamp-reply') {
|
|
359
|
+
throw new Error('TSA request failed: ' + res.status);
|
|
360
|
+
}
|
|
361
|
+
const replyBytes = new Uint8Array(await res.arrayBuffer());
|
|
362
|
+
// TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken TimeStampToken OPTIONAL }
|
|
363
|
+
const resp = asn1.parseOne(replyBytes, 0);
|
|
364
|
+
const [statusInfo, token] = (resp && asn1.parseChildren(resp.value)) || [];
|
|
365
|
+
if (!statusInfo) throw new Error('TSA reply is not a TimeStampResp');
|
|
366
|
+
// PKIStatusInfo.status: 0 = granted, 1 = grantedWithMods — both usable.
|
|
367
|
+
// readInteger returns the raw INTEGER bytes; a PKIStatus is a single byte.
|
|
368
|
+
const statusNode = asn1.parseChildren(statusInfo.value)[0];
|
|
369
|
+
const statusBytes = statusNode && asn1.readInteger(statusNode);
|
|
370
|
+
if (!statusBytes || statusBytes.length !== 1) {
|
|
371
|
+
throw new Error('TSA reply carries no PKIStatus');
|
|
372
|
+
}
|
|
373
|
+
const status = statusBytes[0];
|
|
374
|
+
if (status !== 0 && status !== 1) {
|
|
375
|
+
throw new Error('TSA refused: status ' + status);
|
|
376
|
+
}
|
|
377
|
+
if (!token) throw new Error('TSA granted the request but returned no token');
|
|
378
|
+
// The token is the TLV to return, as-is: slice it out of the reply.
|
|
379
|
+
// parseChildren offsets are relative to resp.value, which starts at resp.valueOff.
|
|
380
|
+
const tokenOff = resp.valueOff + statusInfo.next;
|
|
381
|
+
const tokenNode = asn1.parseOne(replyBytes, tokenOff);
|
|
382
|
+
return replyBytes.subarray(tokenOff, tokenNode.next);
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
`sign()` is synchronous, so an async `fetch`-based `tsaSign` cannot be
|
|
387
|
+
plugged in directly — this is a constraint on the integration, not a
|
|
388
|
+
recipe to follow verbatim. A production integration either pre-fetches
|
|
389
|
+
the timestamp token before calling `sign()` and returns it from a
|
|
390
|
+
synchronous callback, or runs the whole signing step as a two-pass
|
|
391
|
+
integration (compute the digest, `await` the TSA, then call `sign()`
|
|
392
|
+
with a synchronous callback closing over the already-fetched token).
|
|
393
|
+
|
|
394
|
+
The demo itself ships neither of these — it uses a mock TSA instead,
|
|
395
|
+
by deliberate decision:
|
|
396
|
+
|
|
397
|
+
> **Mock TSA in-demo. Default and only path shipped. Zero network.**
|
|
398
|
+
>
|
|
399
|
+
> Rationale: the whole product story is "nothing leaves your machine",
|
|
400
|
+
> and the demo's headline artifact is the empty network tab. A live
|
|
401
|
+
> TSA call would spend that proof to demonstrate a feature the
|
|
402
|
+
> integrator supplies anyway. An external TSA also drags in an
|
|
403
|
+
> availability dependency, a CORS surface and, in the EU, a policy
|
|
404
|
+
> discussion the demo should not host.
|
|
405
|
+
>
|
|
406
|
+
> **Demo-UX consequence**: the timestamped levels must
|
|
407
|
+
> be labelled in the UI as demonstrating the *mechanism*, not a
|
|
408
|
+
> trusted time. Required wording, verbatim: *"Timestamped by an
|
|
409
|
+
> in-page demo TSA — the mechanism is real, the time source is not
|
|
410
|
+
> trusted. A production deployment supplies its own TSA through the
|
|
411
|
+
> `tsaSign` callback."* The privacy panel keeps its "0 network
|
|
412
|
+
> requests" claim at every level. The integration guide documents the
|
|
413
|
+
> external-TSA callback with a worked `fetch` example the demo never
|
|
414
|
+
> executes.
|
|
415
|
+
|
|
416
|
+
## Verifying
|
|
417
|
+
|
|
418
|
+
A single call verifies every signature and timestamp object in a
|
|
419
|
+
document:
|
|
420
|
+
|
|
421
|
+
```js
|
|
422
|
+
const report = pdfSignature.verifyAllSignatures(documentBytes);
|
|
423
|
+
// { signatures: [...], timestamps: [...] }
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
(`fwBundle` is an optional second argument — the module carries its own
|
|
427
|
+
default bundle.)
|
|
428
|
+
|
|
429
|
+
Each entry of `report.signatures` carries these fields, exactly as
|
|
430
|
+
measured:
|
|
431
|
+
`computedDigest`, `errors`, `hashAlg`, `objGen`, `objNum`,
|
|
432
|
+
`pkVerified`, `signatureAlg`, `signerCerts`, `verified` — plus one more
|
|
433
|
+
boolean field, `valid`, a deprecated alias of `verified`, kept for
|
|
434
|
+
compatibility; read `verified`.
|
|
435
|
+
|
|
436
|
+
**`verified` also requires a well-formed `/ByteRange` gap.** Each
|
|
437
|
+
`/Sig`'s gap must be exactly its `/Contents` value: the whole `<…>`
|
|
438
|
+
token (what `sign()` emits) or its hex digits only (what `sign()`
|
|
439
|
+
emitted in earlier versions). Any other gap — off by one byte at either
|
|
440
|
+
end, a delimiter only, part of the digits — gives `verified: false`
|
|
441
|
+
with `pdf/sig/byterange/gap-start-mismatch` and/or
|
|
442
|
+
`pdf/sig/byterange/gap-end-mismatch` in `errors`, even when the
|
|
443
|
+
public-key check itself passed (`pkVerified: true`). The digest is
|
|
444
|
+
always computed over the ranges the file declares. The `/DocTimeStamp`
|
|
445
|
+
entries of `report.timestamps` run the same gap check: a
|
|
446
|
+
non-exact gap gives `verified: false` with the same two codes in
|
|
447
|
+
`errors`, even when the imprint matched (`imprintVerified: true`), and
|
|
448
|
+
each entry names the accepted form in `gapForm` — `'token'`,
|
|
449
|
+
`'digits'`, or `null` for any other gap.
|
|
450
|
+
|
|
451
|
+
This is row 5 of the claim matrix, in the exact wording every public
|
|
452
|
+
sentence about verification must quote:
|
|
453
|
+
|
|
454
|
+
> "Reconstructs the PKCS#7, recomputes the `/ByteRange` digest and
|
|
455
|
+
> verifies the signer's public-key signature — reported as `verified`.
|
|
456
|
+
> This is a cryptographic check, **not** signature validation per EN
|
|
457
|
+
> 319 102."
|
|
458
|
+
|
|
459
|
+
The algorithms this covers (row 6): "RSA-PSS and ECDSA with
|
|
460
|
+
SHA-256/384/512; Ed25519 with SHA-512 (RFC 8419), declared in the document
|
|
461
|
+
per ISO/TS 32002. Adobe Acrobat Reader does not validate Ed25519
|
|
462
|
+
signatures: use ECDSA P-256 where Acrobat must validate the signature."
|
|
463
|
+
|
|
464
|
+
**The PAdES level is not on the verify result.** `verifyAllSignatures`
|
|
465
|
+
never returns a `level` or a `subFilter`; `pdfSigPades.detectPadesProfile`
|
|
466
|
+
is the only level source, and it takes three caller-supplied context
|
|
467
|
+
flags, derived from the raw bytes:
|
|
468
|
+
|
|
469
|
+
```js
|
|
470
|
+
const TS_OID_HEX = '2A864886F70D010910020E'; // id-aa-timeStampToken
|
|
471
|
+
|
|
472
|
+
function levelContext(documentBytes) {
|
|
473
|
+
const txt = new TextDecoder('latin1').decode(documentBytes);
|
|
474
|
+
return {
|
|
475
|
+
// Case-insensitive: sign() itself emits uppercase hex, but a
|
|
476
|
+
// /Contents literal from another producer is not guaranteed to be.
|
|
477
|
+
hasSignatureTimestamp: new RegExp(TS_OID_HEX, 'i').test(txt),
|
|
478
|
+
dss: /\/DSS/.test(txt),
|
|
479
|
+
hasDocTimestampOverDss: /\/DocTimeStamp/.test(txt)
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const level = pdfSigPades.detectPadesProfile(sigDict, levelContext(documentBytes)).level;
|
|
484
|
+
// 'B-B' | 'B-T' | 'B-LT' | 'B-LTA' | null
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## The verification report
|
|
488
|
+
|
|
489
|
+
A demo-shaped report layers the raw verify result with the three
|
|
490
|
+
derived context flags into one object. The shape below is the literal
|
|
491
|
+
measured output for a PAdES-T run, as the source monorepo's PAdES demo
|
|
492
|
+
produces it (`apps/demo/pades/src/report.js`, not published). It is the
|
|
493
|
+
output of one demo run; values such as `documentBytes` and the digest
|
|
494
|
+
differ per run.
|
|
495
|
+
|
|
496
|
+
```json
|
|
497
|
+
{
|
|
498
|
+
"levelClaimed": "T",
|
|
499
|
+
"levelDetected": "B-T",
|
|
500
|
+
"isPades": true,
|
|
501
|
+
"subFilter": "ETSI.CAdES.detached",
|
|
502
|
+
"documentBytes": 18671,
|
|
503
|
+
"signatureCount": 1,
|
|
504
|
+
"byteRangeDigest": "b788862920399635f08a6e9b21b6c89b2a131854d9258657e2a3acb6a4f298772a9bb018b0b2a35d700d5827d8c5d8dbaffaad24ab59149f16e4a0f0d196451e",
|
|
505
|
+
"signatureValid": "verified",
|
|
506
|
+
"pkVerified": true,
|
|
507
|
+
"signatureAlg": "ed25519",
|
|
508
|
+
"hashAlg": "sha512",
|
|
509
|
+
"signerCertCount": 1,
|
|
510
|
+
"chain": "not-evaluated",
|
|
511
|
+
"revocation": "not-evaluated",
|
|
512
|
+
"qualified": "not-evaluated",
|
|
513
|
+
"timestamps": [],
|
|
514
|
+
"errors": []
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Design notes:
|
|
519
|
+
|
|
520
|
+
- `signatureValid` is a **string enum**, not a boolean:
|
|
521
|
+
`'verified' | 'not-verified' | 'no-signature'`. It must never be
|
|
522
|
+
rendered as "valid".
|
|
523
|
+
- `chain`, `revocation`, `qualified` are always present and always
|
|
524
|
+
`'not-evaluated'` today. Keeping the keys is what makes the omission
|
|
525
|
+
visible rather than silent; a future version may widen the enum, never
|
|
526
|
+
drop the key.
|
|
527
|
+
- `levelClaimed` vs `levelDetected` are distinct on purpose — the
|
|
528
|
+
first is what the signer asked for, the second is what the bytes
|
|
529
|
+
say.
|
|
530
|
+
- `timestamps[]` entries are only ever DocTimeStamp objects.
|
|
531
|
+
- `errors` carries the typed `pdf/sig/*` codes verbatim; the demo maps
|
|
532
|
+
them to human text, it does not invent them.
|
|
533
|
+
|
|
534
|
+
## Trust-model boundaries
|
|
535
|
+
|
|
536
|
+
`@awacloud/pdf`'s signing surface draws a hard line between what it
|
|
537
|
+
verifies cryptographically and what your PKI has to supply separately:
|
|
538
|
+
|
|
539
|
+
| The library checks | Your PKI must provide |
|
|
540
|
+
|---|---|
|
|
541
|
+
| The `/ByteRange` digest and the signer's public-key signature over it; for each `/Sig` and each `/DocTimeStamp`, that the `/ByteRange` gap is exactly the `/Contents` value (the `<…>` token, or its hex digits for signatures written by earlier versions). | A certificate chain to a trust anchor. `certChain.validateChainOrder` compares issuer/subject **strings** only; it verifies no certificate signature, no validity dates, no name constraints, no trust anchor. Report as `chain: 'not-evaluated'`. |
|
|
542
|
+
| — nothing on read. | Revocation status. DSS `/CRLs` and `/OCSPs` are *typed on read* (`pdfSigPades.typeDSS`); nothing is fetched, parsed for status, or checked. Report as `revocation: 'not-evaluated'`. |
|
|
543
|
+
| — nothing; the concept does not exist in this component. | Qualified / eIDAS status. eIDAS is market context only — "qualified" is a property of a certificate and a validation process your QTSP provides, never a property of `@awacloud/pdf`. No trust-list handling exists here, in any form. |
|
|
544
|
+
| That an RFC 3161 token is well-formed, once written (T/LT); the DocTimeStamp timestamp, on read (LTA). | Trust in the TSA itself, and — for T/LT — verification of the embedded signature-timestamp: the id-aa-timeStampToken attribute is written but never verified on read; only standalone `/DocTimeStamp` objects (LTA) are. |
|
|
545
|
+
| That LT/LTA structures are emitted and, for LTA, that the document timestamp verifies on read. | Archival policy: renewal before algorithm expiry, and any decision about how long a signature must remain provable, are the integrator's. |
|
|
546
|
+
|
|
547
|
+
## What you may claim
|
|
548
|
+
|
|
549
|
+
This table is normative for every public sentence about the component.
|
|
550
|
+
|
|
551
|
+
| # | Capability | Verdict | Exact wording to use | Evidence |
|
|
552
|
+
|---|---|---|---|---|
|
|
553
|
+
| 1 | PAdES-B signing | **C** | "Produces PAdES-B (B-B) signatures in the browser." | measured B sign + verify round trip |
|
|
554
|
+
| 2 | PAdES-T signing | **CWC** | "Produces PAdES-T signatures; the RFC 3161 timestamp token is supplied by the integrator through a `tsaSign` callback — the library performs no network call." | measured T sign; `sign()` throws `pdf/sign/tsa-required-for-level-T` without `tsaSign` |
|
|
555
|
+
| 3 | PAdES-LT signing | **CWC** | "Emits the LT structure (DSS with the signer certificates). Revocation material (CRL/OCSP) is supplied by the integrator; the library neither fetches nor validates it." | measured LT sign |
|
|
556
|
+
| 4 | PAdES-LTA signing | **CWC** | "Emits the LTA structure (DSS + document timestamp) and re-verifies the document timestamp on read. The archival *policy* (renewal before algorithm expiry) is the integrator's." | measured LTA sign; the DocTimeStamp verifies on read |
|
|
557
|
+
| 5 | Signature verification | **CWC** | "Reconstructs the PKCS#7, recomputes the `/ByteRange` digest and verifies the signer's public-key signature — reported as `verified`. This is a cryptographic check, **not** signature validation per EN 319 102." | measured `verified` / `pkVerified` on a signed document |
|
|
558
|
+
| 6 | Algorithms | **CWC** | "RSA-PSS and ECDSA with SHA-256/384/512; Ed25519 with SHA-512 (RFC 8419), declared in the document per ISO/TS 32002. Adobe Acrobat Reader does not validate Ed25519 signatures: use ECDSA P-256 where Acrobat must validate the signature." | `HASH_TABLE` + `_signDigest` in `src/sig/sign.js`; Ed25519 (SHA-512) and ECDSA P-256 / P-384 (DER `ECDSA-Sig-Value`) measured end to end; Acrobat Reader rejection of Ed25519 measured 2026-10-05, also on an OpenSSL-produced Ed25519 signature, which OpenSSL 3.5.6 verifies |
|
|
559
|
+
| 7 | PAdES level detection | **CWC** | "Detects B-B / B-T / B-LT / B-LTA from the signature dictionary — only for `/SubFilter /ETSI.CAdES.detached`; `adbe.pkcs7.detached` is reported as non-PAdES." | measured `detectPadesProfile` on both SubFilters |
|
|
560
|
+
| 8 | Signature-timestamp verification (T/LT) | **NC** | — Do not claim. The embedded id-aa-timeStampToken attribute is written but never verified on read; only standalone `/DocTimeStamp` objects (LTA) are. | no read path verifies the embedded timestamp attribute |
|
|
561
|
+
| 9 | Certificate-chain validation | **NC** | — Do not claim. `certChain.validateChainOrder` compares issuer/subject **strings** only; it verifies no certificate signature, no validity dates, no name constraints, no trust anchor. Report as `chain: 'not-evaluated'`. | `validateChainOrder` in `src/sig/certChain.js` |
|
|
562
|
+
| 10 | Revocation (CRL/OCSP) | **NC** | — Do not claim. DSS `/CRLs` and `/OCSPs` are *typed on read* (`pdfSigPades.typeDSS`); nothing is fetched, parsed for status, or checked. Report as `revocation: 'not-evaluated'`. | `typeDSS` in `src/extra/sig-pades.js` |
|
|
563
|
+
| 11 | Qualified / eIDAS status | **NC** | — Do not claim, in any form, including "eIDAS-ready" or "qualified-capable". No trust-list handling exists. eIDAS may be named only as market context, never as a property of the component. | no code |
|
|
564
|
+
| 12 | Certificate generation | **NC (as a library claim)** | — The demo generates its own self-signed certificate; **`@awacloud/fw` and `@awacloud/pdf` publish no X.509 emitter**. Phrase as "the demo generates a throwaway demo certificate in your browser", never "the library issues certificates". | no X.509 emitter in `@awacloud/fw` or `@awacloud/pdf` |
|
|
565
|
+
| 13 | PKCS#12 import | **NC** | — Do not claim. No PKCS#12 parser exists. Say: "bring your own key material; PKCS#12 unwrapping is integrator-side." PKCS#8 / SEC1 / SPKI / PEM *key* encoding IS available via `@awacloud/fw/crypto/utils/{keyformat,pem}.js`. | no PKCS#12 parser in `@awacloud/fw` or `@awacloud/pdf` |
|
|
566
|
+
| 14 | Client-side / zero-upload | **C** | "The document never leaves the page: signing and verification are pure computation, with zero network calls." | measured zero network calls; `src/sig/sign.js` has no fetch seam |
|
|
567
|
+
| 15 | Zero npm runtime dependency | **C** | "Zero npm runtime dependency: `@awacloud/pdf` depends only on `@awacloud/fw` and `@awacloud/fonts`, both first-party." | `package.json` `dependencies` |
|
|
568
|
+
| 16 | B-LTA end-to-end assurance | **CWC** | "LTA is emitted and its document timestamp verified; a full real-chain PKCS#7 + nested DocTimeStamp end-to-end test is still an open follow-up." | `validateBLtaChain` B→T→LT→LTA round trip (`CHANGELOG.md`) |
|
|
569
|
+
|
|
570
|
+
Legend: **C** = claimable; **CWC** = claimable with the exact caveat
|
|
571
|
+
quoted; **NC** = not claimable.
|
|
572
|
+
|
|
573
|
+
## See also
|
|
574
|
+
|
|
575
|
+
- [Crypto — risks and limitations](./crypto.md)
|
|
576
|
+
- [`pdfSigPades` API reference](../api/extra/sig-pades.md)
|
|
577
|
+
- [`pdfSignature` API reference](../api/sig/signature.md)
|
|
578
|
+
- The `apps/demo/pades` README — the demo application's own guide, in the
|
|
579
|
+
source monorepo (not published)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Read pipeline
|
|
2
|
+
|
|
3
|
+
`api.read(bytes)` runs five successive steps, each wired to an independently documented module. This guide walks them in order and ends with a complete example.
|
|
4
|
+
|
|
5
|
+
**Prerequisites** — the package `@awacloud/pdf` (root entry
|
|
6
|
+
`@awacloud/pdf`, or the committed `@awacloud/pdf/standalone/*` build) and its
|
|
7
|
+
`@awacloud/fw` / `@awacloud/fonts` dependencies; any modern JavaScript runtime
|
|
8
|
+
(browser main thread or Worker, Bun, Node.js 18+).
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
bytes (Uint8Array)
|
|
14
|
+
│
|
|
15
|
+
▼
|
|
16
|
+
[1] readHeader → %PDF-x.y §7.5.2 → pdfDocument
|
|
17
|
+
│
|
|
18
|
+
▼
|
|
19
|
+
[2] locate + parse xref + /Prev chain §7.5.4-7.5.5 → pdfXref
|
|
20
|
+
│
|
|
21
|
+
▼
|
|
22
|
+
[3] typeTrailer → { size, root, info?, prev?, … } → pdfTrailer
|
|
23
|
+
│
|
|
24
|
+
▼
|
|
25
|
+
[4] resolve(root) + typeCatalog §7.7.2 → pdfCatalog
|
|
26
|
+
│
|
|
27
|
+
▼
|
|
28
|
+
[5] walkPageTree + typePage §7.7.3 → pdfPages / pdfPage
|
|
29
|
+
│
|
|
30
|
+
▼
|
|
31
|
+
doc = { version, catalog, pages, trailer, xref, _raw }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Step by step
|
|
35
|
+
|
|
36
|
+
### 1. Header (`readHeader`)
|
|
37
|
+
|
|
38
|
+
Searches for `%PDF-x.y` in the first 1024 bytes (ISO §7.5.2 tolerance). Returns `{ version, end }`. See [`pdfDocument`](../api/document/document.md).
|
|
39
|
+
|
|
40
|
+
### 2. Xref
|
|
41
|
+
|
|
42
|
+
`locateStartXref(bytes)` scans the last 8192 bytes for `startxref`. `readStartXref` reads the numeric offset. The section walk then follows the `/Prev` chain, choosing a form per section from the bytes it finds: `parseXrefTable` for a classical `xref` section, `pdfCrossRefStream.parseCrossRefStream` for a `/Type /XRef` stream (§7.5.8), with `pdfObjStream` materialising anything stored in a `/Type /ObjStm` container (§7.5.7). A classical trailer's `/XRefStm` pointer (hybrid-reference file) is followed as well. See [`pdfXref`](../api/syntax/xref.md).
|
|
43
|
+
|
|
44
|
+
### 3. Trailer
|
|
45
|
+
|
|
46
|
+
`parseTrailerDict` reads the dict, `typeTrailer` extracts `/Size`, `/Root`, and the optional `/Info`, `/Prev`, `/ID`, `/Encrypt`. See [`pdfTrailer`](../api/syntax/trailer.md).
|
|
47
|
+
|
|
48
|
+
### 4. Catalog
|
|
49
|
+
|
|
50
|
+
`resolve(trailer.root)` materialises the Catalog's indirect object; `typeCatalog` extracts `/Pages`, `/Version`, `/PageLayout`, `/Outlines`, `/Metadata`, … See [`pdfCatalog`](../api/document/catalog.md).
|
|
51
|
+
|
|
52
|
+
### 5. Pages
|
|
53
|
+
|
|
54
|
+
`walkPageTree(catalog.pages, resolve)` produces the ordered list of leaf refs; each is resolved then passed to `typePage`. See [`pdfPages`](../api/document/pages.md) and [`pdfPage`](../api/document/page.md).
|
|
55
|
+
|
|
56
|
+
## Indirect-object resolver
|
|
57
|
+
|
|
58
|
+
The document exposes `doc._raw.resolve(ref)` to follow any untyped `{ type: 'ref', num, gen }` (Metadata, Outlines, Annots, …). The `indirects` cache avoids re-materialising the same object twice.
|
|
59
|
+
|
|
60
|
+
## End-to-end example
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import { ModuleRuntime } from '@awacloud/fw/core/runtime.js';
|
|
64
|
+
import { fw_require, pkg_require, modules } from '@awacloud/pdf';
|
|
65
|
+
|
|
66
|
+
const rt = new ModuleRuntime();
|
|
67
|
+
for (const m of fw_require) rt.register(m);
|
|
68
|
+
for (const m of pkg_require) rt.register(m);
|
|
69
|
+
for (const m of modules) rt.register(m);
|
|
70
|
+
|
|
71
|
+
const api = rt.resolve('pdf');
|
|
72
|
+
const doc = api.read(bytes);
|
|
73
|
+
|
|
74
|
+
for (const page of doc.pages) {
|
|
75
|
+
console.log(page.mediaBox, page.rotate, page.contents.length);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Follow an untyped ref (e.g. outlines)
|
|
79
|
+
if (doc.catalog.outlines) {
|
|
80
|
+
const outlines = doc._raw.resolve(doc.catalog.outlines);
|
|
81
|
+
console.log(outlines.entries);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## See also
|
|
86
|
+
|
|
87
|
+
- [Getting started](./getting-started.md)
|
|
88
|
+
- [Coverage](./coverage.md)
|
|
89
|
+
- [`pdfDocument`](../api/document/document.md)
|