@@ -18,14 +18,33 @@ import assert from 'assert';
1818import { generateKeyPairSync } from 'crypto' ;
1919import * as jose from 'jose' ;
2020import type { Handler , HandlerFn , HandlerFnResult } from '../shared.types' ;
21- import type { CA } from './ca' ;
21+ import type { CA , ExtensionValue } from './ca' ;
2222
2323const CREATE_SIGNING_CERT_PATH = '/api/v2/signingCert' ;
2424const DEFAULT_SUBJECT = 'NO-SUBJECT' ;
2525const DEFAULT_ISSUER = 'https://fake.oidcissuer.com' ;
2626
2727const ISSUER_EXT_OID_V1 = '1.3.6.1.4.1.57264.1.1' ;
28+ const GH_WORKFLOW_TRIGGER_EXT_OID = '1.3.6.1.4.1.57264.1.2' ;
29+ const GH_WORKFLOW_SHA_EXT_OID = '1.3.6.1.4.1.57264.1.3' ;
30+ const GH_WORKFLOW_NAME_EXT_OID = '1.3.6.1.4.1.57264.1.4' ;
31+ const GH_WORKFLOW_REPO_EXT_OID = '1.3.6.1.4.1.57264.1.5' ;
32+ const GH_WORKFLOW_REF_EXT_OID = '1.3.6.1.4.1.57264.1.6' ;
2833const ISSUER_EXT_OID_V2 = '1.3.6.1.4.1.57264.1.8' ;
34+ const BUILD_SIGNER_URI_EXT_OID = '1.3.6.1.4.1.57264.1.9' ;
35+ const BUILD_SIGNER_DIGEST_EXT_OID = '1.3.6.1.4.1.57264.1.10' ;
36+ const RUNNER_ENVIRONMENT_EXT_OID = '1.3.6.1.4.1.57264.1.11' ;
37+ const SOURCE_REPO_URI_EXT_OID = '1.3.6.1.4.1.57264.1.12' ;
38+ const SOURCE_REPO_DIGEST_EXT_OID = '1.3.6.1.4.1.57264.1.13' ;
39+ const SOURCE_REPO_REF_EXT_OID = '1.3.6.1.4.1.57264.1.14' ;
40+ const SOURCE_REPO_ID_EXT_OID = '1.3.6.1.4.1.57264.1.15' ;
41+ const SOURCE_REPO_OWNER_URI_EXT_OID = '1.3.6.1.4.1.57264.1.16' ;
42+ const SOURCE_REPO_OWNER_ID_EXT_OID = '1.3.6.1.4.1.57264.1.17' ;
43+ const BUILD_CONFIG_URI_EXT_OID = '1.3.6.1.4.1.57264.1.18' ;
44+ const BUILD_CONFIG_DIGEST_EXT_OID = '1.3.6.1.4.1.57264.1.19' ;
45+ const BUILD_TRIGGER_EXT_OID = '1.3.6.1.4.1.57264.1.20' ;
46+ const RUN_INVOCATION_URI_EXT_OID = '1.3.6.1.4.1.57264.1.21' ;
47+ const SOURCE_REPO_VISIBILITY_EXT_OID = '1.3.6.1.4.1.57264.1.22' ;
2948
3049interface FulcioHandlerOptions {
3150 strict ?: boolean ;
@@ -52,18 +71,17 @@ function createSigningCertHandler(
5271 return async ( body : string ) : Promise < HandlerFnResult > => {
5372 try {
5473 // Extract relevant fields from the request
55- const { subject, issuer , publicKey } = strict
74+ const { subject, publicKey , claims } = strict
5675 ? parseBody ( body , subjectClaim )
5776 : stubBody ( ) ;
5877
78+ const extensions = extensionFromClaims ( claims ) ;
79+
5980 // Request certificate from CA
6081 const cert = await ca . issueCertificate ( {
6182 publicKey : fromPEM ( publicKey ) ,
6283 subjectAltName : subject ,
63- extensions : [
64- { oid : ISSUER_EXT_OID_V1 , value : issuer , legacy : true } ,
65- { oid : ISSUER_EXT_OID_V2 , value : issuer } ,
66- ] ,
84+ extensions : extensions ,
6785 } ) ;
6886
6987 // Format the response
@@ -80,31 +98,34 @@ function createSigningCertHandler(
8098function parseBody (
8199 body : string ,
82100 subjectClaim : string
83- ) : { subject : string ; issuer : string ; publicKey : string } {
101+ ) : { subject : string ; publicKey : string ; claims : Record < string , string > } {
84102 const json = JSON . parse ( body . toString ( ) ) ;
85103 const oidc = json . credentials . oidcIdentityToken ;
86104 const pem = json . publicKeyRequest . publicKey . content ;
87105
88106 // Decode the JWT
89- /* eslint-disable @typescript-eslint/no-explicit-any */
90- const claims = jose . decodeJwt ( oidc ) as any ;
107+ const claims = jose . decodeJwt ( oidc ) as Record < string , string > ;
91108
92109 /* istanbul ignore next */
93110 return {
94111 subject : claims [ subjectClaim ] || DEFAULT_SUBJECT ,
95- issuer : claims [ 'iss' ] || DEFAULT_ISSUER ,
96112 publicKey : pem ,
113+ claims : { iss : DEFAULT_ISSUER , ...claims } ,
97114 } ;
98115}
99116
100- function stubBody ( ) : { subject : string ; issuer : string ; publicKey : string } {
117+ function stubBody ( ) : {
118+ subject : string ;
119+ publicKey : string ;
120+ claims : Record < string , string > ;
121+ } {
101122 const { publicKey } = generateKeyPairSync ( 'ec' , {
102123 namedCurve : 'P-256' ,
103124 } ) ;
104125 return {
105126 subject : DEFAULT_SUBJECT ,
106- issuer : DEFAULT_ISSUER ,
107127 publicKey : publicKey . export ( { format : 'pem' , type : 'spki' } ) . toString ( ) ,
128+ claims : { iss : DEFAULT_ISSUER } ,
108129 } ;
109130}
110131
@@ -119,6 +140,132 @@ function buildResponse(leaf: Buffer, root: Buffer): string {
119140 return JSON . stringify ( body ) ;
120141}
121142
143+ function extensionFromClaims ( claims : Record < string , string > ) : ExtensionValue [ ] {
144+ const extensions : ExtensionValue [ ] = [ ] ;
145+ const baseURL = 'https://github.com' ;
146+
147+ for ( const [ key , value ] of Object . entries ( claims ) ) {
148+ switch ( key ) {
149+ case 'iss' :
150+ extensions . push ( {
151+ oid : ISSUER_EXT_OID_V1 ,
152+ value : value ,
153+ legacy : true ,
154+ } ) ;
155+ extensions . push ( { oid : ISSUER_EXT_OID_V2 , value : value } ) ;
156+ break ;
157+ case 'event_name' :
158+ extensions . push ( {
159+ oid : GH_WORKFLOW_TRIGGER_EXT_OID ,
160+ value : value ,
161+ legacy : true ,
162+ } ) ;
163+ extensions . push ( { oid : BUILD_TRIGGER_EXT_OID , value : value } ) ;
164+ break ;
165+ case 'sha' :
166+ extensions . push ( {
167+ oid : GH_WORKFLOW_SHA_EXT_OID ,
168+ value : value ,
169+ legacy : true ,
170+ } ) ;
171+ extensions . push ( { oid : SOURCE_REPO_DIGEST_EXT_OID , value : value } ) ;
172+ break ;
173+ case 'workflow' :
174+ extensions . push ( {
175+ oid : GH_WORKFLOW_NAME_EXT_OID ,
176+ value : value ,
177+ legacy : true ,
178+ } ) ;
179+ break ;
180+ case 'repository' :
181+ extensions . push ( {
182+ oid : GH_WORKFLOW_REPO_EXT_OID ,
183+ value : value ,
184+ legacy : true ,
185+ } ) ;
186+ extensions . push ( {
187+ oid : SOURCE_REPO_URI_EXT_OID ,
188+ value : `${ baseURL } /${ value } ` ,
189+ } ) ;
190+ break ;
191+ case 'ref' :
192+ extensions . push ( {
193+ oid : GH_WORKFLOW_REF_EXT_OID ,
194+ value : value ,
195+ legacy : true ,
196+ } ) ;
197+ extensions . push ( {
198+ oid : SOURCE_REPO_REF_EXT_OID ,
199+ value : value ,
200+ } ) ;
201+ break ;
202+ case 'job_workflow_ref' :
203+ extensions . push ( {
204+ oid : BUILD_SIGNER_URI_EXT_OID ,
205+ value : `${ baseURL } /${ value } ` ,
206+ } ) ;
207+ break ;
208+ case 'job_workflow_sha' :
209+ extensions . push ( {
210+ oid : BUILD_SIGNER_DIGEST_EXT_OID ,
211+ value : value ,
212+ } ) ;
213+ break ;
214+ case 'runner_environment' :
215+ extensions . push ( {
216+ oid : RUNNER_ENVIRONMENT_EXT_OID ,
217+ value : value ,
218+ } ) ;
219+ break ;
220+ case 'repository_id' :
221+ extensions . push ( {
222+ oid : SOURCE_REPO_ID_EXT_OID ,
223+ value : value ,
224+ } ) ;
225+ break ;
226+ case 'repository_owner' :
227+ extensions . push ( {
228+ oid : SOURCE_REPO_OWNER_URI_EXT_OID ,
229+ value : `${ baseURL } /${ value } ` ,
230+ } ) ;
231+ break ;
232+ case 'repository_owner_id' :
233+ extensions . push ( {
234+ oid : SOURCE_REPO_OWNER_ID_EXT_OID ,
235+ value : value ,
236+ } ) ;
237+ break ;
238+ case 'workflow_ref' :
239+ extensions . push ( {
240+ oid : BUILD_CONFIG_URI_EXT_OID ,
241+ value : `${ baseURL } /${ value } ` ,
242+ } ) ;
243+ break ;
244+ case 'workflow_sha' :
245+ extensions . push ( {
246+ oid : BUILD_CONFIG_DIGEST_EXT_OID ,
247+ value : value ,
248+ } ) ;
249+ break ;
250+ case 'repository_visibility' :
251+ extensions . push ( {
252+ oid : SOURCE_REPO_VISIBILITY_EXT_OID ,
253+ value : value ,
254+ } ) ;
255+ break ;
256+ }
257+ }
258+
259+ if ( claims [ 'repository' ] && claims [ 'run_id' ] && claims [ 'run_attempt' ] ) {
260+ extensions . push ( {
261+ oid : RUN_INVOCATION_URI_EXT_OID ,
262+ value : `${ baseURL } /${ claims [ 'repository' ] } /actions/runs/${ claims [ 'run_id' ] } /attempts/${ claims [ 'run_attempt' ] } ` ,
263+ } ) ;
264+ }
265+
266+ return extensions ;
267+ }
268+
122269// PEM string to DER-encoded byte buffer conversion
123270function fromPEM ( pem : string ) : Buffer {
124271 return Buffer . from (
0 commit comments