diff --git a/dist/index.js b/dist/index.js index a746823..c7fe2cc 100644 --- a/dist/index.js +++ b/dist/index.js @@ -34535,19 +34535,142 @@ class CliInstaller { } } +;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/linux-signature.ts + + + + + +const execFileAsync = (0,external_util_.promisify)(external_child_process_namespaceObject.execFile); +// 1Password's code-signing GPG key fingerprint. See +// https://www.1password.dev/cli/verify. +const ONEPASSWORD_GPG_KEY_FINGERPRINT = "3FEF9748469ADBE15DA7CA80AC2D62742012EA22"; +// Bundled 1Password code-signing public key `linux-signing-key.asc` in +// this directory. Bundled to avoid a runtime keyserver/URL dependency. +// Source: https://downloads.1password.com/linux/keys/1password.asc +const ONEPASSWORD_GPG_PUBLIC_KEY_PATH = __nccwpck_require__.ab + "linux-signing-key.asc"; +const defaultGpgRunner = async (args) => { + const { stdout } = await execFileAsync("gpg", args); + return stdout; +}; +/** + * Throws unless the binary at opPath carries a valid GPG signature (at + * sigPath) from the pinned 1Password key. The key is bundled with the action. + */ +const verifyLinuxSignature = async (opPath, sigPath, runGpg = defaultGpgRunner) => { + const gpgHome = external_fs_namespaceObject.mkdtempSync(external_path_namespaceObject.join(external_os_namespaceObject.tmpdir(), "op-verify-")); + try { + const baseArgs = ["--homedir", gpgHome, "--batch", "--no-tty"]; + // Import the bundled key into the temp keyring. + await runGpg([...baseArgs, "--import", __nccwpck_require__.ab + "linux-signing-key.asc"]); + // Confirm we imported the pinned key. + const keyringListing = await runGpg([ + ...baseArgs, + "--list-keys", + "--with-colons", + ]); + if (!keyringListing.includes(`${ONEPASSWORD_GPG_KEY_FINGERPRINT}:`)) { + throw new Error(`bundled GPG key does not match expected fingerprint ${ONEPASSWORD_GPG_KEY_FINGERPRINT}.`); + } + // Verify op.sig against op using the imported key. + await runGpg([...baseArgs, "--verify", sigPath, opPath]); + } + catch (err) { + const message = err instanceof Error ? err.message : String(err); + throw new Error(`1Password CLI signature verification failed: ${message}. ` + + "If 1Password has rotated their GPG signing key, this action needs to be updated — please file an issue at https://github.com/1Password/install-cli-action/issues."); + } + finally { + external_fs_namespaceObject.rmSync(gpgHome, { recursive: true, force: true }); + } +}; + ;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/linux.ts + + + + +/** Installs the 1Password CLI on Linux runners. */ class LinuxInstaller extends CliInstaller { platform = "linux"; // Node.js platform identifier for Linux - constructor(version) { - super(version); - } + /** Downloads, verifies, and installs the CLI for the configured version. */ async installCli() { const urlBuilder = cliUrlBuilder[this.platform]; - await super.install(urlBuilder(this.version, this.arch)); + await this.install(urlBuilder(this.version, this.arch)); + } + /** Downloads the zip, verifies op's GPG signature, then adds it to PATH. */ + async install(url) { + console.info(`Downloading 1Password CLI from: ${url}`); + const downloadPath = await downloadTool(url); + console.info("Installing 1Password CLI"); + const extractedPath = await extractZip(downloadPath); + info("Verifying 1Password CLI signature"); + await verifyLinuxSignature(external_path_namespaceObject.join(extractedPath, "op"), external_path_namespaceObject.join(extractedPath, "op.sig")); + info("1Password CLI signature verified"); + addPath(extractedPath); + info("1Password CLI installed"); } } +;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/macos-signature.ts + + +const macos_signature_execFileAsync = (0,external_util_.promisify)(external_child_process_namespaceObject.execFile); +// See https://www.1password.dev/cli/verify. +const APPLE_DEVELOPER_TEAM_ID = "2BUA8C4S2C"; +// Append-only: old certs stay listed so historical `op` versions still verify. +// See https://www.1password.dev/cli/verify. +const ALLOWED_MACOS_SIGNING_CERT_FINGERPRINTS = [ + "CAB578061B0209FB70934DA344EF6FEBCD3279B1C074C54B0D7D555743B9D89F", + "141DD87B2B231211F1440849798007DF621DE6EB3DAB985BC964EE9704C4A1C1", +]; +const defaultPkgutilRunner = async (pkgPath) => { + const { stdout } = await macos_signature_execFileAsync("pkgutil", [ + "--check-signature", + pkgPath, + ]); + return stdout; +}; +// Returns just entry 1 (the signer cert) from the chain. +const extractSignerCertSection = (pkgutilOutput) => { + const chainStart = pkgutilOutput.indexOf("Certificate Chain:"); + if (chainStart === -1) { + return null; + } + const chainBody = pkgutilOutput.slice(chainStart); + const secondCert = /\n\s*2\.\s/.exec(chainBody); + return secondCert ? chainBody.slice(0, secondCert.index) : chainBody; +}; +const parseSignerFingerprint = (signerSection) => { + const match = /SHA256 Fingerprint:\s*\n((?:[ \t]+[0-9A-Fa-f ]+\n?)+)/.exec(signerSection); + const captured = match?.[1]; + return captured ? captured.replace(/\s+/g, "").toUpperCase() : null; +}; +/** + * Hard-fails if the .pkg at pkgPath is not signed by AgileBits Inc. + * (2BUA8C4S2C) with a certificate on the allowlist above. Must run + * before any extraction of the .pkg contents. + */ +const verifyMacOsPackageSignature = async (pkgPath, runPkgutil = defaultPkgutilRunner) => { + const stdout = await runPkgutil(pkgPath); + const signerSection = extractSignerCertSection(stdout); + if (!signerSection) { + throw new Error(`1Password CLI signature verification failed: could not locate certificate chain in pkgutil output.\npkgutil output:\n${stdout}`); + } + if (!signerSection.includes(`(${APPLE_DEVELOPER_TEAM_ID})`)) { + throw new Error(`1Password CLI signature verification failed: expected developer team ID ${APPLE_DEVELOPER_TEAM_ID} not found in signer certificate.\npkgutil output:\n${stdout}`); + } + const signerFingerprint = parseSignerFingerprint(signerSection); + if (!signerFingerprint) { + throw new Error(`1Password CLI signature verification failed: could not parse signer cert SHA-256 fingerprint.\npkgutil output:\n${stdout}`); + } + if (!ALLOWED_MACOS_SIGNING_CERT_FINGERPRINTS.includes(signerFingerprint)) { + throw new Error(`1Password CLI signature verification failed: signer cert SHA-256 fingerprint ${signerFingerprint} is not on the allowlist. ` + + "If 1Password has rotated their installer signing cert, this action needs to be updated — please file an issue at https://github.com/1Password/install-cli-action/issues."); + } +}; + ;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/macos.ts @@ -34557,24 +34680,28 @@ class LinuxInstaller extends CliInstaller { -const execFileAsync = (0,external_util_.promisify)(external_child_process_namespaceObject.execFile); + +const macos_execFileAsync = (0,external_util_.promisify)(external_child_process_namespaceObject.execFile); +/** Installs the 1Password CLI on macOS runners. */ class MacOsInstaller extends CliInstaller { platform = "darwin"; // Node.js platform identifier for macOS - constructor(version) { - super(version); - } + /** Downloads, verifies, and installs the CLI for the configured version. */ async installCli() { const urlBuilder = cliUrlBuilder[this.platform]; await this.install(urlBuilder(this.version)); } - // @actions/tool-cache package does not support .pkg files, so we need to handle the installation manually + // @actions/tool-cache package does not support .pkg files, so we need to handle the installation manually. + /** Downloads the pkg, verifies its signature, expands it, then adds the CLI to PATH. */ async install(downloadUrl) { console.info(`Downloading 1Password CLI from: ${downloadUrl}`); const pkgPath = await downloadTool(downloadUrl); const pkgWithExtension = `${pkgPath}.pkg`; external_fs_namespaceObject.renameSync(pkgPath, pkgWithExtension); + info("Verifying 1Password CLI signature"); + await verifyMacOsPackageSignature(pkgWithExtension); + info("1Password CLI signature verified"); const expandDir = "temp-pkg"; - await execFileAsync("pkgutil", ["--expand", pkgWithExtension, expandDir]); + await macos_execFileAsync("pkgutil", ["--expand", pkgWithExtension, expandDir]); const payloadPath = external_path_namespaceObject.join(expandDir, "op.pkg", "Payload"); console.info("Installing 1Password CLI"); const cliPath = await extractTar(payloadPath); @@ -34585,16 +34712,83 @@ class MacOsInstaller extends CliInstaller { } } +;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/windows-signature.ts + + +const windows_signature_execFileAsync = (0,external_util_.promisify)(external_child_process_namespaceObject.execFile); +// Identifying field of 1Password's Authenticode signing cert for op.exe. +// See https://www.1password.dev/cli/verify. +const WINDOWS_SIGNER_SUBJECT_CN = "Agilebits"; +const defaultPowerShellRunner = async (script) => { + const { stdout } = await windows_signature_execFileAsync("powershell.exe", [ + "-NoProfile", + "-NonInteractive", + "-Command", + script, + ]); + return stdout; +}; +/** + * Verifies op.exe's Authenticode signature against 1Password's signing cert. + * Throws unless the signature is cryptographically valid and the signer is AgileBits. + */ +const verifyAuthenticodeSignature = async (opExePath, runPowerShell = defaultPowerShellRunner) => { + const escapedPath = opExePath.replace(/'/g, "''"); + const script = [ + `$ErrorActionPreference = 'Stop'`, + `$sig = Get-AuthenticodeSignature -LiteralPath '${escapedPath}'`, + `"Status=$($sig.Status)"`, + `"Subject=$($sig.SignerCertificate.Subject)"`, + ].join("; "); + const output = await runPowerShell(script); + const outputLines = output.split("\n").map((l) => l.trim()); + const fieldValue = (prefix) => { + const matchingLine = outputLines.find((l) => l.startsWith(prefix)); + if (!matchingLine) { + return undefined; + } + return matchingLine.slice(prefix.length); + }; + // Reject unsigned or tampered binaries. + const status = fieldValue("Status="); + if (status !== "Valid") { + throw new Error(`Authenticode status is ${status ?? "unknown"}, expected Valid.\nGet-AuthenticodeSignature output:\n${output}`); + } + // Confirm the signer is AgileBits, not some other publisher. Trailing comma + // anchors the CN value so e.g. "CN=AgilebitsAttacker, ..." cannot match. + const subject = fieldValue("Subject=") ?? ""; + const expectedCn = `CN=${WINDOWS_SIGNER_SUBJECT_CN},`; + if (!subject.includes(expectedCn)) { + throw new Error(`1Password CLI signature verification failed: signer Subject (${subject}) does not contain ${expectedCn} ` + + "If 1Password has rotated or renamed their signing identity, this action needs to be updated — please file an issue at https://github.com/1Password/install-cli-action/issues."); + } +}; + ;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/windows.ts + + + + +/** Installs the 1Password CLI on Windows runners. */ class WindowsInstaller extends CliInstaller { platform = "win32"; // Node.js platform identifier for Windows - constructor(version) { - super(version); - } + /** Downloads, verifies, and installs the CLI for the configured version. */ async installCli() { const urlBuilder = cliUrlBuilder[this.platform]; - await super.install(urlBuilder(this.version, this.arch)); + await this.install(urlBuilder(this.version, this.arch)); + } + /** Downloads the zip, verifies op.exe's Authenticode signature, then adds it to PATH. */ + async install(url) { + console.info(`Downloading 1Password CLI from: ${url}`); + const downloadPath = await downloadTool(url); + console.info("Installing 1Password CLI"); + const extractedPath = await extractZip(downloadPath); + info("Verifying 1Password CLI signature"); + await verifyAuthenticodeSignature(external_path_namespaceObject.join(extractedPath, "op.exe")); + info("1Password CLI signature verified"); + addPath(extractedPath); + info("1Password CLI installed"); } } diff --git a/dist/linux-signing-key.asc b/dist/linux-signing-key.asc new file mode 100644 index 0000000..4bd3975 --- /dev/null +++ b/dist/linux-signing-key.asc @@ -0,0 +1,49 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +P4I7jHwe0BN3iNAG1nKbGg== +=+LeX +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/op-cli-installer/github-action/cli-installer/linux-signature.test.ts b/src/op-cli-installer/github-action/cli-installer/linux-signature.test.ts new file mode 100644 index 0000000..15f8832 --- /dev/null +++ b/src/op-cli-installer/github-action/cli-installer/linux-signature.test.ts @@ -0,0 +1,58 @@ +import { + ONEPASSWORD_GPG_KEY_FINGERPRINT, + verifyLinuxSignature, +} from "./linux-signature"; + +describe("verifyLinuxSignature", () => { + const OP_PATH = "/tmp/op"; + const SIG_PATH = `${OP_PATH}.sig`; + const CORRECT_FPR = `fpr:::::::::${ONEPASSWORD_GPG_KEY_FINGERPRINT}:\n`; + const WRONG_FPR = `fpr:::::::::DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF:\n`; + + const gpgRunner = (...responses: (string | Error)[]) => { + const runner = jest.fn, [readonly string[]]>(); + for (const r of responses) { + if (r instanceof Error) { + runner.mockRejectedValueOnce(r); + } else { + runner.mockResolvedValueOnce(r); + } + } + return runner; + }; + + const subcommandsCalled = (runner: ReturnType) => + runner.mock.calls.map(([args]: [readonly string[]]) => + args.find( + (a) => a === "--import" || a === "--list-keys" || a === "--verify", + ), + ); + + it("imports the bundled key and verifies the signature", async () => { + const runner = gpgRunner("", CORRECT_FPR, ""); + await expect( + verifyLinuxSignature(OP_PATH, SIG_PATH, runner), + ).resolves.toBeUndefined(); + + expect(subcommandsCalled(runner)).toEqual([ + "--import", + "--list-keys", + "--verify", + ]); + }); + + it("throws and skips --verify when the imported key has the wrong fingerprint", async () => { + const runner = gpgRunner("", WRONG_FPR); + await expect( + verifyLinuxSignature(OP_PATH, SIG_PATH, runner), + ).rejects.toThrow(/does not match expected/); + expect(subcommandsCalled(runner)).toEqual(["--import", "--list-keys"]); + }); + + it("throws when gpg --verify rejects the signature", async () => { + const runner = gpgRunner("", CORRECT_FPR, new Error("BAD signature")); + await expect( + verifyLinuxSignature(OP_PATH, SIG_PATH, runner), + ).rejects.toThrow(/BAD signature/); + }); +}); diff --git a/src/op-cli-installer/github-action/cli-installer/linux-signature.ts b/src/op-cli-installer/github-action/cli-installer/linux-signature.ts new file mode 100644 index 0000000..b2c4d96 --- /dev/null +++ b/src/op-cli-installer/github-action/cli-installer/linux-signature.ts @@ -0,0 +1,66 @@ +import { execFile } from "child_process"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { promisify } from "util"; + +const execFileAsync = promisify(execFile); + +// 1Password's code-signing GPG key fingerprint. See +// https://www.1password.dev/cli/verify. +export const ONEPASSWORD_GPG_KEY_FINGERPRINT = + "3FEF9748469ADBE15DA7CA80AC2D62742012EA22"; + +// Bundled 1Password code-signing public key `linux-signing-key.asc` in +// this directory. Bundled to avoid a runtime keyserver/URL dependency. +// Source: https://downloads.1password.com/linux/keys/1password.asc +const ONEPASSWORD_GPG_PUBLIC_KEY_PATH = path.join( + __dirname, + "linux-signing-key.asc", +); + +const defaultGpgRunner = async (args: readonly string[]): Promise => { + const { stdout } = await execFileAsync("gpg", args); + return stdout; +}; + +/** + * Throws unless the binary at opPath carries a valid GPG signature (at + * sigPath) from the pinned 1Password key. The key is bundled with the action. + */ +export const verifyLinuxSignature = async ( + opPath: string, + sigPath: string, + runGpg: (args: readonly string[]) => Promise = defaultGpgRunner, +): Promise => { + const gpgHome = fs.mkdtempSync(path.join(os.tmpdir(), "op-verify-")); + try { + const baseArgs = ["--homedir", gpgHome, "--batch", "--no-tty"]; + + // Import the bundled key into the temp keyring. + await runGpg([...baseArgs, "--import", ONEPASSWORD_GPG_PUBLIC_KEY_PATH]); + + // Confirm we imported the pinned key. + const keyringListing = await runGpg([ + ...baseArgs, + "--list-keys", + "--with-colons", + ]); + if (!keyringListing.includes(`${ONEPASSWORD_GPG_KEY_FINGERPRINT}:`)) { + throw new Error( + `bundled GPG key does not match expected fingerprint ${ONEPASSWORD_GPG_KEY_FINGERPRINT}.`, + ); + } + + // Verify op.sig against op using the imported key. + await runGpg([...baseArgs, "--verify", sigPath, opPath]); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + throw new Error( + `1Password CLI signature verification failed: ${message}. ` + + "If 1Password has rotated their GPG signing key, this action needs to be updated — please file an issue at https://github.com/1Password/install-cli-action/issues.", + ); + } finally { + fs.rmSync(gpgHome, { recursive: true, force: true }); + } +}; diff --git a/src/op-cli-installer/github-action/cli-installer/linux-signing-key.asc b/src/op-cli-installer/github-action/cli-installer/linux-signing-key.asc new file mode 100644 index 0000000..4bd3975 --- /dev/null +++ b/src/op-cli-installer/github-action/cli-installer/linux-signing-key.asc @@ -0,0 +1,49 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +P4I7jHwe0BN3iNAG1nKbGg== +=+LeX +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/op-cli-installer/github-action/cli-installer/linux.test.ts b/src/op-cli-installer/github-action/cli-installer/linux.test.ts index db8cd7a..f9877a7 100644 --- a/src/op-cli-installer/github-action/cli-installer/linux.test.ts +++ b/src/op-cli-installer/github-action/cli-installer/linux.test.ts @@ -2,7 +2,6 @@ import os from "os"; import { archMap, - CliInstaller, cliUrlBuilder, type SupportedPlatform, } from "./cli-installer"; @@ -25,9 +24,7 @@ describe("LinuxInstaller", () => { it("should call install with correct URL", async () => { const installer = new LinuxInstaller(version); - const installMock = jest - .spyOn(CliInstaller.prototype, "install") - .mockResolvedValue(); + const installMock = jest.spyOn(installer, "install").mockResolvedValue(); await installer.installCli(); diff --git a/src/op-cli-installer/github-action/cli-installer/linux.ts b/src/op-cli-installer/github-action/cli-installer/linux.ts index 8d7c8ed..2245571 100644 --- a/src/op-cli-installer/github-action/cli-installer/linux.ts +++ b/src/op-cli-installer/github-action/cli-installer/linux.ts @@ -1,19 +1,41 @@ +import * as path from "path"; + +import * as core from "@actions/core"; +import * as tc from "@actions/tool-cache"; + import { CliInstaller, cliUrlBuilder, type SupportedPlatform, } from "./cli-installer"; import type { Installer } from "./installer"; +import { verifyLinuxSignature } from "./linux-signature"; +/** Installs the 1Password CLI on Linux runners. */ export class LinuxInstaller extends CliInstaller implements Installer { private readonly platform: SupportedPlatform = "linux"; // Node.js platform identifier for Linux - public constructor(version: string) { - super(version); - } - + /** Downloads, verifies, and installs the CLI for the configured version. */ public async installCli(): Promise { const urlBuilder = cliUrlBuilder[this.platform]; - await super.install(urlBuilder(this.version, this.arch)); + await this.install(urlBuilder(this.version, this.arch)); + } + + /** Downloads the zip, verifies op's GPG signature, then adds it to PATH. */ + public override async install(url: string): Promise { + console.info(`Downloading 1Password CLI from: ${url}`); + const downloadPath = await tc.downloadTool(url); + console.info("Installing 1Password CLI"); + const extractedPath = await tc.extractZip(downloadPath); + + core.info("Verifying 1Password CLI signature"); + await verifyLinuxSignature( + path.join(extractedPath, "op"), + path.join(extractedPath, "op.sig"), + ); + core.info("1Password CLI signature verified"); + + core.addPath(extractedPath); + core.info("1Password CLI installed"); } } diff --git a/src/op-cli-installer/github-action/cli-installer/macos-signature.test.ts b/src/op-cli-installer/github-action/cli-installer/macos-signature.test.ts new file mode 100644 index 0000000..df16acd --- /dev/null +++ b/src/op-cli-installer/github-action/cli-installer/macos-signature.test.ts @@ -0,0 +1,57 @@ +import { + ALLOWED_MACOS_SIGNING_CERT_FINGERPRINTS, + APPLE_DEVELOPER_TEAM_ID, + verifyMacOsPackageSignature, +} from "./macos-signature"; + +const VALID_FINGERPRINT = ALLOWED_MACOS_SIGNING_CERT_FINGERPRINTS[0] ?? ""; + +const buildPkgutilOutput = ({ + teamId = APPLE_DEVELOPER_TEAM_ID, + signerFingerprint = VALID_FINGERPRINT, +}: { + teamId?: string; + signerFingerprint?: string; +} = {}): string => { + const bytes = signerFingerprint.match(/.{2}/g) ?? []; + const fprLines = ` ${bytes.slice(0, 24).join(" ")}\n ${bytes.slice(24).join(" ")}`; + return `Package "op.pkg": + Certificate Chain: + 1. Developer ID Installer: AgileBits Inc. (${teamId}) + SHA256 Fingerprint: +${fprLines} + ------------------------------------------------------------------------ + 2. Developer ID Certification Authority +`; +}; + +const pkgutilRunner = (output: string) => + jest.fn, [string]>().mockResolvedValue(output); + +describe("verifyMacOsPackageSignature", () => { + it("passes for a pkg signed by AgileBits with an allowlisted cert", async () => { + const runner = pkgutilRunner(buildPkgutilOutput()); + await expect( + verifyMacOsPackageSignature("/tmp/op.pkg", runner), + ).resolves.toBeUndefined(); + }); + + it("throws if the signer is not under the AgileBits team ID", async () => { + const runner = pkgutilRunner(buildPkgutilOutput({ teamId: "ATTACKER" })); + await expect( + verifyMacOsPackageSignature("/tmp/op.pkg", runner), + ).rejects.toThrow(/expected developer team ID 2BUA8C4S2C not found/); + }); + + it("throws if the signer cert fingerprint is not on the allowlist", async () => { + const runner = pkgutilRunner( + buildPkgutilOutput({ + signerFingerprint: + "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + }), + ); + await expect( + verifyMacOsPackageSignature("/tmp/op.pkg", runner), + ).rejects.toThrow(/not on the allowlist/); + }); +}); diff --git a/src/op-cli-installer/github-action/cli-installer/macos-signature.ts b/src/op-cli-installer/github-action/cli-installer/macos-signature.ts new file mode 100644 index 0000000..991d933 --- /dev/null +++ b/src/op-cli-installer/github-action/cli-installer/macos-signature.ts @@ -0,0 +1,80 @@ +import { execFile } from "child_process"; +import { promisify } from "util"; + +const execFileAsync = promisify(execFile); + +// See https://www.1password.dev/cli/verify. +export const APPLE_DEVELOPER_TEAM_ID = "2BUA8C4S2C"; + +// Append-only: old certs stay listed so historical `op` versions still verify. +// See https://www.1password.dev/cli/verify. +export const ALLOWED_MACOS_SIGNING_CERT_FINGERPRINTS = [ + "CAB578061B0209FB70934DA344EF6FEBCD3279B1C074C54B0D7D555743B9D89F", + "141DD87B2B231211F1440849798007DF621DE6EB3DAB985BC964EE9704C4A1C1", +]; + +const defaultPkgutilRunner = async (pkgPath: string): Promise => { + const { stdout } = await execFileAsync("pkgutil", [ + "--check-signature", + pkgPath, + ]); + return stdout; +}; + +// Returns just entry 1 (the signer cert) from the chain. +const extractSignerCertSection = (pkgutilOutput: string): string | null => { + const chainStart = pkgutilOutput.indexOf("Certificate Chain:"); + if (chainStart === -1) { + return null; + } + const chainBody = pkgutilOutput.slice(chainStart); + const secondCert = /\n\s*2\.\s/.exec(chainBody); + return secondCert ? chainBody.slice(0, secondCert.index) : chainBody; +}; + +const parseSignerFingerprint = (signerSection: string): string | null => { + const match = /SHA256 Fingerprint:\s*\n((?:[ \t]+[0-9A-Fa-f ]+\n?)+)/.exec( + signerSection, + ); + const captured = match?.[1]; + return captured ? captured.replace(/\s+/g, "").toUpperCase() : null; +}; + +/** + * Hard-fails if the .pkg at pkgPath is not signed by AgileBits Inc. + * (2BUA8C4S2C) with a certificate on the allowlist above. Must run + * before any extraction of the .pkg contents. + */ +export const verifyMacOsPackageSignature = async ( + pkgPath: string, + runPkgutil: (pkgPath: string) => Promise = defaultPkgutilRunner, +): Promise => { + const stdout = await runPkgutil(pkgPath); + + const signerSection = extractSignerCertSection(stdout); + if (!signerSection) { + throw new Error( + `1Password CLI signature verification failed: could not locate certificate chain in pkgutil output.\npkgutil output:\n${stdout}`, + ); + } + + if (!signerSection.includes(`(${APPLE_DEVELOPER_TEAM_ID})`)) { + throw new Error( + `1Password CLI signature verification failed: expected developer team ID ${APPLE_DEVELOPER_TEAM_ID} not found in signer certificate.\npkgutil output:\n${stdout}`, + ); + } + + const signerFingerprint = parseSignerFingerprint(signerSection); + if (!signerFingerprint) { + throw new Error( + `1Password CLI signature verification failed: could not parse signer cert SHA-256 fingerprint.\npkgutil output:\n${stdout}`, + ); + } + + if (!ALLOWED_MACOS_SIGNING_CERT_FINGERPRINTS.includes(signerFingerprint)) { + throw new Error( + `1Password CLI signature verification failed: signer cert SHA-256 fingerprint ${signerFingerprint} is not on the allowlist. ` + + "If 1Password has rotated their installer signing cert, this action needs to be updated — please file an issue at https://github.com/1Password/install-cli-action/issues.", + ); + } +}; diff --git a/src/op-cli-installer/github-action/cli-installer/macos.ts b/src/op-cli-installer/github-action/cli-installer/macos.ts index f5cd7e5..6ea3339 100644 --- a/src/op-cli-installer/github-action/cli-installer/macos.ts +++ b/src/op-cli-installer/github-action/cli-installer/macos.ts @@ -12,28 +12,32 @@ import { type SupportedPlatform, } from "./cli-installer"; import { type Installer } from "./installer"; +import { verifyMacOsPackageSignature } from "./macos-signature"; const execFileAsync = promisify(execFile); +/** Installs the 1Password CLI on macOS runners. */ export class MacOsInstaller extends CliInstaller implements Installer { private readonly platform: SupportedPlatform = "darwin"; // Node.js platform identifier for macOS - public constructor(version: string) { - super(version); - } - + /** Downloads, verifies, and installs the CLI for the configured version. */ public async installCli(): Promise { const urlBuilder = cliUrlBuilder[this.platform]; await this.install(urlBuilder(this.version)); } - // @actions/tool-cache package does not support .pkg files, so we need to handle the installation manually + // @actions/tool-cache package does not support .pkg files, so we need to handle the installation manually. + /** Downloads the pkg, verifies its signature, expands it, then adds the CLI to PATH. */ public override async install(downloadUrl: string): Promise { console.info(`Downloading 1Password CLI from: ${downloadUrl}`); const pkgPath = await tc.downloadTool(downloadUrl); const pkgWithExtension = `${pkgPath}.pkg`; fs.renameSync(pkgPath, pkgWithExtension); + core.info("Verifying 1Password CLI signature"); + await verifyMacOsPackageSignature(pkgWithExtension); + core.info("1Password CLI signature verified"); + const expandDir = "temp-pkg"; await execFileAsync("pkgutil", ["--expand", pkgWithExtension, expandDir]); const payloadPath = path.join(expandDir, "op.pkg", "Payload"); 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 new file mode 100644 index 0000000..143c516 --- /dev/null +++ b/src/op-cli-installer/github-action/cli-installer/windows-signature.test.ts @@ -0,0 +1,42 @@ +import { + verifyAuthenticodeSignature, + WINDOWS_SIGNER_SUBJECT_CN, +} from "./windows-signature"; + +describe("verifyAuthenticodeSignature", () => { + const OP_EXE = "C:\\op\\op.exe"; + + const buildAuthenticodeOutput = ({ + status = "Valid", + subject = `CN=${WINDOWS_SIGNER_SUBJECT_CN}, O=Agilebits, C=CA`, + }: { status?: string; subject?: string } = {}): string => + [`Status=${status}`, `Subject=${subject}`].join("\n") + "\n"; + + const powershellRunner = (output: string) => + jest.fn, [string]>().mockResolvedValue(output); + + it("passes for a valid AgileBits-signed binary", async () => { + const runner = powershellRunner(buildAuthenticodeOutput()); + await expect( + verifyAuthenticodeSignature(OP_EXE, runner), + ).resolves.toBeUndefined(); + }); + + it("throws if Status is not Valid (unsigned or tampered)", async () => { + const runner = powershellRunner( + buildAuthenticodeOutput({ status: "HashMismatch" }), + ); + await expect(verifyAuthenticodeSignature(OP_EXE, runner)).rejects.toThrow( + /Authenticode status is HashMismatch/, + ); + }); + + it("throws if the signer is not AgileBits", async () => { + const runner = powershellRunner( + buildAuthenticodeOutput({ subject: "CN=Attacker, O=Attacker, C=US" }), + ); + await expect(verifyAuthenticodeSignature(OP_EXE, runner)).rejects.toThrow( + /does not contain CN=Agilebits/, + ); + }); +}); 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 new file mode 100644 index 0000000..b26fee6 --- /dev/null +++ b/src/op-cli-installer/github-action/cli-installer/windows-signature.ts @@ -0,0 +1,65 @@ +import { execFile } from "child_process"; +import { promisify } from "util"; + +const execFileAsync = promisify(execFile); + +// Identifying field of 1Password's Authenticode signing cert for op.exe. +// See https://www.1password.dev/cli/verify. +export const WINDOWS_SIGNER_SUBJECT_CN = "Agilebits"; + +const defaultPowerShellRunner = async (script: string): Promise => { + const { stdout } = await execFileAsync("powershell.exe", [ + "-NoProfile", + "-NonInteractive", + "-Command", + script, + ]); + return stdout; +}; + +/** + * Verifies op.exe's Authenticode signature against 1Password's signing cert. + * Throws unless the signature is cryptographically valid and the signer is AgileBits. + */ +export const verifyAuthenticodeSignature = async ( + opExePath: string, + runPowerShell: (script: string) => Promise = defaultPowerShellRunner, +): Promise => { + const escapedPath = opExePath.replace(/'/g, "''"); + const script = [ + `$ErrorActionPreference = 'Stop'`, + `$sig = Get-AuthenticodeSignature -LiteralPath '${escapedPath}'`, + `"Status=$($sig.Status)"`, + `"Subject=$($sig.SignerCertificate.Subject)"`, + ].join("; "); + + const output = await runPowerShell(script); + const outputLines = output.split("\n").map((l) => l.trim()); + + const fieldValue = (prefix: string): string | undefined => { + const matchingLine = outputLines.find((l) => l.startsWith(prefix)); + if (!matchingLine) { + return undefined; + } + return matchingLine.slice(prefix.length); + }; + + // Reject unsigned or tampered binaries. + const status = fieldValue("Status="); + if (status !== "Valid") { + throw new Error( + `Authenticode status is ${status ?? "unknown"}, expected Valid.\nGet-AuthenticodeSignature output:\n${output}`, + ); + } + + // Confirm the signer is AgileBits, not some other publisher. Trailing comma + // anchors the CN value so e.g. "CN=AgilebitsAttacker, ..." cannot match. + const subject = fieldValue("Subject=") ?? ""; + const expectedCn = `CN=${WINDOWS_SIGNER_SUBJECT_CN},`; + if (!subject.includes(expectedCn)) { + throw new Error( + `1Password CLI signature verification failed: signer Subject (${subject}) does not contain ${expectedCn} ` + + "If 1Password has rotated or renamed their signing identity, this action needs to be updated — please file an issue at https://github.com/1Password/install-cli-action/issues.", + ); + } +}; 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 7b91d3d..eafba3f 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 @@ -2,7 +2,6 @@ import os from "os"; import { archMap, - CliInstaller, cliUrlBuilder, type SupportedPlatform, } from "./cli-installer"; @@ -25,9 +24,7 @@ describe("WindowsInstaller", () => { it("should call install with correct URL", async () => { const installer = new WindowsInstaller(version); - const installMock = jest - .spyOn(CliInstaller.prototype, "install") - .mockResolvedValue(); + const installMock = jest.spyOn(installer, "install").mockResolvedValue(); await installer.installCli(); 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 b1d37cb..ff9b831 100644 --- a/src/op-cli-installer/github-action/cli-installer/windows.ts +++ b/src/op-cli-installer/github-action/cli-installer/windows.ts @@ -1,19 +1,38 @@ +import * as path from "path"; + +import * as core from "@actions/core"; +import * as tc from "@actions/tool-cache"; + import { CliInstaller, cliUrlBuilder, type SupportedPlatform, } from "./cli-installer"; import type { Installer } from "./installer"; +import { verifyAuthenticodeSignature } from "./windows-signature"; +/** Installs the 1Password CLI on Windows runners. */ export class WindowsInstaller extends CliInstaller implements Installer { private readonly platform: SupportedPlatform = "win32"; // Node.js platform identifier for Windows - public constructor(version: string) { - super(version); - } - + /** Downloads, verifies, and installs the CLI for the configured version. */ public async installCli(): Promise { const urlBuilder = cliUrlBuilder[this.platform]; - await super.install(urlBuilder(this.version, this.arch)); + await this.install(urlBuilder(this.version, this.arch)); + } + + /** Downloads the zip, verifies op.exe's Authenticode signature, then adds it to PATH. */ + public override async install(url: string): Promise { + console.info(`Downloading 1Password CLI from: ${url}`); + const downloadPath = await tc.downloadTool(url); + console.info("Installing 1Password CLI"); + const extractedPath = await tc.extractZip(downloadPath); + + core.info("Verifying 1Password CLI signature"); + await verifyAuthenticodeSignature(path.join(extractedPath, "op.exe")); + core.info("1Password CLI signature verified"); + + core.addPath(extractedPath); + core.info("1Password CLI installed"); } }