Generate provenance attestation before performing upload to ghcr

This allows us to check in the backend that a valid attestation exists for a package version before we allow the upload to succeed.
In doing this, we can perform an integrity check that the attestation is valid and all action packages have valid attestations.
This commit is contained in:
Conor Sloan
2024-08-07 17:13:39 +01:00
parent 8215ec2f64
commit c1f237b012
7 changed files with 274 additions and 70 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ export async function publishOCIArtifact(
zipFile: FileMetadata,
tarFile: FileMetadata,
manifest: ociContainer.Manifest
): Promise<{ packageURL: URL; manifestDigest: string }> {
): Promise<{ packageURL: URL; publishedDigest: string }> {
const b64Token = Buffer.from(token).toString('base64')
const checkBlobEndpoint = new URL(
@@ -76,7 +76,7 @@ export async function publishOCIArtifact(
return {
packageURL: new URL(`${repository}:${semver}`, registry),
manifestDigest: digest
publishedDigest: digest
}
}
+23 -13
View File
@@ -50,19 +50,7 @@ export async function run(): Promise<void> {
new Date()
)
const { packageURL, manifestDigest } = await ghcr.publishOCIArtifact(
options.token,
options.containerRegistryUrl,
options.nameWithOwner,
semverTag.raw,
archives.zipFile,
archives.tarFile,
manifest
)
core.setOutput('package-url', packageURL.toString())
core.setOutput('package-manifest', JSON.stringify(manifest))
core.setOutput('package-manifest-sha', manifestDigest)
const manifestDigest = ociContainer.sha256Digest(manifest)
// Attestations are not currently supported in GHES.
if (!options.isEnterprise) {
@@ -75,6 +63,26 @@ export async function run(): Promise<void> {
core.setOutput('attestation-id', attestation.attestationID)
}
}
const { packageURL, publishedDigest } = await ghcr.publishOCIArtifact(
options.token,
options.containerRegistryUrl,
options.nameWithOwner,
semverTag.raw,
archives.zipFile,
archives.tarFile,
manifest
)
if (manifestDigest !== publishedDigest) {
throw new Error(
`Unexpected digest returned for manifest. Expected ${manifestDigest}, got ${publishedDigest}`
)
}
core.setOutput('package-url', packageURL.toString())
core.setOutput('package-manifest', JSON.stringify(manifest))
core.setOutput('package-manifest-sha', publishedDigest)
} catch (error) {
// Fail the workflow run if an error occurs
if (error instanceof Error) core.setFailed(error.message)
@@ -112,6 +120,8 @@ async function generateAttestation(
const subjectName = `${options.nameWithOwner}@${semverTag}`
const subjectDigest = removePrefix(manifestDigest, 'sha256:')
core.info(`Generating attestation ${subjectName} for digest ${subjectDigest}`)
return await attest.attestProvenance({
subjectName,
subjectDigest: { sha256: subjectDigest },
+12
View File
@@ -1,4 +1,5 @@
import { FileMetadata } from './fs-helper'
import * as crypto from 'crypto'
export interface Manifest {
schemaVersion: number
@@ -53,6 +54,17 @@ export function createActionPackageManifest(
return manifest
}
// Calculate the SHA256 digest of a given manifest.
// This should match the digest which the GitHub container registry calculates for this manifest.
export function sha256Digest(manifest: Manifest): string {
const data = JSON.stringify(manifest)
const buffer = Buffer.from(data, 'utf8')
const hash = crypto.createHash('sha256')
hash.update(buffer)
const hexHash = hash.digest('hex')
return `sha256:${hexHash}`
}
function createConfigLayer(): Layer {
const configLayer: Layer = {
mediaType: 'application/vnd.oci.empty.v1+json',