mirror of
https://github.com/1Password/load-secrets-action.git
synced 2026-06-21 06:23:47 +00:00
Add strict mode
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user