diff --git a/__tests__/cosign/install.test.itg.ts b/__tests__/cosign/install.test.itg.ts index bcd0266..97ac8c2 100644 --- a/__tests__/cosign/install.test.itg.ts +++ b/__tests__/cosign/install.test.itg.ts @@ -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'); } diff --git a/__tests__/cosign/install.test.ts b/__tests__/cosign/install.test.ts index 287cd6c..f77fe10 100644 --- a/__tests__/cosign/install.test.ts +++ b/__tests__/cosign/install.test.ts @@ -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); }); diff --git a/__tests__/sigstore/sigstore.test.itg.ts b/__tests__/sigstore/sigstore.test.itg.ts index 58baae6..6d4b330 100644 --- a/__tests__/sigstore/sigstore.test.itg.ts +++ b/__tests__/sigstore/sigstore.test.itg.ts @@ -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); diff --git a/src/cosign/install.ts b/src/cosign/install.ts index 0673b2a..c9134b7 100644 --- a/src/cosign/install.ts +++ b/src/cosign/install.ts @@ -34,6 +34,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 +55,8 @@ export class Install { this.buildx = opts?.buildx || new Buildx(); } - public async download(v: string, ghaNoCache?: boolean, skipState?: boolean): Promise { - const version: DownloadVersion = await Install.getDownloadVersion(v); + public async download(opts: DownloadOpts): Promise { + 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 +75,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 +90,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 +187,27 @@ export class Install { return await new Buildx({standalone: buildStandalone}).getCommand(args); } + private async verifySignature(cosignBinPath: string, downloadURL: string): Promise { + const cosignBootstrapPath = path.join(Context.tmpDir(), `cosign-bootstrap${os.platform() == 'win32' ? '.exe' : ''}`); + fs.copyFileSync(cosignBinPath, cosignBootstrapPath); + fs.chmodSync(cosignBootstrapPath, '0755'); + + 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 cosign binary signature with keyless verification bundle`); + // prettier-ignore + await Exec.exec(cosignBootstrapPath, [ + 'verify-blob', + '--certificate-identity', 'keyless@projectsigstore.iam.gserviceaccount.com', + '--certificate-oidc-issuer', 'https://accounts.google.com', + '--bundle', bundlePath, + cosignBinPath + ]); + } + private filename(): string { let arch: string; switch (os.arch()) {