Add strict mode

This commit is contained in:
Jill Regan
2026-05-21 11:46:38 -04:00
parent d463472f19
commit d1dad6d749
4 changed files with 48 additions and 15 deletions
@@ -72,6 +72,28 @@ describe("verifyAuthenticodeSignature", () => {
/expected publisher EKU.*not found/, /expected publisher EKU.*not found/,
); );
}); });
it("loose mode passes for a Sectigo-issued cert (no Microsoft CS AOC CA issuer)", async () => {
const runner = powershellRunner(
buildAuthenticodeOutput({
issuer:
"CN=Sectigo Public Code Signing CA R36, O=Sectigo Limited, C=GB",
ekus: ["1.3.6.1.5.5.7.3.3"],
}),
);
await expect(
verifyAuthenticodeSignature(OP_EXE, runner, false),
).resolves.toBeUndefined();
});
it("loose mode still rejects an unsigned or wrong-publisher binary", async () => {
const runner = powershellRunner(
buildAuthenticodeOutput({ subject: "CN=Attacker, O=Attacker, C=US" }),
);
await expect(
verifyAuthenticodeSignature(OP_EXE, runner, false),
).rejects.toThrow(/does not contain CN=Agilebits/);
});
}); });
describe("isAzureSignedEra", () => { describe("isAzureSignedEra", () => {
@@ -38,12 +38,19 @@ const defaultPowerShellRunner = async (script: string): Promise<string> => {
return stdout; return stdout;
}; };
// Strict Authenticode check against 1Password's Azure Trusted Signing cert. // Authenticode check against 1Password's signing cert.
// Throws unless Status is Valid, signer is AgileBits, issuer is a Microsoft //
// CS AOC CA, and the publisher EKU is present. // Strict mode (default, for Azure Trusted Signing era): throws unless Status
// is Valid, signer is AgileBits, issuer is a Microsoft CS AOC CA, and the
// publisher EKU is present.
//
// Loose mode (for Sectigo era, pre-v2.31.0): only checks Status is Valid and
// signer is AgileBits. The Sectigo-issued cert has no Microsoft issuer and no
// publisher EKU, so the strict checks don't apply.
export const verifyAuthenticodeSignature = async ( export const verifyAuthenticodeSignature = async (
opExePath: string, opExePath: string,
runPowerShell: (script: string) => Promise<string> = defaultPowerShellRunner, runPowerShell: (script: string) => Promise<string> = defaultPowerShellRunner,
strict = true,
): Promise<void> => { ): Promise<void> => {
// Read the four Authenticode fields we validate below. // Read the four Authenticode fields we validate below.
const escapedPath = opExePath.replace(/'/g, "''"); const escapedPath = opExePath.replace(/'/g, "''");
@@ -56,6 +63,8 @@ export const verifyAuthenticodeSignature = async (
].join("; "); ].join("; ");
const output = await runPowerShell(script); const output = await runPowerShell(script);
// TEMPORARY DEBUG — remove before merging.
console.info(`Authenticode raw output:\n${output}`);
const outputLines = output.split("\n").map((l) => l.trim()); const outputLines = output.split("\n").map((l) => l.trim());
const fieldValue = (prefix: string): string | undefined => { const fieldValue = (prefix: string): string | undefined => {
@@ -82,6 +91,12 @@ export const verifyAuthenticodeSignature = async (
); );
} }
// Loose mode (Sectigo era) stops here. Sectigo certs aren't issued by a
// Microsoft CS AOC CA, so the strict issuer check below doesn't apply.
if (!strict) {
return;
}
// Confirm the cert was issued by Microsoft's expected code signing CA. // Confirm the cert was issued by Microsoft's expected code signing CA.
const issuer = fieldValue("Issuer=") ?? ""; const issuer = fieldValue("Issuer=") ?? "";
if (!issuer.includes(`CN=${WINDOWS_ISSUER_CN_PREFIX}`)) { if (!issuer.includes(`CN=${WINDOWS_ISSUER_CN_PREFIX}`)) {
@@ -16,9 +16,6 @@ jest.mock("./windows-signature", () => ({
verifyAuthenticodeSignature: jest.fn().mockResolvedValue(undefined), verifyAuthenticodeSignature: jest.fn().mockResolvedValue(undefined),
isAzureSignedEra: jest.fn().mockReturnValue(true), isAzureSignedEra: jest.fn().mockReturnValue(true),
})); }));
jest.mock("./gpg-signature", () => ({
verifyGpgSignature: jest.fn().mockResolvedValue(undefined),
}));
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); jest.restoreAllMocks();
@@ -9,7 +9,6 @@ import {
cliUrlBuilder, cliUrlBuilder,
type SupportedPlatform, type SupportedPlatform,
} from "./cli-installer"; } from "./cli-installer";
import { verifyGpgSignature } from "./gpg-signature";
import type { Installer } from "./installer"; import type { Installer } from "./installer";
import { import {
isAzureSignedEra, isAzureSignedEra,
@@ -40,14 +39,14 @@ export class WindowsInstaller extends CliInstaller implements Installer {
core.info("Verifying 1Password CLI signature"); core.info("Verifying 1Password CLI signature");
const opExePath = path.join(extractedPath, "op.exe"); const opExePath = path.join(extractedPath, "op.exe");
if (isAzureSignedEra(this.version)) { // Azure-era (v2.31.0+): strict Authenticode (matches current docs).
await verifyAuthenticodeSignature(opExePath); // Sectigo-era (pre-v2.31.0): loose Authenticode (Subject + Status only;
} else { // the Sectigo cert lacks the Microsoft issuer and publisher EKU).
await verifyGpgSignature( await verifyAuthenticodeSignature(
opExePath, opExePath,
path.join(extractedPath, "op.exe.sig"), undefined,
isAzureSignedEra(this.version),
); );
}
core.info("1Password CLI signature verified"); core.info("1Password CLI signature verified");
core.addPath(extractedPath); core.addPath(extractedPath);