2024-02-18 03:14:10 +00:00
|
|
|
import * as github from '@actions/github'
|
|
|
|
import {mockFulcio, mockRekor, mockTSA} from '@sigstore/mock'
|
2024-03-22 02:25:36 +00:00
|
|
|
import * as jose from 'jose'
|
2024-02-18 03:14:10 +00:00
|
|
|
import nock from 'nock'
|
2024-04-10 23:53:17 +00:00
|
|
|
import {MockAgent, setGlobalDispatcher} from 'undici'
|
2024-05-16 08:55:41 +00:00
|
|
|
import {SIGSTORE_PUBLIC_GOOD, signingEndpoints} from '../src/endpoints'
|
2024-02-18 03:14:10 +00:00
|
|
|
import {attestProvenance, buildSLSAProvenancePredicate} from '../src/provenance'
|
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
describe('provenance functions', () => {
|
2024-02-18 03:14:10 +00:00
|
|
|
const originalEnv = process.env
|
2024-08-16 17:04:14 +00:00
|
|
|
const issuer = 'https://token.actions.foo.ghe.com'
|
2024-03-22 02:25:36 +00:00
|
|
|
const audience = 'nobody'
|
|
|
|
const jwksPath = '/.well-known/jwks.json'
|
|
|
|
const tokenPath = '/token'
|
|
|
|
|
2024-04-10 23:53:17 +00:00
|
|
|
// MockAgent for mocking @actions/github
|
|
|
|
const mockAgent = new MockAgent()
|
|
|
|
setGlobalDispatcher(mockAgent)
|
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
const claims = {
|
|
|
|
iss: issuer,
|
|
|
|
aud: 'nobody',
|
|
|
|
repository: 'owner/repo',
|
|
|
|
ref: 'refs/heads/main',
|
|
|
|
sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
|
2024-06-05 21:36:23 +00:00
|
|
|
job_workflow_ref: 'owner/workflows/.github/workflows/publish.yml@main',
|
2024-03-22 02:25:36 +00:00
|
|
|
workflow_ref: 'owner/repo/.github/workflows/main.yml@main',
|
|
|
|
event_name: 'push',
|
|
|
|
repository_id: 'repo-id',
|
|
|
|
repository_owner_id: 'owner-id',
|
|
|
|
run_id: 'run-id',
|
|
|
|
run_attempt: 'run-attempt',
|
|
|
|
runner_environment: 'github-hosted'
|
2024-02-18 03:14:10 +00:00
|
|
|
}
|
|
|
|
|
2024-10-30 13:02:29 +00:00
|
|
|
const mockIssuer = async (claims: jose.JWTPayload): Promise<void> => {
|
2024-03-22 02:25:36 +00:00
|
|
|
// Generate JWT signing key
|
|
|
|
const key = await jose.generateKeyPair('PS256')
|
|
|
|
|
|
|
|
// Create JWK, JWKS, and JWT
|
|
|
|
const jwk = await jose.exportJWK(key.publicKey)
|
|
|
|
const jwks = {keys: [jwk]}
|
|
|
|
const jwt = await new jose.SignJWT(claims)
|
|
|
|
.setProtectedHeader({alg: 'PS256'})
|
|
|
|
.sign(key.privateKey)
|
|
|
|
|
|
|
|
// Mock OpenID configuration and JWKS endpoints
|
|
|
|
nock(issuer)
|
|
|
|
.get('/.well-known/openid-configuration')
|
|
|
|
.reply(200, {jwks_uri: `${issuer}${jwksPath}`})
|
|
|
|
nock(issuer).get(jwksPath).reply(200, jwks)
|
|
|
|
|
|
|
|
// Mock OIDC token endpoint for populating the provenance
|
|
|
|
nock(issuer).get(tokenPath).query({audience}).reply(200, {value: jwt})
|
2024-10-30 13:02:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
process.env = {
|
|
|
|
...originalEnv,
|
|
|
|
ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`,
|
|
|
|
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token',
|
|
|
|
GITHUB_SERVER_URL: 'https://foo.ghe.com',
|
|
|
|
GITHUB_REPOSITORY: claims.repository
|
|
|
|
}
|
|
|
|
|
|
|
|
await mockIssuer(claims)
|
2024-02-18 03:14:10 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
process.env = originalEnv
|
|
|
|
})
|
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
describe('buildSLSAProvenancePredicate', () => {
|
|
|
|
it('returns a provenance hydrated from an OIDC token', async () => {
|
2024-08-16 17:04:14 +00:00
|
|
|
const predicate = await buildSLSAProvenancePredicate()
|
2024-03-22 02:25:36 +00:00
|
|
|
expect(predicate).toMatchSnapshot()
|
2024-02-18 03:14:10 +00:00
|
|
|
})
|
2024-10-30 13:02:29 +00:00
|
|
|
|
|
|
|
it('handle tags including "@" character', async () => {
|
|
|
|
nock.cleanAll()
|
|
|
|
await mockIssuer({
|
|
|
|
...claims,
|
|
|
|
workflow_ref: 'owner/repo/.github/workflows/main.yml@foo@1.0.0'
|
|
|
|
})
|
|
|
|
const predicate = await buildSLSAProvenancePredicate()
|
|
|
|
expect(predicate).toMatchSnapshot()
|
|
|
|
})
|
2024-03-22 02:25:36 +00:00
|
|
|
})
|
2024-02-18 03:14:10 +00:00
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
describe('attestProvenance', () => {
|
|
|
|
// Subject to attest
|
|
|
|
const subjectName = 'subjective'
|
|
|
|
const subjectDigest = {
|
|
|
|
sha256: '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
|
|
|
|
}
|
2024-02-18 03:14:10 +00:00
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
// Fake an OIDC token
|
|
|
|
const oidcPayload = {sub: 'foo@bar.com', iss: ''}
|
|
|
|
const oidcToken = `.${Buffer.from(JSON.stringify(oidcPayload)).toString(
|
|
|
|
'base64'
|
|
|
|
)}.}`
|
|
|
|
|
|
|
|
const attestationID = '1234567890'
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
nock(issuer)
|
|
|
|
.get(tokenPath)
|
|
|
|
.query({audience: 'sigstore'})
|
|
|
|
.reply(200, {value: oidcToken})
|
2024-02-18 03:14:10 +00:00
|
|
|
})
|
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
describe('when using the github Sigstore instance', () => {
|
|
|
|
beforeEach(async () => {
|
2024-08-16 17:04:14 +00:00
|
|
|
const {fulcioURL, tsaServerURL} = signingEndpoints('github')
|
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
// Mock Sigstore
|
|
|
|
await mockFulcio({baseURL: fulcioURL, strict: false})
|
|
|
|
await mockTSA({baseURL: tsaServerURL})
|
2024-02-18 03:14:10 +00:00
|
|
|
|
2024-04-10 23:53:17 +00:00
|
|
|
mockAgent
|
|
|
|
.get('https://api.github.com')
|
|
|
|
.intercept({
|
|
|
|
path: /^\/repos\/.*\/.*\/attestations$/,
|
|
|
|
method: 'post'
|
|
|
|
})
|
2024-03-22 02:25:36 +00:00
|
|
|
.reply(201, {id: attestationID})
|
2024-02-18 03:14:10 +00:00
|
|
|
})
|
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
describe('when the sigstore instance is explicitly set', () => {
|
|
|
|
it('attests provenance', async () => {
|
|
|
|
const attestation = await attestProvenance({
|
|
|
|
subjectName,
|
|
|
|
subjectDigest,
|
|
|
|
token: 'token',
|
2024-08-16 17:04:14 +00:00
|
|
|
sigstore: 'github'
|
2024-03-22 02:25:36 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
expect(attestation).toBeDefined()
|
|
|
|
expect(attestation.bundle).toBeDefined()
|
|
|
|
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
|
|
|
|
expect(attestation.tlogID).toBeUndefined()
|
|
|
|
expect(attestation.attestationID).toBe(attestationID)
|
|
|
|
})
|
2024-02-18 03:14:10 +00:00
|
|
|
})
|
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
describe('when the sigstore instance is inferred from the repo visibility', () => {
|
|
|
|
const savedRepository = github.context.payload.repository
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
|
|
github.context.payload.repository = {visibility: 'private'} as any
|
2024-02-18 03:14:10 +00:00
|
|
|
})
|
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
afterEach(() => {
|
|
|
|
github.context.payload.repository = savedRepository
|
|
|
|
})
|
|
|
|
|
|
|
|
it('attests provenance', async () => {
|
|
|
|
const attestation = await attestProvenance({
|
|
|
|
subjectName,
|
|
|
|
subjectDigest,
|
2024-08-16 17:04:14 +00:00
|
|
|
token: 'token'
|
2024-03-22 02:25:36 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
expect(attestation).toBeDefined()
|
|
|
|
expect(attestation.bundle).toBeDefined()
|
|
|
|
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
|
|
|
|
expect(attestation.tlogID).toBeUndefined()
|
|
|
|
expect(attestation.attestationID).toBe(attestationID)
|
|
|
|
})
|
2024-02-18 03:14:10 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
describe('when using the public-good Sigstore instance', () => {
|
|
|
|
const {fulcioURL, rekorURL} = SIGSTORE_PUBLIC_GOOD
|
2024-02-18 03:14:10 +00:00
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
beforeEach(async () => {
|
|
|
|
// Mock Sigstore
|
|
|
|
await mockFulcio({baseURL: fulcioURL, strict: false})
|
|
|
|
await mockRekor({baseURL: rekorURL})
|
2024-02-18 03:14:10 +00:00
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
// Mock GH attestations API
|
2024-04-10 23:53:17 +00:00
|
|
|
mockAgent
|
|
|
|
.get('https://api.github.com')
|
|
|
|
.intercept({
|
|
|
|
path: /^\/repos\/.*\/.*\/attestations$/,
|
|
|
|
method: 'post'
|
|
|
|
})
|
2024-03-22 02:25:36 +00:00
|
|
|
.reply(201, {id: attestationID})
|
|
|
|
})
|
2024-02-18 03:14:10 +00:00
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
describe('when the sigstore instance is explicitly set', () => {
|
|
|
|
it('attests provenance', async () => {
|
|
|
|
const attestation = await attestProvenance({
|
|
|
|
subjectName,
|
|
|
|
subjectDigest,
|
|
|
|
token: 'token',
|
2024-08-16 17:04:14 +00:00
|
|
|
sigstore: 'public-good'
|
2024-03-22 02:25:36 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
expect(attestation).toBeDefined()
|
|
|
|
expect(attestation.bundle).toBeDefined()
|
|
|
|
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
|
|
|
|
expect(attestation.tlogID).toBeDefined()
|
|
|
|
expect(attestation.attestationID).toBe(attestationID)
|
|
|
|
})
|
2024-02-18 03:14:10 +00:00
|
|
|
})
|
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
describe('when the sigstore instance is inferred from the repo visibility', () => {
|
|
|
|
const savedRepository = github.context.payload.repository
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
|
|
github.context.payload.repository = {visibility: 'public'} as any
|
|
|
|
})
|
2024-02-18 03:14:10 +00:00
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
afterEach(() => {
|
|
|
|
github.context.payload.repository = savedRepository
|
|
|
|
})
|
|
|
|
|
|
|
|
it('attests provenance', async () => {
|
|
|
|
const attestation = await attestProvenance({
|
|
|
|
subjectName,
|
|
|
|
subjectDigest,
|
2024-08-16 17:04:14 +00:00
|
|
|
token: 'token'
|
2024-03-22 02:25:36 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
expect(attestation).toBeDefined()
|
|
|
|
expect(attestation.bundle).toBeDefined()
|
|
|
|
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
|
|
|
|
expect(attestation.tlogID).toBeDefined()
|
|
|
|
expect(attestation.attestationID).toBe(attestationID)
|
|
|
|
})
|
2024-02-18 03:14:10 +00:00
|
|
|
})
|
2024-03-22 02:25:36 +00:00
|
|
|
})
|
2024-02-18 03:14:10 +00:00
|
|
|
|
2024-03-22 02:25:36 +00:00
|
|
|
describe('when skipWrite is set to true', () => {
|
|
|
|
const {fulcioURL, rekorURL} = SIGSTORE_PUBLIC_GOOD
|
|
|
|
beforeEach(async () => {
|
|
|
|
// Mock Sigstore
|
|
|
|
await mockFulcio({baseURL: fulcioURL, strict: false})
|
|
|
|
await mockRekor({baseURL: rekorURL})
|
2024-02-18 03:14:10 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
it('attests provenance', async () => {
|
|
|
|
const attestation = await attestProvenance({
|
|
|
|
subjectName,
|
|
|
|
subjectDigest,
|
2024-03-22 02:25:36 +00:00
|
|
|
token: 'token',
|
|
|
|
sigstore: 'public-good',
|
2024-08-16 17:04:14 +00:00
|
|
|
skipWrite: true
|
2024-02-18 03:14:10 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
expect(attestation).toBeDefined()
|
|
|
|
expect(attestation.bundle).toBeDefined()
|
|
|
|
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
|
|
|
|
expect(attestation.tlogID).toBeDefined()
|
2024-03-22 02:25:36 +00:00
|
|
|
expect(attestation.attestationID).toBeUndefined()
|
2024-02-18 03:14:10 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|