Skip to content

Commit d58ddf9

Browse files
authored
dynamic construction of oidc issuer (#195)
Signed-off-by: Brian DeHamer <[email protected]>
1 parent f9d4126 commit d58ddf9

File tree

6 files changed

+214
-63
lines changed

6 files changed

+214
-63
lines changed

__tests__/__snapshots__/main.test.ts.snap

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,45 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`main successfully run main 1`] = `
3+
exports[`main when a non-default OIDC issuer is used successfully run main 1`] = `
4+
{
5+
"buildDefinition": {
6+
"buildType": "https://actions.github.io/buildtypes/workflow/v1",
7+
"externalParameters": {
8+
"workflow": {
9+
"path": ".github/workflows/main.yml",
10+
"ref": "main",
11+
"repository": "https://example-01.ghe.com/owner/repo",
12+
},
13+
},
14+
"internalParameters": {
15+
"github": {
16+
"event_name": "push",
17+
"repository_id": "repo-id",
18+
"repository_owner_id": "owner-id",
19+
"runner_environment": "github-hosted",
20+
},
21+
},
22+
"resolvedDependencies": [
23+
{
24+
"digest": {
25+
"gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b",
26+
},
27+
"uri": "git+https://example-01.ghe.com/owner/repo@refs/heads/main",
28+
},
29+
],
30+
},
31+
"runDetails": {
32+
"builder": {
33+
"id": "https://example-01.ghe.com/owner/shared/.github/workflows/build.yml@main",
34+
},
35+
"metadata": {
36+
"invocationId": "https://example-01.ghe.com/owner/repo/actions/runs/run-id/attempts/run-attempt",
37+
},
38+
},
39+
}
40+
`;
41+
42+
exports[`main when the default OIDC issuer is used successfully run main 1`] = `
443
{
544
"buildDefinition": {
645
"buildType": "https://actions.github.io/buildtypes/workflow/v1",

__tests__/main.test.ts

Lines changed: 126 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,76 +13,145 @@ setFailedMock.mockImplementation(() => {})
1313
describe('main', () => {
1414
let outputs = {} as Record<string, string>
1515
const originalEnv = process.env
16-
const issuer = 'https://token.actions.githubusercontent.com'
17-
const audience = 'nobody'
18-
const jwksPath = '/.well-known/jwks.json'
19-
const tokenPath = '/token'
20-
21-
const claims = {
22-
iss: issuer,
23-
aud: 'nobody',
24-
repository: 'owner/repo',
25-
ref: 'refs/heads/main',
26-
sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
27-
workflow_ref: 'owner/repo/.github/workflows/main.yml@main',
28-
job_workflow_ref: 'owner/shared/.github/workflows/build.yml@main',
29-
event_name: 'push',
30-
repository_id: 'repo-id',
31-
repository_owner_id: 'owner-id',
32-
run_id: 'run-id',
33-
run_attempt: 'run-attempt',
34-
runner_environment: 'github-hosted'
35-
}
36-
37-
beforeEach(async () => {
16+
17+
beforeEach(() => {
3818
jest.resetAllMocks()
3919

4020
setOutputMock.mockImplementation((key, value) => {
4121
outputs[key] = value
4222
})
43-
44-
process.env = {
45-
...originalEnv,
46-
ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`,
47-
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token',
48-
GITHUB_SERVER_URL: 'https://github.com',
49-
GITHUB_REPOSITORY: claims.repository
50-
}
51-
52-
// Generate JWT signing key
53-
const key = await jose.generateKeyPair('PS256')
54-
55-
// Create JWK, JWKS, and JWT
56-
const kid = '12345'
57-
const jwk = await jose.exportJWK(key.publicKey)
58-
const jwks = { keys: [{ ...jwk, kid }] }
59-
const jwt = await new jose.SignJWT(claims)
60-
.setProtectedHeader({ alg: 'PS256', kid })
61-
.sign(key.privateKey)
62-
63-
// Mock OpenID configuration and JWKS endpoints
64-
nock(issuer)
65-
.get('/.well-known/openid-configuration')
66-
.reply(200, { jwks_uri: `${issuer}${jwksPath}` })
67-
nock(issuer).get(jwksPath).reply(200, jwks)
68-
69-
// Mock OIDC token endpoint for populating the provenance
70-
nock(issuer).get(tokenPath).query({ audience }).reply(200, { value: jwt })
7123
})
7224

7325
afterEach(() => {
7426
outputs = {}
7527
process.env = originalEnv
7628
})
7729

78-
it('successfully run main', async () => {
79-
// Run the main function
80-
await main.run()
30+
describe('when the default OIDC issuer is used', () => {
31+
const issuer = 'https://token.actions.githubusercontent.com'
32+
const audience = 'nobody'
33+
const jwksPath = '/.well-known/jwks.json'
34+
const tokenPath = '/token'
35+
36+
const claims = {
37+
iss: issuer,
38+
aud: 'nobody',
39+
repository: 'owner/repo',
40+
ref: 'refs/heads/main',
41+
sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
42+
workflow_ref: 'owner/repo/.github/workflows/main.yml@main',
43+
job_workflow_ref: 'owner/shared/.github/workflows/build.yml@main',
44+
event_name: 'push',
45+
repository_id: 'repo-id',
46+
repository_owner_id: 'owner-id',
47+
run_id: 'run-id',
48+
run_attempt: 'run-attempt',
49+
runner_environment: 'github-hosted'
50+
}
51+
52+
beforeEach(async () => {
53+
process.env = {
54+
...originalEnv,
55+
ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`,
56+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token',
57+
GITHUB_SERVER_URL: 'https://github.com',
58+
GITHUB_REPOSITORY: claims.repository
59+
}
60+
61+
// Generate JWT signing key
62+
const key = await jose.generateKeyPair('PS256')
63+
64+
// Create JWK, JWKS, and JWT
65+
const kid = '12345'
66+
const jwk = await jose.exportJWK(key.publicKey)
67+
const jwks = { keys: [{ ...jwk, kid }] }
68+
const jwt = await new jose.SignJWT(claims)
69+
.setProtectedHeader({ alg: 'PS256', kid })
70+
.sign(key.privateKey)
71+
72+
// Mock OpenID configuration and JWKS endpoints
73+
nock(issuer)
74+
.get('/.well-known/openid-configuration')
75+
.reply(200, { jwks_uri: `${issuer}${jwksPath}` })
76+
nock(issuer).get(jwksPath).reply(200, jwks)
77+
78+
// Mock OIDC token endpoint for populating the provenance
79+
nock(issuer).get(tokenPath).query({ audience }).reply(200, { value: jwt })
80+
})
81+
82+
it('successfully run main', async () => {
83+
// Run the main function
84+
await main.run()
85+
86+
// Verify that outputs were set correctly
87+
expect(setOutputMock).toHaveBeenCalledTimes(2)
88+
89+
expect(outputs['predicate']).toMatchSnapshot()
90+
expect(outputs['predicate-type']).toBe('https://slsa.dev/provenance/v1')
91+
})
92+
})
93+
94+
describe('when a non-default OIDC issuer is used', () => {
95+
const issuer = 'https://token.actions.example-01.ghe.com'
96+
const audience = 'nobody'
97+
const jwksPath = '/.well-known/jwks.json'
98+
const tokenPath = '/token'
99+
100+
const claims = {
101+
iss: issuer,
102+
aud: 'nobody',
103+
repository: 'owner/repo',
104+
ref: 'refs/heads/main',
105+
sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
106+
workflow_ref: 'owner/repo/.github/workflows/main.yml@main',
107+
job_workflow_ref: 'owner/shared/.github/workflows/build.yml@main',
108+
event_name: 'push',
109+
repository_id: 'repo-id',
110+
repository_owner_id: 'owner-id',
111+
run_id: 'run-id',
112+
run_attempt: 'run-attempt',
113+
runner_environment: 'github-hosted'
114+
}
115+
116+
beforeEach(async () => {
117+
process.env = {
118+
...originalEnv,
119+
ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`,
120+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token',
121+
GITHUB_SERVER_URL: 'https://example-01.ghe.com',
122+
GITHUB_REPOSITORY: claims.repository
123+
}
81124

82-
// Verify that outputs were set correctly
83-
expect(setOutputMock).toHaveBeenCalledTimes(2)
125+
// Generate JWT signing key
126+
const key = await jose.generateKeyPair('PS256')
84127

85-
expect(outputs['predicate']).toMatchSnapshot()
86-
expect(outputs['predicate-type']).toBe('https://slsa.dev/provenance/v1')
128+
// Create JWK, JWKS, and JWT
129+
const kid = '12345'
130+
const jwk = await jose.exportJWK(key.publicKey)
131+
const jwks = { keys: [{ ...jwk, kid }] }
132+
const jwt = await new jose.SignJWT(claims)
133+
.setProtectedHeader({ alg: 'PS256', kid })
134+
.sign(key.privateKey)
135+
136+
// Mock OpenID configuration and JWKS endpoints
137+
nock(issuer)
138+
.get('/.well-known/openid-configuration')
139+
.reply(200, { jwks_uri: `${issuer}${jwksPath}` })
140+
nock(issuer).get(jwksPath).reply(200, jwks)
141+
142+
// Mock OIDC token endpoint for populating the provenance
143+
nock(issuer).get(tokenPath).query({ audience }).reply(200, { value: jwt })
144+
})
145+
146+
it('successfully run main', async () => {
147+
// Run the main function
148+
await main.run()
149+
150+
// Verify that outputs were set correctly
151+
expect(setOutputMock).toHaveBeenCalledTimes(2)
152+
153+
expect(outputs['predicate']).toMatchSnapshot()
154+
expect(outputs['predicate-type']).toBe('https://slsa.dev/provenance/v1')
155+
})
87156
})
88157
})

dist/index.js

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "actions/attest-build-provenance",
33
"description": "Generate signed build provenance attestations",
4-
"version": "1.1.1",
4+
"version": "1.1.2",
55
"author": "",
66
"private": true,
77
"homepage": "https://github.com/actions/attest-build-provenance",

src/main.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import { buildSLSAProvenancePredicate } from '@actions/attest'
22
import * as core from '@actions/core'
33

4+
const VALID_SERVER_URLS = [
5+
'https://github.com',
6+
new RegExp('^https://[a-z0-9-]+\\.ghe\\.com$')
7+
] as const
8+
49
/**
510
* The main function for the action.
611
* @returns {Promise<void>} Resolves when the action is complete.
712
*/
813
export async function run(): Promise<void> {
914
try {
15+
const issuer = getIssuer()
16+
1017
// Calculate subject from inputs and generate provenance
11-
const predicate = await buildSLSAProvenancePredicate()
18+
const predicate = await buildSLSAProvenancePredicate(issuer)
1219

1320
core.setOutput('predicate', predicate.params)
1421
core.setOutput('predicate-type', predicate.type)
@@ -18,3 +25,21 @@ export async function run(): Promise<void> {
1825
core.setFailed(error.message)
1926
}
2027
}
28+
29+
// Derive the current OIDC issuer based on the server URL
30+
function getIssuer(): string {
31+
const serverURL = process.env.GITHUB_SERVER_URL || 'https://github.com'
32+
33+
// Ensure the server URL is a valid GitHub server URL
34+
if (!VALID_SERVER_URLS.some(valid_url => serverURL.match(valid_url))) {
35+
throw new Error(`Invalid server URL: ${serverURL}`)
36+
}
37+
38+
let host = new URL(serverURL).hostname
39+
40+
if (host === 'github.com') {
41+
host = 'githubusercontent.com'
42+
}
43+
44+
return `https://token.actions.${host}`
45+
}

0 commit comments

Comments
 (0)