Skip to content

Commit ade0696

Browse files
committed
sigstore: verifyArtifact func to verify arbitrary artifact
Signed-off-by: CrazyMax <[email protected]>
1 parent 694b7dd commit ade0696

File tree

4 files changed

+94
-66
lines changed

4 files changed

+94
-66
lines changed

src/buildx/install.ts

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,11 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {X509Certificate} from 'crypto';
1817
import fs from 'fs';
1918
import os from 'os';
2019
import path from 'path';
2120
import * as core from '@actions/core';
2221
import * as tc from '@actions/tool-cache';
23-
import {bundleFromJSON, SerializedBundle} from '@sigstore/bundle';
24-
import * as tuf from '@sigstore/tuf';
25-
import {toSignedEntity, toTrustMaterial, Verifier} from '@sigstore/verify';
2622
import * as semver from 'semver';
2723
import * as util from 'util';
2824

@@ -33,10 +29,12 @@ import {Exec} from '../exec';
3329
import {Docker} from '../docker/docker';
3430
import {Git} from '../git';
3531
import {GitHub} from '../github';
32+
import {Sigstore} from '../sigstore/sigstore';
3633
import {Util} from '../util';
3734

3835
import {DownloadVersion} from '../types/buildx/buildx';
3936
import {GitHubRelease} from '../types/github';
37+
import {SEARCH_URL} from '../types/sigstore/sigstore';
4038

