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