From 36e729c5aaf23b745f9541f5484ef2938d85aa7e Mon Sep 17 00:00:00 2001 From: Conor Sloan Date: Tue, 27 Aug 2024 20:52:44 +0100 Subject: [PATCH] grab attestation media type and predicate type from attestation bundle --- __tests__/ghcr-client.test.ts | 2 ++ __tests__/main.test.ts | 51 +++++++++++++++++++++++++++++---- __tests__/oci-container.test.ts | 4 +++ badges/coverage.svg | 2 +- dist/index.js | 41 +++++++++++++++++--------- src/ghcr-client.ts | 1 - src/main.ts | 35 ++++++++++++++++++---- src/oci-container.ts | 16 ++++++----- 8 files changed, 118 insertions(+), 34 deletions(-) diff --git a/__tests__/ghcr-client.test.ts b/__tests__/ghcr-client.test.ts index fcdb6b8..7696008 100644 --- a/__tests__/ghcr-client.test.ts +++ b/__tests__/ghcr-client.test.ts @@ -606,6 +606,8 @@ function testIndexManifest(): { const manifest = ociContainer.createReferrerTagManifest( 'attestation-digest', 1234, + 'bundle-media-type', + 'bundle-predicate-type', new Date(), new Date() ) diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 1e1a593..fde06bc 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -15,6 +15,8 @@ import * as ghcr from '../src/ghcr-client' import * as ociContainer from '../src/oci-container' const ghcrUrl = new URL('https://ghcr.io') +const predicateType = 'https://slsa.dev/provenance/v1' +const bundleMediaType = 'application/vnd.dev.sigstore.bundle.v0.3+json' // Mock the GitHub Actions core library let setFailedMock: jest.SpyInstance @@ -302,11 +304,14 @@ describe('run', () => { attestationID: 'test-attestation-id', certificate: 'test', bundle: { - mediaType: 'application/vnd.cncf.notary.v2+jwt', + mediaType: bundleMediaType, verificationMaterial: { publicKey: { hint: 'test-hint' } + }, + dsseEnvelope: { + payload: btoa(`{"predicateType": "${predicateType}"}`) } } } @@ -360,11 +365,14 @@ describe('run', () => { attestationID: 'test-attestation-id', certificate: 'test', bundle: { - mediaType: 'application/vnd.cncf.notary.v2+jwt', + mediaType: bundleMediaType, verificationMaterial: { publicKey: { hint: 'test-hint' } + }, + dsseEnvelope: { + payload: btoa(`{"predicateType": "${predicateType}"}`) } } } @@ -426,11 +434,14 @@ describe('run', () => { attestationID: 'test-attestation-id', certificate: 'test', bundle: { - mediaType: 'application/vnd.cncf.notary.v2+jwt', + mediaType: bundleMediaType, verificationMaterial: { publicKey: { hint: 'test-hint' } + }, + dsseEnvelope: { + payload: btoa(`{"predicateType": "${predicateType}"}`) } } } @@ -568,11 +579,14 @@ describe('run', () => { attestationID: 'test-attestation-id', certificate: 'test', bundle: { - mediaType: 'application/vnd.cncf.notary.v2+jwt', + mediaType: bundleMediaType, verificationMaterial: { publicKey: { hint: 'test-hint' } + }, + dsseEnvelope: { + payload: btoa(`{"predicateType": "${predicateType}"}`) } } } @@ -583,6 +597,21 @@ describe('run', () => { expect(repository).toBe(options.nameWithOwner) expect(tag).toBe('sha256-my-test-digest') expect(manifest.mediaType).toBe(ociContainer.imageIndexMediaType) + expect(manifest.annotations['com.github.package.type']).toBe( + ociContainer.actionPackageReferrerTagAnnotationValue + ) + expect(manifest.manifests.length).toBe(1) + expect(manifest.manifests[0].mediaType).toBe( + ociContainer.imageManifestMediaType + ) + expect(manifest.manifests[0].artifactType).toBe(bundleMediaType) + expect( + manifest.manifests[0].annotations['dev.sigstore.bundle.predicateType'] + ).toBe(predicateType) + expect( + manifest.manifests[0].annotations['com.github.package.type'] + ).toBe(ociContainer.actionPackageAttestationAnnotationValue) + return 'sha256:referrer-index-digest' } ) @@ -593,16 +622,23 @@ describe('run', () => { let expectedAnnotationValue = '' let expectedTagValue: string | undefined = undefined let returnValue = '' + let expectedPredicateTypeValue: string | undefined = undefined + + let expectedSubjectMediaType: string | undefined = undefined if (tag === undefined) { expectedAnnotationValue = ociContainer.actionPackageAttestationAnnotationValue const sigStoreLayer = manifest.layers.find( (layer: ociContainer.Descriptor) => - layer.mediaType === ociContainer.sigstoreBundleMediaType + layer.mediaType === bundleMediaType ) + expectedPredicateTypeValue = predicateType expectedBlobKeys = [sigStoreLayer.digest, ociContainer.emptyConfigSha] + + expectedSubjectMediaType = ociContainer.imageManifestMediaType + returnValue = 'sha256:attestation-digest' } else { expectedAnnotationValue = ociContainer.actionPackageAnnotationValue @@ -616,7 +652,12 @@ describe('run', () => { expect(manifest.annotations['com.github.package.type']).toBe( expectedAnnotationValue ) + expect(manifest.annotations['dev.sigstore.bundle.predicateType']).toBe( + expectedPredicateTypeValue + ) expect(tag).toBe(expectedTagValue) + expect(manifest.subject?.mediaType).toBe(expectedSubjectMediaType) + expect(manifest.layers.length).toBe(expectedBlobKeys.length - 1) // Minus config layer expect(blobs.size).toBe(expectedBlobKeys.length) for (const expectedBlobKey of expectedBlobKeys) { diff --git a/__tests__/oci-container.test.ts b/__tests__/oci-container.test.ts index b54eb26..80855b9 100644 --- a/__tests__/oci-container.test.ts +++ b/__tests__/oci-container.test.ts @@ -219,6 +219,8 @@ function testAttestationManifest(setCreated = true): OCIImageManifest { return createSigstoreAttestationManifest( 10, 'bundleDigest', + 'application/vnd.dev.sigstore.bundle.v0.3+json', + 'https://slsa.dev/provenance/v1', 100, 'subjectDigest', setCreated ? date : undefined @@ -230,6 +232,8 @@ function testReferrerIndexManifest(setCreated = true): OCIIndexManifest { return createReferrerTagManifest( 'attDigest', 100, + 'application/vnd.dev.sigstore.bundle.v0.3+json', + 'https://slsa.dev/provenance/v1', date, setCreated ? date : undefined ) diff --git a/badges/coverage.svg b/badges/coverage.svg index 2f3c0cd..d252813 100644 --- a/badges/coverage.svg +++ b/badges/coverage.svg @@ -1 +1 @@ -Coverage: 98.07%Coverage98.07% \ No newline at end of file +Coverage: 97.56%Coverage97.56% \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index cc7713d..c3228d8 100644 --- a/dist/index.js +++ b/dist/index.js @@ -106812,7 +106812,6 @@ class Client { manifestEndpoint(repository, version) { return new URL(`v2/${repository}/manifests/${version}`, this._registry).toString(); } - // TODO: Add retries with backoff async fetchWithDebug(url, config = {}) { core.debug(`Request from ${url} with config: ${JSON.stringify(config)}`); try { @@ -106962,10 +106961,10 @@ async function run() { const ghcrClient = new ghcr.Client(options.token, options.containerRegistryUrl); // Attestations are not supported in GHES. if (!options.isEnterprise) { - const { bundle, bundleDigest } = await generateAttestation(manifestDigest, semverTag.raw, options); + const { bundle, bundleDigest, bundleMediaType, bundlePredicateType } = await generateAttestation(manifestDigest, semverTag.raw, options); const attestationCreated = new Date(); - const attestationManifest = ociContainer.createSigstoreAttestationManifest(bundle.length, bundleDigest, ociContainer.sizeInBytes(manifest), manifestDigest, attestationCreated); - const referrerIndexManifest = ociContainer.createReferrerTagManifest(ociContainer.sha256Digest(attestationManifest), ociContainer.sizeInBytes(attestationManifest), attestationCreated); + const attestationManifest = ociContainer.createSigstoreAttestationManifest(bundle.length, bundleDigest, bundleMediaType, bundlePredicateType, ociContainer.sizeInBytes(manifest), manifestDigest, attestationCreated); + const referrerIndexManifest = ociContainer.createReferrerTagManifest(ociContainer.sha256Digest(attestationManifest), ociContainer.sizeInBytes(attestationManifest), bundleMediaType, bundlePredicateType, attestationCreated); const { attestationSHA, referrerIndexSHA } = await publishAttestation(ghcrClient, options.nameWithOwner, bundle, bundleDigest, manifest, attestationManifest, referrerIndexManifest); if (attestationSHA !== undefined) { core.info(`Uploaded attestation ${attestationSHA}`); @@ -107039,7 +107038,22 @@ async function generateAttestation(manifestDigest, semverTag, options) { const hash = crypto.createHash('sha256'); hash.update(bundleArtifact); const bundleSHA = hash.digest('hex'); - return { bundle: bundleArtifact, bundleDigest: `sha256:${bundleSHA}` }; + // We must base64 decode the dsse envelope to grab the predicate type + const dsseEnvelopeArtifact = attestation.bundle.dsseEnvelope; + if (dsseEnvelopeArtifact === undefined) { + throw new Error('Attestation bundle is missing dsseEnvelope artifact'); + } + const dsseEnvelope = JSON.parse(Buffer.from(dsseEnvelopeArtifact.payload, 'base64').toString('utf-8')); + const predicateType = dsseEnvelope.predicateType; + if (predicateType === undefined) { + throw new Error('Attestation bundle is missing predicateType'); + } + return { + bundle: bundleArtifact, + bundleDigest: `sha256:${bundleSHA}`, + bundleMediaType: attestation.bundle.mediaType, + bundlePredicateType: predicateType + }; } function removePrefix(str, prefix) { if (str.startsWith(prefix)) { @@ -107080,7 +107094,7 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.emptyConfigSha = exports.emptyConfigSize = exports.ociEmptyMediaType = exports.actionPackageReferrerTagAnnotationValue = exports.actionPackageAttestationAnnotationValue = exports.actionPackageAnnotationValue = exports.sigstoreBundleMediaType = exports.actionsPackageZipLayerMediaType = exports.actionsPackageTarLayerMediaType = exports.actionsPackageMediaType = exports.imageManifestMediaType = exports.imageIndexMediaType = void 0; +exports.emptyConfigSha = exports.emptyConfigSize = exports.ociEmptyMediaType = exports.actionPackageReferrerTagAnnotationValue = exports.actionPackageAttestationAnnotationValue = exports.actionPackageAnnotationValue = exports.actionsPackageZipLayerMediaType = exports.actionsPackageTarLayerMediaType = exports.actionsPackageMediaType = exports.imageManifestMediaType = exports.imageIndexMediaType = void 0; exports.createActionPackageManifest = createActionPackageManifest; exports.createSigstoreAttestationManifest = createSigstoreAttestationManifest; exports.createReferrerTagManifest = createReferrerTagManifest; @@ -107093,7 +107107,6 @@ exports.imageManifestMediaType = 'application/vnd.oci.image.manifest.v1+json'; exports.actionsPackageMediaType = 'application/vnd.github.actions.package.v1+json'; exports.actionsPackageTarLayerMediaType = 'application/vnd.github.actions.package.layer.v1.tar+gzip'; exports.actionsPackageZipLayerMediaType = 'application/vnd.github.actions.package.layer.v1.zip'; -exports.sigstoreBundleMediaType = 'application/vnd.dev.sigstore.bundle.v0.3+json'; exports.actionPackageAnnotationValue = 'actions_oci_pkg'; exports.actionPackageAttestationAnnotationValue = 'actions_oci_pkg_attestation'; exports.actionPackageReferrerTagAnnotationValue = 'actions_oci_pkg_referrer_index'; @@ -107125,10 +107138,10 @@ function createActionPackageManifest(tarFile, zipFile, repository, repoId, owner }; return manifest; } -function createSigstoreAttestationManifest(bundleSize, bundleDigest, subjectSize, subjectDigest, created = new Date()) { +function createSigstoreAttestationManifest(bundleSize, bundleDigest, bundleMediaType, bundlePredicateType, subjectSize, subjectDigest, created = new Date()) { const configLayer = createEmptyConfigLayer(); const sigstoreAttestationLayer = { - mediaType: exports.sigstoreBundleMediaType, + mediaType: bundleMediaType, size: bundleSize, digest: bundleDigest }; @@ -107140,34 +107153,34 @@ function createSigstoreAttestationManifest(bundleSize, bundleDigest, subjectSize const manifest = { schemaVersion: 2, mediaType: exports.imageManifestMediaType, - artifactType: exports.sigstoreBundleMediaType, + artifactType: bundleMediaType, config: configLayer, layers: [sigstoreAttestationLayer], subject, annotations: { 'dev.sigstore.bundle.content': 'dsse-envelope', - 'dev.sigstore.bundle.predicateType': 'https://slsa.dev/provenance/v1', + 'dev.sigstore.bundle.predicateType': bundlePredicateType, 'com.github.package.type': exports.actionPackageAttestationAnnotationValue, 'org.opencontainers.image.created': created.toISOString() } }; return manifest; } -function createReferrerTagManifest(attestationDigest, attestationSize, attestationCreated, created = new Date()) { +function createReferrerTagManifest(attestationDigest, attestationSize, bundleMediaType, bundlePredicateType, attestationCreated, created = new Date()) { const manifest = { schemaVersion: 2, mediaType: exports.imageIndexMediaType, manifests: [ { mediaType: exports.imageManifestMediaType, - artifactType: exports.sigstoreBundleMediaType, + artifactType: bundleMediaType, size: attestationSize, digest: attestationDigest, annotations: { 'com.github.package.type': exports.actionPackageAttestationAnnotationValue, 'org.opencontainers.image.created': attestationCreated.toISOString(), 'dev.sigstore.bundle.content': 'dsse-envelope', - 'dev.sigstore.bundle.predicateType': 'https://slsa.dev/provenance/v1' + 'dev.sigstore.bundle.predicateType': bundlePredicateType } } ], diff --git a/src/ghcr-client.ts b/src/ghcr-client.ts index 94fda15..d1d6b1a 100644 --- a/src/ghcr-client.ts +++ b/src/ghcr-client.ts @@ -238,7 +238,6 @@ export class Client { ).toString() } - // TODO: Add retries with backoff private async fetchWithDebug( url: string, config: RequestInit = {} diff --git a/src/main.ts b/src/main.ts index b93cd54..f31eb87 100644 --- a/src/main.ts +++ b/src/main.ts @@ -60,24 +60,26 @@ export async function run(): Promise { // Attestations are not supported in GHES. if (!options.isEnterprise) { - const { bundle, bundleDigest } = await generateAttestation( - manifestDigest, - semverTag.raw, - options - ) + const { bundle, bundleDigest, bundleMediaType, bundlePredicateType } = + await generateAttestation(manifestDigest, semverTag.raw, options) const attestationCreated = new Date() const attestationManifest = ociContainer.createSigstoreAttestationManifest( bundle.length, bundleDigest, + bundleMediaType, + bundlePredicateType, ociContainer.sizeInBytes(manifest), manifestDigest, attestationCreated ) + const referrerIndexManifest = ociContainer.createReferrerTagManifest( ociContainer.sha256Digest(attestationManifest), ociContainer.sizeInBytes(attestationManifest), + bundleMediaType, + bundlePredicateType, attestationCreated ) @@ -221,6 +223,8 @@ async function generateAttestation( ): Promise<{ bundle: Buffer bundleDigest: string + bundleMediaType: string + bundlePredicateType: string }> { const subjectName = `${options.nameWithOwner}@${semverTag}` const subjectDigest = removePrefix(manifestDigest, 'sha256:') @@ -241,7 +245,26 @@ async function generateAttestation( hash.update(bundleArtifact) const bundleSHA = hash.digest('hex') - return { bundle: bundleArtifact, bundleDigest: `sha256:${bundleSHA}` } + // We must base64 decode the dsse envelope to grab the predicate type + const dsseEnvelopeArtifact = attestation.bundle.dsseEnvelope + if (dsseEnvelopeArtifact === undefined) { + throw new Error('Attestation bundle is missing dsseEnvelope artifact') + } + + const dsseEnvelope = JSON.parse( + Buffer.from(dsseEnvelopeArtifact.payload, 'base64').toString('utf-8') + ) + const predicateType = dsseEnvelope.predicateType + if (predicateType === undefined) { + throw new Error('Attestation bundle is missing predicateType') + } + + return { + bundle: bundleArtifact, + bundleDigest: `sha256:${bundleSHA}`, + bundleMediaType: attestation.bundle.mediaType, + bundlePredicateType: predicateType + } } function removePrefix(str: string, prefix: string): string { diff --git a/src/oci-container.ts b/src/oci-container.ts index 1abbbb4..343409d 100644 --- a/src/oci-container.ts +++ b/src/oci-container.ts @@ -10,8 +10,6 @@ export const actionsPackageTarLayerMediaType = 'application/vnd.github.actions.package.layer.v1.tar+gzip' export const actionsPackageZipLayerMediaType = 'application/vnd.github.actions.package.layer.v1.zip' -export const sigstoreBundleMediaType = - 'application/vnd.dev.sigstore.bundle.v0.3+json' export const actionPackageAnnotationValue = 'actions_oci_pkg' export const actionPackageAttestationAnnotationValue = @@ -89,6 +87,8 @@ export function createActionPackageManifest( export function createSigstoreAttestationManifest( bundleSize: number, bundleDigest: string, + bundleMediaType: string, + bundlePredicateType: string, subjectSize: number, subjectDigest: string, created: Date = new Date() @@ -96,7 +96,7 @@ export function createSigstoreAttestationManifest( const configLayer = createEmptyConfigLayer() const sigstoreAttestationLayer: Descriptor = { - mediaType: sigstoreBundleMediaType, + mediaType: bundleMediaType, size: bundleSize, digest: bundleDigest } @@ -110,14 +110,14 @@ export function createSigstoreAttestationManifest( const manifest: OCIImageManifest = { schemaVersion: 2, mediaType: imageManifestMediaType, - artifactType: sigstoreBundleMediaType, + artifactType: bundleMediaType, config: configLayer, layers: [sigstoreAttestationLayer], subject, annotations: { 'dev.sigstore.bundle.content': 'dsse-envelope', - 'dev.sigstore.bundle.predicateType': 'https://slsa.dev/provenance/v1', + 'dev.sigstore.bundle.predicateType': bundlePredicateType, 'com.github.package.type': actionPackageAttestationAnnotationValue, 'org.opencontainers.image.created': created.toISOString() } @@ -129,6 +129,8 @@ export function createSigstoreAttestationManifest( export function createReferrerTagManifest( attestationDigest: string, attestationSize: number, + bundleMediaType: string, + bundlePredicateType: string, attestationCreated: Date, created: Date = new Date() ): OCIIndexManifest { @@ -138,14 +140,14 @@ export function createReferrerTagManifest( manifests: [ { mediaType: imageManifestMediaType, - artifactType: sigstoreBundleMediaType, + artifactType: bundleMediaType, size: attestationSize, digest: attestationDigest, annotations: { 'com.github.package.type': actionPackageAttestationAnnotationValue, 'org.opencontainers.image.created': attestationCreated.toISOString(), 'dev.sigstore.bundle.content': 'dsse-envelope', - 'dev.sigstore.bundle.predicateType': 'https://slsa.dev/provenance/v1' + 'dev.sigstore.bundle.predicateType': bundlePredicateType } } ],