Merge pull request #21652 from Snuffleupagus/signatures-async-helpers

Update the signatures-helpers to actually handle `MissingDataException`s
This commit is contained in:
Tim van der Meij 2026-07-28 21:34:14 +02:00 committed by GitHub
commit f704f2ef02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 99 additions and 52 deletions

View File

@ -1996,7 +1996,7 @@ class PDFDocument {
return shadow(this, "fieldObjects", promise); return shadow(this, "fieldObjects", promise);
} }
#collectSignatureFields(fields, out, visitedRefs) { async #collectSignatureFields(fields, out, visitedRefs) {
if (!Array.isArray(fields)) { if (!Array.isArray(fields)) {
return; return;
} }
@ -2007,14 +2007,18 @@ class PDFDocument {
} }
visitedRefs.put(fieldRef); visitedRefs.put(fieldRef);
} }
const field = this.xref.fetchIfRef(fieldRef); const field = await this.xref.fetchIfRefAsync(fieldRef);
if (!(field instanceof Dict)) { if (!(field instanceof Dict)) {
continue; continue;
} }
if (isName(field.get("FT"), "Sig")) { if (isName(await field.getAsync("FT"), "Sig")) {
const sigDict = this.xref.fetchIfRef(field.get("V")); const sigDict = await field.getAsync("V");
if (sigDict instanceof Dict) { if (sigDict instanceof Dict) {
const parsed = this.#parseSignatureDict(field, sigDict, fieldRef); const parsed = await this.#parseSignatureDict(
field,
sigDict,
fieldRef
);
if (parsed) { if (parsed) {
out.push(parsed); out.push(parsed);
} }
@ -2023,7 +2027,11 @@ class PDFDocument {
if (field.has("Kids")) { if (field.has("Kids")) {
// A terminal field can have Widget annotations as children, so its // A terminal field can have Widget annotations as children, so its
// own signature must be collected before walking the field tree. // own signature must be collected before walking the field tree.
this.#collectSignatureFields(field.get("Kids"), out, visitedRefs); await this.#collectSignatureFields(
await field.getAsync("Kids"),
out,
visitedRefs
);
} }
} }
} }
@ -2070,8 +2078,8 @@ class PDFDocument {
return true; return true;
} }
#parseSignatureDict(field, sigDict, fieldRef) { async #parseSignatureDict(field, sigDict, fieldRef) {
const byteRange = sigDict.get("ByteRange"); const byteRange = await sigDict.getAsync("ByteRange");
if ( if (
!Array.isArray(byteRange) || !Array.isArray(byteRange) ||
byteRange.length !== 4 || byteRange.length !== 4 ||
@ -2079,31 +2087,13 @@ class PDFDocument {
) { ) {
return null; return null;
} }
const contents = sigDict.get("Contents");
if (typeof contents !== "string" || contents.length === 0) {
return null;
}
const filterName = sigDict.get("Filter");
const filter = filterName instanceof Name ? filterName.name : null;
const subFilterName = sigDict.get("SubFilter");
const subFilter = subFilterName instanceof Name ? subFilterName.name : null;
let signatureType = null;
if (subFilter === "adbe.pkcs7.detached") {
signatureType = 0;
} else if (subFilter === "adbe.pkcs7.sha1") {
signatureType = 1;
}
// Slice the two ByteRange byte spans out of the underlying PDF stream. // Slice the two ByteRange byte spans out of the underlying PDF stream.
// ByteRange = [a, b, c, d] means signed bytes are [a..a+b] and [c..c+d]; // ByteRange = [a, b, c, d] means signed bytes are [a..a+b] and [c..c+d];
// the gap covers the /Contents hex blob itself. // the gap covers the /Contents hex blob itself.
const [a, b, c, d] = byteRange; const [a, b, c, d] = byteRange;
const stream = this.stream;
// `/ByteRange` offsets are absolute, so compare against `stream.end` // `/ByteRange` offsets are absolute, so compare against `stream.end`
// (raw buffer end), not `stream.length` (post-`moveStart` payload). // (raw buffer end), not `stream.length` (post-`moveStart` payload).
const fileLength = stream.end || 0; const fileLength = this.stream.end || 0;
// Reject signatures whose /ByteRange is structurally implausible: it // Reject signatures whose /ByteRange is structurally implausible: it
// must start at the file head, define a non-empty first span, leave // must start at the file head, define a non-empty first span, leave
// room for the /Contents blob between the two spans, and fit within // room for the /Contents blob between the two spans, and fit within
@ -2118,22 +2108,46 @@ class PDFDocument {
) { ) {
return null; return null;
} }
const pkcs7 = stringToBytes(contents);
const t = field.get("T"); const contents = await sigDict.getAsync("Contents");
const fieldName = typeof t === "string" ? stringToPDFString(t) : ""; if (typeof contents !== "string" || contents.length === 0) {
const name = sigDict.get("Name"); return null;
const reason = sigDict.get("Reason"); }
const location = sigDict.get("Location");
const contactInfo = sigDict.get("ContactInfo");
const m = sigDict.get("M");
const [
filterName,
subFilterName,
t,
name,
reason,
location,
contactInfo,
m,
] = await Promise.all([
sigDict.getAsync("Filter"),
sigDict.getAsync("SubFilter"),
field.getAsync("T"),
sigDict.getAsync("Name"),
sigDict.getAsync("Reason"),
sigDict.getAsync("Location"),
sigDict.getAsync("ContactInfo"),
sigDict.getAsync("M"),
]);
const filter = filterName instanceof Name ? filterName.name : null,
subFilter = subFilterName instanceof Name ? subFilterName.name : null;
let signatureType = null;
if (subFilter === "adbe.pkcs7.detached") {
signatureType = 0;
} else if (subFilter === "adbe.pkcs7.sha1") {
signatureType = 1;
}
const refKey = fieldRef instanceof Ref ? fieldRef.toString() : "inline"; const refKey = fieldRef instanceof Ref ? fieldRef.toString() : "inline";
const id = `${refKey}:${a}-${b}-${c}-${d}`;
return { return {
id, id: `${refKey}:${a}-${b}-${c}-${d}`,
fieldName, fieldName: typeof t === "string" ? stringToPDFString(t) : "",
signerName: typeof name === "string" ? stringToPDFString(name) : null, signerName: typeof name === "string" ? stringToPDFString(name) : null,
reason: typeof reason === "string" ? stringToPDFString(reason) : null, reason: typeof reason === "string" ? stringToPDFString(reason) : null,
location: location:
@ -2145,7 +2159,7 @@ class PDFDocument {
subFilter, subFilter,
signatureType, signatureType,
byteRange, byteRange,
pkcs7, pkcs7: stringToBytes(contents),
revisionIndex: 0, revisionIndex: 0,
parentId: null, parentId: null,
}; };
@ -2165,7 +2179,7 @@ class PDFDocument {
const fields = annotationGlobals.acroForm.get("Fields"); const fields = annotationGlobals.acroForm.get("Fields");
const collected = []; const collected = [];
this.#collectSignatureFields(fields, collected, new RefSet()); await this.#collectSignatureFields(fields, collected, new RefSet());
await Promise.all( await Promise.all(
collected.map(async signature => { collected.map(async signature => {

View File

@ -322,6 +322,10 @@ describe("document", function () {
{ ref: sigRef, data: sigDict }, { ref: sigRef, data: sigDict },
{ ref: fieldRef, data: fieldDict }, { ref: fieldRef, data: fieldDict },
]); ]);
acroForm.assignXref(xref);
sigDict.assignXref(xref);
fieldDict.assignXref(xref);
acroForm.set("Fields", [fieldRef]); acroForm.set("Fields", [fieldRef]);
const pdfDocument = getDocument(acroForm, xref); const pdfDocument = getDocument(acroForm, xref);
@ -362,6 +366,10 @@ describe("document", function () {
{ ref: sigRef, data: sigDict }, { ref: sigRef, data: sigDict },
{ ref: fieldRef, data: fieldDict }, { ref: fieldRef, data: fieldDict },
]); ]);
acroForm.assignXref(xref);
sigDict.assignXref(xref);
fieldDict.assignXref(xref);
acroForm.set("Fields", [fieldRef]); acroForm.set("Fields", [fieldRef]);
const documentStream = new StringStream( const documentStream = new StringStream(
@ -386,6 +394,10 @@ describe("document", function () {
{ ref: sigRef, data: sigDict }, { ref: sigRef, data: sigDict },
{ ref: fieldRef, data: fieldDict }, { ref: fieldRef, data: fieldDict },
]); ]);
acroForm.assignXref(xref);
sigDict.assignXref(xref);
fieldDict.assignXref(xref);
acroForm.set("Fields", [fieldRef]); acroForm.set("Fields", [fieldRef]);
const documentStream = new StringStream( const documentStream = new StringStream(
@ -418,6 +430,11 @@ describe("document", function () {
{ ref: sigFieldRef, data: sigField }, { ref: sigFieldRef, data: sigField },
{ ref: containerRef, data: container }, { ref: containerRef, data: container },
]); ]);
acroForm.assignXref(xref);
sigDict.assignXref(xref);
sigField.assignXref(xref);
container.assignXref(xref);
acroForm.set("Fields", [containerRef]); acroForm.set("Fields", [containerRef]);
const pdfDocument = getDocument(acroForm, xref); const pdfDocument = getDocument(acroForm, xref);
@ -450,6 +467,11 @@ describe("document", function () {
{ ref: sigFieldRef, data: sigField }, { ref: sigFieldRef, data: sigField },
{ ref: widgetRef, data: widget }, { ref: widgetRef, data: widget },
]); ]);
acroForm.assignXref(xref);
sigDict.assignXref(xref);
sigField.assignXref(xref);
widget.assignXref(xref);
acroForm.set("Fields", [sigFieldRef]); acroForm.set("Fields", [sigFieldRef]);
const pdfDocument = getDocument(acroForm, xref); const pdfDocument = getDocument(acroForm, xref);
@ -472,6 +494,10 @@ describe("document", function () {
{ ref: sigRef, data: sigDict }, { ref: sigRef, data: sigDict },
{ ref: fieldRef, data: fieldDict }, { ref: fieldRef, data: fieldDict },
]); ]);
acroForm.assignXref(xref);
sigDict.assignXref(xref);
fieldDict.assignXref(xref);
acroForm.set("Fields", [fieldRef]); acroForm.set("Fields", [fieldRef]);
const pdfDocument = getDocument(acroForm, xref); const pdfDocument = getDocument(acroForm, xref);
@ -498,18 +524,21 @@ describe("document", function () {
name: "Inner", name: "Inner",
}); });
const outerField = makeSigField({ T: "outer", sigRef: outerSigRef });
const innerField = makeSigField({ T: "inner", sigRef: innerSigRef });
const xref = new XRefMock([ const xref = new XRefMock([
{ ref: outerSigRef, data: outerSig }, { ref: outerSigRef, data: outerSig },
{ { ref: outerFieldRef, data: outerField },
ref: outerFieldRef,
data: makeSigField({ T: "outer", sigRef: outerSigRef }),
},
{ ref: innerSigRef, data: innerSig }, { ref: innerSigRef, data: innerSig },
{ { ref: innerFieldRef, data: innerField },
ref: innerFieldRef,
data: makeSigField({ T: "inner", sigRef: innerSigRef }),
},
]); ]);
acroForm.assignXref(xref);
outerSig.assignXref(xref);
innerSig.assignXref(xref);
outerField.assignXref(xref);
innerField.assignXref(xref);
acroForm.set("Fields", [outerFieldRef, innerFieldRef]); acroForm.set("Fields", [outerFieldRef, innerFieldRef]);
const pdfDocument = getDocument(acroForm, xref); const pdfDocument = getDocument(acroForm, xref);
@ -535,14 +564,18 @@ describe("document", function () {
byteRange: [0, 10, 20, 30], byteRange: [0, 10, 20, 30],
subFilter, subFilter,
}); });
const sigField = makeSigField({ T: "sig", sigRef });
const xref = new XRefMock([ const xref = new XRefMock([
{ ref: sigRef, data: sigDict }, { ref: sigRef, data: sigDict },
{ { ref: fieldRef, data: sigField },
ref: fieldRef,
data: makeSigField({ T: "sig", sigRef }),
},
]); ]);
acroForm.assignXref(xref);
sigDict.assignXref(xref);
sigField.assignXref(xref);
acroForm.set("Fields", [fieldRef]); acroForm.set("Fields", [fieldRef]);
const pdfDocument = getDocument(acroForm, xref); const pdfDocument = getDocument(acroForm, xref);
const [sig] = await pdfDocument.signatures; const [sig] = await pdfDocument.signatures;
return sig.signatureType; return sig.signatureType;