diff --git a/__tests__/sigstore/sigstore-cosign-old.test.itg.ts b/__tests__/sigstore/sigstore-cosign-old.test.itg.ts deleted file mode 100644 index 054503c..0000000 --- a/__tests__/sigstore/sigstore-cosign-old.test.itg.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright 2026 actions-toolkit authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import {beforeAll, describe, expect, it} from 'vitest'; -import * as path from 'path'; - -import {Buildx} from '../../src/buildx/buildx.js'; -import {Build} from '../../src/buildx/build.js'; -import {Install as CosignInstall} from '../../src/cosign/install.js'; -import {Docker} from '../../src/docker/docker.js'; -import {Exec} from '../../src/exec.js'; -import {Sigstore} from '../../src/sigstore/sigstore.js'; - -const fixturesDir = path.join(__dirname, '..', '.fixtures'); - -const runTest = process.env.GITHUB_ACTIONS && process.env.GITHUB_ACTIONS === 'true' && process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu'); - -const maybeIdToken = runTest && process.env.ACTIONS_ID_TOKEN_REQUEST_URL ? describe : describe.skip; - -beforeAll(async () => { - const cosignInstall = new CosignInstall(); - const cosignBinPath = await cosignInstall.download({ - version: 'v3.0.2' - }); - await cosignInstall.install(cosignBinPath); -}, 100000); - -maybeIdToken('signAttestationManifests', () => { - it('build, sign and verify', async () => { - const buildx = new Buildx(); - const build = new Build({buildx: buildx}); - const imageName = 'ghcr.io/docker/actions-toolkit/test'; - - await expect( - (async () => { - await Docker.getExecOutput(['login', '--password-stdin', '--username', process.env.GITHUB_REPOSITORY_OWNER || 'docker', 'ghcr.io'], { - input: Buffer.from(process.env.GITHUB_TOKEN || '') - }); - })() - ).resolves.not.toThrow(); - - await expect( - (async () => { - // prettier-ignore - const buildCmd = await buildx.getCommand([ - '--builder', process.env.CTN_BUILDER_NAME ?? 'default', - 'build', - '-f', path.join(fixturesDir, 'hello.Dockerfile'), - '--provenance=mode=max', - '--tag', `${imageName}:sigstore-itg`, - '--platform', 'linux/amd64,linux/arm64', - '--push', - '--metadata-file', build.getMetadataFilePath(), - fixturesDir - ]); - await Exec.exec(buildCmd.command, buildCmd.args); - })() - ).resolves.not.toThrow(); - - const metadata = build.resolveMetadata(); - expect(metadata).toBeDefined(); - const buildDigest = build.resolveDigest(metadata); - expect(buildDigest).toBeDefined(); - - const sigstore = new Sigstore(); - const signResults = await sigstore.signAttestationManifests({ - imageNames: [imageName], - imageDigest: buildDigest! - }); - expect(Object.keys(signResults).length).toEqual(2); - - const verifyResults = await sigstore.verifySignedManifests(signResults, { - certificateIdentityRegexp: `^https://github.com/docker/actions-toolkit/.github/workflows/test.yml.*$` - }); - expect(Object.keys(verifyResults).length).toEqual(2); - }, 100000); -}); diff --git a/__tests__/sigstore/sigstore.test.itg.ts b/__tests__/sigstore/sigstore.test.itg.ts index f756444..57612d8 100644 --- a/__tests__/sigstore/sigstore.test.itg.ts +++ b/__tests__/sigstore/sigstore.test.itg.ts @@ -16,10 +16,12 @@ import {beforeAll, describe, expect, it, test} from 'vitest'; import fs from 'fs'; +import os from 'os'; import * as path from 'path'; import {Buildx} from '../../src/buildx/buildx.js'; import {Build} from '../../src/buildx/build.js'; +import {Cosign} from '../../src/cosign/cosign.js'; import {Install as CosignInstall} from '../../src/cosign/install.js'; import {Docker} from '../../src/docker/docker.js'; import {Exec} from '../../src/exec.js'; @@ -33,73 +35,106 @@ const runTest = process.env.GITHUB_ACTIONS && process.env.GITHUB_ACTIONS === 'tr const maybe = runTest ? describe : describe.skip; const maybeIdToken = runTest && process.env.ACTIONS_ID_TOKEN_REQUEST_URL ? describe : describe.skip; -beforeAll(async () => { - const cosignInstall = new CosignInstall(); - const cosignBinPath = await cosignInstall.download({ - version: 'v3.0.6' +const imageName = 'ghcr.io/docker/actions-toolkit/test'; +const currentCosignVersion = 'v3.0.6'; +const signAttestationCosignVersions = ['v3.0.2', currentCosignVersion] as const; +const installedCosign = new Map>(); + +async function installCosign(version: string): Promise { + let installedPath = installedCosign.get(version); + if (!installedPath) { + installedPath = (async () => { + const cosignInstall = new CosignInstall(); + const cosignBinPath = await cosignInstall.download({ + version + }); + const installDir = fs.mkdtempSync(path.join(process.env.RUNNER_TEMP || os.tmpdir(), `sigstore-cosign-${version.replace(/[^a-zA-Z0-9]+/g, '-')}-`)); + return await cosignInstall.install(cosignBinPath, installDir); + })(); + installedCosign.set(version, installedPath); + } + return await installedPath; +} + +for (const cosignVersion of signAttestationCosignVersions) { + maybeIdToken(`signAttestationManifests with cosign ${cosignVersion}`, () => { + let sigstore: Sigstore; + + beforeAll(async () => { + sigstore = new Sigstore({ + cosign: new Cosign({ + binPath: await installCosign(cosignVersion) + }) + }); + }, 100000); + + it('build, sign and verify', async () => { + const buildx = new Buildx(); + const build = new Build({buildx: buildx}); + const versionTag = cosignVersion.replace(/^v/, '').replace(/\./g, '-'); + + await expect( + (async () => { + await Docker.getExecOutput(['login', '--password-stdin', '--username', process.env.GITHUB_REPOSITORY_OWNER || 'docker', 'ghcr.io'], { + input: Buffer.from(process.env.GITHUB_TOKEN || '') + }); + })() + ).resolves.not.toThrow(); + + await expect( + (async () => { + // prettier-ignore + const buildCmd = await buildx.getCommand([ + '--builder', process.env.CTN_BUILDER_NAME ?? 'default', + 'build', + '-f', path.join(fixturesDir, 'hello.Dockerfile'), + '--provenance=mode=max', + '--tag', `${imageName}:sigstore-itg-cosign-${versionTag}`, + '--platform', 'linux/amd64,linux/arm64', + '--push', + '--metadata-file', build.getMetadataFilePath(), + fixturesDir + ]); + await Exec.exec(buildCmd.command, buildCmd.args); + })() + ).resolves.not.toThrow(); + + const metadata = build.resolveMetadata(); + expect(metadata).toBeDefined(); + const buildDigest = build.resolveDigest(metadata); + expect(buildDigest).toBeDefined(); + + const signResults = await sigstore.signAttestationManifests({ + imageNames: [imageName], + imageDigest: buildDigest! + }); + expect(Object.keys(signResults).length).toEqual(2); + + const verifyResults = await sigstore.verifySignedManifests(signResults, { + certificateIdentityRegexp: `^https://github.com/docker/actions-toolkit/.github/workflows/test.yml.*$` + }); + expect(Object.keys(verifyResults).length).toEqual(2); + }, 200000); }); - await cosignInstall.install(cosignBinPath); -}, 100000); - -maybeIdToken('signAttestationManifests', () => { - it('build, sign and verify', async () => { - const buildx = new Buildx(); - const build = new Build({buildx: buildx}); - const imageName = 'ghcr.io/docker/actions-toolkit/test'; - - await expect( - (async () => { - await Docker.getExecOutput(['login', '--password-stdin', '--username', process.env.GITHUB_REPOSITORY_OWNER || 'docker', 'ghcr.io'], { - input: Buffer.from(process.env.GITHUB_TOKEN || '') - }); - })() - ).resolves.not.toThrow(); - - await expect( - (async () => { - // prettier-ignore - const buildCmd = await buildx.getCommand([ - '--builder', process.env.CTN_BUILDER_NAME ?? 'default', - 'build', - '-f', path.join(fixturesDir, 'hello.Dockerfile'), - '--provenance=mode=max', - '--tag', `${imageName}:sigstore-itg`, - '--platform', 'linux/amd64,linux/arm64', - '--push', - '--metadata-file', build.getMetadataFilePath(), - fixturesDir - ]); - await Exec.exec(buildCmd.command, buildCmd.args); - })() - ).resolves.not.toThrow(); - - const metadata = build.resolveMetadata(); - expect(metadata).toBeDefined(); - const buildDigest = build.resolveDigest(metadata); - expect(buildDigest).toBeDefined(); - - const sigstore = new Sigstore(); - const signResults = await sigstore.signAttestationManifests({ - imageNames: [imageName], - imageDigest: buildDigest! - }); - expect(Object.keys(signResults).length).toEqual(2); - - const verifyResults = await sigstore.verifySignedManifests(signResults, { - certificateIdentityRegexp: `^https://github.com/docker/actions-toolkit/.github/workflows/test.yml.*$` - }); - expect(Object.keys(verifyResults).length).toEqual(2); - }, 100000); -}); +} maybe('verifyImageAttestations', () => { + let sigstore: Sigstore; + + beforeAll(async () => { + sigstore = new Sigstore({ + cosign: new Cosign({ + binPath: await installCosign(currentCosignVersion) + }) + }); + }, 100000); + test.each([ ['moby/buildkit:master@sha256:84014da3581b2ff2c14cb4f60029cf9caa272b79e58f2e89c651ea6966d7a505', `^https://github.com/docker/github-builder-experimental/.github/workflows/bake.yml.*$`], ['docker/dockerfile-upstream:master@sha256:3e8cd5ebf48acd1a1939649ad1c62ca44c029852b22493c16a9307b654334958', `^https://github.com/docker/github-builder-experimental/.github/workflows/bake.yml.*$`] ])( 'given %p', async (image, certificateIdentityRegexp) => { - const sigstore = new Sigstore(); const verifyResults = await sigstore.verifyImageAttestations(image, { certificateIdentityRegexp: certificateIdentityRegexp }); @@ -114,7 +149,6 @@ maybe('verifyImageAttestations', () => { ); it('default platform', async () => { - const sigstore = new Sigstore(); const verifyResults = await sigstore.verifyImageAttestations('moby/buildkit:master@sha256:84014da3581b2ff2c14cb4f60029cf9caa272b79e58f2e89c651ea6966d7a505', { certificateIdentityRegexp: `^https://github.com/docker/github-builder-experimental/.github/workflows/bake.yml.*$`, platform: OCI.defaultPlatform() @@ -161,8 +195,17 @@ maybeIdToken('signProvenanceBlobs', () => { }); maybeIdToken('verifySignedArtifacts', () => { + let sigstore: Sigstore; + + beforeAll(async () => { + sigstore = new Sigstore({ + cosign: new Cosign({ + binPath: await installCosign(currentCosignVersion) + }) + }); + }, 100000); + it('sign and verify', async () => { - const sigstore = new Sigstore(); const signResults = await sigstore.signProvenanceBlobs({ localExportDir: path.join(fixturesDir, 'sigstore', 'multi') }); diff --git a/src/cosign/cosign.ts b/src/cosign/cosign.ts index 1b83cd0..43998fc 100644 --- a/src/cosign/cosign.ts +++ b/src/cosign/cosign.ts @@ -38,7 +38,7 @@ export interface CosignCommandError { } export class Cosign { - private readonly binPath: string; + public readonly binPath: string; private _version: string; private _versionOnce: boolean; diff --git a/src/sigstore/sigstore.ts b/src/sigstore/sigstore.ts index 2662a15..895cb5b 100644 --- a/src/sigstore/sigstore.ts +++ b/src/sigstore/sigstore.ts @@ -97,7 +97,7 @@ export class Sigstore { if (noTransparencyLog) { createConfigArgs.push('--no-default-rekor=true'); } - await Exec.exec('cosign', createConfigArgs, { + await Exec.exec(this.cosign.binPath, createConfigArgs, { env: Object.assign({}, process.env, { COSIGN_EXPERIMENTAL: '1' }) as { @@ -132,8 +132,8 @@ export class Sigstore { '--new-bundle-format', ...cosignExtraArgs ]; - core.info(`[command]cosign ${[...cosignArgs, attestationRef].join(' ')}`); - const execRes = await Exec.getExecOutput('cosign', ['--verbose', ...cosignArgs, attestationRef], { + core.info(`[command]${this.cosign.binPath} ${[...cosignArgs, attestationRef].join(' ')}`); + const execRes = await Exec.getExecOutput(this.cosign.binPath, ['--verbose', ...cosignArgs, attestationRef], { ignoreReturnCode: true, silent: true, env: Object.assign({}, process.env, { @@ -229,8 +229,8 @@ export class Sigstore { } if (!opts.retryOnManifestUnknown) { - core.info(`[command]cosign ${[...cosignArgs, attestationRef].join(' ')}`); - const execRes = await Exec.getExecOutput('cosign', ['--verbose', ...cosignArgs, attestationRef], { + core.info(`[command]${this.cosign.binPath} ${[...cosignArgs, attestationRef].join(' ')}`); + const execRes = await Exec.getExecOutput(this.cosign.binPath, ['--verbose', ...cosignArgs, attestationRef], { ignoreReturnCode: true, silent: true, env: Object.assign({}, process.env, { @@ -250,9 +250,9 @@ export class Sigstore { const retries = opts.retryLimit ?? 15; let lastError: Error | undefined; - core.info(`[command]cosign ${[...cosignArgs, attestationRef].join(' ')}`); + core.info(`[command]${this.cosign.binPath} ${[...cosignArgs, attestationRef].join(' ')}`); for (let attempt = 0; attempt < retries; attempt++) { - const execRes = await Exec.getExecOutput('cosign', ['--verbose', ...cosignArgs, attestationRef], { + const execRes = await Exec.getExecOutput(this.cosign.binPath, ['--verbose', ...cosignArgs, attestationRef], { ignoreReturnCode: true, silent: true, env: Object.assign({}, process.env, { @@ -361,7 +361,7 @@ export class Sigstore { // if there is no tlog entry, we skip tlog verification but still verify the signed timestamp cosignArgs.push('--use-signed-timestamps', '--insecure-ignore-tlog'); } - const execRes = await Exec.getExecOutput('cosign', [...cosignArgs, '--bundle', signedRes.bundlePath, artifactPath], { + const execRes = await Exec.getExecOutput(this.cosign.binPath, [...cosignArgs, '--bundle', signedRes.bundlePath, artifactPath], { ignoreReturnCode: true }); if (execRes.stderr.length > 0 && execRes.exitCode != 0) {