diff --git a/src/op-cli-installer/github-action/cli-installer/windows-signature.test.ts b/src/op-cli-installer/github-action/cli-installer/windows-signature.test.ts index 2d41b5b..cc376f4 100644 --- a/src/op-cli-installer/github-action/cli-installer/windows-signature.test.ts +++ b/src/op-cli-installer/github-action/cli-installer/windows-signature.test.ts @@ -72,6 +72,28 @@ describe("verifyAuthenticodeSignature", () => { /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", () => { diff --git a/src/op-cli-installer/github-action/cli-installer/windows-signature.ts b/src/op-cli-installer/github-action/cli-installer/windows-signature.ts index 4083361..b170746 100644 --- a/src/op-cli-installer/github-action/cli-installer/windows-signature.ts +++ b/src/op-cli-installer/github-action/cli-installer/windows-signature.ts @@ -38,12 +38,19 @@ const defaultPowerShellRunner = async (script: string): Promise => { return stdout; }; -// Strict Authenticode check against 1Password's Azure Trusted Signing cert. -// Throws unless Status is Valid, signer is AgileBits, issuer is a Microsoft -// CS AOC CA, and the publisher EKU is present. +// Authenticode check against 1Password's signing cert. +// +// 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 ( opExePath: string, runPowerShell: (script: string) => Promise = defaultPowerShellRunner, + strict = true, ): Promise => { // Read the four Authenticode fields we validate below. const escapedPath = opExePath.replace(/'/g, "''"); @@ -56,6 +63,8 @@ export const verifyAuthenticodeSignature = async ( ].join("; "); 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 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. const issuer = fieldValue("Issuer=") ?? ""; if (!issuer.includes(`CN=${WINDOWS_ISSUER_CN_PREFIX}`)) { diff --git a/src/op-cli-installer/github-action/cli-installer/windows.test.ts b/src/op-cli-installer/github-action/cli-installer/windows.test.ts index b23a38c..aca1600 100644 --- a/src/op-cli-installer/github-action/cli-installer/windows.test.ts +++ b/src/op-cli-installer/github-action/cli-installer/windows.test.ts @@ -16,9 +16,6 @@ jest.mock("./windows-signature", () => ({ verifyAuthenticodeSignature: jest.fn().mockResolvedValue(undefined), isAzureSignedEra: jest.fn().mockReturnValue(true), })); -jest.mock("./gpg-signature", () => ({ - verifyGpgSignature: jest.fn().mockResolvedValue(undefined), -})); afterEach(() => { jest.restoreAllMocks(); diff --git a/src/op-cli-installer/github-action/cli-installer/windows.ts b/src/op-cli-installer/github-action/cli-installer/windows.ts index 3f6530d..d43ac73 100644 --- a/src/op-cli-installer/github-action/cli-installer/windows.ts +++ b/src/op-cli-installer/github-action/cli-installer/windows.ts @@ -9,7 +9,6 @@ import { cliUrlBuilder, type SupportedPlatform, } from "./cli-installer"; -import { verifyGpgSignature } from "./gpg-signature"; import type { Installer } from "./installer"; import { isAzureSignedEra, @@ -40,14 +39,14 @@ export class WindowsInstaller extends CliInstaller implements Installer { core.info("Verifying 1Password CLI signature"); const opExePath = path.join(extractedPath, "op.exe"); - if (isAzureSignedEra(this.version)) { - await verifyAuthenticodeSignature(opExePath); - } else { - await verifyGpgSignature( - opExePath, - path.join(extractedPath, "op.exe.sig"), - ); - } + // Azure-era (v2.31.0+): strict Authenticode (matches current docs). + // Sectigo-era (pre-v2.31.0): loose Authenticode (Subject + Status only; + // the Sectigo cert lacks the Microsoft issuer and publisher EKU). + await verifyAuthenticodeSignature( + opExePath, + undefined, + isAzureSignedEra(this.version), + ); core.info("1Password CLI signature verified"); core.addPath(extractedPath);