From 3025b6542e98c73cf52c4ac2120ab9abd907bb97 Mon Sep 17 00:00:00 2001 From: James Healy Date: Mon, 7 Aug 2023 00:09:36 +1000 Subject: [PATCH] Update Buildkite issuer to include some of the new certificate extensions The Buildkite Issuer was added in #890, prior to the efforts to standardise certificate extensions for CI providers, and #1074 calls for the Buildkite issuer to be updated to use the new extensions (where applicable). This is an early attempt to make those changes. I've added the extensions that make the most sense in a Buildkite context, like RunInvocationURI, RunnerEnvironment and SourceRepositoryDiget. Many of the other extensions don't apply because we're not a code host as well, or need further discussion. I have not added tests yet. This is my first contribution to fulcio and I'm keen to confirm I'm heading in the right direction before adding tests. However, I have tested this locally with a Buildkite agent and OIDC token, and the certificate was issued as expected. Using `git cat-file commit HEAD` and piping it through `openssl pkcs7 -print -print_certs -text`, the extensions section looks like this: X509v3 extensions: X509v3 Key Usage: critical Digital Signature X509v3 Extended Key Usage: Code Signing X509v3 Subject Key Identifier: 19:9E:E7:53:4D:F6:65:D4:23:9D:60:21:B8:F3:12:80:FD:11:30:7F X509v3 Authority Key Identifier: 8A:3E:9E:34:19:F7:32:18:3D:2A:1B:F9:5F:60:29:24:0F:70:0B:B4 X509v3 Subject Alternative Name: critical URI:https://buildkite.com/yob-opensource/oidc-signing-experiment 1.3.6.1.4.1.57264.1.1: https://agent.buildkite.com 1.3.6.1.4.1.57264.1.8: ..https://agent.buildkite.com 1.3.6.1.4.1.57264.1.11: ..self-hosted 1.3.6.1.4.1.57264.1.13: .(5242de9e5c2ca164cb3dfc500fb605f0b8b86763 1.3.6.1.4.1.57264.1.21: .mhttps://buildkite.com/yob-opensource/oidc-signing-experiment/builds/35%230189cb29-62fa-41af-8641-62e1d6c5edfd Fixes #1074 Signed-off-by: James Healy --- pkg/identity/buildkite/principal.go | 73 ++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/pkg/identity/buildkite/principal.go b/pkg/identity/buildkite/principal.go index 219deb4d7..35182fa01 100644 --- a/pkg/identity/buildkite/principal.go +++ b/pkg/identity/buildkite/principal.go @@ -17,9 +17,10 @@ package buildkite import ( "context" "crypto/x509" + "encoding/json" "errors" - "fmt" "net/url" + "strconv" "github.com/coreos/go-oidc/v3/oidc" "github.com/sigstore/fulcio/pkg/certificate" @@ -35,16 +36,38 @@ type jobPrincipal struct { // https://agent.buildkite.com/.well-known/openid-configuration issuer string - // The URL of the pipeline, the container of many builds. This will be - // set as a human-friendly SubjectAlternativeName URI in the certificate. + // Buildkite's domain url string + + // Unique identifier for a Buildkite customer + organizationSlug string + + // Unique identifier (within the scope of an OrganizationSlug) for a container of many builds. + pipelineSlug string + + // Incrementing number within each pipeline + buildNumber int64 + + // The commit sha being tested by a build + buildCommit string + + // UUID that identifies a single unique execution within a build + jobId string + + // Did the job run in a cloud hosted environment or self hosted by the customer. All + // Buildkite jobs execute on self hosted agents, so this will always be `self-hosted` + runnerEnvironment string } func JobPrincipalFromIDToken(_ context.Context, token *oidc.IDToken) (identity.Principal, error) { var claims struct { - OrganizationSlug string `json:"organization_slug"` - PipelineSlug string `json:"pipeline_slug"` + OrganizationSlug string `json:"organization_slug"` + PipelineSlug string `json:"pipeline_slug"` + BuildNumber json.Number `json:"build_number"` + BuildCommit string `json:"build_commit"` + JobId string `json:"job_id"` } + if err := token.Claims(&claims); err != nil { return nil, err } @@ -57,10 +80,29 @@ func JobPrincipalFromIDToken(_ context.Context, token *oidc.IDToken) (identity.P return nil, errors.New("missing pipeline_slug claim in ID token") } + buildNumber, err := claims.BuildNumber.Int64() + if err != nil { + return nil, errors.New("error parsing build_number claim in ID token") + } + + if claims.BuildCommit == "" { + return nil, errors.New("missing build_commit claim in ID token") + } + + if claims.JobId == "" { + return nil, errors.New("missing job_id claim in ID token") + } + return &jobPrincipal{ - subject: token.Subject, - issuer: token.Issuer, - url: fmt.Sprintf("https://buildkite.com/%s/%s", claims.OrganizationSlug, claims.PipelineSlug), + subject: token.Subject, + issuer: token.Issuer, + url: "https://buildkite.com", + organizationSlug: claims.OrganizationSlug, + pipelineSlug: claims.PipelineSlug, + buildNumber: buildNumber, + buildCommit: claims.BuildCommit, + jobId: claims.JobId, + runnerEnvironment: "self-hosted", }, nil } @@ -69,16 +111,23 @@ func (p jobPrincipal) Name(_ context.Context) string { } func (p jobPrincipal) Embed(_ context.Context, cert *x509.Certificate) error { - // Set SubjectAlternativeName to the pipeline URL on the certificate - parsed, err := url.Parse(p.url) + baseURL, err := url.Parse(p.url) if err != nil { return err } - cert.URIs = []*url.URL{parsed} + + pipelineUrl := baseURL.JoinPath(p.organizationSlug, p.pipelineSlug) + jobUrl := baseURL.JoinPath(p.organizationSlug, p.pipelineSlug, "builds", strconv.FormatInt(p.buildNumber, 10)+"#"+p.jobId) + + // Set SubjectAlternativeName to the pipeline URL on the certificate + cert.URIs = []*url.URL{pipelineUrl} // Embed additional information into custom extensions cert.ExtraExtensions, err = certificate.Extensions{ - Issuer: p.issuer, + Issuer: p.issuer, + RunInvocationURI: jobUrl.String(), + RunnerEnvironment: p.runnerEnvironment, + SourceRepositoryDigest: p.buildCommit, }.Render() if err != nil { return err