buildx(install): use sigstore module to verify signature

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2026-01-12 13:48:46 +01:00
parent 0969a99659
commit d830716b30
3 changed files with 60 additions and 10 deletions

View File

@@ -29,7 +29,12 @@ maybe('download', () => {
const install = new Install({ const install = new Install({
standalone: true standalone: true
}); });
const toolPath = await install.download(version); const toolPath = await install.download({
version: version,
verifySignature: true,
ghaNoCache: true,
disableHtc: true
});
if (!fs.existsSync(toolPath)) { if (!fs.existsSync(toolPath)) {
throw new Error('toolPath does not exist'); throw new Error('toolPath does not exist');
} }

View File

@@ -39,7 +39,7 @@ describe('download', () => {
])( ])(
'acquires %p of buildx (standalone: %p)', async (version, standalone) => { 'acquires %p of buildx (standalone: %p)', async (version, standalone) => {
const install = new Install({standalone: standalone}); const install = new Install({standalone: standalone});
const toolPath = await install.download(version); const toolPath = await install.download({version});
expect(fs.existsSync(toolPath)).toBe(true); expect(fs.existsSync(toolPath)).toBe(true);
let buildxBin: string; let buildxBin: string;
if (standalone) { if (standalone) {
@@ -58,7 +58,7 @@ describe('download', () => {
])( ])(
'acquires %p of buildx with cache', async (version) => { 'acquires %p of buildx with cache', async (version) => {
const install = new Install({standalone: false}); const install = new Install({standalone: false});
const toolPath = await install.download(version); const toolPath = await install.download({version});
expect(fs.existsSync(toolPath)).toBe(true); expect(fs.existsSync(toolPath)).toBe(true);
}, 100000); }, 100000);
@@ -69,7 +69,7 @@ describe('download', () => {
])( ])(
'acquires %p of buildx without cache', async (version) => { 'acquires %p of buildx without cache', async (version) => {
const install = new Install({standalone: false}); const install = new Install({standalone: false});
const toolPath = await install.download(version, true); const toolPath = await install.download({version: version, ghaNoCache: true});
expect(fs.existsSync(toolPath)).toBe(true); expect(fs.existsSync(toolPath)).toBe(true);
}, 100000); }, 100000);
@@ -89,7 +89,7 @@ describe('download', () => {
mockPlatform(os as NodeJS.Platform); mockPlatform(os as NodeJS.Platform);
mockArch(arch); mockArch(arch);
const install = new Install(); const install = new Install();
const buildxBin = await install.download('latest'); const buildxBin = await install.download({version: 'latest'});
expect(fs.existsSync(buildxBin)).toBe(true); expect(fs.existsSync(buildxBin)).toBe(true);
}, 100000); }, 100000);
}); });

View File

