From c6c9b9a156be361728ab4c59b856256b21a56e21 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:17:49 +0200 Subject: [PATCH 1/2] sigstore: handle cosign 3.1.1 signing defaults Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- src/sigstore/sigstore.ts | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/sigstore/sigstore.ts b/src/sigstore/sigstore.ts index 7b50c59c..00e34980 100644 --- a/src/sigstore/sigstore.ts +++ b/src/sigstore/sigstore.ts @@ -88,7 +88,7 @@ export class Sigstore { '--yes', '--oidc-provider', 'github-actions', '--registry-referrers-mode', 'oci-1-1', - '--new-bundle-format', + ...(await this.bundleFormatArgs()), ...cosignExtraArgs ]; core.info(`[command]${this.cosign.binPath} ${[...cosignArgs, attestationRef].join(' ')}`); @@ -113,7 +113,7 @@ export class Sigstore { } const parsedBundle = Sigstore.parseBundle(bundleFromJSON(signResult.bundle)); if (parsedBundle.tlogID) { - core.info(`Uploaded to Rekor transparency log: ${SEARCH_URL}?logIndex=${parsedBundle.tlogID}`); + await this.logTransparencyUpload(parsedBundle.tlogID, opts.noTransparencyLog); } core.info(`Signature manifest pushed: https://oci.dag.dev/?referrers=${attestationRef}`); result[attestationRef] = { @@ -179,7 +179,7 @@ export class Sigstore { const cosignArgs = [ 'verify', '--experimental-oci11', - '--new-bundle-format', + ...(await this.bundleFormatArgs()), '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', '--certificate-identity-regexp', opts.certificateIdentityRegexp ]; @@ -273,7 +273,7 @@ export class Sigstore { 'attest-blob', '--yes', '--oidc-provider', 'github-actions', - '--new-bundle-format', + ...(await this.bundleFormatArgs()), '--statement', p, '--type', COSIGN_PREDICATE_SLSA_PROVENANCE_V1, '--bundle', bundlePath, @@ -306,7 +306,7 @@ export class Sigstore { core.info(` - ${subject.name} (${digestAlg}:${digestValue})`); } if (parsedBundle.tlogID) { - core.info(`Attestation signature uploaded to Rekor transparency log: ${SEARCH_URL}?logIndex=${parsedBundle.tlogID}`); + await this.logTransparencyUpload(parsedBundle.tlogID, opts.noTransparencyLog); } core.info(`Sigstore bundle written to: ${bundlePath}`); result[p] = { @@ -336,7 +336,7 @@ export class Sigstore { // prettier-ignore const cosignArgs = [ 'verify-blob-attestation', - '--new-bundle-format', + ...(await this.bundleFormatArgs()), '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', '--certificate-identity-regexp', opts.certificateIdentityRegexp, '--type', opts.predicateType ?? COSIGN_PREDICATE_SLSA_PROVENANCE_V1 @@ -417,6 +417,7 @@ export class Sigstore { core.info(`Upload to transparency log: ${disableTransparencyLog ? 'disabled' : 'enabled'}`); if (await this.cosign.versionSatisfies('>=3.0.4')) { + const useRekorV2 = await this.useRekorV2(noTransparencyLog); await core.group(`Creating Sigstore protobuf signing config`, async () => { const signingConfig = Context.tmpName({ template: 'signing-config-XXXXXX.json', @@ -426,7 +427,7 @@ export class Sigstore { const createConfigArgs = [ 'signing-config', 'create', - '--with-default-services=true', + useRekorV2 ? '--with-default-rekor-v2=true' : '--with-default-services=true', `--out=${signingConfig}` ]; if (disableTransparencyLog) { @@ -451,6 +452,18 @@ export class Sigstore { return cosignExtraArgs; } + private async logTransparencyUpload(tlogID: string, noTransparencyLog?: boolean): Promise { + if (await this.useRekorV2(noTransparencyLog)) { + core.info(`Attestation signature uploaded to Rekor v2 transparency log with index ${tlogID}`); + return; + } + core.info(`Attestation signature uploaded to Rekor transparency log: ${SEARCH_URL}?logIndex=${tlogID}`); + } + + private async useRekorV2(noTransparencyLog?: boolean): Promise { + return !Sigstore.noTransparencyLog(noTransparencyLog) && (await this.cosign.versionSatisfies('>=3.1.1')); + } + private static getProvenanceBlobs(opts: SignProvenanceBlobsOpts): Record { // For single platform build const singleProvenance = path.join(opts.localExportDir, 'provenance.json'); @@ -511,4 +524,12 @@ export class Sigstore { } return new X509Certificate(certBytes); } + + private async bundleFormatArgs(): Promise { + // Cosign 3.1.1 makes the new bundle format the default and deprecates this flag. + if (await this.cosign.versionSatisfies('>=3.1.1')) { + return []; + } + return ['--new-bundle-format']; + } } From 8c401e58dd7b7ae3b2ddc780f56097edd7d37521 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:10:20 +0200 Subject: [PATCH 2/2] sigstore: make rekor v2 signing opt-in Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- __tests__/sigstore/sigstore.test.itg.ts | 86 ++++++++++++++++--------- src/sigstore/sigstore.ts | 35 ++++++---- src/types/sigstore/sigstore.ts | 2 + 3 files changed, 80 insertions(+), 43 deletions(-) diff --git a/__tests__/sigstore/sigstore.test.itg.ts b/__tests__/sigstore/sigstore.test.itg.ts index abb0e2bf..27d99a9e 100644 --- a/__tests__/sigstore/sigstore.test.itg.ts +++ b/__tests__/sigstore/sigstore.test.itg.ts @@ -18,6 +18,7 @@ import {beforeAll, describe, expect, it, test} from 'vitest'; import fs from 'fs'; import os from 'os'; import * as path from 'path'; +import * as semver from 'semver'; import {Buildx} from '../../src/buildx/buildx.js'; import {Build} from '../../src/buildx/build.js'; @@ -37,7 +38,14 @@ const maybeIdToken = runTest && process.env.ACTIONS_ID_TOKEN_REQUEST_URL ? descr const imageName = 'ghcr.io/docker/actions-toolkit-test'; const currentCosignVersion = 'v3.1.1'; -const signAttestationCosignVersions = ['v3.0.2', 'v3.0.6', currentCosignVersion] as const; +const signingCosignVersions = ['v3.0.2', 'v3.0.6', currentCosignVersion] as const; +const signingCases = signingCosignVersions.flatMap(cosignVersion => { + const cases = [{cosignVersion, rekorV2: false}]; + if (semver.satisfies(cosignVersion, '>=3.1.1')) { + cases.push({cosignVersion, rekorV2: true}); + } + return cases; +}); const installedCosign = new Map>(); async function installCosign(version: string): Promise { @@ -56,8 +64,8 @@ async function installCosign(version: string): Promise { return await installedPath; } -for (const cosignVersion of signAttestationCosignVersions) { - maybeIdToken(`signAttestationManifests with cosign ${cosignVersion}`, () => { +for (const {cosignVersion, rekorV2} of signingCases) { + maybeIdToken(`signAttestationManifests with cosign ${cosignVersion}${rekorV2 ? ' and Rekor v2' : ''}`, () => { let sigstore: Sigstore; beforeAll(async () => { @@ -71,7 +79,7 @@ for (const cosignVersion of signAttestationCosignVersions) { it('build, sign and verify', async () => { const buildx = new Buildx(); const build = new Build({buildx: buildx}); - const versionTag = cosignVersion.replace(/^v/, '').replace(/\./g, '-'); + const versionTag = `${cosignVersion.replace(/^v/, '').replace(/\./g, '-')}${rekorV2 ? '-rekor-v2' : ''}`; await expect( (async () => { @@ -107,6 +115,7 @@ for (const cosignVersion of signAttestationCosignVersions) { const signResults = await sigstore.signAttestationManifests({ imageNames: [imageName], imageDigest: buildDigest!, + rekorV2, retryOnManifestUnknown: true }); expect(Object.keys(signResults).length).toEqual(2); @@ -164,37 +173,50 @@ maybe('verifyImageAttestations', () => { }); }); -maybeIdToken('signProvenanceBlobs', () => { - it('single platform', async () => { - const sigstore = new Sigstore(); - const results = await sigstore.signProvenanceBlobs({ - localExportDir: path.join(fixturesDir, 'sigstore', 'single') +for (const {cosignVersion, rekorV2} of signingCases) { + maybeIdToken(`signProvenanceBlobs with cosign ${cosignVersion}${rekorV2 ? ' and Rekor v2' : ''}`, () => { + let sigstore: Sigstore; + + beforeAll(async () => { + sigstore = new Sigstore({ + cosign: new Cosign({ + binPath: await installCosign(cosignVersion) + }) + }); + }, 100000); + + it('single platform', async () => { + const results = await sigstore.signProvenanceBlobs({ + localExportDir: path.join(fixturesDir, 'sigstore', 'single'), + rekorV2 + }); + expect(Object.keys(results).length).toEqual(1); + const provenancePath = Object.keys(results)[0]; + expect(provenancePath).toEqual(path.join(fixturesDir, 'sigstore', 'single', 'provenance.json')); + expect(fs.existsSync(results[provenancePath].bundlePath)).toBe(true); + expect(results[provenancePath].payload).toBeDefined(); + expect(results[provenancePath].certificate).toBeDefined(); + expect(results[provenancePath].tlogID).toBeDefined(); + console.log(provenancePath, JSON.stringify(results[provenancePath].payload, null, 2)); }); - expect(Object.keys(results).length).toEqual(1); - const provenancePath = Object.keys(results)[0]; - expect(provenancePath).toEqual(path.join(fixturesDir, 'sigstore', 'single', 'provenance.json')); - expect(fs.existsSync(results[provenancePath].bundlePath)).toBe(true); - expect(results[provenancePath].payload).toBeDefined(); - expect(results[provenancePath].certificate).toBeDefined(); - expect(results[provenancePath].tlogID).toBeDefined(); - console.log(provenancePath, JSON.stringify(results[provenancePath].payload, null, 2)); - }); - it('multi-platform', async () => { - const sigstore = new Sigstore(); - const results = await sigstore.signProvenanceBlobs({ - localExportDir: path.join(fixturesDir, 'sigstore', 'multi') + + it('multi-platform', async () => { + const results = await sigstore.signProvenanceBlobs({ + localExportDir: path.join(fixturesDir, 'sigstore', 'multi'), + rekorV2 + }); + expect(Object.keys(results).length).toEqual(2); + for (const [provenancePath, res] of Object.entries(results)) { + expect(provenancePath).toMatch(/linux_(amd64|arm64)\/provenance.json/); + expect(fs.existsSync(res.bundlePath)).toBe(true); + expect(res.payload).toBeDefined(); + expect(res.certificate).toBeDefined(); + expect(res.tlogID).toBeDefined(); + console.log(provenancePath, JSON.stringify(res.payload, null, 2)); + } }); - expect(Object.keys(results).length).toEqual(2); - for (const [provenancePath, res] of Object.entries(results)) { - expect(provenancePath).toMatch(/linux_(amd64|arm64)\/provenance.json/); - expect(fs.existsSync(res.bundlePath)).toBe(true); - expect(res.payload).toBeDefined(); - expect(res.certificate).toBeDefined(); - expect(res.tlogID).toBeDefined(); - console.log(provenancePath, JSON.stringify(res.payload, null, 2)); - } }); -}); +} maybeIdToken('verifySignedArtifacts', () => { let sigstore: Sigstore; diff --git a/src/sigstore/sigstore.ts b/src/sigstore/sigstore.ts index 00e34980..e86745e5 100644 --- a/src/sigstore/sigstore.ts +++ b/src/sigstore/sigstore.ts @@ -50,6 +50,11 @@ export interface SigstoreOpts { imageTools?: ImageTools; } +interface CosignSigningConfigOpts { + noTransparencyLog?: boolean; + rekorV2?: boolean; +} + const COSIGN_PREDICATE_SLSA_PROVENANCE_V1 = 'slsaprovenance1'; export class Sigstore { @@ -71,7 +76,11 @@ export class Sigstore { throw new Error('missing "id-token" permission. Please add "permissions: id-token: write" to your workflow.'); } - const cosignExtraArgs = await this.cosignSigningConfigArgs(opts.noTransparencyLog); + const cosignSigningConfigOpts = { + noTransparencyLog: opts.noTransparencyLog, + rekorV2: opts.rekorV2 + }; + const cosignExtraArgs = await this.cosignSigningConfigArgs(cosignSigningConfigOpts); for (const imageName of opts.imageNames) { const attestationDigests = await this.imageTools.attestationDigests({ @@ -113,7 +122,7 @@ export class Sigstore { } const parsedBundle = Sigstore.parseBundle(bundleFromJSON(signResult.bundle)); if (parsedBundle.tlogID) { - await this.logTransparencyUpload(parsedBundle.tlogID, opts.noTransparencyLog); + await this.logTransparencyUpload(parsedBundle.tlogID, cosignSigningConfigOpts); } core.info(`Signature manifest pushed: https://oci.dag.dev/?referrers=${attestationRef}`); result[attestationRef] = { @@ -255,7 +264,11 @@ export class Sigstore { throw new Error('missing "id-token" permission. Please add "permissions: id-token: write" to your workflow.'); } - const cosignExtraArgs = await this.cosignSigningConfigArgs(opts.noTransparencyLog); + const cosignSigningConfigOpts = { + noTransparencyLog: opts.noTransparencyLog, + rekorV2: opts.rekorV2 + }; + const cosignExtraArgs = await this.cosignSigningConfigArgs(cosignSigningConfigOpts); const provenanceBlobs = Sigstore.getProvenanceBlobs(opts); for (const p of Object.keys(provenanceBlobs)) { @@ -306,7 +319,7 @@ export class Sigstore { core.info(` - ${subject.name} (${digestAlg}:${digestValue})`); } if (parsedBundle.tlogID) { - await this.logTransparencyUpload(parsedBundle.tlogID, opts.noTransparencyLog); + await this.logTransparencyUpload(parsedBundle.tlogID, cosignSigningConfigOpts); } core.info(`Sigstore bundle written to: ${bundlePath}`); result[p] = { @@ -410,14 +423,14 @@ export class Sigstore { return noTransparencyLog ?? GitHub.context.payload.repository?.private ?? false; } - private async cosignSigningConfigArgs(noTransparencyLog?: boolean): Promise { + private async cosignSigningConfigArgs(opts: CosignSigningConfigOpts): Promise { const cosignExtraArgs: string[] = []; - const disableTransparencyLog = Sigstore.noTransparencyLog(noTransparencyLog); + const disableTransparencyLog = Sigstore.noTransparencyLog(opts.noTransparencyLog); core.info(`Upload to transparency log: ${disableTransparencyLog ? 'disabled' : 'enabled'}`); if (await this.cosign.versionSatisfies('>=3.0.4')) { - const useRekorV2 = await this.useRekorV2(noTransparencyLog); + const useRekorV2 = await this.useRekorV2(opts); await core.group(`Creating Sigstore protobuf signing config`, async () => { const signingConfig = Context.tmpName({ template: 'signing-config-XXXXXX.json', @@ -452,16 +465,16 @@ export class Sigstore { return cosignExtraArgs; } - private async logTransparencyUpload(tlogID: string, noTransparencyLog?: boolean): Promise { - if (await this.useRekorV2(noTransparencyLog)) { + private async logTransparencyUpload(tlogID: string, opts: CosignSigningConfigOpts): Promise { + if (await this.useRekorV2(opts)) { core.info(`Attestation signature uploaded to Rekor v2 transparency log with index ${tlogID}`); return; } core.info(`Attestation signature uploaded to Rekor transparency log: ${SEARCH_URL}?logIndex=${tlogID}`); } - private async useRekorV2(noTransparencyLog?: boolean): Promise { - return !Sigstore.noTransparencyLog(noTransparencyLog) && (await this.cosign.versionSatisfies('>=3.1.1')); + private async useRekorV2(opts: CosignSigningConfigOpts): Promise { + return opts.rekorV2 === true && !Sigstore.noTransparencyLog(opts.noTransparencyLog) && (await this.cosign.versionSatisfies('>=3.1.1')); } private static getProvenanceBlobs(opts: SignProvenanceBlobsOpts): Record { diff --git a/src/types/sigstore/sigstore.ts b/src/types/sigstore/sigstore.ts index 9fd20e59..fd47176b 100644 --- a/src/types/sigstore/sigstore.ts +++ b/src/types/sigstore/sigstore.ts @@ -31,6 +31,7 @@ export interface SignAttestationManifestsOpts { imageNames: Array; imageDigest: string; noTransparencyLog?: boolean; + rekorV2?: boolean; retryOnManifestUnknown?: boolean; retryLimit?: number; } @@ -56,6 +57,7 @@ export interface SignProvenanceBlobsOpts { localExportDir: string; name?: string; noTransparencyLog?: boolean; + rekorV2?: boolean; } export interface SignProvenanceBlobsResult extends ParsedBundle {