diff --git a/__tests__/sigstore/sigstore-cosign-old.test.itg.ts b/__tests__/sigstore/sigstore-cosign-old.test.itg.ts new file mode 100644 index 00000000..c3efb2f8 --- /dev/null +++ b/__tests__/sigstore/sigstore-cosign-old.test.itg.ts @@ -0,0 +1,93 @@ +/** + * 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, jest, it} from '@jest/globals'; +import * as path from 'path'; + +import {Buildx} from '../../src/buildx/buildx'; +import {Build} from '../../src/buildx/build'; +import {Install as CosignInstall} from '../../src/cosign/install'; +import {Docker} from '../../src/docker/docker'; +import {Exec} from '../../src/exec'; +import {Sigstore} from '../../src/sigstore/sigstore'; + +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; + +// needs current GitHub repo info +jest.unmock('@actions/github'); + +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 6fff3f63..90483f32 100644 --- a/__tests__/sigstore/sigstore.test.itg.ts +++ b/__tests__/sigstore/sigstore.test.itg.ts @@ -38,7 +38,7 @@ jest.unmock('@actions/github'); beforeAll(async () => { const cosignInstall = new CosignInstall(); const cosignBinPath = await cosignInstall.download({ - version: 'v3.0.2' + version: 'v3.0.4' }); await cosignInstall.install(cosignBinPath); }, 100000); diff --git a/src/sigstore/sigstore.ts b/src/sigstore/sigstore.ts index 8c2b3e55..d8b899af 100644 --- a/src/sigstore/sigstore.ts +++ b/src/sigstore/sigstore.ts @@ -22,6 +22,7 @@ import * as core from '@actions/core'; import {bundleFromJSON, bundleToJSON} from '@sigstore/bundle'; import {Artifact, Bundle, CIContextProvider, DSSEBundleBuilder, FulcioSigner, RekorWitness, TSAWitness, Witness} from '@sigstore/sign'; +import {Context} from '../context'; import {Cosign} from '../cosign/cosign'; import {Exec} from '../exec'; import {GitHub} from '../github'; @@ -73,6 +74,40 @@ export class Sigstore { core.info(`Using Sigstore signing endpoint: ${endpoints.fulcioURL}`); const noTransparencyLog = Sigstore.noTransparencyLog(opts.noTransparencyLog); + const cosignExtraArgs: string[] = []; + if (await this.cosign.versionSatisfies('>=3.0.4')) { + await core.group(`Creating Sigstore protobuf signing config`, async () => { + const signingConfig = Context.tmpName({ + template: 'signing-config-XXXXXX.json', + tmpdir: Context.tmpDir() + }); + // prettier-ignore + const createConfigArgs = [ + 'signing-config', + 'create', + '--with-default-services=true', + `--out=${signingConfig}` + ]; + if (noTransparencyLog) { + createConfigArgs.push('--no-default-rekor=true'); + } + await Exec.exec('cosign', createConfigArgs, { + env: Object.assign({}, process.env, { + COSIGN_EXPERIMENTAL: '1' + }) as { + [key: string]: string; + } + }); + core.info(JSON.stringify(JSON.parse(fs.readFileSync(signingConfig, {encoding: 'utf-8'})), null, 2)); + cosignExtraArgs.push(`--signing-config=${signingConfig}`); + }); + } else { + cosignExtraArgs.push('--use-signing-config'); + if (noTransparencyLog) { + cosignExtraArgs.push('--tlog-upload=false'); + } + } + for (const imageName of opts.imageNames) { const attestationDigests = await this.imageTools.attestationDigests(`${imageName}@${opts.imageDigest}`); for (const attestationDigest of attestationDigests) { @@ -85,11 +120,8 @@ export class Sigstore { '--oidc-provider', 'github-actions', '--registry-referrers-mode', 'oci-1-1', '--new-bundle-format', - '--use-signing-config' + ...cosignExtraArgs ]; - if (noTransparencyLog) { - cosignArgs.push('--tlog-upload=false'); - } core.info(`[command]cosign ${[...cosignArgs, attestationRef].join(' ')}`); const execRes = await Exec.getExecOutput('cosign', ['--verbose', ...cosignArgs, attestationRef], { ignoreReturnCode: true,