@@ -19,6 +19,9 @@ import os from 'os';
import path from 'path'; import path from 'path';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as tc from '@actions/tool-cache'; import * as tc from '@actions/tool-cache';
import {bundleFromJSON, SerializedBundle} from '@sigstore/bundle';
import * as tuf from '@sigstore/tuf';
import {toSignedEntity, toTrustMaterial, Verifier} from '@sigstore/verify';
import * as semver from 'semver'; import * as semver from 'semver';
import * as util from 'util'; import * as util from 'util';
@@ -34,6 +37,14 @@ import {Util} from '../util.js';
import {DownloadVersion} from '../types/buildx/buildx.js'; import {DownloadVersion} from '../types/buildx/buildx.js';
import {GitHubRelease} from '../types/github.js'; import {GitHubRelease} from '../types/github.js';
export interface DownloadOpts {
version: string;
ghaNoCache?: boolean;
disableHtc?: boolean;
skipState?: boolean;
verifySignature?: boolean;
}
export interface InstallOpts { export interface InstallOpts {
standalone?: boolean; standalone?: boolean;
githubToken?: string; githubToken?: string;
@@ -54,8 +65,8 @@ export class Install {
* @param ghaNoCache: disable binary caching in GitHub Actions cache backend * @param ghaNoCache: disable binary caching in GitHub Actions cache backend
* @returns path to the buildx binary * @returns path to the buildx binary
*/ */
public async download(v: string, ghaNoCache?: boolean): Promise<string> { public async download(opts: DownloadOpts): Promise<string> {
const version: DownloadVersion = await Install.getDownloadVersion(v); const version: DownloadVersion = await Install.getDownloadVersion(opts.version);
core.debug(`Install.download version: ${version.version}`); core.debug(`Install.download version: ${version.version}`);
const release: GitHubRelease = await Install.getRelease(version, this.githubToken); const release: GitHubRelease = await Install.getRelease(version, this.githubToken);
@@ -74,11 +85,11 @@ export class Install {
htcVersion: vspec, htcVersion: vspec,
baseCacheDir: path.join(Buildx.configDir, '.bin'), baseCacheDir: path.join(Buildx.configDir, '.bin'),
cacheFile: os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx', cacheFile: os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx',
ghaNoCache: ghaNoCache ghaNoCache: opts.ghaNoCache
}); });
const cacheFoundPath = await installCache.find(); const cacheFoundPath = await installCache.find();
if (cacheFoundPath) { if (!opts.disableHtc && cacheFoundPath) {
core.info(`Buildx binary found in ${cacheFoundPath}`); core.info(`Buildx binary found in ${cacheFoundPath}`);
return cacheFoundPath; return cacheFoundPath;
} }
@@ -89,7 +100,11 @@ export class Install {
const htcDownloadPath = await tc.downloadTool(downloadURL, undefined, this.githubToken); const htcDownloadPath = await tc.downloadTool(downloadURL, undefined, this.githubToken);
core.debug(`Install.download htcDownloadPath: ${htcDownloadPath}`); core.debug(`Install.download htcDownloadPath: ${htcDownloadPath}`);
const cacheSavePath = await installCache.save(htcDownloadPath); if (opts.verifySignature && semver.satisfies(vspec, '>=0.31.0-0', {includePrerelease: true})) {
await this.verifySignature(htcDownloadPath, downloadURL);
}
const cacheSavePath = await installCache.save(htcDownloadPath, opts.skipState);
core.info(`Cached to ${cacheSavePath}`); core.info(`Cached to ${cacheSavePath}`);
return cacheSavePath; return cacheSavePath;
} }
@@ -213,6 +228,36 @@ export class Install {
return standalone; return standalone;
} }
private async verifySignature(binPath: string, downloadURL: string): Promise<void> {
const bundleURL = `${downloadURL.replace(/\.exe$/, '')}.sigstore.json`;
core.info(`Downloading keyless verification bundle at ${bundleURL}`);
const bundlePath = await tc.downloadTool(bundleURL, undefined, this.githubToken);
core.debug(`Install.verifySignature bundlePath: ${bundlePath}`);
core.info(`Verifying keyless verification bundle signature`);
const parsedBundle = JSON.parse(fs.readFileSync(bundlePath, 'utf-8')) as SerializedBundle;
const bundle = bundleFromJSON(parsedBundle);
core.info(`Fetching Sigstore TUF trusted root metadata`);
const trustedRoot = await tuf.getTrustedRoot();
const trustMaterial = toTrustMaterial(trustedRoot);
try {
core.info(`Verifying Buildx binary signature`);
const signedEntity = toSignedEntity(bundle, fs.readFileSync(binPath));
const verifier = new Verifier(trustMaterial);
const signer = verifier.verify(signedEntity, {
// FIXME: uncomment when subjectAlternativeName check with regex is supported: https://github.com/docker/actions-toolkit/pull/929#discussion_r2682150413
//subjectAlternativeName: /^https:\/\/github\.com\/docker\/(github-builder-experimental|github-builder)\/\.github\/workflows\/build\.yml.*$/,
extensions: {issuer: 'https://token.actions.githubusercontent.com'}
});
core.debug(`Install.verifySignature signer: ${JSON.stringify(signer)}`);
core.info(`Buildx binary signature verified!`);
} catch (err) {
throw new Error(`Failed to verify Buildx binary signature: ${err}`);
}
}
private filename(version: string): string { private filename(version: string): string {
let arch: string; let arch: string;
switch (os.arch()) { switch (os.arch()) {