From 40a094b8c6f1a7cd896953cbe8d15dd7515256e2 Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Thu, 8 Aug 2024 15:44:39 -0700 Subject: [PATCH] dynamic construction of oidc issuer Signed-off-by: Brian DeHamer --- __tests__/__snapshots__/main.test.ts.snap | 41 ++++- __tests__/main.test.ts | 183 +++++++++++++++------- dist/index.js | 20 ++- package-lock.json | 4 +- package.json | 2 +- src/main.ts | 27 +++- 6 files changed, 214 insertions(+), 63 deletions(-) diff --git a/__tests__/__snapshots__/main.test.ts.snap b/__tests__/__snapshots__/main.test.ts.snap index bf3722e0..545ffc93 100644 --- a/__tests__/__snapshots__/main.test.ts.snap +++ b/__tests__/__snapshots__/main.test.ts.snap @@ -1,6 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`main successfully run main 1`] = ` +exports[`main when a non-default OIDC issuer is used successfully run main 1`] = ` +{ + "buildDefinition": { + "buildType": "https://actions.github.io/buildtypes/workflow/v1", + "externalParameters": { + "workflow": { + "path": ".github/workflows/main.yml", + "ref": "main", + "repository": "https://example-01.ghe.com/owner/repo", + }, + }, + "internalParameters": { + "github": { + "event_name": "push", + "repository_id": "repo-id", + "repository_owner_id": "owner-id", + "runner_environment": "github-hosted", + }, + }, + "resolvedDependencies": [ + { + "digest": { + "gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b", + }, + "uri": "git+https://example-01.ghe.com/owner/repo@refs/heads/main", + }, + ], + }, + "runDetails": { + "builder": { + "id": "https://example-01.ghe.com/owner/shared/.github/workflows/build.yml@main", + }, + "metadata": { + "invocationId": "https://example-01.ghe.com/owner/repo/actions/runs/run-id/attempts/run-attempt", + }, + }, +} +`; + +exports[`main when the default OIDC issuer is used successfully run main 1`] = ` { "buildDefinition": { "buildType": "https://actions.github.io/buildtypes/workflow/v1", diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index b9e7ee0d..e437044b 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -13,61 +13,13 @@ setFailedMock.mockImplementation(() => {}) describe('main', () => { let outputs = {} as Record const originalEnv = process.env - const issuer = 'https://token.actions.githubusercontent.com' - const audience = 'nobody' - const jwksPath = '/.well-known/jwks.json' - const tokenPath = '/token' - - const claims = { - iss: issuer, - aud: 'nobody', - repository: 'owner/repo', - ref: 'refs/heads/main', - sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b', - workflow_ref: 'owner/repo/.github/workflows/main.yml@main', - job_workflow_ref: 'owner/shared/.github/workflows/build.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' - } - - beforeEach(async () => { + + beforeEach(() => { jest.resetAllMocks() setOutputMock.mockImplementation((key, value) => { outputs[key] = value }) - - process.env = { - ...originalEnv, - ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`, - ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token', - GITHUB_SERVER_URL: 'https://github.com', - GITHUB_REPOSITORY: claims.repository - } - - // Generate JWT signing key - const key = await jose.generateKeyPair('PS256') - - // Create JWK, JWKS, and JWT - const kid = '12345' - const jwk = await jose.exportJWK(key.publicKey) - const jwks = { keys: [{ ...jwk, kid }] } - const jwt = await new jose.SignJWT(claims) - .setProtectedHeader({ alg: 'PS256', kid }) - .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 }) }) afterEach(() => { @@ -75,14 +27,131 @@ describe('main', () => { process.env = originalEnv }) - it('successfully run main', async () => { - // Run the main function - await main.run() + describe('when the default OIDC issuer is used', () => { + const issuer = 'https://token.actions.githubusercontent.com' + const audience = 'nobody' + const jwksPath = '/.well-known/jwks.json' + const tokenPath = '/token' + + const claims = { + iss: issuer, + aud: 'nobody', + repository: 'owner/repo', + ref: 'refs/heads/main', + sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b', + workflow_ref: 'owner/repo/.github/workflows/main.yml@main', + job_workflow_ref: 'owner/shared/.github/workflows/build.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' + } + + beforeEach(async () => { + process.env = { + ...originalEnv, + ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`, + ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token', + GITHUB_SERVER_URL: 'https://github.com', + GITHUB_REPOSITORY: claims.repository + } + + // Generate JWT signing key + const key = await jose.generateKeyPair('PS256') + + // Create JWK, JWKS, and JWT + const kid = '12345' + const jwk = await jose.exportJWK(key.publicKey) + const jwks = { keys: [{ ...jwk, kid }] } + const jwt = await new jose.SignJWT(claims) + .setProtectedHeader({ alg: 'PS256', kid }) + .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 }) + }) + + it('successfully run main', async () => { + // Run the main function + await main.run() + + // Verify that outputs were set correctly + expect(setOutputMock).toHaveBeenCalledTimes(2) + + expect(outputs['predicate']).toMatchSnapshot() + expect(outputs['predicate-type']).toBe('https://slsa.dev/provenance/v1') + }) + }) + + describe('when a non-default OIDC issuer is used', () => { + const issuer = 'https://token.actions.example-01.ghe.com' + const audience = 'nobody' + const jwksPath = '/.well-known/jwks.json' + const tokenPath = '/token' + + const claims = { + iss: issuer, + aud: 'nobody', + repository: 'owner/repo', + ref: 'refs/heads/main', + sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b', + workflow_ref: 'owner/repo/.github/workflows/main.yml@main', + job_workflow_ref: 'owner/shared/.github/workflows/build.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' + } + + beforeEach(async () => { + process.env = { + ...originalEnv, + ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`, + ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token', + GITHUB_SERVER_URL: 'https://example-01.ghe.com', + GITHUB_REPOSITORY: claims.repository + } - // Verify that outputs were set correctly - expect(setOutputMock).toHaveBeenCalledTimes(2) + // Generate JWT signing key + const key = await jose.generateKeyPair('PS256') - expect(outputs['predicate']).toMatchSnapshot() - expect(outputs['predicate-type']).toBe('https://slsa.dev/provenance/v1') + // Create JWK, JWKS, and JWT + const kid = '12345' + const jwk = await jose.exportJWK(key.publicKey) + const jwks = { keys: [{ ...jwk, kid }] } + const jwt = await new jose.SignJWT(claims) + .setProtectedHeader({ alg: 'PS256', kid }) + .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 }) + }) + + it('successfully run main', async () => { + // Run the main function + await main.run() + + // Verify that outputs were set correctly + expect(setOutputMock).toHaveBeenCalledTimes(2) + + expect(outputs['predicate']).toMatchSnapshot() + expect(outputs['predicate-type']).toBe('https://slsa.dev/provenance/v1') + }) }) }) diff --git a/dist/index.js b/dist/index.js index 811e50bb..3e3c0d69 100644 --- a/dist/index.js +++ b/dist/index.js @@ -68862,14 +68862,19 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.run = run; const attest_1 = __nccwpck_require__(74113); const core = __importStar(__nccwpck_require__(42186)); +const VALID_SERVER_URLS = [ + 'https://github.com', + new RegExp('^https://[a-z0-9-]+\\.ghe\\.com$') +]; /** * The main function for the action. * @returns {Promise} Resolves when the action is complete. */ async function run() { try { + const issuer = getIssuer(); // Calculate subject from inputs and generate provenance - const predicate = await (0, attest_1.buildSLSAProvenancePredicate)(); + const predicate = await (0, attest_1.buildSLSAProvenancePredicate)(issuer); core.setOutput('predicate', predicate.params); core.setOutput('predicate-type', predicate.type); } @@ -68879,6 +68884,19 @@ async function run() { core.setFailed(error.message); } } +// Derive the current OIDC issuer based on the server URL +function getIssuer() { + 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/package-lock.json b/package-lock.json index 8a36bb0c..ee2cd8c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "actions/attest-build-provenance", - "version": "1.1.1", + "version": "1.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "actions/attest-build-provenance", - "version": "1.1.1", + "version": "1.1.2", "license": "MIT", "dependencies": { "@actions/attest": "^1.3.1", diff --git a/package.json b/package.json index e7973114..97761bf0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "actions/attest-build-provenance", "description": "Generate signed build provenance attestations", - "version": "1.1.1", + "version": "1.1.2", "author": "", "private": true, "homepage": "https://github.com/actions/attest-build-provenance", diff --git a/src/main.ts b/src/main.ts index 0b1f21ae..f623157e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,21 @@ import { buildSLSAProvenancePredicate } from '@actions/attest' import * as core from '@actions/core' +const VALID_SERVER_URLS = [ + 'https://github.com', + new RegExp('^https://[a-z0-9-]+\\.ghe\\.com$') +] as const + /** * The main function for the action. * @returns {Promise} Resolves when the action is complete. */ export async function run(): Promise { try { + const issuer = getIssuer() + // Calculate subject from inputs and generate provenance - const predicate = await buildSLSAProvenancePredicate() + const predicate = await buildSLSAProvenancePredicate(issuer) core.setOutput('predicate', predicate.params) core.setOutput('predicate-type', predicate.type) @@ -18,3 +25,21 @@ export async function run(): Promise { core.setFailed(error.message) } } + +// 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}` +}