From c188012280dbf08a1ad124b615cd6b6b066a9051 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 5 Jul 2026 12:15:40 +0200 Subject: [PATCH] Add a `FakeSignatureVerifier` for development mode and TESTING builds (PR 21247 follow-up) Despite the `enableSignatureVerification` option/preference being enabled in development mode and TESTING builds, the lack of any `SignatureVerifier` implementation still renders the UI disabled. This seems unfortunate, since it makes it more difficult to check that the digital signature UI works correctly: - That the button is visible when it's supposed to be, and that the panel can be opened/closed. - That the UI looks correct, w.r.t. the HTML elements and their CSS rules. - Currently the viewer JS code is effectively uncovered, with overall test-coverage dropping from `90` to `89` percent when PR 21247 landed. To improve the current situation this patch adds a `FakeSignatureVerifier` class, limited to only development mode and TESTING builds, that treats every digital signature as invalid. This allows easier manual testing, and also the addition of a few *very rudimentary* integration-tests. *Note:* It probably wouldn't be all that difficult to add additional integration-tests for *valid* certificates, by having the test-cases instruct (during setup) the `FakeSignatureVerifier` how to respond to simulate verified certificates. --- test/integration/digital_signature_spec.mjs | 107 ++++++++++++++++++++ test/integration/jasmine-boot.js | 1 + web/external_services.js | 3 +- web/genericcom.js | 34 +++++++ 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 test/integration/digital_signature_spec.mjs diff --git a/test/integration/digital_signature_spec.mjs b/test/integration/digital_signature_spec.mjs new file mode 100644 index 000000000..3e294ea0c --- /dev/null +++ b/test/integration/digital_signature_spec.mjs @@ -0,0 +1,107 @@ +/* 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 { closePages, FSI, loadAndWait, PDI } from "./test_utils.mjs"; + +describe("Digital signatures", () => { + describe("Document without signatures", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".textLayer .endOfContent"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("check that the signatureProperties button is hidden", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const buttonHidden = await page.$eval( + "#signatureProperties", + el => el.hidden + ); + expect(buttonHidden).withContext(`In ${browserName}`).toBeTrue(); + + const separatorHidden = await page.$eval( + "#signaturePropertiesSeparator", + el => el.hidden + ); + expect(separatorHidden).withContext(`In ${browserName}`).toBeTrue(); + }) + ); + }); + }); + + describe("Document with signatures", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("issue20433.pdf", ".textLayer .endOfContent"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("check that the signatureProperties button is visible", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const buttonHidden = await page.$eval( + "#signatureProperties", + el => el.hidden + ); + expect(buttonHidden).withContext(`In ${browserName}`).toBeFalse(); + + const separatorHidden = await page.$eval( + "#signaturePropertiesSeparator", + el => el.hidden + ); + expect(separatorHidden).withContext(`In ${browserName}`).toBeFalse(); + }) + ); + }); + + it("check that the signatureProperties panel contains two signatures", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.click("#signatureProperties"); + await page.waitForSelector("#signaturePropertiesPanel", { + hidden: false, + }); + + await page.waitForFunction( + `document.getElementById("signaturePropertiesBanner").textContent !== ""` + ); + const bannerMsg = await page.$eval( + "#signaturePropertiesBanner", + el => el.textContent + ); + expect(bannerMsg) + .withContext(`In ${browserName}`) + .toEqual( + `Document signed but ${FSI}2${PDI} digital signatures could not be verified` + ); + + await page.keyboard.press("Escape"); + await page.waitForSelector("#signaturePropertiesPanel", { + hidden: true, + }); + }) + ); + }); + }); +}); diff --git a/test/integration/jasmine-boot.js b/test/integration/jasmine-boot.js index 9a11871da..d31195025 100644 --- a/test/integration/jasmine-boot.js +++ b/test/integration/jasmine-boot.js @@ -34,6 +34,7 @@ async function runTests(results) { "comment_spec.mjs", "copy_paste_spec.mjs", "cursor_tools_spec.mjs", + "digital_signature_spec.mjs", "document_properties_spec.mjs", "find_spec.mjs", "freetext_editor_spec.mjs", diff --git a/web/external_services.js b/web/external_services.js index 4f767fbdd..7b43d05c0 100644 --- a/web/external_services.js +++ b/web/external_services.js @@ -61,8 +61,7 @@ class BaseExternalServices { * Properties UI should subclass `BaseExternalServices` and return * an object exposing `verify(signature)` (and optionally * `viewCertificate(certificate)`) that resolves to a - * `VerificationResult` — see `web/firefoxcom.js` for the exact - * shape. + * `VerificationResult` — see `web/firefoxcom.js` for the exact shape. * * @returns {Object|null} */ diff --git a/web/genericcom.js b/web/genericcom.js index c0f7e8877..33e573080 100644 --- a/web/genericcom.js +++ b/web/genericcom.js @@ -66,6 +66,13 @@ class ExternalServices extends BaseExternalServices { createSignatureStorage(eventBus, signal) { return new SignatureStorage(eventBus, signal); } + + createSignatureVerifier() { + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { + return new FakeSignatureVerifier(); + } + return null; + } } class MLManager { @@ -166,6 +173,33 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { this.enableGuessAltText = enabled; } }; + + // eslint-disable-next-line no-var + var FakeSignatureVerifier = class { + async verify(signature) { + if (signature.signatureType === null) { + return { + status: "unknown", + errorCode: "SUBFILTER_NOT_SUPPORTED", + message: signature.subFilter, + certificate: null, + documentModifiedAfterSigning: !signature.coversWholeDocument, + }; + } + + return { + status: "unknown", + errorCode: "EMPTY_RESPONSE", + message: null, + certificate: null, + documentModifiedAfterSigning: !signature.coversWholeDocument, + }; + } + + async viewCertificate(certificate) { + return false; + } + }; } export { ExternalServices, initCom, MLManager, Preferences };