Merge pull request #904 from crazy-max/cosign-bin-verify
Some checks failed
publish / publish (push) Has been cancelled

cosign(install): verify binary signature with keyless verification bundle
This commit is contained in:
CrazyMax
2025-12-18 09:19:02 +01:00
committed by GitHub
6 changed files with 188 additions and 11 deletions

View File

@@ -27,7 +27,10 @@ describe('download', () => {
'install cosign %s', async (version) => {
await expect((async () => {
const install = new Install();
const toolPath = await install.download(version);
const toolPath = await install.download({
version: version,
verifySignature: true
});
if (!fs.existsSync(toolPath)) {
throw new Error('toolPath does not exist');
}

View File

@@ -38,7 +38,7 @@ describe('download', () => {
])(
'acquires %p of cosign', async (version) => {
const install = new Install();
const toolPath = await install.download(version);
const toolPath = await install.download({version});
expect(fs.existsSync(toolPath)).toBe(true);
const cosignBin = await install.install(toolPath, tmpDir);
expect(fs.existsSync(cosignBin)).toBe(true);
@@ -52,7 +52,7 @@ describe('download', () => {
])(
'acquires %p of cosign with cache', async (version) => {
const install = new Install();
const toolPath = await install.download(version);
const toolPath = await install.download({version});
expect(fs.existsSync(toolPath)).toBe(true);
}, 100000);
@@ -63,7 +63,10 @@ describe('download', () => {
])(
'acquires %p of cosign without cache', async (version) => {
const install = new Install();
const toolPath = await install.download(version, true);
const toolPath = await install.download({
version: version,
ghaNoCache: true
});
expect(fs.existsSync(toolPath)).toBe(true);
}, 100000);
@@ -80,7 +83,9 @@ describe('download', () => {
jest.spyOn(osm, 'platform').mockImplementation(() => os as NodeJS.Platform);
jest.spyOn(osm, 'arch').mockImplementation(() => arch);
const install = new Install();
const cosignBin = await install.download('latest');
const cosignBin = await install.download({
version: 'latest'
});
expect(fs.existsSync(cosignBin)).toBe(true);
}, 100000);
});

View File

@@ -30,7 +30,9 @@ jest.unmock('@actions/github');
beforeAll(async () => {
const cosignInstall = new CosignInstall();
const cosignBinPath = await cosignInstall.download('v3.0.2', true);
const cosignBinPath = await cosignInstall.download({
version: 'v3.0.2'
});
await cosignInstall.install(cosignBinPath);
}, 100000);

View File

@@ -58,6 +58,8 @@
"@octokit/plugin-rest-endpoint-methods": "^10.4.1",
"@sigstore/bundle": "^4.0.0",
"@sigstore/sign": "^4.0.1",
"@sigstore/tuf": "^4.0.0",
"@sigstore/verify": "^3.0.0",
"async-retry": "^1.3.3",
"csv-parse": "^6.1.0",
"gunzip-maybe": "^1.4.2",

View File

@@ -19,6 +19,9 @@ import os from 'os';
import path from 'path';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import {bundleFromJSON, SerializedBundle} from '@sigstore/bundle';
import * as tuf from '@sigstore/tuf';
import {toSignedEntity, toTrustMaterial, Verifier} from '@sigstore/verify';
import * as semver from 'semver';
import * as util from 'util';
@@ -34,6 +37,13 @@ import {DownloadVersion} from '../types/cosign/cosign';
import {GitHubRelease} from '../types/github';
import {dockerfileContent} from './dockerfile';
export interface DownloadOpts {
version: string;
ghaNoCache?: boolean;
skipState?: boolean;
verifySignature?: boolean;
}
export interface InstallOpts {
githubToken?: string;
buildx?: Buildx;
@@ -48,8 +58,8 @@ export class Install {
this.buildx = opts?.buildx || new Buildx();
}
public async download(v: string, ghaNoCache?: boolean, skipState?: boolean): Promise<string> {
const version: DownloadVersion = await Install.getDownloadVersion(v);
public async download(opts: DownloadOpts): Promise<string> {
const version: DownloadVersion = await Install.getDownloadVersion(opts.version);
core.debug(`Install.download version: ${version.version}`);
const release: GitHubRelease = await Install.getRelease(version, this.githubToken);
@@ -68,7 +78,7 @@ export class Install {
htcVersion: vspec,
baseCacheDir: path.join(os.homedir(), '.bin'),
cacheFile: os.platform() == 'win32' ? 'cosign.exe' : 'cosign',
ghaNoCache: ghaNoCache
ghaNoCache: opts.ghaNoCache
});
const cacheFoundPath = await installCache.find();
@@ -83,7 +93,11 @@ export class Install {
const htcDownloadPath = await tc.downloadTool(downloadURL, undefined, this.githubToken);
core.debug(`Install.download htcDownloadPath: ${htcDownloadPath}`);
const cacheSavePath = await installCache.save(htcDownloadPath, skipState);
if (opts.verifySignature && semver.satisfies(vspec, '>=3.0.1')) {
await this.verifySignature(htcDownloadPath, downloadURL);
}
const cacheSavePath = await installCache.save(htcDownloadPath, opts.skipState);
core.info(`Cached to ${cacheSavePath}`);
return cacheSavePath;
}
@@ -176,6 +190,35 @@ export class Install {
return await new Buildx({standalone: buildStandalone}).getCommand(args);
}
private async verifySignature(cosignBinPath: string, downloadURL: string): Promise<void> {
const bundleURL = `${downloadURL}.sigstore.json`;
core.info(`Downloading keyless verification bundle at ${bundleURL}`);
const bundlePath = await tc.downloadTool(bundleURL, undefined, this.githubToken);
core.debug(`Install.verifySignature bundlePath: ${bundlePath}`);
core.info(`Verifying keyless verification bundle signature`);
const parsedBundle = JSON.parse(fs.readFileSync(bundlePath, 'utf-8')) as SerializedBundle;
const bundle = bundleFromJSON(parsedBundle);
core.info(`Fetching Sigstore TUF trusted root metadata`);
const trustedRoot = await tuf.getTrustedRoot();
const trustMaterial = toTrustMaterial(trustedRoot);
try {
core.info(`Verifying cosign binary signature`);
const signedEntity = toSignedEntity(bundle, fs.readFileSync(cosignBinPath));
const verifier = new Verifier(trustMaterial);
const signer = verifier.verify(signedEntity, {
subjectAlternativeName: 'keyless@projectsigstore.iam.gserviceaccount.com',
extensions: {issuer: 'https://accounts.google.com'}
});
core.debug(`Install.verifySignature signer: ${JSON.stringify(signer)}`);
core.info(`Cosign binary signature verified!`);
} catch (err) {
throw new Error(`Failed to verify cosign binary signature: ${err}`);
}
}
private filename(): string {
let arch: string;
switch (os.arch()) {

124
yarn.lock
View File

@@ -1185,6 +1185,8 @@ __metadata:
"@sigstore/bundle": "npm:^4.0.0"
"@sigstore/rekor-types": "npm:^3.0.0"
"@sigstore/sign": "npm:^4.0.1"
"@sigstore/tuf": "npm:^4.0.0"
"@sigstore/verify": "npm:^3.0.0"
"@types/gunzip-maybe": "npm:^1.4.3"
"@types/he": "npm:^1.2.3"
"@types/js-yaml": "npm:^4.0.9"
@@ -2273,6 +2275,27 @@ __metadata:
languageName: node
linkType: hard
"@sigstore/tuf@npm:^4.0.0":
version: 4.0.0
resolution: "@sigstore/tuf@npm:4.0.0"
dependencies:
"@sigstore/protobuf-specs": "npm:^0.5.0"
tuf-js: "npm:^4.0.0"
checksum: 10/8f47a0bc814a8ee1ef59bc90eb7954e0bb33734a913c77c04bdbf08fce2622d406feb0b243191154453a046224fcc512e916c1c919563fab902070b66837ad5e
languageName: node
linkType: hard
"@sigstore/verify@npm:^3.0.0":
version: 3.0.0
resolution: "@sigstore/verify@npm:3.0.0"
dependencies:
"@sigstore/bundle": "npm:^4.0.0"
"@sigstore/core": "npm:^3.0.0"
"@sigstore/protobuf-specs": "npm:^0.5.0"
checksum: 10/c5b4891f42586a4c68fb22f127f19dd16b0bda0388ae8a40727cedd2443919006df3ec1ac4d6c3bd2786cff4c3f8d987135e87979262790e718bcc53e8a3a6c1
languageName: node
linkType: hard
"@sinclair/typebox@npm:^0.34.0":
version: 0.34.41
resolution: "@sinclair/typebox@npm:0.34.41"
@@ -2333,6 +2356,23 @@ __metadata:
languageName: node
linkType: hard
"@tufjs/canonical-json@npm:2.0.0":
version: 2.0.0
resolution: "@tufjs/canonical-json@npm:2.0.0"
checksum: 10/cc719a1d0d0ae1aa1ba551a82c87dcbefac088e433c03a3d8a1d547ea721350e47dab4ab5b0fca40d5c7ab1f4882e72edc39c9eae15bf47c45c43bcb6ee39f4f
languageName: node
linkType: hard
"@tufjs/models@npm:4.0.0":
version: 4.0.0
resolution: "@tufjs/models@npm:4.0.0"
dependencies:
"@tufjs/canonical-json": "npm:2.0.0"
minimatch: "npm:^9.0.5"
checksum: 10/1b8d119b4144018d92237aa0dfcf4ac85ee609dd0062d15817736cfd0d0d594761e9179dd7b580894a6e7f67dd06d4421f16534756b66441c8838e8644e77632
languageName: node
linkType: hard
"@tybys/wasm-util@npm:^0.10.0":
version: 0.10.1
resolution: "@tybys/wasm-util@npm:0.10.1"
@@ -3947,6 +3987,18 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:^4.4.1":
version: 4.4.3
resolution: "debug@npm:4.4.3"
dependencies:
ms: "npm:^2.1.3"
peerDependenciesMeta:
supports-color:
optional: true
checksum: 10/9ada3434ea2993800bd9a1e320bd4aa7af69659fb51cca685d390949434bc0a8873c21ed7c9b852af6f2455a55c6d050aa3937d52b3c69f796dab666f762acad
languageName: node
linkType: hard
"dedent@npm:^1.6.0":
version: 1.7.0
resolution: "dedent@npm:1.7.0"
@@ -7062,6 +7114,25 @@ __metadata:
languageName: node
linkType: hard
"make-fetch-happen@npm:^15.0.0":
version: 15.0.3
resolution: "make-fetch-happen@npm:15.0.3"
dependencies:
"@npmcli/agent": "npm:^4.0.0"
cacache: "npm:^20.0.1"
http-cache-semantics: "npm:^4.1.1"
minipass: "npm:^7.0.2"
minipass-fetch: "npm:^5.0.0"
minipass-flush: "npm:^1.0.5"
minipass-pipeline: "npm:^1.2.4"
negotiator: "npm:^1.0.0"
proc-log: "npm:^6.0.0"
promise-retry: "npm:^2.0.1"
ssri: "npm:^13.0.0"
checksum: 10/78da4fc1df83cb596e2bae25aa0653b8a9c6cbdd6674a104894e03be3acfcd08c70b78f06ef6407fbd6b173f6a60672480d78641e693d05eb71c09c13ee35278
languageName: node
linkType: hard
"make-fetch-happen@npm:^15.0.2":
version: 15.0.2
resolution: "make-fetch-happen@npm:15.0.2"
@@ -7175,6 +7246,15 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^9.0.5":
version: 9.0.5
resolution: "minimatch@npm:9.0.5"
dependencies:
brace-expansion: "npm:^2.0.1"
checksum: 10/dd6a8927b063aca6d910b119e1f2df6d2ce7d36eab91de83167dd136bb85e1ebff97b0d3de1cb08bd1f7e018ca170b4962479fefab5b2a69e2ae12cb2edc8348
languageName: node
linkType: hard
"minimist@npm:^1.2.0, minimist@npm:^1.2.6":
version: 1.2.7
resolution: "minimist@npm:1.2.7"
@@ -7237,6 +7317,21 @@ __metadata:
languageName: node
linkType: hard
"minipass-fetch@npm:^5.0.0":
version: 5.0.0
resolution: "minipass-fetch@npm:5.0.0"
dependencies:
encoding: "npm:^0.1.13"
minipass: "npm:^7.0.3"
minipass-sized: "npm:^1.0.3"
minizlib: "npm:^3.0.1"
dependenciesMeta:
encoding:
optional: true
checksum: 10/4fb7dca630a64e6970a8211dade505bfe260d0b8d60beb348dcdfb95fe35ef91d977b29963929c9017ae0805686aa3f413107dc6bc5deac9b9e26b0b41c3b86c
languageName: node
linkType: hard
"minipass-flush@npm:^1.0.5":
version: 1.0.5
resolution: "minipass-flush@npm:1.0.5"
@@ -7347,7 +7442,7 @@ __metadata:
languageName: node
linkType: hard
"ms@npm:^2.0.0, ms@npm:^2.1.1":
"ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3":
version: 2.1.3
resolution: "ms@npm:2.1.3"
checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d
@@ -7879,6 +7974,13 @@ __metadata:
languageName: node
linkType: hard
"proc-log@npm:^6.0.0":
version: 6.1.0
resolution: "proc-log@npm:6.1.0"
checksum: 10/9033f30f168ed5a0991b773d0c50ff88384c4738e9a0a67d341de36bf7293771eed648ab6a0562f62276da12fde91f3bbfc75ffff6e71ad49aafd74fc646be66
languageName: node
linkType: hard
"process-nextick-args@npm:~2.0.0":
version: 2.0.1
resolution: "process-nextick-args@npm:2.0.1"
@@ -8552,6 +8654,15 @@ __metadata:
languageName: node
linkType: hard
"ssri@npm:^13.0.0":
version: 13.0.0
resolution: "ssri@npm:13.0.0"
dependencies:
minipass: "npm:^7.0.3"
checksum: 10/fd59bfedf0659c1b83f6e15459162da021f08ec0f5834dd9163296f8b77ee82f9656aa1d415c3d3848484293e0e6aefdd482e863e52ddb53d520bb73da1eeec1
languageName: node
linkType: hard
"ssri@npm:^9.0.0":
version: 9.0.1
resolution: "ssri@npm:9.0.1"
@@ -9067,6 +9178,17 @@ __metadata:
languageName: node
linkType: hard
"tuf-js@npm:^4.0.0":
version: 4.0.0
resolution: "tuf-js@npm:4.0.0"
dependencies:
"@tufjs/models": "npm:4.0.0"
debug: "npm:^4.4.1"
make-fetch-happen: "npm:^15.0.0"
checksum: 10/7de216e39578f7abd449b2eaed7977b9e99f3b66bcc7ff24f4f4a4a4bcca032a1c180e2a3fd20019ed820d898010fcd9f2654446c87dbf93a9b13f163bb99422
languageName: node
linkType: hard
"tunnel@npm:^0.0.6":
version: 0.0.6
resolution: "tunnel@npm:0.0.6"