diff --git a/packages/attest/RELEASES.md b/packages/attest/RELEASES.md index 90be9d8e..9b68907f 100644 --- a/packages/attest/RELEASES.md +++ b/packages/attest/RELEASES.md @@ -3,6 +3,7 @@ ### 1.4.0 - Add new `headers` parameter to the `attest` and `attestProvenance` functions. +- Update `buildSLSAProvenancePredicate`/`attestProvenance` to automatically derive default OIDC issuer URL from current execution context. ### 1.3.1 diff --git a/packages/attest/__tests__/__snapshots__/provenance.test.ts.snap b/packages/attest/__tests__/__snapshots__/provenance.test.ts.snap index 39a57c22..4c199dae 100644 --- a/packages/attest/__tests__/__snapshots__/provenance.test.ts.snap +++ b/packages/attest/__tests__/__snapshots__/provenance.test.ts.snap @@ -9,7 +9,7 @@ exports[`provenance functions buildSLSAProvenancePredicate returns a provenance "workflow": { "path": ".github/workflows/main.yml", "ref": "main", - "repository": "https://github.com/owner/repo", + "repository": "https://foo.ghe.com/owner/repo", }, }, "internalParameters": { @@ -25,16 +25,16 @@ exports[`provenance functions buildSLSAProvenancePredicate returns a provenance "digest": { "gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b", }, - "uri": "git+https://github.com/owner/repo@refs/heads/main", + "uri": "git+https://foo.ghe.com/owner/repo@refs/heads/main", }, ], }, "runDetails": { "builder": { - "id": "https://github.com/owner/workflows/.github/workflows/publish.yml@main", + "id": "https://foo.ghe.com/owner/workflows/.github/workflows/publish.yml@main", }, "metadata": { - "invocationId": "https://github.com/owner/repo/actions/runs/run-id/attempts/run-attempt", + "invocationId": "https://foo.ghe.com/owner/repo/actions/runs/run-id/attempts/run-attempt", }, }, }, diff --git a/packages/attest/__tests__/provenance.test.ts b/packages/attest/__tests__/provenance.test.ts index 3d61fff9..4dbfef58 100644 --- a/packages/attest/__tests__/provenance.test.ts +++ b/packages/attest/__tests__/provenance.test.ts @@ -8,7 +8,7 @@ import {attestProvenance, buildSLSAProvenancePredicate} from '../src/provenance' describe('provenance functions', () => { const originalEnv = process.env - const issuer = 'https://example.com' + const issuer = 'https://token.actions.foo.ghe.com' const audience = 'nobody' const jwksPath = '/.well-known/jwks.json' const tokenPath = '/token' @@ -38,7 +38,7 @@ describe('provenance functions', () => { ...originalEnv, ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`, ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token', - GITHUB_SERVER_URL: 'https://github.com', + GITHUB_SERVER_URL: 'https://foo.ghe.com', GITHUB_REPOSITORY: claims.repository } @@ -68,7 +68,7 @@ describe('provenance functions', () => { describe('buildSLSAProvenancePredicate', () => { it('returns a provenance hydrated from an OIDC token', async () => { - const predicate = await buildSLSAProvenancePredicate(issuer) + const predicate = await buildSLSAProvenancePredicate() expect(predicate).toMatchSnapshot() }) }) @@ -96,9 +96,9 @@ describe('provenance functions', () => { }) describe('when using the github Sigstore instance', () => { - const {fulcioURL, tsaServerURL} = signingEndpoints('github') - beforeEach(async () => { + const {fulcioURL, tsaServerURL} = signingEndpoints('github') + // Mock Sigstore await mockFulcio({baseURL: fulcioURL, strict: false}) await mockTSA({baseURL: tsaServerURL}) @@ -118,8 +118,7 @@ describe('provenance functions', () => { subjectName, subjectDigest, token: 'token', - sigstore: 'github', - issuer + sigstore: 'github' }) expect(attestation).toBeDefined() @@ -146,8 +145,7 @@ describe('provenance functions', () => { const attestation = await attestProvenance({ subjectName, subjectDigest, - token: 'token', - issuer + token: 'token' }) expect(attestation).toBeDefined() @@ -183,8 +181,7 @@ describe('provenance functions', () => { subjectName, subjectDigest, token: 'token', - sigstore: 'public-good', - issuer + sigstore: 'public-good' }) expect(attestation).toBeDefined() @@ -211,8 +208,7 @@ describe('provenance functions', () => { const attestation = await attestProvenance({ subjectName, subjectDigest, - token: 'token', - issuer + token: 'token' }) expect(attestation).toBeDefined() @@ -238,8 +234,7 @@ describe('provenance functions', () => { subjectDigest, token: 'token', sigstore: 'public-good', - skipWrite: true, - issuer + skipWrite: true }) expect(attestation).toBeDefined() diff --git a/packages/attest/src/oidc.ts b/packages/attest/src/oidc.ts index 69e18d97..f855469c 100644 --- a/packages/attest/src/oidc.ts +++ b/packages/attest/src/oidc.ts @@ -4,6 +4,11 @@ import * as jose from 'jose' const OIDC_AUDIENCE = 'nobody' +const VALID_SERVER_URLS = [ + 'https://github.com', + new RegExp('^https://[a-z0-9-]+\\.ghe\\.com$') +] as const + const REQUIRED_CLAIMS = [ 'iss', 'ref', @@ -25,7 +30,8 @@ type OIDCConfig = { jwks_uri: string } -export const getIDTokenClaims = async (issuer: string): Promise => { +export const getIDTokenClaims = async (issuer?: string): Promise => { + issuer = issuer || getIssuer() try { const token = await getIDToken(OIDC_AUDIENCE) const claims = await decodeOIDCToken(token, issuer) @@ -82,3 +88,21 @@ function assertClaimSet(claims: jose.JWTPayload): asserts claims is ClaimSet { throw new Error(`Missing claims: ${missingClaims.join(', ')}`) } } + +// Derive the current OIDC issuer based on the server URL +function getIssuer(): string { + const serverURL = process.env.GITHUB_SERVER_URL || 'https://github.com' + + // Ensure the server URL is a valid GitHub server URL + if (!VALID_SERVER_URLS.some(valid_url => serverURL.match(valid_url))) { + throw new Error(`Invalid server URL: ${serverURL}`) + } + + let host = new URL(serverURL).hostname + + if (host === 'github.com') { + host = 'githubusercontent.com' + } + + return `https://token.actions.${host}` +} diff --git a/packages/attest/src/provenance.ts b/packages/attest/src/provenance.ts index 0ef89e01..09aa64f7 100644 --- a/packages/attest/src/provenance.ts +++ b/packages/attest/src/provenance.ts @@ -5,8 +5,6 @@ import type {Attestation, Predicate} from './shared.types' const SLSA_PREDICATE_V1_TYPE = 'https://slsa.dev/provenance/v1' const GITHUB_BUILD_TYPE = 'https://actions.github.io/buildtypes/workflow/v1' -const DEFAULT_ISSUER = 'https://token.actions.githubusercontent.com' - export type AttestProvenanceOptions = Omit< AttestOptions, 'predicate' | 'predicateType' @@ -24,7 +22,7 @@ export type AttestProvenanceOptions = Omit< * @returns The SLSA provenance predicate. */ export const buildSLSAProvenancePredicate = async ( - issuer: string = DEFAULT_ISSUER + issuer?: string ): Promise => { const serverURL = process.env.GITHUB_SERVER_URL const claims = await getIDTokenClaims(issuer)