Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 208 additions & 14 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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);
Expand All @@ -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");
}
}

Expand Down
49 changes: 49 additions & 0 deletions dist/linux-signing-key.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

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
P4I7jHwe0BN3iNAG1nKbGg==
=+LeX
-----END PGP PUBLIC KEY BLOCK-----
Original file line number Diff line number Diff line change
@@ -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<Promise<string>, [readonly string[]]>();
for (const r of responses) {
if (r instanceof Error) {
runner.mockRejectedValueOnce(r);
} else {
runner.mockResolvedValueOnce(r);
}
}
return runner;
};

const subcommandsCalled = (runner: ReturnType<typeof gpgRunner>) =>
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/);
});
});
Loading
Loading