mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-03 13:45:49 +02:00
- Replace the `loadFromDocument` and `reset` methods with a single `setDocument` method, since that's consistent with many other viewer components. - Replace the internal `#loadToken` field with simple `pdfDocument` checks, when checking if the document is still current, which again is consistent with (all) other viewer components. - Remove a couple of comments, which didn't add a lot of value and sounded a whole lot like "AI speak".
710 lines
22 KiB
JavaScript
710 lines
22 KiB
JavaScript
/* Copyright 2026 Mozilla Foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { AnnotationEditorType, makeArr, PDFDateString } from "pdfjs-lib";
|
|
|
|
// Per-status descriptor keyed by the status code returned by the verifier.
|
|
// `priority` drives worst-status aggregation, `severity` drives the banner
|
|
// colour bucket, and the two Fluent IDs are the explicit strings used by
|
|
// the banner / status rows (no `${status}` template construction so the
|
|
// IDs are greppable).
|
|
// The status row only ever shows one of three distinct strings: the
|
|
// signature crypto verified, the signature is invalid, or the format is
|
|
// unsupported. Cert chain issues (untrusted / expired / revoked) live
|
|
// on the cert row, so those statuses still report "Signature verified"
|
|
// on the status row.
|
|
const STATUS_ROW_VERIFIED =
|
|
"pdfjs-digital-signature-properties-status-verified";
|
|
const STATUS_ROW_INVALID = "pdfjs-digital-signature-properties-status-invalid";
|
|
const STATUS_ROW_UNKNOWN = "pdfjs-digital-signature-properties-status-unknown";
|
|
|
|
const STATUS_INFO = {
|
|
verified: {
|
|
priority: 0,
|
|
severity: "verified",
|
|
bannerId: "pdfjs-digital-signature-properties-banner-verified",
|
|
statusId: STATUS_ROW_VERIFIED,
|
|
},
|
|
unknown: {
|
|
priority: 1,
|
|
severity: "error",
|
|
bannerId: "pdfjs-digital-signature-properties-banner-unknown",
|
|
statusId: STATUS_ROW_UNKNOWN,
|
|
},
|
|
untrusted: {
|
|
priority: 2,
|
|
severity: "warn",
|
|
bannerId: "pdfjs-digital-signature-properties-banner-untrusted",
|
|
statusId: STATUS_ROW_VERIFIED,
|
|
},
|
|
expired: {
|
|
priority: 3,
|
|
severity: "warn",
|
|
bannerId: "pdfjs-digital-signature-properties-banner-expired",
|
|
statusId: STATUS_ROW_VERIFIED,
|
|
},
|
|
revoked: {
|
|
priority: 4,
|
|
severity: "error",
|
|
bannerId: "pdfjs-digital-signature-properties-banner-revoked",
|
|
statusId: STATUS_ROW_VERIFIED,
|
|
},
|
|
invalid: {
|
|
priority: 5,
|
|
severity: "error",
|
|
bannerId: "pdfjs-digital-signature-properties-banner-invalid",
|
|
statusId: STATUS_ROW_INVALID,
|
|
},
|
|
};
|
|
|
|
const CERT_L10N_IDS = {
|
|
trusted: "pdfjs-digital-signature-properties-certificate-trusted",
|
|
unknown: "pdfjs-digital-signature-properties-certificate-unknown",
|
|
untrusted: "pdfjs-digital-signature-properties-certificate-untrusted",
|
|
expired: "pdfjs-digital-signature-properties-certificate-expired",
|
|
revoked: "pdfjs-digital-signature-properties-certificate-revoked",
|
|
};
|
|
|
|
const CERT_EXPIRED_WITH_DATE_L10N_ID =
|
|
"pdfjs-digital-signature-properties-certificate-expired-with-date";
|
|
|
|
function bannerStateForResults(results) {
|
|
if (results.length === 0) {
|
|
return { worst: "unknown", severity: "error", count: 0 };
|
|
}
|
|
let worst = "verified";
|
|
for (const r of results) {
|
|
if (
|
|
r?.status &&
|
|
STATUS_INFO[r.status].priority > STATUS_INFO[worst].priority
|
|
) {
|
|
worst = r.status;
|
|
}
|
|
}
|
|
// Count how many signatures are at the worst level — this drives the
|
|
// singular/plural variant of the banner message.
|
|
let count = 0;
|
|
for (const r of results) {
|
|
if (r?.status === worst) {
|
|
count++;
|
|
}
|
|
}
|
|
return { worst, severity: STATUS_INFO[worst].severity, count };
|
|
}
|
|
|
|
// For an `untrusted` certificate, pick the most specific Fluent label.
|
|
// When the error code matches one of the recognised cases we have a
|
|
// structured "Certificate: <reason> (<issuer>)" string; otherwise we
|
|
// fall back to the bare "Certificate: Untrusted".
|
|
function untrustedCertLabel(errorCode, issuerCN) {
|
|
const code = (errorCode || "").toUpperCase();
|
|
const args = issuerCN ? { issuer: issuerCN } : null;
|
|
if (code.includes("UNKNOWN_ISSUER") && args) {
|
|
return {
|
|
id: "pdfjs-digital-signature-properties-certificate-untrusted-unknown-issuer",
|
|
args,
|
|
};
|
|
}
|
|
if (code.includes("SELF_SIGNED") && args) {
|
|
return {
|
|
id: "pdfjs-digital-signature-properties-certificate-untrusted-self-signed",
|
|
args,
|
|
};
|
|
}
|
|
if (code.includes("UNTRUSTED_ISSUER") && args) {
|
|
return {
|
|
id: "pdfjs-digital-signature-properties-certificate-untrusted-untrusted-issuer",
|
|
args,
|
|
};
|
|
}
|
|
return {
|
|
id: "pdfjs-digital-signature-properties-certificate-untrusted",
|
|
args: null,
|
|
};
|
|
}
|
|
|
|
// For an `expired` certificate: NSS may have flagged either the leaf
|
|
// (SEC_ERROR_EXPIRED_CERTIFICATE) or any issuer up the chain
|
|
// (SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE). We want the parenthetical
|
|
// to show the date that actually expired, so walk leaf + chain and
|
|
// return the first notAfter that is already in the past as a Date.
|
|
// If nothing is in the past we return null and the caller renders the
|
|
// generic "Certificate: Expired" label without a date.
|
|
function expirationDateForCert(cert) {
|
|
if (!cert) {
|
|
return null;
|
|
}
|
|
const now = Date.now();
|
|
const entries =
|
|
Array.isArray(cert.chain) && cert.chain.length ? cert.chain : [cert];
|
|
// Return the first past notAfter in the chain — that's the cert that
|
|
// actually caused the "expired" verdict. We deliberately do NOT fall
|
|
// back to a future date if no past date is found: NSS sometimes does
|
|
// not surface the expired issuer in `signerCertificate.issuer`, in
|
|
// which case the chain only has the leaf with a still-valid notAfter.
|
|
// Showing that as "Expired (Jan 1, 2027)" would be misleading, so the
|
|
// caller renders the dateless label instead.
|
|
for (const entry of entries) {
|
|
if (typeof entry?.notAfter !== "string" || !entry.notAfter) {
|
|
continue;
|
|
}
|
|
const date = new Date(entry.notAfter);
|
|
const ts = date.getTime();
|
|
if (Number.isFinite(ts) && ts < now) {
|
|
return date;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
class SignaturePropertiesManager {
|
|
#appConfig;
|
|
|
|
#verifier;
|
|
|
|
#eventBus;
|
|
|
|
#signatures = [];
|
|
|
|
#results = new Map(); // signatureId -> VerificationResult
|
|
|
|
#pendingVerify = new Set(); // signatureId set, in-flight
|
|
|
|
#isOpen = false;
|
|
|
|
#isLoading = false;
|
|
|
|
// Set whenever state changes while the panel is closed, so that opening it
|
|
// forces a fresh render. While the panel is hidden, building the list /
|
|
// banner DOM is pure churn — only the toolbar button is visible, and that
|
|
// is updated via #updateButtonState().
|
|
#needsRender = false;
|
|
|
|
#pdfDocument = null;
|
|
|
|
constructor({ appConfig, verifier, eventBus }) {
|
|
this.#appConfig = appConfig;
|
|
this.#verifier = verifier;
|
|
this.#eventBus = eventBus;
|
|
|
|
const button = appConfig.signaturePropertiesButton;
|
|
// Loading dots: three real spans (hidden by `.toolbarButton > span`)
|
|
// that the `state-loading` CSS modifier turns into pulsing circles
|
|
// with staggered `animation-delay`. Real elements (not gradient
|
|
// keyframes) let each dot animate independently.
|
|
for (let i = 0; i < 3; i++) {
|
|
const dot = document.createElement("span");
|
|
dot.className = "loadingDot";
|
|
button.append(dot);
|
|
}
|
|
button.addEventListener("click", () => {
|
|
this.#toggle();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean} `true` while the doorhanger is visible.
|
|
*/
|
|
get isOpen() {
|
|
return this.#isOpen;
|
|
}
|
|
|
|
/**
|
|
* Close the doorhanger if it is open. The viewer's existing Escape
|
|
* handler and outside-click logic call this — the manager doesn't
|
|
* register its own document-level listeners.
|
|
*/
|
|
close() {
|
|
if (this.#isOpen) {
|
|
this.#close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Element} target Click target. The viewer's outside-click
|
|
* handler uses this to decide whether to close the panel.
|
|
* @returns {boolean} `true` if the click is outside both the toolbar
|
|
* button and the doorhanger and the panel should be closed.
|
|
*/
|
|
shouldCloseOnClick(target) {
|
|
if (!this.#isOpen) {
|
|
return false;
|
|
}
|
|
return !(
|
|
this.#appConfig.signaturePropertiesButton.contains(target) ||
|
|
this.#appConfig.signaturePropertiesPanel.contains(target)
|
|
);
|
|
}
|
|
|
|
async setDocument(pdfDocument) {
|
|
if (this.#pdfDocument) {
|
|
this.#signatures = [];
|
|
this.#results.clear();
|
|
this.#pendingVerify.clear();
|
|
this.#needsRender = false;
|
|
this.#hideButton();
|
|
this.#close();
|
|
this.#updateButtonState();
|
|
}
|
|
this.#pdfDocument = pdfDocument;
|
|
|
|
if (!pdfDocument) {
|
|
return;
|
|
}
|
|
this.#signatures = [];
|
|
this.#results.clear();
|
|
this.#pendingVerify.clear();
|
|
this.#isLoading = true;
|
|
this.#render();
|
|
|
|
let signatures;
|
|
try {
|
|
signatures = await pdfDocument.getSignatures();
|
|
} catch (ex) {
|
|
console.warn("getSignatures failed:", ex);
|
|
signatures = [];
|
|
}
|
|
if (pdfDocument !== this.#pdfDocument) {
|
|
return;
|
|
}
|
|
this.#signatures = signatures || [];
|
|
this.#isLoading = false;
|
|
|
|
if (this.#signatures.length === 0) {
|
|
this.#hideButton();
|
|
return;
|
|
}
|
|
this.#showButton();
|
|
|
|
// Seed each signature with an "unknown" placeholder result so the
|
|
// banner / badge / cards have something to render while the worker
|
|
// verifies them in the background.
|
|
for (const sig of this.#signatures) {
|
|
this.#results.set(sig.id, {
|
|
status: "unknown",
|
|
errorCode: null,
|
|
message: null,
|
|
certificate: null,
|
|
documentModifiedAfterSigning: !sig.coversWholeDocument,
|
|
});
|
|
}
|
|
this.#render();
|
|
this.#updateButtonState();
|
|
// Kick off verification automatically — the toolbar button reflects the
|
|
// aggregate state and updates as each signature resolves.
|
|
for (const sig of this.#signatures) {
|
|
this.#verify(sig, pdfDocument);
|
|
}
|
|
}
|
|
|
|
#showButton() {
|
|
const root = this.#appConfig.signaturePropertiesButton.parentElement;
|
|
if (root) {
|
|
root.hidden = false;
|
|
}
|
|
// The separator only makes sense when our toolbar group is visible.
|
|
const sep = this.#appConfig.signaturePropertiesSeparator;
|
|
if (sep) {
|
|
sep.hidden = false;
|
|
}
|
|
}
|
|
|
|
#hideButton() {
|
|
const root = this.#appConfig.signaturePropertiesButton.parentElement;
|
|
if (root) {
|
|
root.hidden = true;
|
|
}
|
|
const sep = this.#appConfig.signaturePropertiesSeparator;
|
|
if (sep) {
|
|
sep.hidden = true;
|
|
}
|
|
}
|
|
|
|
#toggle() {
|
|
if (this.#isOpen) {
|
|
this.#close();
|
|
} else {
|
|
this.#open();
|
|
}
|
|
}
|
|
|
|
#open() {
|
|
this.#isOpen = true;
|
|
// Close any other open editor doorhanger (Ink, FreeText, Highlight, …)
|
|
// and the find bar / secondary toolbar via global onClick — same pattern
|
|
// the Comment doorhanger uses.
|
|
this.#eventBus?.dispatch("switchannotationeditormode", {
|
|
source: this,
|
|
mode: AnnotationEditorType.NONE,
|
|
});
|
|
this.#eventBus?.dispatch("findbarclose", { source: this });
|
|
this.#appConfig.signaturePropertiesPanel.classList.remove("hidden");
|
|
this.#appConfig.signaturePropertiesButton.setAttribute(
|
|
"aria-expanded",
|
|
"true"
|
|
);
|
|
if (this.#needsRender) {
|
|
this.#render();
|
|
}
|
|
}
|
|
|
|
#close() {
|
|
this.#isOpen = false;
|
|
this.#appConfig.signaturePropertiesPanel.classList.add("hidden");
|
|
this.#appConfig.signaturePropertiesButton.setAttribute(
|
|
"aria-expanded",
|
|
"false"
|
|
);
|
|
}
|
|
|
|
#render() {
|
|
if (!this.#isOpen) {
|
|
// Defer DOM work until the user actually opens the panel.
|
|
this.#needsRender = true;
|
|
return;
|
|
}
|
|
this.#needsRender = false;
|
|
const list = this.#appConfig.signaturePropertiesList;
|
|
const banner = this.#appConfig.signaturePropertiesBanner;
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
if (this.#isLoading) {
|
|
// The worker call that populates `#signatures` resolves in tens of
|
|
// milliseconds — fast enough that we'd rather show nothing than
|
|
// flash a placeholder. The toolbar button's loading-dot animation
|
|
// already conveys "work in progress".
|
|
banner.hidden = true;
|
|
list.replaceChildren();
|
|
return;
|
|
}
|
|
|
|
// Banner.
|
|
const { worst, severity, count } = bannerStateForResults([
|
|
...this.#results.values(),
|
|
]);
|
|
banner.replaceChildren();
|
|
banner.hidden = false;
|
|
banner.className = `sigBanner ${severity}`;
|
|
banner.setAttribute("data-l10n-id", STATUS_INFO[worst].bannerId);
|
|
banner.setAttribute("data-l10n-args", JSON.stringify({ count }));
|
|
|
|
// Group sub-signatures under their parent.
|
|
const byParent = new Map();
|
|
const topLevel = [];
|
|
for (const sig of this.#signatures) {
|
|
if (sig.parentId) {
|
|
byParent.getOrInsertComputed(sig.parentId, makeArr).push(sig);
|
|
} else {
|
|
topLevel.push(sig);
|
|
}
|
|
}
|
|
|
|
// Green icons are reserved for the top-level card when *every*
|
|
// signature in the document is verified. Anywhere else (any
|
|
// sub-signature, or a top-level when something further down is
|
|
// expired/untrusted/etc.) keeps the muted grey check.
|
|
const everythingFine = severity === "verified";
|
|
|
|
for (const sig of topLevel) {
|
|
fragment.append(
|
|
this.#renderCard(sig, byParent, /* depth = */ 0, everythingFine)
|
|
);
|
|
}
|
|
list.replaceChildren(fragment);
|
|
}
|
|
|
|
#renderCard(sig, byParent, depth, everythingFine) {
|
|
const subs = byParent.get(sig.id) || [];
|
|
const li = document.createElement("li");
|
|
li.classList.add("sigCard");
|
|
if (depth === 0 && everythingFine) {
|
|
li.classList.add("sigCard--top-allfine");
|
|
}
|
|
|
|
const result = this.#results.get(sig.id);
|
|
|
|
const subjectCN = result?.certificate?.subjectCN;
|
|
if (subjectCN) {
|
|
const signer = document.createElement("div");
|
|
signer.className = "signer";
|
|
signer.textContent = subjectCN;
|
|
li.append(signer);
|
|
}
|
|
|
|
// Status row.
|
|
const statusRow = document.createElement("div");
|
|
statusRow.classList.add("row", `status--${result.status}`);
|
|
const statusLabel = document.createElement("span");
|
|
statusLabel.setAttribute(
|
|
"data-l10n-id",
|
|
STATUS_INFO[result.status].statusId
|
|
);
|
|
statusRow.append(statusLabel);
|
|
li.append(statusRow);
|
|
|
|
if (result.status === "invalid" && result.message) {
|
|
const reason = document.createElement("div");
|
|
reason.className = "detail";
|
|
reason.setAttribute(
|
|
"data-l10n-id",
|
|
"pdfjs-digital-signature-properties-reason"
|
|
);
|
|
reason.setAttribute(
|
|
"data-l10n-args",
|
|
JSON.stringify({ reason: result.message })
|
|
);
|
|
li.append(reason);
|
|
}
|
|
|
|
// Certificate row — skipped when the signature itself is invalid:
|
|
// NSS doesn't return a signerCertificate in that case, so the row
|
|
// would be empty noise next to the more informative "Reason: …"
|
|
// line.
|
|
const cert = result.certificate;
|
|
let certKind = "unknown";
|
|
if (cert) {
|
|
switch (result.status) {
|
|
case "verified":
|
|
certKind = "trusted";
|
|
break;
|
|
case "expired":
|
|
certKind = "expired";
|
|
break;
|
|
case "revoked":
|
|
certKind = "revoked";
|
|
break;
|
|
case "untrusted":
|
|
certKind = "untrusted";
|
|
break;
|
|
default:
|
|
certKind = "unknown";
|
|
}
|
|
}
|
|
if (result.status !== "invalid") {
|
|
const certRow = document.createElement("div");
|
|
certRow.classList.add("row", `cert--${certKind}`);
|
|
const certLabel = document.createElement("span");
|
|
let l10nId = CERT_L10N_IDS[certKind];
|
|
let l10nArgs = null;
|
|
if (cert?.issuerCN && certKind === "trusted") {
|
|
l10nArgs = { issuer: cert.issuerCN };
|
|
} else if (certKind === "expired") {
|
|
// For expired, the parenthetical is the expiration date itself
|
|
// (could be the leaf or any issuer up the chain). Pass a Date
|
|
// through Fluent so the viewer locale formats it, not the
|
|
// browser locale.
|
|
const date = expirationDateForCert(cert);
|
|
if (date) {
|
|
l10nId = CERT_EXPIRED_WITH_DATE_L10N_ID;
|
|
l10nArgs = { dateObj: date.valueOf() };
|
|
}
|
|
} else if (certKind === "untrusted") {
|
|
const label = untrustedCertLabel(result.errorCode, cert?.issuerCN);
|
|
l10nId = label.id;
|
|
l10nArgs = label.args;
|
|
}
|
|
certLabel.setAttribute("data-l10n-id", l10nId);
|
|
if (l10nArgs) {
|
|
certLabel.setAttribute("data-l10n-args", JSON.stringify(l10nArgs));
|
|
}
|
|
certRow.append(certLabel);
|
|
li.append(certRow);
|
|
}
|
|
|
|
if (result.status === "untrusted" && result.message) {
|
|
const detail = document.createElement("div");
|
|
detail.className = "detail";
|
|
detail.textContent = result.message;
|
|
li.append(detail);
|
|
}
|
|
if (result.status === "expired" && result.message) {
|
|
const detail = document.createElement("div");
|
|
detail.className = "detail";
|
|
detail.textContent = result.message;
|
|
li.append(detail);
|
|
}
|
|
|
|
const signingDate = PDFDateString.toDateObject(sig.signingTime);
|
|
if (signingDate) {
|
|
const ts = document.createElement("div");
|
|
ts.className = "detail";
|
|
ts.setAttribute(
|
|
"data-l10n-id",
|
|
"pdfjs-digital-signature-properties-timestamp"
|
|
);
|
|
ts.setAttribute(
|
|
"data-l10n-args",
|
|
JSON.stringify({ dateObj: signingDate.valueOf() })
|
|
);
|
|
li.append(ts);
|
|
}
|
|
|
|
if (cert && typeof this.#verifier?.viewCertificate === "function") {
|
|
const viewCert = document.createElement("button");
|
|
viewCert.className = "viewCert";
|
|
viewCert.type = "button";
|
|
viewCert.setAttribute(
|
|
"data-l10n-id",
|
|
"pdfjs-digital-signature-properties-view-certificate"
|
|
);
|
|
viewCert.addEventListener("click", e => {
|
|
e.stopPropagation();
|
|
this.#verifier.viewCertificate(cert);
|
|
});
|
|
li.append(viewCert);
|
|
}
|
|
|
|
if (subs.length > 0) {
|
|
const subList = document.createElement("ul");
|
|
subList.classList.add("signaturePropertiesList", "nested");
|
|
for (const sub of subs) {
|
|
subList.append(
|
|
this.#renderCard(sub, byParent, depth + 1, everythingFine)
|
|
);
|
|
}
|
|
|
|
if (depth === 0) {
|
|
// Only the top-level card gets the collapsible header. Deeper
|
|
// signatures are always rendered inline; the nested border + indent
|
|
// already shows the parent→child relationship.
|
|
const details = document.createElement("details");
|
|
details.className = "subSignatures";
|
|
details.open = true;
|
|
const summary = document.createElement("summary");
|
|
summary.setAttribute(
|
|
"data-l10n-id",
|
|
"pdfjs-digital-signature-properties-sub-signatures"
|
|
);
|
|
summary.setAttribute(
|
|
"data-l10n-args",
|
|
JSON.stringify({ count: this.#countDescendants(sig.id, byParent) })
|
|
);
|
|
details.append(summary);
|
|
details.append(subList);
|
|
li.append(details);
|
|
} else {
|
|
li.append(subList);
|
|
}
|
|
}
|
|
|
|
// Note: per-card progress is already conveyed via the toolbar
|
|
// button's loading-dot animation (state-loading class) — we used to
|
|
// drop an additional pulsing bar at the bottom of each in-flight
|
|
// card but it was visual noise without clear meaning.
|
|
|
|
return li;
|
|
}
|
|
|
|
#countDescendants(id, byParent) {
|
|
const direct = byParent.get(id);
|
|
if (!direct) {
|
|
return 0;
|
|
}
|
|
let total = direct.length;
|
|
for (const sub of direct) {
|
|
total += this.#countDescendants(sub.id, byParent);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
async #verify(signature, pdfDocument) {
|
|
if (!this.#verifier || this.#pendingVerify.has(signature.id)) {
|
|
return;
|
|
}
|
|
this.#pendingVerify.add(signature.id);
|
|
this.#render();
|
|
|
|
let result;
|
|
try {
|
|
// Fetch the signature's byte payload (pkcs7 + signed-data spans)
|
|
// lazily, one signature at a time, so the bytes never sit in main
|
|
// thread memory for the document's lifetime. `bytes` goes out of
|
|
// scope as soon as the verifier returns.
|
|
const bytes = await pdfDocument.getSignatureData(signature.id);
|
|
if (pdfDocument !== this.#pdfDocument) {
|
|
return;
|
|
}
|
|
if (!bytes) {
|
|
throw new Error("missing signature data");
|
|
}
|
|
result = await this.#verifier.verify({ ...signature, ...bytes });
|
|
} catch (ex) {
|
|
console.warn("signature verify failed:", ex);
|
|
result = {
|
|
status: "unknown",
|
|
errorCode: "BRIDGE_ERROR",
|
|
message: ex?.message ?? null,
|
|
certificate: null,
|
|
documentModifiedAfterSigning: !signature.coversWholeDocument,
|
|
};
|
|
}
|
|
this.#pendingVerify.delete(signature.id);
|
|
if (pdfDocument !== this.#pdfDocument) {
|
|
// The user switched documents while this verify was in flight; the
|
|
// result belongs to a defunct load and would corrupt the new doc.
|
|
return;
|
|
}
|
|
this.#results.set(signature.id, result);
|
|
this.#render();
|
|
this.#updateButtonState();
|
|
}
|
|
|
|
#updateButtonState() {
|
|
const button = this.#appConfig.signaturePropertiesButton;
|
|
button.classList.remove(
|
|
"state-loading",
|
|
"state-verified",
|
|
"state-warn",
|
|
"state-error"
|
|
);
|
|
if (this.#signatures.length === 0) {
|
|
return;
|
|
}
|
|
if (this.#pendingVerify.size > 0) {
|
|
button.classList.add("state-loading");
|
|
return;
|
|
}
|
|
let worst = "verified";
|
|
for (const r of this.#results.values()) {
|
|
if (!r) {
|
|
continue;
|
|
}
|
|
if (STATUS_INFO[r.status].priority > STATUS_INFO[worst].priority) {
|
|
worst = r.status;
|
|
}
|
|
}
|
|
switch (worst) {
|
|
case "invalid":
|
|
case "revoked":
|
|
case "unknown":
|
|
// `unknown` means the verifier completed but could not give a
|
|
// definitive answer (unsupported subfilter, bridge error,
|
|
// CMS NOT_YET_ATTEMPTED). Treat that as a verification failure
|
|
// — the loading dots are reserved for the in-flight case
|
|
// handled above.
|
|
button.classList.add("state-error");
|
|
break;
|
|
case "expired":
|
|
case "untrusted":
|
|
button.classList.add("state-warn");
|
|
break;
|
|
default:
|
|
button.classList.add("state-verified");
|
|
}
|
|
}
|
|
}
|
|
|
|
export { SignaturePropertiesManager };
|