4139
export interface DownloadOpts {
4240
version: string;
@@ -48,15 +46,18 @@ export interface DownloadOpts {
4846
export interface InstallOpts {
4947
standalone?: boolean;
5048
githubToken?: string;
49+
sigstore?: Sigstore;
5150
}
5251

5352
export class Install {
5453
private readonly standalone: boolean | undefined;
5554
private readonly githubToken: string | undefined;
55+
private readonly sigstore: Sigstore;
5656

5757
constructor(opts?: InstallOpts) {
5858
this.standalone = opts?.standalone;
5959
this.githubToken = opts?.githubToken || process.env.GITHUB_TOKEN;
60+
this.sigstore = opts?.sigstore || new Sigstore();
6061
}
6162

6263
/*
@@ -234,32 +235,12 @@ export class Install {
234235
const bundlePath = await tc.downloadTool(bundleURL, undefined, this.githubToken);
235236
core.debug(`Install.verifySignature bundlePath: ${bundlePath}`);
236237

237-
core.info(`Verifying keyless verification bundle signature`);
238-
const parsedBundle = JSON.parse(fs.readFileSync(bundlePath, 'utf-8')) as SerializedBundle;
239-
const bundle = bundleFromJSON(parsedBundle);
240-
241-
core.info(`Fetching Sigstore TUF trusted root metadata`);
242-
const trustedRoot = await tuf.getTrustedRoot();
243-
const trustMaterial = toTrustMaterial(trustedRoot);
238+
const verifyResult = await this.sigstore.verifyArtifact(binPath, bundlePath, {
239+
subjectAlternativeName: /^https:\/\/github\.com\/docker\/(github-builder-experimental|github-builder)\/\.github\/workflows\/bake\.yml.*$/,
240+
issuer: 'https://token.actions.githubusercontent.com'
241+
});
244242

245-
try {
246-
core.info(`Verifying Buildx binary signature`);
247-
const signedEntity = toSignedEntity(bundle, fs.readFileSync(binPath));
248-
const signingCert = new X509Certificate(signedEntity.signature.signature);
249-
if (!signingCert.subjectAltName?.match(/^https:\/\/github\.com\/docker\/(github-builder-experimental|github-builder)\/\.github\/workflows\/bake\.yml.*$/)) {
250-
throw new Error(`Signing certificate subjectAlternativeName "${signingCert.subjectAltName}" does not match expected pattern`);
251-
}
252-
const verifier = new Verifier(trustMaterial);
253-
const signer = verifier.verify(signedEntity, {
254-
// FIXME: uncomment when subjectAlternativeName check with regex is supported: https://github.com/docker/actions-toolkit/pull/929#discussion_r2682150413
255-
//subjectAlternativeName: /^https:\/\/github\.com\/docker\/(github-builder-experimental|github-builder)\/\.github\/workflows\/bake\.yml.*$/,
256-
extensions: {issuer: 'https://token.actions.githubusercontent.com'}
257-
});
258-
core.debug(`Install.verifySignature signer: ${JSON.stringify(signer)}`);
259-
core.info(`Buildx binary signature verified!`);
260-
} catch (err) {
261-
throw new Error(`Failed to verify Buildx binary signature: ${err}`);
262-
}
243+
core.info(`Buildx binary signature verified! ${verifyResult.tlogID ? `${SEARCH_URL}?logIndex=${verifyResult.tlogID}` : ''}`);
263244
}
264245

265246
private filename(version: string): string {

src/cosign/install.ts

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ import os from 'os';
1919
import path from 'path';
2020
import * as core from '@actions/core';
2121
import * as tc from '@actions/tool-cache';
22-
import {bundleFromJSON, SerializedBundle} from '@sigstore/bundle';
23-
import * as tuf from '@sigstore/tuf';
24-
import {toSignedEntity, toTrustMaterial, Verifier} from '@sigstore/verify';
2522
import * as semver from 'semver';
2623
import * as util from 'util';
2724

@@ -31,11 +28,13 @@ import {Context} from '../context';
3128
import {Exec} from '../exec';
3229
import {Git} from '../git';
3330
import {GitHub} from '../github';
31+
import {Sigstore} from '../sigstore/sigstore';
3432
import {Util} from '../util';
3533

3634
import {DownloadVersion} from '../types/cosign/cosign';
3735
import {GitHubRelease} from '../types/github';
3836
import {dockerfileContent} from './dockerfile';
37+
import {SEARCH_URL} from '../types/sigstore/sigstore';
3938

4039
export interface DownloadOpts {
4140
version: string;
@@ -47,15 +46,18 @@ export interface DownloadOpts {
4746
export interface InstallOpts {
4847
githubToken?: string;
4948
buildx?: Buildx;
49+
sigstore?: Sigstore;
5050
}
5151

5252
export class Install {
5353
private readonly githubToken: string | undefined;
5454
private readonly buildx: Buildx;
55+
private readonly sigstore: Sigstore;
5556

5657
constructor(opts?: InstallOpts) {
5758
this.githubToken = opts?.githubToken || process.env.GITHUB_TOKEN;
5859
this.buildx = opts?.buildx || new Buildx();
60+
this.sigstore = opts?.sigstore || new Sigstore();
5961
}
6062

6163
public async download(opts: DownloadOpts): Promise<string> {
@@ -196,27 +198,12 @@ export class Install {
196198
const bundlePath = await tc.downloadTool(bundleURL, undefined, this.githubToken);
197199
core.debug(`Install.verifySignature bundlePath: ${bundlePath}`);
198200

199-
core.info(`Verifying keyless verification bundle signature`);
200-
const parsedBundle = JSON.parse(fs.readFileSync(bundlePath, 'utf-8')) as SerializedBundle;
201-
const bundle = bundleFromJSON(parsedBundle);
202-
203-
core.info(`Fetching Sigstore TUF trusted root metadata`);
204-
const trustedRoot = await tuf.getTrustedRoot();
205-
const trustMaterial = toTrustMaterial(trustedRoot);
206-
207-
try {
208-
core.info(`Verifying cosign binary signature`);
209-
const signedEntity = toSignedEntity(bundle, fs.readFileSync(cosignBinPath));
210-
const verifier = new Verifier(trustMaterial);
211-
const signer = verifier.verify(signedEntity, {
212-
subjectAlternativeName: '[email protected]',
213-
extensions: {issuer: 'https://accounts.google.com'}
214-
});
215-
core.debug(`Install.verifySignature signer: ${JSON.stringify(signer)}`);
216-
core.info(`Cosign binary signature verified!`);
217-
} catch (err) {
218-
throw new Error(`Failed to verify cosign binary signature: ${err}`);
219-
}
201+
const verifyResult = await this.sigstore.verifyArtifact(cosignBinPath, bundlePath, {
202+
subjectAlternativeName: '[email protected]',
203+
issuer: 'https://accounts.google.com'
204+
});
205+
206+
core.info(`Cosign binary signature verified! ${verifyResult.tlogID ? `${SEARCH_URL}?logIndex=${verifyResult.tlogID}` : ''}`);
220207
}
221208

222209
private filename(): string {

src/sigstore/sigstore.ts

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import fs from 'fs';
1919
import path from 'path';
2020

2121
import * as core from '@actions/core';
22-
import {bundleFromJSON, bundleToJSON} from '@sigstore/bundle';
22+
import {bundleFromJSON, bundleToJSON, SerializedBundle} from '@sigstore/bundle';
2323
import {Artifact, Bundle, CIContextProvider, DSSEBundleBuilder, FulcioSigner, RekorWitness, TSAWitness, Witness} from '@sigstore/sign';
24+
import * as tuf from '@sigstore/tuf';
25+
import {toSignedEntity, toTrustMaterial, Verifier} from '@sigstore/verify';
2426

2527
import {Cosign} from '../cosign/cosign';
2628
import {Exec} from '../exec';
@@ -39,6 +41,8 @@ import {
3941
SignProvenanceBlobsOpts,
4042
SignProvenanceBlobsResult,
4143
TSASERVER_URL,
44+
VerifyArtifactOpts,
45+
VerifyArtifactResult,
4246
VerifySignedArtifactsOpts,
4347
VerifySignedArtifactsResult,
4448
VerifySignedManifestsOpts,
@@ -329,6 +333,48 @@ export class Sigstore {
329333
return result;
330334
}
331335

336+
public async verifyArtifact(artifactPath: string, bundlePath: string, opts?: VerifyArtifactOpts): Promise<VerifyArtifactResult> {
337+
core.info(`Verifying keyless verification bundle signature`);
338+
const parsedBundle = JSON.parse(fs.readFileSync(bundlePath, 'utf-8')) as SerializedBundle;
339+
const bundle = bundleFromJSON(parsedBundle);
340+
341+
core.info(`Fetching Sigstore TUF trusted root metadata`);
342+
const trustedRoot = await tuf.getTrustedRoot();
343+
const trustMaterial = toTrustMaterial(trustedRoot);
344+
345+
try {
346+
core.info(`Verifying artifact signature`);
347+
const signedEntity = toSignedEntity(bundle, fs.readFileSync(artifactPath));
348+
const signingCert = Sigstore.parseCertificate(bundle);
349+
350+
// collect transparency log ID if available
351+
const tlogEntries = bundle.verificationMaterial.tlogEntries;
352+
const tlogID = tlogEntries.length > 0 ? tlogEntries[0].logIndex : undefined;
353+
354+
// TODO: remove when subjectAlternativeName check with regex is supported: https://github.com/sigstore/sigstore-js/pull/1556
355+
if (opts?.subjectAlternativeName && opts?.subjectAlternativeName instanceof RegExp) {
356+
if (!signingCert.subjectAltName?.match(opts.subjectAlternativeName)) {
357+
throw new Error(`Signing certificate subjectAlternativeName "${signingCert.subjectAltName}" does not match expected pattern`);
358+
}
359+
}
360+
361+
const verifier = new Verifier(trustMaterial);
362+
const signer = verifier.verify(signedEntity, {
363+
subjectAlternativeName: opts?.subjectAlternativeName && typeof opts.subjectAlternativeName === 'string' ? opts.subjectAlternativeName : undefined,
364+
extensions: opts?.issuer ? {issuer: opts.issuer} : undefined
365+
});
366+
core.debug(`Sigstore.verifyArtifact signer: ${JSON.stringify(signer)}`);
367+
368+
return {
369+
payload: parsedBundle,
370+
certificate: signingCert.toString(),
371+
tlogID: tlogID
372+
};
373+
} catch (err) {
374+
throw new Error(`Failed to verify artifact signature: ${err}`);
375+
}
376+
}
377+
332378
private signingEndpoints(noTransparencyLog?: boolean): Endpoints {
333379
noTransparencyLog = Sigstore.noTransparencyLog(noTransparencyLog);
334380
core.info(`Upload to transparency log: ${noTransparencyLog ? 'disabled' : 'enabled'}`);
@@ -410,6 +456,20 @@ export class Sigstore {
410456
}
411457

412458
private static parseBundle(bundle: Bundle): ParsedBundle {
459+
const signingCert = Sigstore.parseCertificate(bundle);
460+
461+
// collect transparency log ID if available
462+
const tlogEntries = bundle.verificationMaterial.tlogEntries;
463+
const tlogID = tlogEntries.length > 0 ? tlogEntries[0].logIndex : undefined;
464+
465+
return {
466+
payload: bundleToJSON(bundle),
467+
certificate: signingCert.toString(),
468+
tlogID: tlogID
469+
};
470+
}
471+
472+
private static parseCertificate(bundle: Bundle): X509Certificate {
413473
let certBytes: Buffer;
414474
switch (bundle.verificationMaterial.content.$case) {
415475
case 'x509CertificateChain':
@@ -421,17 +481,6 @@ export class Sigstore {
421481
default:
422482
throw new Error('Bundle must contain an x509 certificate');
423483
}
424-
425-
const signingCert = new X509Certificate(certBytes);
426-
427-
// collect transparency log ID if available
428-
const tlogEntries = bundle.verificationMaterial.tlogEntries;
429-
const tlogID = tlogEntries.length > 0 ? tlogEntries[0].logIndex : undefined;
430-
431-
return {
432-
payload: bundleToJSON(bundle),
433-
certificate: signingCert.toString(),
434-
tlogID: tlogID
435-
};
484+
return new X509Certificate(certBytes);
436485
}
437486
}

src/types/sigstore/sigstore.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,14 @@ export interface VerifySignedArtifactsResult {
7878
bundlePath: string;
7979
cosignArgs: Array<string>;
8080
}
81+
82+
export interface VerifyArtifactOpts {
83+
subjectAlternativeName: string | RegExp;
84+
issuer?: string;
85+
}
86+
87+
export interface VerifyArtifactResult {
88+
payload: SerializedBundle;
89+
certificate: string;
90+
tlogID?: string;
91+
}

0 commit comments

Comments
 (0)