mirror of
https://github.com/1Password/load-secrets-action.git
synced 2026-06-21 14:23:48 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| feec3fd0c1 | |||
| 7b7cb42941 | |||
| cc789f0882 | |||
| d1dad6d749 | |||
| d463472f19 | |||
| da7c7c6490 | |||
| d367634d5e | |||
| 1953bd007b | |||
| f3b8e180f2 | |||
| 6ec01615e5 |
Vendored
+180
-3
@@ -35369,8 +35369,60 @@ class CliInstaller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/linux-signature.ts
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const execFileAsync = (0,external_util_.promisify)(external_child_process_.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_.mkdtempSync(external_path_.join(external_os_.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/load-secrets-action/issues.");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
external_fs_.rmSync(gpgHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/linux.ts
|
;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/linux.ts
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LinuxInstaller extends CliInstaller {
|
class LinuxInstaller extends CliInstaller {
|
||||||
platform = "linux"; // Node.js platform identifier for Linux
|
platform = "linux"; // Node.js platform identifier for Linux
|
||||||
constructor(version) {
|
constructor(version) {
|
||||||
@@ -35378,10 +35430,77 @@ class LinuxInstaller extends CliInstaller {
|
|||||||
}
|
}
|
||||||
async installCli() {
|
async installCli() {
|
||||||
const urlBuilder = cliUrlBuilder[this.platform];
|
const urlBuilder = cliUrlBuilder[this.platform];
|
||||||
await super.install(urlBuilder(this.version, this.arch));
|
await this.install(urlBuilder(this.version, this.arch));
|
||||||
|
}
|
||||||
|
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_.join(extractedPath, "op"), external_path_.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_.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/load-secrets-action/issues.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/macos.ts
|
;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/macos.ts
|
||||||
|
|
||||||
|
|
||||||
@@ -35391,7 +35510,8 @@ class LinuxInstaller extends CliInstaller {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const execFileAsync = (0,external_util_.promisify)(external_child_process_.execFile);
|
|
||||||
|
const macos_execFileAsync = (0,external_util_.promisify)(external_child_process_.execFile);
|
||||||
class MacOsInstaller extends CliInstaller {
|
class MacOsInstaller extends CliInstaller {
|
||||||
platform = "darwin"; // Node.js platform identifier for macOS
|
platform = "darwin"; // Node.js platform identifier for macOS
|
||||||
constructor(version) {
|
constructor(version) {
|
||||||
@@ -35407,8 +35527,11 @@ class MacOsInstaller extends CliInstaller {
|
|||||||
const pkgPath = await downloadTool(downloadUrl);
|
const pkgPath = await downloadTool(downloadUrl);
|
||||||
const pkgWithExtension = `${pkgPath}.pkg`;
|
const pkgWithExtension = `${pkgPath}.pkg`;
|
||||||
external_fs_.renameSync(pkgPath, pkgWithExtension);
|
external_fs_.renameSync(pkgPath, pkgWithExtension);
|
||||||
|
info("Verifying 1Password CLI signature");
|
||||||
|
await verifyMacOsPackageSignature(pkgWithExtension);
|
||||||
|
info("1Password CLI signature verified");
|
||||||
const expandDir = "temp-pkg";
|
const expandDir = "temp-pkg";
|
||||||
await execFileAsync("pkgutil", ["--expand", pkgWithExtension, expandDir]);
|
await macos_execFileAsync("pkgutil", ["--expand", pkgWithExtension, expandDir]);
|
||||||
const payloadPath = external_path_.join(expandDir, "op.pkg", "Payload");
|
const payloadPath = external_path_.join(expandDir, "op.pkg", "Payload");
|
||||||
console.info("Installing 1Password CLI");
|
console.info("Installing 1Password CLI");
|
||||||
const cliPath = await extractTar(payloadPath);
|
const cliPath = await extractTar(payloadPath);
|
||||||
@@ -35419,11 +35542,62 @@ 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_.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 = [
|
||||||
|
`$sig = Get-AuthenticodeSignature -FilePath '${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/load-secrets-action/issues.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/windows.ts
|
;// CONCATENATED MODULE: ./src/op-cli-installer/github-action/cli-installer/windows.ts
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class WindowsInstaller extends CliInstaller {
|
class WindowsInstaller extends CliInstaller {
|
||||||
platform = "win32"; // Node.js platform identifier for Windows
|
platform = "win32"; // Node.js platform identifier for Windows
|
||||||
constructor(version) {
|
constructor(version) {
|
||||||
@@ -35442,6 +35616,9 @@ class WindowsInstaller extends CliInstaller {
|
|||||||
external_fs_.renameSync(downloadPath, zipPath);
|
external_fs_.renameSync(downloadPath, zipPath);
|
||||||
console.info("Installing 1Password CLI");
|
console.info("Installing 1Password CLI");
|
||||||
const extractedPath = await extractZip(zipPath);
|
const extractedPath = await extractZip(zipPath);
|
||||||
|
info("Verifying 1Password CLI signature");
|
||||||
|
await verifyAuthenticodeSignature(external_path_.join(extractedPath, "op.exe"));
|
||||||
|
info("1Password CLI signature verified");
|
||||||
addPath(extractedPath);
|
addPath(extractedPath);
|
||||||
info("1Password CLI installed");
|
info("1Password CLI installed");
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+49
@@ -0,0 +1,49 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
P4I7jHwe0BN3iNAG1nKbGg==
|
||||||
|
=+LeX
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
@@ -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/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
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<string> => {
|
||||||
|
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<string> = defaultGpgRunner,
|
||||||
|
): Promise<void> => {
|
||||||
|
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/load-secrets-action/issues.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(gpgHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
P4I7jHwe0BN3iNAG1nKbGg==
|
||||||
|
=+LeX
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
@@ -2,7 +2,6 @@ import os from "os";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
archMap,
|
archMap,
|
||||||
CliInstaller,
|
|
||||||
cliUrlBuilder,
|
cliUrlBuilder,
|
||||||
type SupportedPlatform,
|
type SupportedPlatform,
|
||||||
} from "./cli-installer";
|
} from "./cli-installer";
|
||||||
@@ -25,9 +24,7 @@ describe("LinuxInstaller", () => {
|
|||||||
|
|
||||||
it("should call install with correct URL", async () => {
|
it("should call install with correct URL", async () => {
|
||||||
const installer = new LinuxInstaller(version);
|
const installer = new LinuxInstaller(version);
|
||||||
const installMock = jest
|
const installMock = jest.spyOn(installer, "install").mockResolvedValue();
|
||||||
.spyOn(CliInstaller.prototype, "install")
|
|
||||||
.mockResolvedValue();
|
|
||||||
|
|
||||||
await installer.installCli();
|
await installer.installCli();
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
import * as tc from "@actions/tool-cache";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CliInstaller,
|
CliInstaller,
|
||||||
cliUrlBuilder,
|
cliUrlBuilder,
|
||||||
type SupportedPlatform,
|
type SupportedPlatform,
|
||||||
} from "./cli-installer";
|
} from "./cli-installer";
|
||||||
import type { Installer } from "./installer";
|
import type { Installer } from "./installer";
|
||||||
|
import { verifyLinuxSignature } from "./linux-signature";
|
||||||
|
|
||||||
export class LinuxInstaller extends CliInstaller implements Installer {
|
export class LinuxInstaller extends CliInstaller implements Installer {
|
||||||
private readonly platform: SupportedPlatform = "linux"; // Node.js platform identifier for Linux
|
private readonly platform: SupportedPlatform = "linux"; // Node.js platform identifier for Linux
|
||||||
@@ -14,6 +20,23 @@ export class LinuxInstaller extends CliInstaller implements Installer {
|
|||||||
|
|
||||||
public async installCli(): Promise<void> {
|
public async installCli(): Promise<void> {
|
||||||
const urlBuilder = cliUrlBuilder[this.platform];
|
const urlBuilder = cliUrlBuilder[this.platform];
|
||||||
await super.install(urlBuilder(this.version, this.arch));
|
await this.install(urlBuilder(this.version, this.arch));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async install(url: string): Promise<void> {
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Promise<string>, [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/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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<string> => {
|
||||||
|
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<string> = defaultPkgutilRunner,
|
||||||
|
): Promise<void> => {
|
||||||
|
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/load-secrets-action/issues.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
type SupportedPlatform,
|
type SupportedPlatform,
|
||||||
} from "./cli-installer";
|
} from "./cli-installer";
|
||||||
import { type Installer } from "./installer";
|
import { type Installer } from "./installer";
|
||||||
|
import { verifyMacOsPackageSignature } from "./macos-signature";
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
@@ -34,6 +35,10 @@ export class MacOsInstaller extends CliInstaller implements Installer {
|
|||||||
const pkgWithExtension = `${pkgPath}.pkg`;
|
const pkgWithExtension = `${pkgPath}.pkg`;
|
||||||
fs.renameSync(pkgPath, pkgWithExtension);
|
fs.renameSync(pkgPath, pkgWithExtension);
|
||||||
|
|
||||||
|
core.info("Verifying 1Password CLI signature");
|
||||||
|
await verifyMacOsPackageSignature(pkgWithExtension);
|
||||||
|
core.info("1Password CLI signature verified");
|
||||||
|
|
||||||
const expandDir = "temp-pkg";
|
const expandDir = "temp-pkg";
|
||||||
await execFileAsync("pkgutil", ["--expand", pkgWithExtension, expandDir]);
|
await execFileAsync("pkgutil", ["--expand", pkgWithExtension, expandDir]);
|
||||||
const payloadPath = path.join(expandDir, "op.pkg", "Payload");
|
const payloadPath = path.join(expandDir, "op.pkg", "Payload");
|
||||||
|
|||||||
@@ -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<Promise<string>, [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/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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<string> => {
|
||||||
|
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<string> = defaultPowerShellRunner,
|
||||||
|
): Promise<void> => {
|
||||||
|
const escapedPath = opExePath.replace(/'/g, "''");
|
||||||
|
const script = [
|
||||||
|
`$sig = Get-AuthenticodeSignature -FilePath '${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/load-secrets-action/issues.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -12,6 +12,9 @@ import {
|
|||||||
import { WindowsInstaller } from "./windows";
|
import { WindowsInstaller } from "./windows";
|
||||||
|
|
||||||
jest.mock("fs");
|
jest.mock("fs");
|
||||||
|
jest.mock("./windows-signature", () => ({
|
||||||
|
verifyAuthenticodeSignature: jest.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import * as tc from "@actions/tool-cache";
|
import * as tc from "@actions/tool-cache";
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
type SupportedPlatform,
|
type SupportedPlatform,
|
||||||
} from "./cli-installer";
|
} from "./cli-installer";
|
||||||
import type { Installer } from "./installer";
|
import type { Installer } from "./installer";
|
||||||
|
import { verifyAuthenticodeSignature } from "./windows-signature";
|
||||||
|
|
||||||
export class WindowsInstaller extends CliInstaller implements Installer {
|
export class WindowsInstaller extends CliInstaller implements Installer {
|
||||||
private readonly platform: SupportedPlatform = "win32"; // Node.js platform identifier for Windows
|
private readonly platform: SupportedPlatform = "win32"; // Node.js platform identifier for Windows
|
||||||
@@ -31,6 +33,11 @@ export class WindowsInstaller extends CliInstaller implements Installer {
|
|||||||
fs.renameSync(downloadPath, zipPath);
|
fs.renameSync(downloadPath, zipPath);
|
||||||
console.info("Installing 1Password CLI");
|
console.info("Installing 1Password CLI");
|
||||||
const extractedPath = await tc.extractZip(zipPath);
|
const extractedPath = await tc.extractZip(zipPath);
|
||||||
|
|
||||||
|
core.info("Verifying 1Password CLI signature");
|
||||||
|
await verifyAuthenticodeSignature(path.join(extractedPath, "op.exe"));
|
||||||
|
core.info("1Password CLI signature verified");
|
||||||
|
|
||||||
core.addPath(extractedPath);
|
core.addPath(extractedPath);
|
||||||
core.info("1Password CLI installed");
|
core.info("1Password CLI installed");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user