init attest build provenance

This commit is contained in:
ejahnGithub
2024-02-22 08:29:05 -08:00
parent 1a4323c55b
commit 381fca5703
45 changed files with 76808 additions and 372 deletions
@@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`generateProvenance returns a provenance hydrated from env vars 1`] = `
{
"_type": "https://in-toto.io/Statement/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"path": ".github/workflows/main.yml",
"ref": "main",
"repository": "https://github.com/owner/repo",
},
},
"internalParameters": {
"github": {
"event_name": "push",
"repository_id": "repo-id",
"repository_owner_id": "owner-id",
},
},
"resolvedDependencies": [
{
"digest": {
"gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b",
},
"uri": "git+https://github.com/owner/repo@refs/heads/main",
},
],
},
"runDetails": {
"builder": {
"id": "https://github.com/actions/runner/github-hosted",
},
"metadata": {
"invocationId": "https://github.com/owner/repo/actions/runs/run-id/attempts/run-attempt",
},
},
},
"predicateType": "https://slsa.dev/provenance/v1",
"subject": [
{
"digest": {
"sha256": "7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32",
},
"name": "subjecty",
},
],
}
`;
@@ -0,0 +1,154 @@
import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock'
import nock from 'nock'
import { attestProvenance } from '../attest'
describe('attest functions', () => {
// Capture original environment variables and GitHub context so we can restore
// them after each test
const originalEnv = process.env
// Fake an OIDC token
const subject = 'foo@bar.com'
const oidcPayload = { sub: subject, iss: '' }
const oidcToken = `.${Buffer.from(JSON.stringify(oidcPayload)).toString(
'base64'
)}.}`
const tokenURL = 'https://token.url'
const fulcioURL = 'https://fulcio.url'
const rekorURL = 'https://rekor.url'
const tsaServerURL = 'https://tsa.url'
const attestationID = '1234567890'
beforeEach(async () => {
jest.clearAllMocks()
nock(tokenURL)
.get('/')
.query({ audience: 'sigstore' })
.reply(200, { value: oidcToken })
// Mock Fulcio endpoint
await mockFulcio({ baseURL: fulcioURL, strict: false })
// Set-up GHA environment variables
process.env = {
...originalEnv,
ACTIONS_ID_TOKEN_REQUEST_URL: tokenURL,
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token'
}
})
afterEach(() => {
// Restore the original environment
process.env = originalEnv
})
describe('#attestProvenance', () => {
const env = {
GITHUB_REPOSITORY: 'owner/repo',
GITHUB_REF: 'refs/heads/main',
GITHUB_SHA: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
GITHUB_WORKFLOW_REF: 'owner/repo/.github/workflows/main.yml@main',
GITHUB_SERVER_URL: 'https://github.com',
GITHUB_EVENT_NAME: 'push',
GITHUB_REPOSITORY_ID: 'repo-id',
GITHUB_REPOSITORY_OWNER_ID: 'owner-id',
GITHUB_RUN_ID: 'run-id',
GITHUB_RUN_ATTEMPT: 'run-attempt',
RUNNER_ENVIRONMENT: 'github-hosted'
}
beforeEach(() => {
process.env = { ...process.env, ...env }
})
describe('when the timestamp authority URL is set', () => {
beforeEach(async () => {
await mockTSA({ baseURL: tsaServerURL })
// Mock GH attestations API
nock('https://api.github.com')
.post(/^\/repos\/.*\/.*\/attestations$/)
.reply(201, { id: attestationID })
})
it('attests provenance', async () => {
const attestation = await attestProvenance({
subjectName: 'subjective',
subjectDigest: {
sha256:
'7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
},
token: 'token',
fulcioURL,
tsaServerURL
})
expect(attestation).toBeDefined()
expect(attestation.bundle).toBeDefined()
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
expect(attestation.tlogID).toBeUndefined()
expect(attestation.attestationID).toBe(attestationID)
})
})
describe('when the transparency log URL is set', () => {
beforeEach(async () => {
await mockRekor({ baseURL: rekorURL })
// Mock GH attestations API
nock('https://api.github.com')
.post(/^\/repos\/.*\/.*\/attestations$/)
.reply(201, { id: attestationID })
})
it('attests provenance', async () => {
const attestation = await attestProvenance({
subjectName: 'subjective',
subjectDigest: {
sha256:
'7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
},
token: 'token',
fulcioURL,
rekorURL
})
expect(attestation).toBeDefined()
expect(attestation.bundle).toBeDefined()
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
expect(attestation.tlogID).toBeDefined()
expect(attestation.attestationID).toBe(attestationID)
})
})
describe('when skipWrite is set to true', () => {
beforeEach(async () => {
await mockRekor({ baseURL: rekorURL })
await mockTSA({ baseURL: tsaServerURL })
})
it('attests provenance', async () => {
const attestation = await attestProvenance({
subjectName: 'subjective',
subjectDigest: {
sha256:
'7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
},
token: 'token',
fulcioURL,
rekorURL,
tsaServerURL,
skipWrite: true
})
expect(attestation).toBeDefined()
expect(attestation.bundle).toBeDefined()
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
expect(attestation.tlogID).toBeDefined()
expect(attestation.attestationID).toBeUndefined()
})
})
})
})
@@ -0,0 +1,32 @@
import { fromPartial } from '@total-typescript/shoehorn'
import {
AttestOptions,
AttestProvenanceOptions,
Attestation,
Predicate,
Subject,
attest,
attestProvenance
} from '..'
it('exports functions', () => {
expect(attestProvenance).toBeInstanceOf(Function)
expect(attest).toBeInstanceOf(Function)
})
it('exports types', async () => {
const attestation: Attestation = fromPartial({})
expect(attestation).toBeDefined()
const attestOptions: AttestOptions = fromPartial({})
expect(attestOptions).toBeDefined()
const attestProvenanceOptions: AttestProvenanceOptions = fromPartial({})
expect(attestProvenanceOptions).toBeDefined()
const subject: Subject = fromPartial({})
expect(subject).toBeDefined()
const predicate: Predicate = fromPartial({})
expect(predicate).toBeDefined()
})
@@ -0,0 +1,30 @@
import { generateProvenance } from '../provenance'
import type { Subject } from '../shared.types'
describe('generateProvenance', () => {
const subject: Subject = {
name: 'subjecty',
digest: {
sha256: '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
}
}
const env = {
GITHUB_REPOSITORY: 'owner/repo',
GITHUB_REF: 'refs/heads/main',
GITHUB_SHA: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
GITHUB_WORKFLOW_REF: 'owner/repo/.github/workflows/main.yml@main',
GITHUB_SERVER_URL: 'https://github.com',
GITHUB_EVENT_NAME: 'push',
GITHUB_REPOSITORY_ID: 'repo-id',
GITHUB_REPOSITORY_OWNER_ID: 'owner-id',
GITHUB_RUN_ID: 'run-id',
GITHUB_RUN_ATTEMPT: 'run-attempt',
RUNNER_ENVIRONMENT: 'github-hosted'
}
it('returns a provenance hydrated from env vars', () => {
const provenance = generateProvenance(subject, env)
expect(provenance).toMatchSnapshot()
})
})
+105
View File
@@ -0,0 +1,105 @@
import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock'
import nock from 'nock'
import { Payload, signPayload } from '../sign'
describe('signProvenance', () => {
const originalEnv = process.env
// Fake an OIDC token
const subject = 'foo@bar.com'
const oidcPayload = { sub: subject, iss: '' }
const oidcToken = `.${Buffer.from(JSON.stringify(oidcPayload)).toString(
'base64'
)}.}`
// Dummy provenance to be signed
const provenance = {
_type: 'https://in-toto.io/Statement/v1',
subject: {
name: 'subjective',
digest: {
sha256:
'7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
}
}
}
const payload: Payload = {
body: Buffer.from(JSON.stringify(provenance)),
type: 'application/vnd.in-toto+json'
}
const fulcioURL = 'https://fulcio.url'
const rekorURL = 'https://rekor.url'
const tsaServerURL = 'https://tsa.url'
beforeEach(() => {
// Mock OIDC token endpoint
const tokenURL = 'https://token.url'
process.env = {
...originalEnv,
ACTIONS_ID_TOKEN_REQUEST_URL: tokenURL,
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token'
}
nock(tokenURL)
.get('/')
.query({ audience: 'sigstore' })
.reply(200, { value: oidcToken })
})
afterEach(() => {
process.env = originalEnv
})
describe('when visibility is public', () => {
beforeEach(async () => {
await mockFulcio({ baseURL: fulcioURL, strict: false })
await mockRekor({ baseURL: rekorURL })
})
it('returns a bundle', async () => {
const att = await signPayload(payload, { fulcioURL, rekorURL })
expect(att).toBeDefined()
expect(att.mediaType).toEqual(
'application/vnd.dev.sigstore.bundle+json;version=0.2'
)
expect(att.content.$case).toEqual('dsseEnvelope')
expect(att.verificationMaterial.content.$case).toEqual(
'x509CertificateChain'
)
expect(att.verificationMaterial.tlogEntries).toHaveLength(1)
expect(
att.verificationMaterial.timestampVerificationData?.rfc3161Timestamps
).toHaveLength(0)
})
})
describe('when visibility is private', () => {
beforeEach(async () => {
await mockFulcio({ baseURL: fulcioURL, strict: false })
await mockTSA({ baseURL: tsaServerURL })
})
it('returns a bundle', async () => {
const att = await signPayload(payload, { fulcioURL, tsaServerURL })
expect(att).toBeDefined()
expect(att.mediaType).toEqual(
'application/vnd.dev.sigstore.bundle+json;version=0.2'
)
expect(att.content.$case).toEqual('dsseEnvelope')
expect(att.verificationMaterial.content.$case).toEqual(
'x509CertificateChain'
)
expect(att.verificationMaterial.tlogEntries).toHaveLength(0)
expect(
att.verificationMaterial.timestampVerificationData?.rfc3161Timestamps
).toHaveLength(1)
})
})
})
@@ -0,0 +1,45 @@
import nock from 'nock'
import { writeAttestation } from '../store'
describe('writeAttestation', () => {
const originalEnv = process.env
const attestation = { foo: 'bar ' }
const token = 'token'
beforeEach(() => {
process.env = {
...originalEnv,
GITHUB_REPOSITORY: 'foo/bar'
}
})
afterEach(() => {
process.env = originalEnv
})
describe('when the api call is successful', () => {
beforeEach(() => {
nock('https://api.github.com')
.matchHeader('authorization', `token ${token}`)
.post('/repos/foo/bar/attestations', { bundle: attestation })
.reply(201, { id: '123' })
})
it('persists the attestation', async () => {
await expect(writeAttestation(attestation, token)).resolves.toEqual('123')
})
})
describe('when the api call fails', () => {
beforeEach(() => {
nock('https://api.github.com')
.matchHeader('authorization', `token ${token}`)
.post('/repos/foo/bar/attestations', { bundle: attestation })
.reply(500, 'oops')
})
it('persists the attestation', async () => {
await expect(writeAttestation(attestation, token)).rejects.toThrow(/oops/)
})
})
})