Skip to content

Commit 379edea

Browse files
committed
sigstore: use signing config with cosign
Signed-off-by: CrazyMax <[email protected]>
1 parent a3d5eee commit 379edea

File tree

5 files changed

+250
-11
lines changed

5 files changed

+250
-11
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ jobs:
149149
permissions:
150150
contents: read
151151
id-token: write # needed for signing with GitHub OIDC Token
152+
packages: write # needed for pushing to GitHub Container Registry
152153
steps:
153154
-
154155
name: Checkout

__tests__/sigstore/sigstore.test.itg.ts renamed to __tests__/sigstore/sigstore-artifact.test.itg.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jest.unmock('@actions/github');
3131
beforeAll(async () => {
3232
const cosignInstall = new CosignInstall();
3333
const cosignBinPath = await cosignInstall.download({
34-
version: 'v3.0.2'
34+
version: 'v3.0.4'
3535
});
3636
await cosignInstall.install(cosignBinPath);
3737
}, 100000);
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Copyright 2026 actions-toolkit authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {describe, expect, jest, it, beforeAll} from '@jest/globals';
18+
import * as path from 'path';
19+
20+
import {Buildx} from '../../src/buildx/buildx';
21+
import {Build} from '../../src/buildx/build';
22+
import {Install as CosignInstall} from '../../src/cosign/install';
23+
import {Docker} from '../../src/docker/docker';
24+
import {Exec} from '../../src/exec';
25+
import {Sigstore} from '../../src/sigstore/sigstore';
26+
27+
const fixturesDir = path.join(__dirname, '..', '.fixtures');
28+
29+
const maybe =
30+
process.env.GITHUB_ACTIONS &&
31+
process.env.GITHUB_ACTIONS === 'true' &&
32+
process.env.ACTIONS_ID_TOKEN_REQUEST_URL &&
33+
process.env.GITHUB_EVENT_NAME &&
34+
process.env.GITHUB_EVENT_NAME !== 'pull_request' &&
35+
process.env.ImageOS &&
36+
process.env.ImageOS.startsWith('ubuntu')
37+
? describe
38+
: describe.skip;
39+
40+
// needs current GitHub repo info
41+
jest.unmock('@actions/github');
42+
43+
beforeAll(async () => {
44+
const cosignInstall = new CosignInstall();
45+
const cosignBinPath = await cosignInstall.download({
46+
version: 'v3.0.2'
47+
});
48+
await cosignInstall.install(cosignBinPath);
49+
}, 100000);
50+
51+
maybe('signAttestationManifests', () => {
52+
it('without signing config', async () => {
53+
const buildx = new Buildx();
54+
const build = new Build({buildx: buildx});
55+
const imageName = 'ghcr.io/docker/actions-toolkit/test';
56+
57+
await expect(
58+
(async () => {
59+
await Docker.getExecOutput(['login', '--password-stdin', '--username', process.env.GITHUB_REPOSITORY_OWNER || 'docker', 'ghcr.io'], {
60+
input: Buffer.from(process.env.GITHUB_TOKEN || '')
61+
});
62+
})()
63+
).resolves.not.toThrow();
64+
65+
await expect(
66+
(async () => {
67+
// prettier-ignore
68+
const buildCmd = await buildx.getCommand([
69+
'--builder', process.env.CTN_BUILDER_NAME ?? 'default',
70+
'build',
71+
'-f', path.join(fixturesDir, 'hello.Dockerfile'),
72+
'--provenance=mode=max',
73+
'--tag', `${imageName}:sigstore-itg`,
74+
'--platform', 'linux/amd64,linux/arm64',
75+
'--push',
76+
'--metadata-file', build.getMetadataFilePath(),
77+
fixturesDir
78+
]);
79+
await Exec.exec(buildCmd.command, buildCmd.args);
80+
})()
81+
).resolves.not.toThrow();
82+
83+
const metadata = build.resolveMetadata();
84+
expect(metadata).toBeDefined();
85+
const buildDigest = build.resolveDigest(metadata);
86+
expect(buildDigest).toBeDefined();
87+
88+
const sigstore = new Sigstore();
89+
const signResults = await sigstore.signAttestationManifests({
90+
imageNames: [imageName],
91+
imageDigest: buildDigest!
92+
});
93+
expect(Object.keys(signResults).length).toEqual(2);
94+
95+
const verifyResults = await sigstore.verifySignedManifests(
96+
{
97+
certificateIdentityRegexp: `^https://github.com/docker/actions-toolkit/.github/workflows/test.yml.*$`
98+
},
99+
signResults
100+
);
101+
expect(Object.keys(verifyResults).length).toEqual(2);
102+
}, 100000);
103+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Copyright 2025 actions-toolkit authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {describe, expect, jest, it, beforeAll} from '@jest/globals';
18+
import * as path from 'path';
19+
20+
import {Buildx} from '../../src/buildx/buildx';
21+
import {Build} from '../../src/buildx/build';
22+
import {Install as CosignInstall} from '../../src/cosign/install';
23+
import {Docker} from '../../src/docker/docker';
24+
import {Exec} from '../../src/exec';
25+
import {Sigstore} from '../../src/sigstore/sigstore';
26+
27+
const fixturesDir = path.join(__dirname, '..', '.fixtures');
28+
29+
const maybe =
30+
process.env.GITHUB_ACTIONS &&
31+
process.env.GITHUB_ACTIONS === 'true' &&
32+
process.env.ACTIONS_ID_TOKEN_REQUEST_URL &&
33+
process.env.GITHUB_EVENT_NAME &&
34+
process.env.GITHUB_EVENT_NAME !== 'pull_request' &&
35+
process.env.ImageOS &&
36+
process.env.ImageOS.startsWith('ubuntu')
37+
? describe
38+
: describe.skip;
39+
40+
// needs current GitHub repo info
41+
jest.unmock('@actions/github');
42+
43+
beforeAll(async () => {
44+
const cosignInstall = new CosignInstall();
45+
const cosignBinPath = await cosignInstall.download({
46+
version: 'v3.0.4'
47+
});
48+
await cosignInstall.install(cosignBinPath);
49+
}, 100000);
50+
51+
maybe('signAttestationManifests', () => {
52+
it('with signing config', async () => {
53+
const buildx = new Buildx();
54+
const build = new Build({buildx: buildx});
55+
const imageName = 'ghcr.io/docker/actions-toolkit/test';
56+
57+
await expect(
58+
(async () => {
59+
await Docker.getExecOutput(['login', '--password-stdin', '--username', process.env.GITHUB_REPOSITORY_OWNER || 'docker', 'ghcr.io'], {
60+
input: Buffer.from(process.env.GITHUB_TOKEN || '')
61+
});
62+
})()
63+
).resolves.not.toThrow();
64+
65+
await expect(
66+
(async () => {
67+
// prettier-ignore
68+
const buildCmd = await buildx.getCommand([
69+
'--builder', process.env.CTN_BUILDER_NAME ?? 'default',
70+
'build',
71+
'-f', path.join(fixturesDir, 'hello.Dockerfile'),
72+
'--provenance=mode=max',
73+
'--tag', `${imageName}:sigstore-config-itg`,
74+
'--platform', 'linux/amd64,linux/arm64',
75+
'--push',
76+
'--metadata-file', build.getMetadataFilePath(),
77+
fixturesDir
78+
]);
79+
await Exec.exec(buildCmd.command, buildCmd.args);
80+
})()
81+
).resolves.not.toThrow();
82+
83+
const metadata = build.resolveMetadata();
84+
expect(metadata).toBeDefined();
85+
const buildDigest = build.resolveDigest(metadata);
86+
expect(buildDigest).toBeDefined();
87+
88+
const sigstore = new Sigstore();
89+
const signResults = await sigstore.signAttestationManifests({
90+
imageNames: [imageName],
91+
imageDigest: buildDigest!
92+
});
93+
expect(Object.keys(signResults).length).toEqual(2);
94+
95+
const verifyResults = await sigstore.verifySignedManifests(
96+
{
97+
certificateIdentityRegexp: `^https://github.com/docker/actions-toolkit/.github/workflows/test.yml.*$`
98+
},
99+
signResults
100+
);
101+
expect(Object.keys(verifyResults).length).toEqual(2);
102+
});
103+
});

src/sigstore/sigstore.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as core from '@actions/core';
2222
import {bundleFromJSON, bundleToJSON} from '@sigstore/bundle';
2323
import {Artifact, Bundle, CIContextProvider, DSSEBundleBuilder, FulcioSigner, RekorWitness, TSAWitness, Witness} from '@sigstore/sign';
2424

25+
import {Context} from '../context';
2526
import {Cosign} from '../cosign/cosign';
2627
import {Exec} from '../exec';
2728
import {GitHub} from '../github';
@@ -73,23 +74,54 @@ export class Sigstore {
7374
core.info(`Using Sigstore signing endpoint: ${endpoints.fulcioURL}`);
7475
const noTransparencyLog = Sigstore.noTransparencyLog(opts.noTransparencyLog);
7576

77+
const cosignExtraArgs: string[] = [];
78+
if (await this.cosign.versionSatisfies('>=3.0.4')) {
79+
await core.group(`Creating Sigstore protobuf signing config`, async () => {
80+
const signingConfig = Context.tmpName({
81+
template: 'signing-config-XXXXXX.json',
82+
tmpdir: Context.tmpDir()
83+
});
84+
// prettier-ignore
85+
const createConfigArgs = [
86+
'signing-config',
87+
'create',
88+
'--with-default-services=true',
89+
`--out=${signingConfig}`
90+
];
91+
if (noTransparencyLog) {
92+
createConfigArgs.push('--no-default-rekor=true');
93+
}
94+
await Exec.exec('cosign', createConfigArgs, {
95+
env: Object.assign({}, process.env, {
96+
COSIGN_EXPERIMENTAL: '1'
97+
}) as {
98+
[key: string]: string;
99+
}
100+
});
101+
core.info(JSON.stringify(JSON.parse(fs.readFileSync(signingConfig, {encoding: 'utf-8'})), null, 2));
102+
cosignExtraArgs.push(`--signing-config=${signingConfig}`);
103+
});
104+
} else {
105+
cosignExtraArgs.push('--use-signing-config');
106+
if (noTransparencyLog) {
107+
cosignExtraArgs.push('--tlog-upload=false');
108+
}
109+
}
110+
76111
for (const imageName of opts.imageNames) {
77112
const attestationDigests = await this.imageTools.attestationDigests(`${imageName}@${opts.imageDigest}`);
78113
for (const attestationDigest of attestationDigests) {
79114
const attestationRef = `${imageName}@${attestationDigest}`;
80115
await core.group(`Signing attestation manifest ${attestationRef}`, async () => {
81116
// prettier-ignore
82117
const cosignArgs = [
83-
'sign',
84-
'--yes',
85-
'--oidc-provider', 'github-actions',
86-
'--registry-referrers-mode', 'oci-1-1',
87-
'--new-bundle-format',
88-
'--use-signing-config'
89-
];
90-
if (noTransparencyLog) {
91-
cosignArgs.push('--tlog-upload=false');
92-
}
118+
'sign',
119+
'--yes',
120+
'--oidc-provider', 'github-actions',
121+
'--registry-referrers-mode', 'oci-1-1',
122+
'--new-bundle-format',
123+
...cosignExtraArgs
124+
];
93125
core.info(`[command]cosign ${[...cosignArgs, attestationRef].join(' ')}`);
94126
const execRes = await Exec.getExecOutput('cosign', ['--verbose', ...cosignArgs, attestationRef], {
95127
ignoreReturnCode: true,

0 commit comments

Comments
 (0)