Skip to content

Commit cfc3325

Browse files
leodidoona-agent
andcommitted
feat(signing): support top-level job_workflow_ref claim as fallback
Add support for extracting job_workflow_ref from top-level JWT claim when not found embedded in the sub claim. This provides flexibility for different OIDC token formats while maintaining compatibility with Fulcio's certificate identity generation. - Try extracting job_workflow_ref from sub claim first (primary path) - Fall back to top-level job_workflow_ref claim if not in sub - Add test case for top-level claim scenario - Update documentation to clarify both extraction paths Co-authored-by: Ona <no-reply@ona.com>
1 parent b467877 commit cfc3325

File tree

2 files changed

+52
-11
lines changed

2 files changed

+52
-11
lines changed

pkg/leeway/signing/attestation.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -378,23 +378,26 @@ func validateSigstoreEnvironment() error {
378378
return nil
379379
}
380380

381-
// extractBuilderIDFromOIDC extracts the builder ID from the GitHub OIDC token's sub claim.
381+
// extractBuilderIDFromOIDC extracts the builder ID from the GitHub OIDC token.
382382
// This ensures the builder ID matches the certificate identity issued by Fulcio, which is
383383
// critical for slsa-verifier compatibility, especially with reusable workflows.
384384
//
385385
// For reusable workflows, the OIDC token contains:
386-
// - sub claim: Contains job_workflow_ref pointing to the actual executing workflow (e.g., _build.yml)
387-
// - workflow_ref: Points to the calling workflow (e.g., build-main.yml)
386+
// - job_workflow_ref claim: Points to the actual executing workflow (e.g., _build.yml)
387+
// - sub claim: May contain job_workflow_ref embedded in colon-separated format
388+
// - workflow_ref env var: Points to the calling workflow (e.g., build-main.yml)
388389
//
389-
// Fulcio uses the sub claim for the certificate identity, so we must use it for the builder ID.
390+
// Fulcio uses the sub claim for the certificate identity. For reusable workflows,
391+
// the sub claim includes job_workflow_ref in the format:
392+
// repo:OWNER/REPO:ref:REF:job_workflow_ref:OWNER/REPO/.github/workflows/WORKFLOW@REF
390393
func extractBuilderIDFromOIDC(ctx context.Context, githubCtx *GitHubContext) (string, error) {
391394
// Fetch the OIDC token with sigstore audience
392395
idToken, err := fetchGitHubOIDCToken(ctx, "sigstore")
393396
if err != nil {
394397
return "", fmt.Errorf("failed to fetch OIDC token: %w", err)
395398
}
396399

397-
// Parse the JWT token to extract the sub claim
400+
// Parse the JWT token to extract claims
398401
// JWT format: header.payload.signature
399402
parts := strings.Split(idToken, ".")
400403
if len(parts) != 3 {
@@ -413,17 +416,27 @@ func extractBuilderIDFromOIDC(ctx context.Context, githubCtx *GitHubContext) (st
413416
return "", fmt.Errorf("failed to parse JWT claims: %w", err)
414417
}
415418

416-
// Extract the sub claim
419+
// Extract the sub claim (required for Fulcio certificate identity)
417420
sub, ok := claims["sub"].(string)
418421
if !ok || sub == "" {
419422
return "", fmt.Errorf("sub claim not found or empty in OIDC token")
420423
}
421424

422-
// Extract job_workflow_ref from the sub claim
423-
// Format: repo:OWNER/REPO:ref:REF:job_workflow_ref:OWNER/REPO/.github/workflows/WORKFLOW@REF
425+
// Try to extract job_workflow_ref from the sub claim first
426+
// This is the format that Fulcio embeds in the certificate
424427
jobWorkflowRef := extractJobWorkflowRef(sub)
428+
429+
// If not found in sub, try the top-level job_workflow_ref claim
430+
// (GitHub provides both, but Fulcio uses the one from sub)
431+
if jobWorkflowRef == "" {
432+
if jwfRef, ok := claims["job_workflow_ref"].(string); ok && jwfRef != "" {
433+
jobWorkflowRef = jwfRef
434+
log.WithField("job_workflow_ref", jobWorkflowRef).Debug("Using top-level job_workflow_ref claim (not found in sub)")
435+
}
436+
}
437+
425438
if jobWorkflowRef == "" {
426-
return "", fmt.Errorf("job_workflow_ref not found in sub claim: %s", sub)
439+
return "", fmt.Errorf("job_workflow_ref not found in sub claim or top-level claims: %s", sub)
427440
}
428441

429442
// Construct the builder ID URL

pkg/leeway/signing/attestation_test.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,7 +1410,33 @@ func TestExtractBuilderIDFromOIDC(t *testing.T) {
14101410
errorMsg: "sub claim not found",
14111411
},
14121412
{
1413-
name: "missing job_workflow_ref in sub claim",
1413+
name: "job_workflow_ref in top-level claim (not in sub)",
1414+
setupServer: func() *httptest.Server {
1415+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1416+
header := base64EncodeForTest(`{"alg":"RS256","typ":"JWT"}`)
1417+
payload := base64EncodeForTest(`{
1418+
"sub": "repo:org/repo:environment:prod",
1419+
"aud": "sigstore",
1420+
"job_workflow_ref": "org/repo/.github/workflows/deploy.yml@refs/heads/main"
1421+
}`)
1422+
signature := base64EncodeForTest("fake-signature")
1423+
token := fmt.Sprintf("%s.%s.%s", header, payload, signature)
1424+
1425+
w.Header().Set("Content-Type", "application/json")
1426+
if err := json.NewEncoder(w).Encode(map[string]string{"value": token}); err != nil {
1427+
t.Errorf("Failed to encode response: %v", err)
1428+
}
1429+
}))
1430+
},
1431+
githubCtx: &GitHubContext{
1432+
ServerURL: "https://github.com",
1433+
Repository: "org/repo",
1434+
},
1435+
expectError: false,
1436+
expectedID: "https://github.com/org/repo/.github/workflows/deploy.yml@refs/heads/main",
1437+
},
1438+
{
1439+
name: "missing job_workflow_ref in sub claim and top-level",
14141440
setupServer: func() *httptest.Server {
14151441
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14161442
header := base64EncodeForTest(`{"alg":"RS256","typ":"JWT"}`)
@@ -1422,7 +1448,9 @@ func TestExtractBuilderIDFromOIDC(t *testing.T) {
14221448
token := fmt.Sprintf("%s.%s.%s", header, payload, signature)
14231449

14241450
w.Header().Set("Content-Type", "application/json")
1425-
json.NewEncoder(w).Encode(map[string]string{"value": token})
1451+
if err := json.NewEncoder(w).Encode(map[string]string{"value": token}); err != nil {
1452+
t.Errorf("Failed to encode response: %v", err)
1453+
}
14261454
}))
14271455
},
14281456
githubCtx: &GitHubContext{

0 commit comments

Comments
 (0)