Skip to content

Commit 9442c70

Browse files
authored
Merge pull request #904 from crazy-max/cosign-bin-verify
cosign(install): verify binary signature with keyless verification bundle
2 parents 36dc518 + eb8ed6b commit 9442c70

6 files changed

Lines changed: 188 additions & 11 deletions

File tree

__tests__/cosign/install.test.itg.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ describe('download', () => {
2727
'install cosign %s', async (version) => {
2828
await expect((async () => {
2929
const install = new Install();
30-
const toolPath = await install.download(version);
30+
const toolPath = await install.download({
31+
version: version,
32+
verifySignature: true
33+
});
3134
if (!fs.existsSync(toolPath)) {
3235
throw new Error('toolPath does not exist');
3336
}

__tests__/cosign/install.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('download', () => {
3838
])(
3939
'acquires %p of cosign', async (version) => {
4040
const install = new Install();
41-
const toolPath = await install.download(version);
41+
const toolPath = await install.download({version});
4242
expect(fs.existsSync(toolPath)).toBe(true);
4343
const cosignBin = await install.install(toolPath, tmpDir);
4444
expect(fs.existsSync(cosignBin)).toBe(true);
@@ -52,7 +52,7 @@ describe('download', () => {
5252
])(
5353
'acquires %p of cosign with cache', async (version) => {
5454
const install = new Install();
55-
const toolPath = await install.download(version);
55+
const toolPath = await install.download({version});
5656
expect(fs.existsSync(toolPath)).toBe(true);
5757
}, 100000);
5858

@@ -63,7 +63,10 @@ describe('download', () => {
6363
])(
6464
'acquires %p of cosign without cache', async (version) => {
6565
const install = new Install();
66-
const toolPath = await install.download(version, true);
66+
const toolPath = await install.download({
67+
version: version,
68+
ghaNoCache: true
69+
});
6770
expect(fs.existsSync(toolPath)).toBe(true);
6871
}, 100000);
6972

@@ -80,7 +83,9 @@ describe('download', () => {
8083
jest.spyOn(osm, 'platform').mockImplementation(() => os as NodeJS.Platform);
8184
jest.spyOn(osm, 'arch').mockImplementation(() => arch);
8285
const install = new Install();
83-
const cosignBin = await install.download('latest');
86+
const cosignBin = await install.download({
87+
version: 'latest'
88+
});
8489
expect(fs.existsSync(cosignBin)).toBe(true);
8590
}, 100000);
8691
});

__tests__/sigstore/sigstore.test.itg.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ jest.unmock('@actions/github');
3030

3131
beforeAll(async () => {
3232
const cosignInstall = new CosignInstall();
33-
const cosignBinPath = await cosignInstall.download('v3.0.2', true);
33+
const cosignBinPath = await cosignInstall.download({
34+
version: 'v3.0.2'
35+
});
3436
await cosignInstall.install(cosignBinPath);
3537
}, 100000);
3638

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
"@octokit/plugin-rest-endpoint-methods": "^10.4.1",
5959
"@sigstore/bundle": "^4.0.0",
6060
"@sigstore/sign": "^4.0.1",
61+
"@sigstore/tuf": "^4.0.0",
62+
"@sigstore/verify": "^3.0.0",
6163
"async-retry": "^1.3.3",
6264
"csv-parse": "^6.1.0",
6365
"gunzip-maybe": "^1.4.2",

src/cosign/install.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import os from 'os';
1919
import path from 'path';
2020
import * as core from '@actions/core';
2121
import * as tc from '@actions/tool-cache';
22+
import {bundleFromJSON, SerializedBundle} from '@sigstore/bundle';
23+
import * as tuf from '@sigstore/tuf';
24+
import {toSignedEntity, toTrustMaterial, Verifier} from '@sigstore/verify';
2225
import * as semver from 'semver';
2326
import * as util from 'util';
2427

@@ -34,6 +37,13 @@ import {DownloadVersion} from '../types/cosign/cosign';
3437
import {GitHubRelease} from '../types/github';
3538
import {dockerfileContent} from './dockerfile';
3639

40+
export interface DownloadOpts {
41+
version: string;
42+
ghaNoCache?: boolean;
43+
skipState?: boolean;
44+
verifySignature?: boolean;
45+
}
46+
3747
export interface InstallOpts {
3848
githubToken?: string;
3949
buildx?: Buildx;
@@ -48,8 +58,8 @@ export class Install {
4858
this.buildx = opts?.buildx || new Buildx();
4959
}
5060

51-
public async download(v: string, ghaNoCache?: boolean, skipState?: boolean): Promise<string> {
52-
const version: DownloadVersion = await Install.getDownloadVersion(v);
61+
public async download(opts: DownloadOpts): Promise<string> {
62+
const version: DownloadVersion = await Install.getDownloadVersion(opts.version);
5363
core.debug(`Install.download version: ${version.version}`);
5464

5565
const release: GitHubRelease = await Install.getRelease(version, this.githubToken);
@@ -68,7 +78,7 @@ export class Install {
6878
htcVersion: vspec,
6979
baseCacheDir: path.join(os.homedir(), '.bin'),
7080
cacheFile: os.platform() == 'win32' ? 'cosign.exe' : 'cosign',
71-
ghaNoCache: ghaNoCache
81+
ghaNoCache: opts.ghaNoCache
7282
});
7383

7484
const cacheFoundPath = await installCache.find();
@@ -83,7 +93,11 @@ export class Install {
8393
const htcDownloadPath = await tc.downloadTool(downloadURL, undefined, this.githubToken);
8494
core.debug(`Install.download htcDownloadPath: ${htcDownloadPath}`);
8595

86-
const cacheSavePath = await installCache.save(htcDownloadPath, skipState);
96+
if (opts.verifySignature && semver.satisfies(vspec, '>=3.0.1')) {
97+
await this.verifySignature(htcDownloadPath, downloadURL);
98+
}
99+
100+
const cacheSavePath = await installCache.save(htcDownloadPath, opts.skipState);
87101
core.info(`Cached to ${cacheSavePath}`);
88102
return cacheSavePath;
89103
}
@@ -176,6 +190,35 @@ export class Install {
176190
return await new Buildx({standalone: buildStandalone}).getCommand(args);
177191
}
178192

193+
private async verifySignature(cosignBinPath: string, downloadURL: string): Promise<void> {
194+
const bundleURL = `${downloadURL}.sigstore.json`;
195+
core.info(`Downloading keyless verification bundle at ${bundleURL}`);
196+
const bundlePath = await tc.downloadTool(bundleURL, undefined, this.githubToken);
197+
core.debug(`Install.verifySignature bundlePath: ${bundlePath}`);
198+
199+
core.info(`Verifying keyless verification bundle signature`);
200+
const parsedBundle = JSON.parse(fs.readFileSync(bundlePath, 'utf-8')) as SerializedBundle;
201+
const bundle = bundleFromJSON(parsedBundle);
202+
203+
core.info(`Fetching Sigstore TUF trusted root metadata`);
204+
const trustedRoot = await tuf.getTrustedRoot();
205+
const trustMaterial = toTrustMaterial(trustedRoot);
206+
207+
try {
208+
core.info(`Verifying cosign binary signature`);
209+
const signedEntity = toSignedEntity(bundle, fs.readFileSync(cosignBinPath));
210+
const verifier = new Verifier(trustMaterial);
211+
const signer = verifier.verify(signedEntity, {
212+
subjectAlternativeName: '[email protected]',
213+
extensions: {issuer: 'https://accounts.google.com'}
214+
});
215+
core.debug(`Install.verifySignature signer: ${JSON.stringify(signer)}`);
216+
core.info(`Cosign binary signature verified!`);
217+
} catch (err) {
218+
throw new Error(`Failed to verify cosign binary signature: ${err}`);
219+
}
220+
}
221+
179222
private filename(): string {
180223
let arch: string;
181224
switch (os.arch()) {

yarn.lock

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,8 @@ __metadata:
11851185
"@sigstore/bundle": "npm:^4.0.0"
11861186
"@sigstore/rekor-types": "npm:^3.0.0"
11871187
"@sigstore/sign": "npm:^4.0.1"
1188+
"@sigstore/tuf": "npm:^4.0.0"
1189+
"@sigstore/verify": "npm:^3.0.0"
11881190
"@types/gunzip-maybe": "npm:^1.4.3"
11891191
"@types/he": "npm:^1.2.3"
11901192
"@types/js-yaml": "npm:^4.0.9"
@@ -2273,6 +2275,27 @@ __metadata:
22732275
languageName: node
22742276
linkType: hard
22752277

2278+
"@sigstore/tuf@npm:^4.0.0":
2279+
version: 4.0.0
2280+
resolution: "@sigstore/tuf@npm:4.0.0"
2281+
dependencies:
2282+
"@sigstore/protobuf-specs": "npm:^0.5.0"
2283+
tuf-js: "npm:^4.0.0"
2284+
checksum: 10/8f47a0bc814a8ee1ef59bc90eb7954e0bb33734a913c77c04bdbf08fce2622d406feb0b243191154453a046224fcc512e916c1c919563fab902070b66837ad5e
2285+
languageName: node
2286+
linkType: hard
2287+
2288+
"@sigstore/verify@npm:^3.0.0":
2289+
version: 3.0.0
2290+
resolution: "@sigstore/verify@npm:3.0.0"
2291+
dependencies:
2292+
"@sigstore/bundle": "npm:^4.0.0"
2293+
"@sigstore/core": "npm:^3.0.0"
2294+
"@sigstore/protobuf-specs": "npm:^0.5.0"
2295+
checksum: 10/c5b4891f42586a4c68fb22f127f19dd16b0bda0388ae8a40727cedd2443919006df3ec1ac4d6c3bd2786cff4c3f8d987135e87979262790e718bcc53e8a3a6c1
2296+
languageName: node
2297+
linkType: hard
2298+
22762299
"@sinclair/typebox@npm:^0.34.0":
22772300
version: 0.34.41
22782301
resolution: "@sinclair/typebox@npm:0.34.41"
@@ -2333,6 +2356,23 @@ __metadata:
23332356
languageName: node
23342357
linkType: hard
23352358

2359+
"@tufjs/canonical-json@npm:2.0.0":
2360+
version: 2.0.0
2361+
resolution: "@tufjs/canonical-json@npm:2.0.0"
2362+
checksum: 10/cc719a1d0d0ae1aa1ba551a82c87dcbefac088e433c03a3d8a1d547ea721350e47dab4ab5b0fca40d5c7ab1f4882e72edc39c9eae15bf47c45c43bcb6ee39f4f
2363+
languageName: node
2364+
linkType: hard
2365+
2366+
"@tufjs/models@npm:4.0.0":
2367+
version: 4.0.0
2368+
resolution: "@tufjs/models@npm:4.0.0"
2369+
dependencies:
2370+
"@tufjs/canonical-json": "npm:2.0.0"
2371+
minimatch: "npm:^9.0.5"
2372+
checksum: 10/1b8d119b4144018d92237aa0dfcf4ac85ee609dd0062d15817736cfd0d0d594761e9179dd7b580894a6e7f67dd06d4421f16534756b66441c8838e8644e77632
2373+
languageName: node
2374+
linkType: hard
2375+
23362376
"@tybys/wasm-util@npm:^0.10.0":
23372377
version: 0.10.1
23382378
resolution: "@tybys/wasm-util@npm:0.10.1"
@@ -3947,6 +3987,18 @@ __metadata:
39473987
languageName: node
39483988
linkType: hard
39493989

3990+
"debug@npm:^4.4.1":
3991+
version: 4.4.3
3992+
resolution: "debug@npm:4.4.3"
3993+
dependencies:
3994+
ms: "npm:^2.1.3"
3995+
peerDependenciesMeta:
3996+
supports-color:
3997+
optional: true
3998+
checksum: 10/9ada3434ea2993800bd9a1e320bd4aa7af69659fb51cca685d390949434bc0a8873c21ed7c9b852af6f2455a55c6d050aa3937d52b3c69f796dab666f762acad
3999+
languageName: node
4000+
linkType: hard
4001+
39504002
"dedent@npm:^1.6.0":
39514003
version: 1.7.0
39524004
resolution: "dedent@npm:1.7.0"
@@ -7062,6 +7114,25 @@ __metadata:
70627114
languageName: node
70637115
linkType: hard
70647116

7117+
"make-fetch-happen@npm:^15.0.0":
7118+
version: 15.0.3
7119+
resolution: "make-fetch-happen@npm:15.0.3"
7120+
dependencies:
7121+
"@npmcli/agent": "npm:^4.0.0"
7122+
cacache: "npm:^20.0.1"
7123+
http-cache-semantics: "npm:^4.1.1"
7124+
minipass: "npm:^7.0.2"
7125+
minipass-fetch: "npm:^5.0.0"
7126+
minipass-flush: "npm:^1.0.5"
7127+
minipass-pipeline: "npm:^1.2.4"
7128+
negotiator: "npm:^1.0.0"
7129+
proc-log: "npm:^6.0.0"
7130+
promise-retry: "npm:^2.0.1"
7131+
ssri: "npm:^13.0.0"
7132+
checksum: 10/78da4fc1df83cb596e2bae25aa0653b8a9c6cbdd6674a104894e03be3acfcd08c70b78f06ef6407fbd6b173f6a60672480d78641e693d05eb71c09c13ee35278
7133+
languageName: node
7134+
linkType: hard
7135+
70657136
"make-fetch-happen@npm:^15.0.2":
70667137
version: 15.0.2
70677138
resolution: "make-fetch-happen@npm:15.0.2"
@@ -7175,6 +7246,15 @@ __metadata:
71757246
languageName: node
71767247
linkType: hard
71777248

7249+
"minimatch@npm:^9.0.5":
7250+
version: 9.0.5
7251+
resolution: "minimatch@npm:9.0.5"
7252+
dependencies:
7253+
brace-expansion: "npm:^2.0.1"
7254+
checksum: 10/dd6a8927b063aca6d910b119e1f2df6d2ce7d36eab91de83167dd136bb85e1ebff97b0d3de1cb08bd1f7e018ca170b4962479fefab5b2a69e2ae12cb2edc8348
7255+
languageName: node
7256+
linkType: hard
7257+
71787258
"minimist@npm:^1.2.0, minimist@npm:^1.2.6":
71797259
version: 1.2.7
71807260
resolution: "minimist@npm:1.2.7"
@@ -7237,6 +7317,21 @@ __metadata:
72377317
languageName: node
72387318
linkType: hard
72397319

7320+
"minipass-fetch@npm:^5.0.0":
7321+
version: 5.0.0
7322+
resolution: "minipass-fetch@npm:5.0.0"
7323+
dependencies:
7324+
encoding: "npm:^0.1.13"
7325+
minipass: "npm:^7.0.3"
7326+
minipass-sized: "npm:^1.0.3"
7327+
minizlib: "npm:^3.0.1"
7328+
dependenciesMeta:
7329+
encoding:
7330+
optional: true
7331+
checksum: 10/4fb7dca630a64e6970a8211dade505bfe260d0b8d60beb348dcdfb95fe35ef91d977b29963929c9017ae0805686aa3f413107dc6bc5deac9b9e26b0b41c3b86c
7332+
languageName: node
7333+
linkType: hard
7334+
72407335
"minipass-flush@npm:^1.0.5":
72417336
version: 1.0.5
72427337
resolution: "minipass-flush@npm:1.0.5"
@@ -7347,7 +7442,7 @@ __metadata:
73477442
languageName: node
73487443
linkType: hard
73497444

7350-
"ms@npm:^2.0.0, ms@npm:^2.1.1":
7445+
"ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3":
73517446
version: 2.1.3
73527447
resolution: "ms@npm:2.1.3"
73537448
checksum: 10/aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d
@@ -7879,6 +7974,13 @@ __metadata:
78797974
languageName: node
78807975
linkType: hard
78817976

7977+
"proc-log@npm:^6.0.0":
7978+
version: 6.1.0
7979+
resolution: "proc-log@npm:6.1.0"
7980+
checksum: 10/9033f30f168ed5a0991b773d0c50ff88384c4738e9a0a67d341de36bf7293771eed648ab6a0562f62276da12fde91f3bbfc75ffff6e71ad49aafd74fc646be66
7981+
languageName: node
7982+
linkType: hard
7983+
78827984
"process-nextick-args@npm:~2.0.0":
78837985
version: 2.0.1
78847986
resolution: "process-nextick-args@npm:2.0.1"
@@ -8552,6 +8654,15 @@ __metadata:
85528654
languageName: node
85538655
linkType: hard
85548656

8657+
"ssri@npm:^13.0.0":
8658+
version: 13.0.0
8659+
resolution: "ssri@npm:13.0.0"
8660+
dependencies:
8661+
minipass: "npm:^7.0.3"
8662+
checksum: 10/fd59bfedf0659c1b83f6e15459162da021f08ec0f5834dd9163296f8b77ee82f9656aa1d415c3d3848484293e0e6aefdd482e863e52ddb53d520bb73da1eeec1
8663+
languageName: node
8664+
linkType: hard
8665+
85558666
"ssri@npm:^9.0.0":
85568667
version: 9.0.1
85578668
resolution: "ssri@npm:9.0.1"
@@ -9067,6 +9178,17 @@ __metadata:
90679178
languageName: node
90689179
linkType: hard
90699180

9181+
"tuf-js@npm:^4.0.0":
9182+
version: 4.0.0
9183+
resolution: "tuf-js@npm:4.0.0"
9184+
dependencies:
9185+
"@tufjs/models": "npm:4.0.0"
9186+
debug: "npm:^4.4.1"
9187+
make-fetch-happen: "npm:^15.0.0"
9188+
checksum: 10/7de216e39578f7abd449b2eaed7977b9e99f3b66bcc7ff24f4f4a4a4bcca032a1c180e2a3fd20019ed820d898010fcd9f2654446c87dbf93a9b13f163bb99422
9189+
languageName: node
9190+
linkType: hard
9191+
90709192
"tunnel@npm:^0.0.6":
90719193
version: 0.0.6
90729194
resolution: "tunnel@npm:0.0.6"

0 commit comments

Comments
 (0)