@@ -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,35 @@ 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
89107 /* eslint-disable @typescript-eslint/no-explicit-any */
90- const claims = jose . decodeJwt ( oidc ) as any ;
108+ const claims = jose . decodeJwt ( oidc ) as Record < string , string > ;
91109
92110 /* istanbul ignore next */
93111 return {
94112 subject : claims [ subjectClaim ] || DEFAULT_SUBJECT ,
95- issuer : claims [ 'iss' ] || DEFAULT_ISSUER ,
96113 publicKey : pem ,
114+ claims : { iss : DEFAULT_ISSUER , ...claims } ,
97115 } ;
98116}
99117
100- function stubBody ( ) : { subject : string ; issuer : string ; publicKey : string } {
118+ function stubBody ( ) : {
119+ subject : string ;
120+ publicKey : string ;
121+ claims : Record < string , string > ;
122+ } {
101123 const { publicKey } = generateKeyPairSync ( 'ec' , {
102124 namedCurve : 'P-256' ,
103125 } ) ;
104126 return {
105127 subject : DEFAULT_SUBJECT ,
106- issuer : DEFAULT_ISSUER ,
107128 publicKey : publicKey . export ( { format : 'pem' , type : 'spki' } ) . toString ( ) ,
129+ claims : { iss : DEFAULT_ISSUER } ,
108130 } ;
109131}
110132
@@ -119,6 +141,132 @@ function buildResponse(leaf: Buffer, root: Buffer): string {
119141 return JSON . stringify ( body ) ;
120142}
121143
144+ function extensionFromClaims ( claims : Record < string , string > ) : ExtensionValue [ ] {
145+ const extensions : ExtensionValue [ ] = [ ] ;
146+ const baseURL = 'https://github.com' ;
147+
148+ for ( const [ key , value ] of Object . entries ( claims ) ) {
149+ switch ( key ) {
150+ case 'iss' :
151+ extensions . push ( {
152+ oid : ISSUER_EXT_OID_V1 ,
153+ value : value ,
154+ legacy : true ,
155+ } ) ;
156+ extensions . push ( { oid : ISSUER_EXT_OID_V2 , value : value } ) ;
157+ break ;
158+ case 'event_name' :
159+ extensions . push ( {
160+ oid : GH_WORKFLOW_TRIGGER_EXT_OID ,
161+ value : value ,
162+ legacy : true ,
163+ } ) ;
164+ extensions . push ( { oid : BUILD_TRIGGER_EXT_OID , value : value } ) ;
165+ break ;
166+ case 'sha' :
167+ extensions . push ( {
168+ oid : GH_WORKFLOW_SHA_EXT_OID ,
169+ value : value ,
170+ legacy : true ,
171+ } ) ;
172+ extensions . push ( { oid : SOURCE_REPO_DIGEST_EXT_OID , value : value } ) ;
173+ break ;
174+ case 'workflow' :
175+ extensions . push ( {
176+ oid : GH_WORKFLOW_NAME_EXT_OID ,
177+ value : value ,
178+ legacy : true ,
179+ } ) ;
180+ break ;
181+ case 'repository' :
182+ extensions . push ( {
183+ oid : GH_WORKFLOW_REPO_EXT_OID ,
184+ value : value ,
185+ legacy : true ,
186+ } ) ;
187+ extensions . push ( {
188+ oid : SOURCE_REPO_URI_EXT_OID ,
189+ value : `${ baseURL } /${ value } ` ,
190+ } ) ;
191+ break ;
192+ case 'ref' :
193+ extensions . push ( {
194+ oid : GH_WORKFLOW_REF_EXT_OID ,
195+ value : value ,
196+ legacy : true ,
197+ } ) ;
198+ extensions . push ( {
199+ oid : SOURCE_REPO_REF_EXT_OID ,
200+ value : value ,
201+ } ) ;
202+ break ;
203+ case 'job_workflow_ref' :
204+ extensions . push ( {
205+ oid : BUILD_SIGNER_URI_EXT_OID ,
206+ value : `${ baseURL } /${ value } ` ,
207+ } ) ;
208+ break ;
209+ case 'job_workflow_sha' :
210+ extensions . push ( {
211+ oid : BUILD_SIGNER_DIGEST_EXT_OID ,
212+ value : value ,
213+ } ) ;
214+ break ;
215+ case 'runner_environment' :
216+ extensions . push ( {
217+ oid : RUNNER_ENVIRONMENT_EXT_OID ,
218+ value : value ,
219+ } ) ;
220+ break ;
221+ case 'repository_id' :
222+ extensions . push ( {
223+ oid : SOURCE_REPO_ID_EXT_OID ,
224+ value : value ,
225+ } ) ;
226+ break ;
227+ case 'repository_owner' :
228+ extensions . push ( {
229+ oid : SOURCE_REPO_OWNER_URI_EXT_OID ,
230+ value : `${ baseURL } /${ value } ` ,
231+ } ) ;
232+ break ;
233+ case 'repository_owner_id' :
234+ extensions . push ( {
235+ oid : SOURCE_REPO_OWNER_ID_EXT_OID ,
236+ value : value ,
237+ } ) ;
238+ break ;
239+ case 'workflow_ref' :
240+ extensions . push ( {
241+ oid : BUILD_CONFIG_URI_EXT_OID ,
242+ value : `${ baseURL } /${ value } ` ,
243+ } ) ;
244+ break ;
245+ case 'workflow_sha' :
246+ extensions . push ( {
247+ oid : BUILD_CONFIG_DIGEST_EXT_OID ,
248+ value : value ,
249+ } ) ;
250+ break ;
251+ case 'repository_visibility' :
252+ extensions . push ( {
253+ oid : SOURCE_REPO_VISIBILITY_EXT_OID ,
254+ value : value ,
255+ } ) ;
256+ break ;
257+ }
258+ }
259+
260+ if ( claims [ 'repository' ] && claims [ 'run_id' ] && claims [ 'run_attempt' ] ) {
261+ extensions . push ( {
262+ oid : RUN_INVOCATION_URI_EXT_OID ,
263+ value : `${ baseURL } /${ claims [ 'repository' ] } /actions/runs/${ claims [ 'run_id' ] } /attempts/${ claims [ 'run_attempt' ] } ` ,
264+ } ) ;
265+ }
266+
267+ return extensions ;
268+ }
269+
122270// PEM string to DER-encoded byte buffer conversion
123271function fromPEM ( pem : string ) : Buffer {
124272 return Buffer . from (
0 commit comments