add new OCI manifests for attestations
This commit is contained in:
@@ -182,7 +182,7 @@ function configureFetchMock(
|
||||
)
|
||||
}
|
||||
|
||||
const testManifest: ociContainer.Manifest = {
|
||||
const testManifest: ociContainer.OCIImageManifest = {
|
||||
schemaVersion: 2,
|
||||
mediaType: 'application/vnd.oci.image.manifest.v1+json',
|
||||
artifactType: 'application/vnd.oci.image.manifest.v1+json',
|
||||
@@ -526,8 +526,10 @@ function validateRequestConfig(url: string, config: any): void {
|
||||
}
|
||||
}
|
||||
|
||||
function cloneLayers(layers: ociContainer.Layer[]): ociContainer.Layer[] {
|
||||
const result: ociContainer.Layer[] = []
|
||||
function cloneLayers(
|
||||
layers: ociContainer.Descriptor[]
|
||||
): ociContainer.Descriptor[] {
|
||||
const result: ociContainer.Descriptor[] = []
|
||||
for (const layer of layers) {
|
||||
result.push({ ...layer }) // this is _NOT_ a deep clone
|
||||
}
|
||||
|
||||
+183
-93
@@ -1,8 +1,168 @@
|
||||
import { createActionPackageManifest, sha256Digest } from '../src/oci-container'
|
||||
import {
|
||||
createActionPackageManifest,
|
||||
sha256Digest,
|
||||
sizeInBytes,
|
||||
OCIImageManifest,
|
||||
createSigstoreAttestationManifest,
|
||||
OCIIndexManifest,
|
||||
createReferrerTagManifest
|
||||
} from '../src/oci-container'
|
||||
import { FileMetadata } from '../src/fs-helper'
|
||||
|
||||
const createdTimestamp = '2021-01-01T00:00:00.000Z'
|
||||
|
||||
describe('sha256Digest', () => {
|
||||
it('calculates the SHA256 digest of the provided manifest', () => {
|
||||
const { manifest } = testActionPackageManifest()
|
||||
const digest = sha256Digest(manifest)
|
||||
const expectedDigest =
|
||||
'sha256:dd8537ef913cf87e25064a074973ed2c62699f1dbd74d0dd78e85d394a5758b5'
|
||||
|
||||
expect(digest).toEqual(expectedDigest)
|
||||
})
|
||||
})
|
||||
|
||||
describe('size', () => {
|
||||
it('returns the total size of the provided manifest', () => {
|
||||
const { manifest } = testActionPackageManifest()
|
||||
const size = sizeInBytes(manifest)
|
||||
expect(size).toBe(1133)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createActionPackageManifest', () => {
|
||||
it('creates a manifest containing the provided information', () => {
|
||||
const { manifest, zipFile, tarFile } = testActionPackageManifest()
|
||||
|
||||
const expectedJSON = `{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"artifactType": "application/vnd.github.actions.package.v1+json",
|
||||
"config": {
|
||||
"mediaType":"application/vnd.oci.empty.v1+json",
|
||||
"size":2,
|
||||
"digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
|
||||
},
|
||||
"layers":[
|
||||
{
|
||||
"mediaType":"application/vnd.oci.empty.v1+json",
|
||||
"size":2,
|
||||
"digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
|
||||
},
|
||||
{
|
||||
"mediaType":"application/vnd.github.actions.package.layer.v1.tar+gzip",
|
||||
"size":${tarFile.size},
|
||||
"digest":"${tarFile.sha256}",
|
||||
"annotations":{
|
||||
"org.opencontainers.image.title":"test-org-test-repo_1.2.3.tar.gz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType":"application/vnd.github.actions.package.layer.v1.zip",
|
||||
"size":${zipFile.size},
|
||||
"digest":"${zipFile.sha256}",
|
||||
"annotations":{
|
||||
"org.opencontainers.image.title":"test-org-test-repo_1.2.3.zip"
|
||||
}
|
||||
}
|
||||
],
|
||||
"annotations":{
|
||||
"org.opencontainers.image.created":"${createdTimestamp}",
|
||||
"action.tar.gz.digest":"${tarFile.sha256}",
|
||||
"action.zip.digest":"${zipFile.sha256}",
|
||||
"com.github.package.type":"actions_oci_pkg",
|
||||
"com.github.package.version":"1.2.3",
|
||||
"com.github.source.repo.id":"123",
|
||||
"com.github.source.repo.owner.id":"456",
|
||||
"com.github.source.commit":"abc"
|
||||
}
|
||||
}`
|
||||
|
||||
const manifestJSON = JSON.stringify(manifest)
|
||||
expect(manifestJSON).toEqual(expectedJSON.replace(/\s/g, ''))
|
||||
})
|
||||
})
|
||||
|
||||
describe('createSigstoreAttestationManifest', () => {
|
||||
it('creates a manifest containing the provided information', () => {
|
||||
const manifest = testAttestationManifest()
|
||||
|
||||
const expectedJSON = `{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"artifactType": "application/vnd.dev.sigstore.bundle.v0.3+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.empty.v1+json",
|
||||
"size": 2,
|
||||
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
|
||||
"size": 10,
|
||||
"digest": "bundleDigest"
|
||||
}
|
||||
],
|
||||
"subject": {
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"size": 100,
|
||||
"digest": "subjectDigest"
|
||||
},
|
||||
"annotations": {
|
||||
"dev.sigstore.bundle.content": "dsse-envelope",
|
||||
"dev.sigstore.bundle.predicateType": "https://slsa.dev/provenance/v1",
|
||||
"com.github.package.type": "actions_oci_pkg_attestation",
|
||||
"org.opencontainers.image.created": "2021-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const manifestJSON = JSON.stringify(manifest)
|
||||
|
||||
expect(manifestJSON).toEqual(expectedJSON.replace(/\s/g, ''))
|
||||
})
|
||||
})
|
||||
|
||||
describe('createReferrerIndexManifest', () => {
|
||||
it('creates a manifest containing the provided information', () => {
|
||||
const manifest = testReferrerIndexManifest()
|
||||
|
||||
const expectedJSON = `
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"artifactType": "application/vnd.dev.sigstore.bundle.v0.3+json",
|
||||
"size": 100,
|
||||
"digest": "attDigest",
|
||||
"annotations": {
|
||||
"com.github.package.type": "actions_oci_pkg_attestation",
|
||||
"org.opencontainers.image.created": "2021-01-01T00:00:00.000Z",
|
||||
"dev.sigstore.bundle.content": "dsse-envelope",
|
||||
"dev.sigstore.bundle.predicateType": "https://slsa.dev/provenance/v1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"com.github.package.type": "actions_oci_pkg_referrer_tag",
|
||||
"org.opencontainers.image.created": "2021-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const manifestJSON = JSON.stringify(manifest)
|
||||
|
||||
expect(manifestJSON).toEqual(expectedJSON.replace(/\s/g, ''))
|
||||
})
|
||||
})
|
||||
|
||||
function testActionPackageManifest(): {
|
||||
manifest: OCIImageManifest
|
||||
tarFile: FileMetadata
|
||||
zipFile: FileMetadata
|
||||
} {
|
||||
const date = new Date('2021-01-01T00:00:00Z')
|
||||
const repo = 'test-org/test-repo'
|
||||
const version = '1.2.3'
|
||||
@@ -31,98 +191,28 @@ describe('sha256Digest', () => {
|
||||
date
|
||||
)
|
||||
|
||||
const digest = sha256Digest(manifest)
|
||||
const expectedDigest =
|
||||
'sha256:dd8537ef913cf87e25064a074973ed2c62699f1dbd74d0dd78e85d394a5758b5'
|
||||
return {
|
||||
manifest,
|
||||
tarFile,
|
||||
zipFile
|
||||
}
|
||||
}
|
||||
|
||||
expect(digest).toEqual(expectedDigest)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createActionPackageManifest', () => {
|
||||
it('creates a manifest containing the provided information', () => {
|
||||
const date = new Date()
|
||||
const repo = 'test-org/test-repo'
|
||||
const sanitizedRepo = 'test-org-test-repo'
|
||||
const version = '1.2.3'
|
||||
const repoId = '123'
|
||||
const ownerId = '456'
|
||||
const sourceCommit = 'abc'
|
||||
const tarFile: FileMetadata = {
|
||||
path: '/test/test/test.tar.gz',
|
||||
sha256: 'tarSha',
|
||||
size: 123
|
||||
}
|
||||
const zipFile: FileMetadata = {
|
||||
path: '/test/test/test.zip',
|
||||
sha256: 'zipSha',
|
||||
size: 456
|
||||
}
|
||||
|
||||
const expectedJSON = `{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"artifactType": "application/vnd.github.actions.package.v1+json",
|
||||
"config": {
|
||||
"mediaType":"application/vnd.oci.empty.v1+json",
|
||||
"size":2,
|
||||
"digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
|
||||
},
|
||||
"layers":[
|
||||
{
|
||||
"mediaType":"application/vnd.oci.empty.v1+json",
|
||||
"size":2,
|
||||
"digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
|
||||
},
|
||||
{
|
||||
"mediaType":"application/vnd.github.actions.package.layer.v1.tar+gzip",
|
||||
"size":${tarFile.size},
|
||||
"digest":"${tarFile.sha256}",
|
||||
"annotations":{
|
||||
"org.opencontainers.image.title":"${sanitizedRepo}_${version}.tar.gz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType":"application/vnd.github.actions.package.layer.v1.zip",
|
||||
"size":${zipFile.size},
|
||||
"digest":"${zipFile.sha256}",
|
||||
"annotations":{
|
||||
"org.opencontainers.image.title":"${sanitizedRepo}_${version}.zip"
|
||||
}
|
||||
}
|
||||
],
|
||||
"annotations":{
|
||||
"org.opencontainers.image.created":"${date.toISOString()}",
|
||||
"action.tar.gz.digest":"${tarFile.sha256}",
|
||||
"action.zip.digest":"${zipFile.sha256}",
|
||||
"com.github.package.type":"actions_oci_pkg",
|
||||
"com.github.package.version":"1.2.3",
|
||||
"com.github.source.repo.id":"123",
|
||||
"com.github.source.repo.owner.id":"456",
|
||||
"com.github.source.commit":"abc"
|
||||
}
|
||||
}`
|
||||
|
||||
const manifest = createActionPackageManifest(
|
||||
{
|
||||
path: 'test.tar.gz',
|
||||
size: tarFile.size,
|
||||
sha256: tarFile.sha256
|
||||
},
|
||||
{
|
||||
path: 'test.zip',
|
||||
size: zipFile.size,
|
||||
sha256: zipFile.sha256
|
||||
},
|
||||
repo,
|
||||
repoId,
|
||||
ownerId,
|
||||
sourceCommit,
|
||||
version,
|
||||
date
|
||||
function testAttestationManifest(): OCIImageManifest {
|
||||
return createSigstoreAttestationManifest(
|
||||
10,
|
||||
'bundleDigest',
|
||||
100,
|
||||
'subjectDigest',
|
||||
new Date(createdTimestamp)
|
||||
)
|
||||
}
|
||||
|
||||
const manifestJSON = JSON.stringify(manifest)
|
||||
expect(manifestJSON).toEqual(expectedJSON.replace(/\s/g, ''))
|
||||
})
|
||||
})
|
||||
function testReferrerIndexManifest(): OCIIndexManifest {
|
||||
return createReferrerTagManifest(
|
||||
'attDigest',
|
||||
100,
|
||||
new Date(createdTimestamp),
|
||||
new Date(createdTimestamp)
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 97.17%"><title>Coverage: 97.17%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">97.17%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">97.17%</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 97.39%"><title>Coverage: 97.39%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">97.39%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">97.39%</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
+81
-9
@@ -107754,25 +107754,40 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.createActionPackageManifest = createActionPackageManifest;
|
||||
exports.createSigstoreAttestationManifest = createSigstoreAttestationManifest;
|
||||
exports.createReferrerTagManifest = createReferrerTagManifest;
|
||||
exports.sha256Digest = sha256Digest;
|
||||
exports.sizeInBytes = sizeInBytes;
|
||||
const crypto = __importStar(__nccwpck_require__(6113));
|
||||
const imageIndexMediaType = 'application/vnd.oci.image.index.v1+json';
|
||||
const imageManifestMediaType = 'application/vnd.oci.image.manifest.v1+json';
|
||||
const actionsPackageMediaType = 'application/vnd.github.actions.package.v1+json';
|
||||
const actionsPackageTarLayerMediaType = 'application/vnd.github.actions.package.layer.v1.tar+gzip';
|
||||
const actionsPackageZipLayerMediaType = 'application/vnd.github.actions.package.layer.v1.zip';
|
||||
const sigstoreBundleMediaType = 'application/vnd.dev.sigstore.bundle.v0.3+json';
|
||||
const ociEmptyMediaType = 'application/vnd.oci.empty.v1+json';
|
||||
const actionPackageAnnotationValue = 'actions_oci_pkg';
|
||||
const actionPackageAttestationAnnotationValue = 'actions_oci_pkg_attestation';
|
||||
const actionPackageReferrerTagAnnotationValue = 'actions_oci_pkg_referrer_tag';
|
||||
const emptyConfigSize = 2;
|
||||
const emptyConfigSha = 'sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a';
|
||||
// Given a name and archive metadata, creates a manifest in the format expected by GHCR for an Actions Package.
|
||||
function createActionPackageManifest(tarFile, zipFile, repository, repoId, ownerId, sourceCommit, version, created) {
|
||||
function createActionPackageManifest(tarFile, zipFile, repository, repoId, ownerId, sourceCommit, version, created = new Date()) {
|
||||
const configLayer = createConfigLayer();
|
||||
const sanitizedRepo = sanitizeRepository(repository);
|
||||
const tarLayer = createTarLayer(tarFile, sanitizedRepo, version);
|
||||
const zipLayer = createZipLayer(zipFile, sanitizedRepo, version);
|
||||
const manifest = {
|
||||
schemaVersion: 2,
|
||||
mediaType: 'application/vnd.oci.image.manifest.v1+json',
|
||||
artifactType: 'application/vnd.github.actions.package.v1+json',
|
||||
mediaType: imageManifestMediaType,
|
||||
artifactType: actionsPackageMediaType,
|
||||
config: configLayer,
|
||||
layers: [configLayer, tarLayer, zipLayer],
|
||||
annotations: {
|
||||
'org.opencontainers.image.created': created.toISOString(),
|
||||
'action.tar.gz.digest': tarFile.sha256,
|
||||
'action.zip.digest': zipFile.sha256,
|
||||
'com.github.package.type': 'actions_oci_pkg',
|
||||
'com.github.package.type': actionPackageAnnotationValue,
|
||||
'com.github.package.version': version,
|
||||
'com.github.source.repo.id': repoId,
|
||||
'com.github.source.repo.owner.id': ownerId,
|
||||
@@ -107781,6 +107796,59 @@ function createActionPackageManifest(tarFile, zipFile, repository, repoId, owner
|
||||
};
|
||||
return manifest;
|
||||
}
|
||||
function createSigstoreAttestationManifest(bundleSize, bundleDigest, subjectSize, subjectDigest, created = new Date()) {
|
||||
const configLayer = createConfigLayer();
|
||||
const sigstoreAttestationLayer = {
|
||||
mediaType: sigstoreBundleMediaType,
|
||||
size: bundleSize,
|
||||
digest: bundleDigest
|
||||
};
|
||||
const subject = {
|
||||
mediaType: imageManifestMediaType,
|
||||
size: subjectSize,
|
||||
digest: subjectDigest
|
||||
};
|
||||
const manifest = {
|
||||
schemaVersion: 2,
|
||||
mediaType: imageManifestMediaType,
|
||||
artifactType: sigstoreBundleMediaType,
|
||||
config: configLayer,
|
||||
layers: [sigstoreAttestationLayer],
|
||||
subject,
|
||||
annotations: {
|
||||
'dev.sigstore.bundle.content': 'dsse-envelope',
|
||||
'dev.sigstore.bundle.predicateType': 'https://slsa.dev/provenance/v1',
|
||||
'com.github.package.type': actionPackageAttestationAnnotationValue,
|
||||
'org.opencontainers.image.created': created.toISOString()
|
||||
}
|
||||
};
|
||||
return manifest;
|
||||
}
|
||||
function createReferrerTagManifest(attestationDigest, attestationSize, attestationCreated, created = new Date()) {
|
||||
const manifest = {
|
||||
schemaVersion: 2,
|
||||
mediaType: imageIndexMediaType,
|
||||
manifests: [
|
||||
{
|
||||
mediaType: imageManifestMediaType,
|
||||
artifactType: sigstoreBundleMediaType,
|
||||
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'
|
||||
}
|
||||
}
|
||||
],
|
||||
annotations: {
|
||||
'com.github.package.type': actionPackageReferrerTagAnnotationValue,
|
||||
'org.opencontainers.image.created': created.toISOString()
|
||||
}
|
||||
};
|
||||
return manifest;
|
||||
}
|
||||
// Calculate the SHA256 digest of a given manifest.
|
||||
// This should match the digest which the GitHub container registry calculates for this manifest.
|
||||
function sha256Digest(manifest) {
|
||||
@@ -107791,17 +107859,21 @@ function sha256Digest(manifest) {
|
||||
const hexHash = hash.digest('hex');
|
||||
return `sha256:${hexHash}`;
|
||||
}
|
||||
function sizeInBytes(manifest) {
|
||||
const data = JSON.stringify(manifest);
|
||||
return Buffer.byteLength(data, 'utf8');
|
||||
}
|
||||
function createConfigLayer() {
|
||||
const configLayer = {
|
||||
mediaType: 'application/vnd.oci.empty.v1+json',
|
||||
size: 2,
|
||||
digest: 'sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a'
|
||||
mediaType: ociEmptyMediaType,
|
||||
size: emptyConfigSize,
|
||||
digest: emptyConfigSha
|
||||
};
|
||||
return configLayer;
|
||||
}
|
||||
function createZipLayer(zipFile, repository, version) {
|
||||
const zipLayer = {
|
||||
mediaType: 'application/vnd.github.actions.package.layer.v1.zip',
|
||||
mediaType: actionsPackageZipLayerMediaType,
|
||||
size: zipFile.size,
|
||||
digest: zipFile.sha256,
|
||||
annotations: {
|
||||
@@ -107812,7 +107884,7 @@ function createZipLayer(zipFile, repository, version) {
|
||||
}
|
||||
function createTarLayer(tarFile, repository, version) {
|
||||
const tarLayer = {
|
||||
mediaType: 'application/vnd.github.actions.package.layer.v1.tar+gzip',
|
||||
mediaType: actionsPackageTarLayerMediaType,
|
||||
size: tarFile.size,
|
||||
digest: tarFile.sha256,
|
||||
annotations: {
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ export async function publishOCIArtifact(
|
||||
semver: string,
|
||||
zipFile: FileMetadata,
|
||||
tarFile: FileMetadata,
|
||||
manifest: ociContainer.Manifest
|
||||
manifest: ociContainer.OCIImageManifest
|
||||
): Promise<{ packageURL: URL; publishedDigest: string }> {
|
||||
const b64Token = Buffer.from(token).toString('base64')
|
||||
|
||||
@@ -81,7 +81,7 @@ export async function publishOCIArtifact(
|
||||
}
|
||||
|
||||
async function uploadLayer(
|
||||
layer: ociContainer.Layer,
|
||||
layer: ociContainer.Descriptor,
|
||||
file: FileMetadata,
|
||||
registryURL: URL,
|
||||
checkBlobEndpoint: string,
|
||||
|
||||
+130
-23
@@ -1,19 +1,46 @@
|
||||
import { FileMetadata } from './fs-helper'
|
||||
import * as crypto from 'crypto'
|
||||
|
||||
export interface Manifest {
|
||||
const imageIndexMediaType = 'application/vnd.oci.image.index.v1+json'
|
||||
const imageManifestMediaType = 'application/vnd.oci.image.manifest.v1+json'
|
||||
const actionsPackageMediaType = 'application/vnd.github.actions.package.v1+json'
|
||||
const actionsPackageTarLayerMediaType =
|
||||
'application/vnd.github.actions.package.layer.v1.tar+gzip'
|
||||
const actionsPackageZipLayerMediaType =
|
||||
'application/vnd.github.actions.package.layer.v1.zip'
|
||||
const sigstoreBundleMediaType = 'application/vnd.dev.sigstore.bundle.v0.3+json'
|
||||
const ociEmptyMediaType = 'application/vnd.oci.empty.v1+json'
|
||||
|
||||
const actionPackageAnnotationValue = 'actions_oci_pkg'
|
||||
const actionPackageAttestationAnnotationValue = 'actions_oci_pkg_attestation'
|
||||
const actionPackageReferrerTagAnnotationValue = 'actions_oci_pkg_referrer_tag'
|
||||
|
||||
const emptyConfigSize = 2
|
||||
const emptyConfigSha =
|
||||
'sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a'
|
||||
|
||||
export interface OCIImageManifest {
|
||||
schemaVersion: number
|
||||
mediaType: string
|
||||
artifactType: string
|
||||
config: Layer
|
||||
layers: Layer[]
|
||||
config: Descriptor
|
||||
layers: Descriptor[]
|
||||
subject?: Descriptor
|
||||
annotations: { [key: string]: string }
|
||||
}
|
||||
|
||||
export interface Layer {
|
||||
export interface OCIIndexManifest {
|
||||
schemaVersion: number
|
||||
mediaType: string
|
||||
manifests: Descriptor[]
|
||||
annotations: { [key: string]: string }
|
||||
}
|
||||
|
||||
export interface Descriptor {
|
||||
mediaType: string
|
||||
size: number
|
||||
digest: string
|
||||
artifactType?: string
|
||||
annotations?: { [key: string]: string }
|
||||
}
|
||||
|
||||
@@ -26,24 +53,24 @@ export function createActionPackageManifest(
|
||||
ownerId: string,
|
||||
sourceCommit: string,
|
||||
version: string,
|
||||
created: Date
|
||||
): Manifest {
|
||||
created: Date = new Date()
|
||||
): OCIImageManifest {
|
||||
const configLayer = createConfigLayer()
|
||||
const sanitizedRepo = sanitizeRepository(repository)
|
||||
const tarLayer = createTarLayer(tarFile, sanitizedRepo, version)
|
||||
const zipLayer = createZipLayer(zipFile, sanitizedRepo, version)
|
||||
|
||||
const manifest: Manifest = {
|
||||
const manifest: OCIImageManifest = {
|
||||
schemaVersion: 2,
|
||||
mediaType: 'application/vnd.oci.image.manifest.v1+json',
|
||||
artifactType: 'application/vnd.github.actions.package.v1+json',
|
||||
mediaType: imageManifestMediaType,
|
||||
artifactType: actionsPackageMediaType,
|
||||
config: configLayer,
|
||||
layers: [configLayer, tarLayer, zipLayer],
|
||||
annotations: {
|
||||
'org.opencontainers.image.created': created.toISOString(),
|
||||
'action.tar.gz.digest': tarFile.sha256,
|
||||
'action.zip.digest': zipFile.sha256,
|
||||
'com.github.package.type': 'actions_oci_pkg',
|
||||
'com.github.package.type': actionPackageAnnotationValue,
|
||||
'com.github.package.version': version,
|
||||
'com.github.source.repo.id': repoId,
|
||||
'com.github.source.repo.owner.id': ownerId,
|
||||
@@ -54,9 +81,83 @@ export function createActionPackageManifest(
|
||||
return manifest
|
||||
}
|
||||
|
||||
export function createSigstoreAttestationManifest(
|
||||
bundleSize: number,
|
||||
bundleDigest: string,
|
||||
subjectSize: number,
|
||||
subjectDigest: string,
|
||||
created: Date = new Date()
|
||||
): OCIImageManifest {
|
||||
const configLayer = createConfigLayer()
|
||||
|
||||
const sigstoreAttestationLayer: Descriptor = {
|
||||
mediaType: sigstoreBundleMediaType,
|
||||
size: bundleSize,
|
||||
digest: bundleDigest
|
||||
}
|
||||
|
||||
const subject: Descriptor = {
|
||||
mediaType: imageManifestMediaType,
|
||||
size: subjectSize,
|
||||
digest: subjectDigest
|
||||
}
|
||||
|
||||
const manifest: OCIImageManifest = {
|
||||
schemaVersion: 2,
|
||||
mediaType: imageManifestMediaType,
|
||||
artifactType: sigstoreBundleMediaType,
|
||||
config: configLayer,
|
||||
layers: [sigstoreAttestationLayer],
|
||||
subject,
|
||||
|
||||
annotations: {
|
||||
'dev.sigstore.bundle.content': 'dsse-envelope',
|
||||
'dev.sigstore.bundle.predicateType': 'https://slsa.dev/provenance/v1',
|
||||
'com.github.package.type': actionPackageAttestationAnnotationValue,
|
||||
'org.opencontainers.image.created': created.toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
return manifest
|
||||
}
|
||||
|
||||
export function createReferrerTagManifest(
|
||||
attestationDigest: string,
|
||||
attestationSize: number,
|
||||
attestationCreated: Date,
|
||||
created: Date = new Date()
|
||||
): OCIIndexManifest {
|
||||
const manifest: OCIIndexManifest = {
|
||||
schemaVersion: 2,
|
||||
mediaType: imageIndexMediaType,
|
||||
manifests: [
|
||||
{
|
||||
mediaType: imageManifestMediaType,
|
||||
artifactType: sigstoreBundleMediaType,
|
||||
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'
|
||||
}
|
||||
}
|
||||
],
|
||||
annotations: {
|
||||
'com.github.package.type': actionPackageReferrerTagAnnotationValue,
|
||||
'org.opencontainers.image.created': created.toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
export function sha256Digest(
|
||||
manifest: OCIImageManifest | OCIIndexManifest
|
||||
): string {
|
||||
const data = JSON.stringify(manifest)
|
||||
const buffer = Buffer.from(data, 'utf8')
|
||||
const hash = crypto.createHash('sha256')
|
||||
@@ -65,12 +166,18 @@ export function sha256Digest(manifest: Manifest): string {
|
||||
return `sha256:${hexHash}`
|
||||
}
|
||||
|
||||
function createConfigLayer(): Layer {
|
||||
const configLayer: Layer = {
|
||||
mediaType: 'application/vnd.oci.empty.v1+json',
|
||||
size: 2,
|
||||
digest:
|
||||
'sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a'
|
||||
export function sizeInBytes(
|
||||
manifest: OCIImageManifest | OCIIndexManifest
|
||||
): number {
|
||||
const data = JSON.stringify(manifest)
|
||||
return Buffer.byteLength(data, 'utf8')
|
||||
}
|
||||
|
||||
function createConfigLayer(): Descriptor {
|
||||
const configLayer: Descriptor = {
|
||||
mediaType: ociEmptyMediaType,
|
||||
size: emptyConfigSize,
|
||||
digest: emptyConfigSha
|
||||
}
|
||||
|
||||
return configLayer
|
||||
@@ -80,9 +187,9 @@ function createZipLayer(
|
||||
zipFile: FileMetadata,
|
||||
repository: string,
|
||||
version: string
|
||||
): Layer {
|
||||
const zipLayer: Layer = {
|
||||
mediaType: 'application/vnd.github.actions.package.layer.v1.zip',
|
||||
): Descriptor {
|
||||
const zipLayer: Descriptor = {
|
||||
mediaType: actionsPackageZipLayerMediaType,
|
||||
size: zipFile.size,
|
||||
digest: zipFile.sha256,
|
||||
annotations: {
|
||||
@@ -97,9 +204,9 @@ function createTarLayer(
|
||||
tarFile: FileMetadata,
|
||||
repository: string,
|
||||
version: string
|
||||
): Layer {
|
||||
const tarLayer: Layer = {
|
||||
mediaType: 'application/vnd.github.actions.package.layer.v1.tar+gzip',
|
||||
): Descriptor {
|
||||
const tarLayer: Descriptor = {
|
||||
mediaType: actionsPackageTarLayerMediaType,
|
||||
size: tarFile.size,
|
||||
digest: tarFile.sha256,
|
||||
annotations: {
|
||||
|
||||
Reference in New Issue
Block a user