From e4ac94694b9bda36a5d1f373db99a72ba8b27f28 Mon Sep 17 00:00:00 2001 From: marshall007 Date: Fri, 2 Jun 2023 14:00:25 -0400 Subject: [PATCH] Update GitLab claim mappings for build configs Assigns new `pipeline_ref/sha` claims to `Build Config` and `Build Signer` related OIDs. Depends on https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121597 Related to https://github.com/sigstore/fulcio/issues/1182 --- pkg/identity/gitlabcom/issuer_test.go | 1 + pkg/identity/gitlabcom/principal.go | 38 +++++++++- pkg/identity/gitlabcom/principal_test.go | 93 +++++++++++++++++++++--- 3 files changed, 118 insertions(+), 14 deletions(-) diff --git a/pkg/identity/gitlabcom/issuer_test.go b/pkg/identity/gitlabcom/issuer_test.go index b6466a6fa..de1452ade 100644 --- a/pkg/identity/gitlabcom/issuer_test.go +++ b/pkg/identity/gitlabcom/issuer_test.go @@ -53,6 +53,7 @@ func TestIssuer(t *testing.T) { "user_email": "cpanato@example.com", "pipeline_id": "757451528", "pipeline_source": "push", + "pipeline_ref": "gitlab.com/cpanto/testing-cosign/.gitlab-ci.yml@refs/head/main", "job_id": "3659681386", "sha": "714a629c0b401fdce83e847fc9589983fc6f46bc", "runner_id": 1, diff --git a/pkg/identity/gitlabcom/principal.go b/pkg/identity/gitlabcom/principal.go index dbb8eb999..6e659a0c0 100644 --- a/pkg/identity/gitlabcom/principal.go +++ b/pkg/identity/gitlabcom/principal.go @@ -44,6 +44,13 @@ type jobPrincipal struct { // Pipeline ID pipelineID string + // Ref of top-level pipeline definition. E.g. gitlab.com/my-group/my-project/.gitlab-ci.yml@refs/heads/main + pipelineRef string + + // Commit sha of top-level pipeline definition, and is + // only populated when `pipelineRef` is local to the GitLab instance + pipelineSha string + // Repository building built repository string @@ -78,6 +85,8 @@ func JobPrincipalFromIDToken(_ context.Context, token *oidc.IDToken) (identity.P ProjectID string `json:"project_id"` PipelineSource string `json:"pipeline_source"` PipelineID string `json:"pipeline_id"` + PipelineRef string `json:"pipeline_ref"` + PipelineSha string `json:"pipeline_sha"` NamespacePath string `json:"namespace_path"` NamespaceID string `json:"namespace_id"` JobID string `json:"job_id"` @@ -104,6 +113,10 @@ func JobPrincipalFromIDToken(_ context.Context, token *oidc.IDToken) (identity.P return nil, errors.New("missing pipeline_id claim in ID token") } + if claims.PipelineRef == "" { + return nil, errors.New("missing pipeline_ref claim in ID token") + } + if claims.JobID == "" { return nil, errors.New("missing job_id claim in ID token") } @@ -156,6 +169,8 @@ func JobPrincipalFromIDToken(_ context.Context, token *oidc.IDToken) (identity.P url: `https://gitlab.com/`, eventName: claims.PipelineSource, pipelineID: claims.PipelineID, + pipelineRef: claims.PipelineRef, + pipelineSha: claims.PipelineSha, repository: claims.ProjectPath, ref: ref, repositoryID: claims.ProjectID, @@ -178,13 +193,30 @@ func (p jobPrincipal) Embed(_ context.Context, cert *x509.Certificate) error { return err } + // pipeline_ref claim is a URI that does not include protocol scheme so we need to normalize it + pipelineRefURL, err := url.Parse(p.pipelineRef) + if err != nil { + return err + } + + // default to https + pipelineRefURL.Scheme = "https" + + // or use scheme from issuer if from the same host + if baseURL.Host == pipelineRefURL.Host { + pipelineRefURL.Scheme = baseURL.Scheme + } + // Set workflow ref URL to SubjectAlternativeName on certificate - cert.URIs = []*url.URL{baseURL.JoinPath(fmt.Sprintf("%s@%s", p.repository, p.ref))} + cert.URIs = []*url.URL{pipelineRefURL} // Embed additional information into custom extensions cert.ExtraExtensions, err = certificate.Extensions{ Issuer: p.issuer, - BuildSignerURI: baseURL.JoinPath(p.repository, "/-/jobs/", p.jobID).String(), + BuildConfigURI: pipelineRefURL.String(), + BuildConfigDigest: p.pipelineSha, + BuildSignerURI: pipelineRefURL.String(), + BuildSignerDigest: p.pipelineSha, RunnerEnvironment: p.runnerEnvironment, SourceRepositoryURI: baseURL.JoinPath(p.repository).String(), SourceRepositoryDigest: p.sha, @@ -193,7 +225,7 @@ func (p jobPrincipal) Embed(_ context.Context, cert *x509.Certificate) error { SourceRepositoryOwnerURI: baseURL.JoinPath(p.repositoryOwner).String(), SourceRepositoryOwnerIdentifier: p.repositoryOwnerID, BuildTrigger: p.eventName, - RunInvocationURI: baseURL.JoinPath(p.repository, "/-/pipelines/", p.pipelineID).String(), + RunInvocationURI: baseURL.JoinPath(p.repository, "/-/jobs/", p.jobID).String(), }.Render() if err != nil { return err diff --git a/pkg/identity/gitlabcom/principal_test.go b/pkg/identity/gitlabcom/principal_test.go index ec115ecaa..3c5fd146c 100644 --- a/pkg/identity/gitlabcom/principal_test.go +++ b/pkg/identity/gitlabcom/principal_test.go @@ -15,7 +15,6 @@ package gitlabcom import ( - "bytes" "context" "crypto/x509" "encoding/asn1" @@ -50,6 +49,8 @@ func TestJobPrincipalFromIDToken(t *testing.T) { "namespace_id": "1730270", "pipeline_id": "757451528", "pipeline_source": "push", + "pipeline_ref": "gitlab.com/cpanto/testing-cosign/.gitlab-ci.yml@refs/head/main", + "pipeline_sha": "714a629c0b401fdce83e847fc9589983fc6f46bc", "job_id": "3659681386", "ref": "main", "ref_type": "branch", @@ -63,6 +64,8 @@ func TestJobPrincipalFromIDToken(t *testing.T) { url: "https://gitlab.com/", eventName: "push", pipelineID: "757451528", + pipelineRef: "gitlab.com/cpanto/testing-cosign/.gitlab-ci.yml@refs/head/main", + pipelineSha: "714a629c0b401fdce83e847fc9589983fc6f46bc", repository: "cpanato/testing-cosign", repositoryID: "42831435", repositoryOwner: "cpanato", @@ -81,6 +84,8 @@ func TestJobPrincipalFromIDToken(t *testing.T) { "exp": 0, "iss": "https://gitlab.com", "sub": "project_path:cpanato/testing-cosign:ref_type:branch:ref:main", + "pipeline_ref": "gitlab.com/cpanto/testing-cosign/.gitlab-ci.yml@refs/head/main", + "pipeline_sha": "714a629c0b401fdce83e847fc9589983fc6f46bc", "project_id": "42831435", "project_path": "cpanato/testing-cosign", "namespace_path": "cpanato", @@ -99,6 +104,8 @@ func TestJobPrincipalFromIDToken(t *testing.T) { "exp": 0, "iss": "https://gitlab.com", "sub": "project_path:cpanato/testing-cosign:ref_type:branch:ref:main", + "pipeline_ref": "gitlab.com/cpanto/testing-cosign/.gitlab-ci.yml@refs/head/main", + "pipeline_sha": "714a629c0b401fdce83e847fc9589983fc6f46bc", "project_id": "42831435", "pipeline_id": "757451528", "namespace_id": "1730270", @@ -111,6 +118,45 @@ func TestJobPrincipalFromIDToken(t *testing.T) { WantErr: true, ErrContains: "project_path", }, + `Token missing pipeline_sha claim is ok`: { + Claims: map[string]interface{}{ + "aud": "sigstore", + "exp": 0, + "iss": "https://gitlab.com", + "sub": "project_path:cpanato/testing-cosign:ref_type:branch:ref:main", + "project_id": "42831435", + "project_path": "cpanato/testing-cosign", + "namespace_path": "cpanato", + "namespace_id": "1730270", + "pipeline_id": "757451528", + "pipeline_source": "push", + "pipeline_ref": "example.com/ci/config.yml", + "job_id": "3659681386", + "ref": "main", + "ref_type": "branch", + "sha": "714a629c0b401fdce83e847fc9589983fc6f46bc", + "runner_id": 1, + "runner_environment": "gitlab-hosted", + }, + ExpectPrincipal: jobPrincipal{ + issuer: "https://gitlab.com", + subject: "project_path:cpanato/testing-cosign:ref_type:branch:ref:main", + url: "https://gitlab.com/", + eventName: "push", + pipelineID: "757451528", + pipelineRef: "example.com/ci/config.yml", + repository: "cpanato/testing-cosign", + repositoryID: "42831435", + repositoryOwner: "cpanato", + repositoryOwnerID: "1730270", + jobID: "3659681386", + ref: "refs/heads/main", + runnerID: 1, + runnerEnvironment: "gitlab-hosted", + sha: "714a629c0b401fdce83e847fc9589983fc6f46bc", + }, + WantErr: false, + }, } for name, test := range tests { @@ -174,6 +220,7 @@ func TestName(t *testing.T) { "project_id": "42831435", "project_path": "cpanato/testing-cosign", "pipeline_id": "757451528", + "pipeline_ref": "gitlab.com/cpanto/testing-cosign/.gitlab-ci.yml@refs/head/main", "pipeline_source": "push", "namespace_path": "cpanato", "namespace_id": "1730270", @@ -222,13 +269,39 @@ func TestEmbed(t *testing.T) { }{ `GitLab job challenge should have issue, subject and url embedded`: { Principal: &jobPrincipal{ - issuer: "https://gitlab.com", - subject: "doesntmatter", - url: `https://gitlab.com/honk/honk-repo/-/job/123456`, + issuer: "https://gitlab.com", + subject: "project_path:cpanato/testing-cosign:ref_type:branch:ref:main", + url: "https://gitlab.com/", + eventName: "push", + pipelineID: "757451528", + pipelineRef: "gitlab.com/cpanto/testing-cosign/.gitlab-ci.yml@refs/head/main", + pipelineSha: "714a629c0b401fdce83e847fc9589983fc6f46bc", + repository: "cpanato/testing-cosign", + repositoryID: "42831435", + repositoryOwner: "cpanato", + repositoryOwnerID: "1730270", + jobID: "3659681386", + ref: "ref", + runnerID: 1, + runnerEnvironment: "gitlab-hosted", + sha: "sha", }, WantErr: false, WantFacts: map[string]func(x509.Certificate) error{ - `Certifificate should have correct issuer`: factIssuerIs(`https://gitlab.com`), + `Certificate has correct issuer (v2) extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 8}, "https://gitlab.com"), + `Certificate has correct builder signer URI extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 9}, "https://gitlab.com/cpanto/testing-cosign/.gitlab-ci.yml@refs/head/main"), + `Certificate has correct builder signer digest extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 10}, "714a629c0b401fdce83e847fc9589983fc6f46bc"), + `Certificate has correct runner environment extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 11}, "gitlab-hosted"), + `Certificate has correct source repo URI extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 12}, "https://gitlab.com/cpanato/testing-cosign"), + `Certificate has correct source repo digest extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 13}, "sha"), + `Certificate has correct source repo ref extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 14}, "ref"), + `Certificate has correct source repo ID extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 15}, "42831435"), + `Certificate has correct source repo owner URI extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 16}, "https://gitlab.com/cpanato"), + `Certificate has correct source repo owner ID extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 17}, "1730270"), + `Certificate has correct build config URI extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 18}, "https://gitlab.com/cpanto/testing-cosign/.gitlab-ci.yml@refs/head/main"), + `Certificate has correct build config digest extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 19}, "714a629c0b401fdce83e847fc9589983fc6f46bc"), + `Certificate has correct build trigger extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 20}, "push"), + `Certificate has correct run invocation ID extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 21}, "https://gitlab.com/cpanato/testing-cosign/-/jobs/3659681386"), }, }, `GitLab job principal with bad URL fails`: { @@ -263,16 +336,14 @@ func TestEmbed(t *testing.T) { } } -func factIssuerIs(issuer string) func(x509.Certificate) error { - return factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, issuer) -} - func factExtensionIs(oid asn1.ObjectIdentifier, value string) func(x509.Certificate) error { return func(cert x509.Certificate) error { for _, ext := range cert.ExtraExtensions { if ext.Id.Equal(oid) { - if !bytes.Equal(ext.Value, []byte(value)) { - return fmt.Errorf("expected oid %v to be %s, but got %s", oid, value, ext.Value) + var strVal string + _, _ = asn1.Unmarshal(ext.Value, &strVal) + if value != strVal { + return fmt.Errorf("expected oid %v to be %s, but got %s", oid, value, strVal) } return nil }