mirror of
https://github.com/1Password/load-secrets-action.git
synced 2026-06-21 06:23:47 +00:00
Verify valid signature and signer
This commit is contained in:
+10
-10
@@ -1,10 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
ONEPASSWORD_GPG_KEY_FINGERPRINT,
|
ONEPASSWORD_GPG_KEY_FINGERPRINT,
|
||||||
ONEPASSWORD_GPG_KEYSERVER,
|
ONEPASSWORD_GPG_KEYSERVER,
|
||||||
verifyGpgSignature,
|
verifyLinuxSignature,
|
||||||
} from "./gpg-signature";
|
} from "./linux-signature";
|
||||||
|
|
||||||
describe("verifyGpgSignature", () => {
|
describe("verifyLinuxSignature", () => {
|
||||||
const OP_PATH = "/tmp/op";
|
const OP_PATH = "/tmp/op";
|
||||||
const SIG_PATH = `${OP_PATH}.sig`;
|
const SIG_PATH = `${OP_PATH}.sig`;
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ describe("verifyGpgSignature", () => {
|
|||||||
it("fetches the pinned key by fingerprint and verifies the signature", async () => {
|
it("fetches the pinned key by fingerprint and verifies the signature", async () => {
|
||||||
const runner = gpgRunner("", "");
|
const runner = gpgRunner("", "");
|
||||||
await expect(
|
await expect(
|
||||||
verifyGpgSignature(OP_PATH, SIG_PATH, runner),
|
verifyLinuxSignature(OP_PATH, SIG_PATH, runner),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(subcommandsCalled(runner)).toEqual(["--recv-keys", "--verify"]);
|
expect(subcommandsCalled(runner)).toEqual(["--recv-keys", "--verify"]);
|
||||||
@@ -46,16 +46,16 @@ describe("verifyGpgSignature", () => {
|
|||||||
|
|
||||||
it("throws if recv-keys fails (e.g., wrong fingerprint or keyserver unreachable)", async () => {
|
it("throws if recv-keys fails (e.g., wrong fingerprint or keyserver unreachable)", async () => {
|
||||||
const runner = gpgRunner(new Error("No data"));
|
const runner = gpgRunner(new Error("No data"));
|
||||||
await expect(verifyGpgSignature(OP_PATH, SIG_PATH, runner)).rejects.toThrow(
|
await expect(
|
||||||
/No data/,
|
verifyLinuxSignature(OP_PATH, SIG_PATH, runner),
|
||||||
);
|
).rejects.toThrow(/No data/);
|
||||||
expect(subcommandsCalled(runner)).toEqual(["--recv-keys"]);
|
expect(subcommandsCalled(runner)).toEqual(["--recv-keys"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws if gpg --verify rejects the signature", async () => {
|
it("throws if gpg --verify rejects the signature", async () => {
|
||||||
const runner = gpgRunner("", new Error("BAD signature"));
|
const runner = gpgRunner("", new Error("BAD signature"));
|
||||||
await expect(verifyGpgSignature(OP_PATH, SIG_PATH, runner)).rejects.toThrow(
|
await expect(
|
||||||
/BAD signature/,
|
verifyLinuxSignature(OP_PATH, SIG_PATH, runner),
|
||||||
);
|
).rejects.toThrow(/BAD signature/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+2
-2
@@ -7,7 +7,7 @@ import { promisify } from "util";
|
|||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
// 1Password's code-signing GPG key fingerprint. Used to verify the detached
|
// 1Password's code-signing GPG key fingerprint. Used to verify the detached
|
||||||
// `op.sig` / `op.exe.sig` inside the Linux and Windows release zips.
|
// `op.sig` inside the Linux release zip.
|
||||||
// See https://www.1password.dev/cli/verify.
|
// See https://www.1password.dev/cli/verify.
|
||||||
export const ONEPASSWORD_GPG_KEY_FINGERPRINT =
|
export const ONEPASSWORD_GPG_KEY_FINGERPRINT =
|
||||||
"3FEF9748469ADBE15DA7CA80AC2D62742012EA22";
|
"3FEF9748469ADBE15DA7CA80AC2D62742012EA22";
|
||||||
@@ -23,7 +23,7 @@ const defaultGpgRunner = async (args: readonly string[]): Promise<string> => {
|
|||||||
//
|
//
|
||||||
// gpg --keyserver keyserver.ubuntu.com --recv-keys <fingerprint>
|
// gpg --keyserver keyserver.ubuntu.com --recv-keys <fingerprint>
|
||||||
// gpg --verify <sigPath> <opPath>
|
// gpg --verify <sigPath> <opPath>
|
||||||
export const verifyGpgSignature = async (
|
export const verifyLinuxSignature = async (
|
||||||
opPath: string,
|
opPath: string,
|
||||||
sigPath: string,
|
sigPath: string,
|
||||||
runGpg: (args: readonly string[]) => Promise<string> = defaultGpgRunner,
|
runGpg: (args: readonly string[]) => Promise<string> = defaultGpgRunner,
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type SupportedPlatform,
|
type SupportedPlatform,
|
||||||
} from "./cli-installer";
|
} from "./cli-installer";
|
||||||
import type { Installer } from "./installer";
|
import type { Installer } from "./installer";
|
||||||
import { verifyGpgSignature } from "./gpg-signature";
|
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
|
||||||
@@ -30,7 +30,7 @@ export class LinuxInstaller extends CliInstaller implements Installer {
|
|||||||
const extractedPath = await tc.extractZip(downloadPath);
|
const extractedPath = await tc.extractZip(downloadPath);
|
||||||
|
|
||||||
core.info("Verifying 1Password CLI signature");
|
core.info("Verifying 1Password CLI signature");
|
||||||
await verifyGpgSignature(
|
await verifyLinuxSignature(
|
||||||
path.join(extractedPath, "op"),
|
path.join(extractedPath, "op"),
|
||||||
path.join(extractedPath, "op.sig"),
|
path.join(extractedPath, "op.sig"),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
isAzureSignedEra,
|
|
||||||
verifyAuthenticodeSignature,
|
verifyAuthenticodeSignature,
|
||||||
WINDOWS_ISSUER_CN_PREFIX,
|
|
||||||
WINDOWS_PUBLISHER_EKU,
|
|
||||||
WINDOWS_SIGNER_SUBJECT_CN,
|
WINDOWS_SIGNER_SUBJECT_CN,
|
||||||
} from "./windows-signature";
|
} from "./windows-signature";
|
||||||
|
|
||||||
@@ -11,37 +8,30 @@ describe("verifyAuthenticodeSignature", () => {
|
|||||||
|
|
||||||
const buildAuthenticodeOutput = ({
|
const buildAuthenticodeOutput = ({
|
||||||
status = "Valid",
|
status = "Valid",
|
||||||
subject = `CN=${WINDOWS_SIGNER_SUBJECT_CN}, O=Agilebits, L=Toronto, S=Ontario, C=CA`,
|
subject = `CN=${WINDOWS_SIGNER_SUBJECT_CN}, O=Agilebits, C=CA`,
|
||||||
issuer = `CN=${WINDOWS_ISSUER_CN_PREFIX} 03, O=Microsoft Corporation, C=US`,
|
}: { status?: string; subject?: string } = {}): string =>
|
||||||
ekus = [
|
[`Status=${status}`, `Subject=${subject}`].join("\n") + "\n";
|
||||||
"1.3.6.1.4.1.311.97.1.0",
|
|
||||||
"1.3.6.1.5.5.7.3.3",
|
|
||||||
WINDOWS_PUBLISHER_EKU,
|
|
||||||
],
|
|
||||||
}: {
|
|
||||||
status?: string;
|
|
||||||
subject?: string;
|
|
||||||
issuer?: string;
|
|
||||||
ekus?: string[];
|
|
||||||
} = {}): string =>
|
|
||||||
[
|
|
||||||
`Status=${status}`,
|
|
||||||
`Subject=${subject}`,
|
|
||||||
`Issuer=${issuer}`,
|
|
||||||
...ekus.map((e) => `EKU=${e}`),
|
|
||||||
].join("\n") + "\n";
|
|
||||||
|
|
||||||
const powershellRunner = (output: string) =>
|
const powershellRunner = (output: string) =>
|
||||||
jest.fn<Promise<string>, [string]>().mockResolvedValue(output);
|
jest.fn<Promise<string>, [string]>().mockResolvedValue(output);
|
||||||
|
|
||||||
it("passes for an Azure-signed op.exe", async () => {
|
it("passes for a valid AgileBits-signed binary", async () => {
|
||||||
const runner = powershellRunner(buildAuthenticodeOutput());
|
const runner = powershellRunner(buildAuthenticodeOutput());
|
||||||
await expect(
|
await expect(
|
||||||
verifyAuthenticodeSignature(OP_EXE, runner),
|
verifyAuthenticodeSignature(OP_EXE, runner),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws if the signer Subject is not AgileBits", async () => {
|
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(
|
const runner = powershellRunner(
|
||||||
buildAuthenticodeOutput({ subject: "CN=Attacker, O=Attacker, C=US" }),
|
buildAuthenticodeOutput({ subject: "CN=Attacker, O=Attacker, C=US" }),
|
||||||
);
|
);
|
||||||
@@ -49,77 +39,4 @@ describe("verifyAuthenticodeSignature", () => {
|
|||||||
/does not contain CN=Agilebits/,
|
/does not contain CN=Agilebits/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws if the Issuer is not the expected Microsoft CA", async () => {
|
|
||||||
const runner = powershellRunner(
|
|
||||||
buildAuthenticodeOutput({
|
|
||||||
issuer:
|
|
||||||
"CN=Sectigo Public Code Signing CA R36, O=Sectigo Limited, C=GB",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await expect(verifyAuthenticodeSignature(OP_EXE, runner)).rejects.toThrow(
|
|
||||||
/does not contain CN=Microsoft ID Verified/,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws if the publisher EKU is missing", async () => {
|
|
||||||
const runner = powershellRunner(
|
|
||||||
buildAuthenticodeOutput({
|
|
||||||
ekus: ["1.3.6.1.4.1.311.97.1.0", "1.3.6.1.5.5.7.3.3"],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await expect(verifyAuthenticodeSignature(OP_EXE, runner)).rejects.toThrow(
|
|
||||||
/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", () => {
|
|
||||||
it("returns true for the cutoff version (2.31.0)", () => {
|
|
||||||
expect(isAzureSignedEra("2.31.0")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for the first Azure beta (2.31.0-beta.01)", () => {
|
|
||||||
expect(isAzureSignedEra("2.31.0-beta.01")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for versions newer than the cutoff", () => {
|
|
||||||
expect(isAzureSignedEra("2.34.0")).toBe(true);
|
|
||||||
expect(isAzureSignedEra("v3.0.0")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false for the last Sectigo version (2.30.3)", () => {
|
|
||||||
expect(isAzureSignedEra("2.30.3")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false for older versions", () => {
|
|
||||||
expect(isAzureSignedEra("2.20.0")).toBe(false);
|
|
||||||
expect(isAzureSignedEra("v2.0.0")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for unrecognized version formats (fail closed)", () => {
|
|
||||||
expect(isAzureSignedEra("not-a-version")).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,32 +1,11 @@
|
|||||||
import { execFile } from "child_process";
|
import { execFile } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
import semver from "semver";
|
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
// Identifying fields of 1Password's Authenticode signing cert for op.exe.
|
// Identifying field of 1Password's Authenticode signing cert for op.exe.
|
||||||
// See https://www.1password.dev/cli/verify.
|
// See https://www.1password.dev/cli/verify.
|
||||||
export const WINDOWS_SIGNER_SUBJECT_CN = "Agilebits";
|
export const WINDOWS_SIGNER_SUBJECT_CN = "Agilebits";
|
||||||
export const WINDOWS_ISSUER_CN_PREFIX = "Microsoft ID Verified CS AOC CA";
|
|
||||||
export const WINDOWS_PUBLISHER_EKU =
|
|
||||||
"1.3.6.1.4.1.311.97.661420558.769123285.207353056.500447802";
|
|
||||||
|
|
||||||
// First op.exe version signed via Azure Trusted Signing (v2.31.0-beta.01,
|
|
||||||
// April 2025). Earlier versions are Sectigo-signed and verified via GPG.
|
|
||||||
export const AZURE_SIGNING_CUTOFF = "2.31.0-0";
|
|
||||||
|
|
||||||
export const isAzureSignedEra = (cliVersion: string): boolean => {
|
|
||||||
try {
|
|
||||||
const normalized = cliVersion
|
|
||||||
.replace(/^v/, "")
|
|
||||||
.replace(/-beta\.0*(\d+)/, "-beta.$1");
|
|
||||||
return semver.gte(normalized, AZURE_SIGNING_CUTOFF);
|
|
||||||
} catch {
|
|
||||||
// Unrecognized version format — default to modern (strict) verification.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultPowerShellRunner = async (script: string): Promise<string> => {
|
const defaultPowerShellRunner = async (script: string): Promise<string> => {
|
||||||
const { stdout } = await execFileAsync("powershell.exe", [
|
const { stdout } = await execFileAsync("powershell.exe", [
|
||||||
@@ -38,33 +17,21 @@ const defaultPowerShellRunner = async (script: string): Promise<string> => {
|
|||||||
return stdout;
|
return stdout;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Authenticode check against 1Password's signing cert.
|
// Verifies op.exe's Authenticode signature against 1Password's signing cert.
|
||||||
//
|
// Throws unless the signature is cryptographically valid and the signer is
|
||||||
// Strict mode (default, for Azure Trusted Signing era): throws unless Status
|
// AgileBits.
|
||||||
// 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.
|
|
||||||
const escapedPath = opExePath.replace(/'/g, "''");
|
const escapedPath = opExePath.replace(/'/g, "''");
|
||||||
const script = [
|
const script = [
|
||||||
`$sig = Get-AuthenticodeSignature -FilePath '${escapedPath}'`,
|
`$sig = Get-AuthenticodeSignature -FilePath '${escapedPath}'`,
|
||||||
`"Status=$($sig.Status)"`,
|
`"Status=$($sig.Status)"`,
|
||||||
`"Subject=$($sig.SignerCertificate.Subject)"`,
|
`"Subject=$($sig.SignerCertificate.Subject)"`,
|
||||||
`"Issuer=$($sig.SignerCertificate.Issuer)"`,
|
|
||||||
`$sig.SignerCertificate.EnhancedKeyUsageList | %{ "EKU=$($_.ObjectId)" }`,
|
|
||||||
].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 => {
|
||||||
@@ -90,27 +57,4 @@ export const verifyAuthenticodeSignature = async (
|
|||||||
`signer Subject (${subject}) does not contain CN=${WINDOWS_SIGNER_SUBJECT_CN}.`,
|
`signer Subject (${subject}) does not contain CN=${WINDOWS_SIGNER_SUBJECT_CN}.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loose mode (Sectigo era) stops here. Sectigo certs aren't issued by a
|
|
||||||
// Microsoft CS AOC CA, so the strict issuer check below doesn't apply.
|
|
||||||
if (!strict) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm the cert was issued by Microsoft's expected code signing CA.
|
|
||||||
const issuer = fieldValue("Issuer=") ?? "";
|
|
||||||
if (!issuer.includes(`CN=${WINDOWS_ISSUER_CN_PREFIX}`)) {
|
|
||||||
throw new Error(
|
|
||||||
`issuer (${issuer}) does not contain CN=${WINDOWS_ISSUER_CN_PREFIX}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ekus = outputLines
|
|
||||||
.filter((l) => l.startsWith("EKU="))
|
|
||||||
.map((l) => l.slice("EKU=".length));
|
|
||||||
if (!ekus.includes(WINDOWS_PUBLISHER_EKU)) {
|
|
||||||
throw new Error(
|
|
||||||
`expected publisher EKU ${WINDOWS_PUBLISHER_EKU} not found in (${ekus.join(", ") || "none"}).`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { WindowsInstaller } from "./windows";
|
|||||||
jest.mock("fs");
|
jest.mock("fs");
|
||||||
jest.mock("./windows-signature", () => ({
|
jest.mock("./windows-signature", () => ({
|
||||||
verifyAuthenticodeSignature: jest.fn().mockResolvedValue(undefined),
|
verifyAuthenticodeSignature: jest.fn().mockResolvedValue(undefined),
|
||||||
isAzureSignedEra: jest.fn().mockReturnValue(true),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ import {
|
|||||||
type SupportedPlatform,
|
type SupportedPlatform,
|
||||||
} from "./cli-installer";
|
} from "./cli-installer";
|
||||||
import type { Installer } from "./installer";
|
import type { Installer } from "./installer";
|
||||||
import {
|
import { verifyAuthenticodeSignature } from "./windows-signature";
|
||||||
isAzureSignedEra,
|
|
||||||
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
|
||||||
@@ -38,15 +35,7 @@ export class WindowsInstaller extends CliInstaller implements Installer {
|
|||||||
const extractedPath = await tc.extractZip(zipPath);
|
const extractedPath = await tc.extractZip(zipPath);
|
||||||
|
|
||||||
core.info("Verifying 1Password CLI signature");
|
core.info("Verifying 1Password CLI signature");
|
||||||
const opExePath = path.join(extractedPath, "op.exe");
|
await verifyAuthenticodeSignature(path.join(extractedPath, "op.exe"));
|
||||||
// Azure-era (v2.31.0+): strict Authenticode (matches current docs).
|
|
||||||
// Sectigo-era (pre-v2.31.0): loose Authenticode (Subject + Status only;
|
|
||||||
// the Sectigo cert lacks the Microsoft issuer and publisher EKU).
|
|
||||||
await verifyAuthenticodeSignature(
|
|
||||||
opExePath,
|
|
||||||
undefined,
|
|
||||||
isAzureSignedEra(this.version),
|
|
||||||
);
|
|
||||||
core.info("1Password CLI signature verified");
|
core.info("1Password CLI signature verified");
|
||||||
|
|
||||||
core.addPath(extractedPath);
|
core.addPath(extractedPath);
|
||||||
|
|||||||
Reference in New Issue
Block a user