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