From a4b3e128c119123138889cf57bf65c4f3fae811a Mon Sep 17 00:00:00 2001 From: Marshall Cottrell Date: Mon, 10 Jul 2023 11:20:56 -0400 Subject: [PATCH] Update GitLab claim mappings for build configs (#1206) * 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 Signed-off-by: marshall007 * update docs to reflect GitLab claim mappings Signed-off-by: marshall007 * updates to match new claim names Signed-off-by: marshall007 * fix issuer test Signed-off-by: marshall007 * address PR feedback Signed-off-by: marshall007 * fix failing grpc server tests Signed-off-by: marshall007 * fix lint error Signed-off-by: marshall007 --------- Signed-off-by: marshall007 --- docs/oid-info.md | 11 +- pkg/identity/gitlabcom/issuer_test.go | 1 + pkg/identity/gitlabcom/principal.go | 38 ++++++- pkg/identity/gitlabcom/principal_test.go | 138 +++++++++++++++++++---- pkg/server/grpc_server_test.go | 19 +++- 5 files changed, 169 insertions(+), 38 deletions(-) diff --git a/docs/oid-info.md b/docs/oid-info.md index 7b6808541..57bf58d58 100644 --- a/docs/oid-info.md +++ b/docs/oid-info.md @@ -191,8 +191,8 @@ that Sigstore operates. | exp | exp | exp | exp | N/A | Only used to validate the JWT. | | nbf | nbf | nbf | nbf | N/A | Only used to validate the JWT. Optional, as per the OIDC spec | | iat | iat | iat | iat | N/A | Only used to validate the JWT. | -| server_url + job_workflow_ref | server_url + project_path + /-/jobs/ + job_id | ?? | ?? | Build Signer URI | Reference to specific build instructions that are responsible for signing. Can be the same as Build Config URI. For example a reusable workflow in GitHub Actions or a Circle CI Orbs. | -| job_workflow_sha | N/A | ?? | ?? | Build Signer Digest | An immutable reference to the specific version of the build instructions that is responsible for signing. Should include the digest type followed by the digest, e.g. `sha1:abc123`. | +| server_url + job_workflow_ref | ci_config_ref_uri ([WIP][gitlab-wip-cliams]) | ?? | ?? | Build Signer URI | Reference to specific build instructions that are responsible for signing. Can be the same as Build Config URI. For example a reusable workflow in GitHub Actions or a Circle CI Orbs. | +| job_workflow_sha | ci_config_sha ([WIP][gitlab-wip-cliams]) | ?? | ?? | Build Signer Digest | An immutable reference to the specific version of the build instructions that is responsible for signing. Should include the digest type followed by the digest, e.g. `sha1:abc123`. | | runner_environment | runner_environment | ?? | ?? | Runner Environment | For platforms to specify whether the build took place in platform-hosted cloud infrastructure or customer-hosted infrastructure. For example: `platform-hosted` and `self-hosted`. | | server_url + repository | server_url + project_path | ?? | ?? | Source Repository URI | Should include a fully qualified repository URL. | | sha | sha | ?? | build_commit | Source Repository Digest | An immutable reference to a specific version of the source code. Should include the digest type followed by the digest, e.g. `sha1:abc123`. | @@ -200,10 +200,11 @@ that Sigstore operates. | repository_id | project_id | ?? | ?? | Source Repository Identifier | Stable identifier for the owner of the source repository. | | server_url + repository_owner | server_url + namespace_path | ?? | ?? | Source Repository Owner URI | Fully qualified URL for the owner of the source repository. | | repository_owner_id | namespace_id | ?? | ?? | Source Repository Owner Identifier | Stable identifier for the owner of the source repository. | -| server_url + workflow_ref | pipeline_ref ([WIP](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117923)) | ?? | ?? | Build Config URI | A reference to the initiating build instructions. | -| workflow_sha | pipeline_sha ([WIP](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117923)) | ?? | ?? | Build Config Digest | An immutable reference to the specific version of the top-level build instructions. Should include the digest type followed by the digest, e.g. `sha1:abc123`. | +| server_url + workflow_ref | ci_config_ref_uri ([WIP][gitlab-wip-cliams]) | ?? | ?? | Build Config URI | A reference to the initiating build instructions. | +| workflow_sha | ci_config_sha ([WIP][gitlab-wip-cliams]) | ?? | ?? | Build Config Digest | An immutable reference to the specific version of the top-level build instructions. Should include the digest type followed by the digest, e.g. `sha1:abc123`. | | event_name | pipeline_source | ?? | ?? | Build Trigger | The event or action that triggered the build. | -| server_url + repository + "/actions/runs/" + run_id + "/attempts/" + run_attempt | server_url + project_path + /-/pipelines/ + pipeline_id | ?? | ?? | Run Invocation URI | An immutable identifier that can uniquely identify the build execution | +| server_url + repository + "/actions/runs/" + run_id + "/attempts/" + run_attempt | server_url + project_path + /-/jobs/ + job_id | ?? | ?? | Run Invocation URI | An immutable identifier that can uniquely identify the build execution | [github-oidc-doc]: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token [oid-link]: http://oid-info.com/get/1.3.6.1.4.1.57264 +[gitlab-wip-cliams]: https://gitlab.com/gitlab-org/gitlab/-/issues/404722 diff --git a/pkg/identity/gitlabcom/issuer_test.go b/pkg/identity/gitlabcom/issuer_test.go index b6466a6fa..5e2c9700d 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", + "ci_config_ref_uri": "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..f3debe46e 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 + ciConfigRefURI string + + // Commit sha of top-level pipeline definition, and is + // only populated when `ciConfigRefURI` is local to the GitLab instance + ciConfigSha 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"` + CiConfigRefURI string `json:"ci_config_ref_uri"` + CiConfigSha string `json:"ci_config_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.CiConfigRefURI == "" { + return nil, errors.New("missing ci_config_ref_uri 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, + ciConfigRefURI: claims.CiConfigRefURI, + ciConfigSha: claims.CiConfigSha, repository: claims.ProjectPath, ref: ref, repositoryID: claims.ProjectID, @@ -178,13 +193,30 @@ func (p jobPrincipal) Embed(_ context.Context, cert *x509.Certificate) error { return err } + // ci_config_ref_uri claim is a URI that does not include protocol scheme so we need to normalize it + ciConfigRefURL, err := url.Parse(p.ciConfigRefURI) + if err != nil { + return err + } + + // default to https + ciConfigRefURL.Scheme = "https" + + // or use scheme from issuer if from the same host + if baseURL.Host == ciConfigRefURL.Host { + ciConfigRefURL.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{ciConfigRefURL} // Embed additional information into custom extensions cert.ExtraExtensions, err = certificate.Extensions{ Issuer: p.issuer, - BuildSignerURI: baseURL.JoinPath(p.repository, "/-/jobs/", p.jobID).String(), + BuildConfigURI: ciConfigRefURL.String(), + BuildConfigDigest: p.ciConfigSha, + BuildSignerURI: ciConfigRefURL.String(), + BuildSignerDigest: p.ciConfigSha, 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..93a7a4493 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", + "ci_config_ref_uri": "gitlab.com/cpanto/testing-cosign//.gitlab-ci.yml@refs/head/main", + "ci_config_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", + ciConfigRefURI: "gitlab.com/cpanto/testing-cosign//.gitlab-ci.yml@refs/head/main", + ciConfigSha: "714a629c0b401fdce83e847fc9589983fc6f46bc", repository: "cpanato/testing-cosign", repositoryID: "42831435", repositoryOwner: "cpanato", @@ -77,29 +80,52 @@ func TestJobPrincipalFromIDToken(t *testing.T) { }, `Token missing pipeline_source claim should be rejected`: { 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", - "job_id": "3659681386", - "ref": "main", - "ref_type": "branch", + "aud": "sigstore", + "exp": 0, + "iss": "https://gitlab.com", + "sub": "project_path:cpanato/testing-cosign:ref_type:branch:ref:main", + "ci_config_ref_uri": "gitlab.com/cpanto/testing-cosign//.gitlab-ci.yml@refs/head/main", + "ci_config_sha": "714a629c0b401fdce83e847fc9589983fc6f46bc", + "project_id": "42831435", + "project_path": "cpanato/testing-cosign", + "namespace_path": "cpanato", + "namespace_id": "1730270", + "pipeline_id": "757451528", + "job_id": "3659681386", + "ref": "main", + "ref_type": "branch", }, WantErr: true, ErrContains: "pipeline_source", }, `Token missing project_path claim should be rejected`: { + Claims: map[string]interface{}{ + "aud": "sigstore", + "exp": 0, + "iss": "https://gitlab.com", + "sub": "project_path:cpanato/testing-cosign:ref_type:branch:ref:main", + "ci_config_ref_uri": "gitlab.com/cpanto/testing-cosign//.gitlab-ci.yml@refs/head/main", + "ci_config_sha": "714a629c0b401fdce83e847fc9589983fc6f46bc", + "project_id": "42831435", + "pipeline_id": "757451528", + "namespace_id": "1730270", + "pipeline_source": "push", + "namespace_path": "cpanato", + "job_id": "3659681386", + "ref": "main", + "ref_type": "branch", + }, + WantErr: true, + ErrContains: "project_path", + }, + `Token missing ci_config_ref_uri claim should be rejected`: { 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", "pipeline_id": "757451528", "namespace_id": "1730270", "pipeline_source": "push", @@ -109,7 +135,46 @@ func TestJobPrincipalFromIDToken(t *testing.T) { "ref_type": "branch", }, WantErr: true, - ErrContains: "project_path", + ErrContains: "ci_config_ref_uri", + }, + `Token missing ci_config_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", + "ci_config_ref_uri": "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", + ciConfigRefURI: "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, }, } @@ -174,6 +239,7 @@ func TestName(t *testing.T) { "project_id": "42831435", "project_path": "cpanato/testing-cosign", "pipeline_id": "757451528", + "ci_config_ref_uri": "gitlab.com/cpanto/testing-cosign//.gitlab-ci.yml@refs/head/main", "pipeline_source": "push", "namespace_path": "cpanato", "namespace_id": "1730270", @@ -222,13 +288,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", + ciConfigRefURI: "gitlab.com/cpanto/testing-cosign//.gitlab-ci.yml@refs/head/main", + ciConfigSha: "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 +355,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 } diff --git a/pkg/server/grpc_server_test.go b/pkg/server/grpc_server_test.go index 267136f5c..c906e4702 100644 --- a/pkg/server/grpc_server_test.go +++ b/pkg/server/grpc_server_test.go @@ -956,6 +956,8 @@ type gitlabClaims struct { ProjectID string `json:"project_id"` PipelineSource string `json:"pipeline_source"` PipelineID string `json:"pipeline_id"` + CiConfigRefURI string `json:"ci_config_ref_uri"` + CiConfigSha string `json:"ci_config_sha"` NamespacePath string `json:"namespace_path"` NamespaceID string `json:"namespace_id"` JobID string `json:"job_id"` @@ -989,6 +991,8 @@ func TestAPIWithGitLab(t *testing.T) { ProjectID: "42831435", PipelineSource: "push", PipelineID: "757451528", + CiConfigRefURI: "gitlab.com/cpanato/testing-cosign//.gitlab-ci.yml@refs/heads/main", + CiConfigSha: "714a629c0b401fdce83e847fc9589983fc6f46bc", NamespacePath: "cpanato", NamespaceID: "1730270", JobID: "3659681386", @@ -1051,7 +1055,8 @@ func TestAPIWithGitLab(t *testing.T) { t.Fatalf("unexpected length of leaf certificate URIs, expected 1, got %d", len(leafCert.URIs)) } - gitLabURL := fmt.Sprintf("https://gitlab.com/%s@refs/heads/%s", claims.ProjectPath, claims.Ref) + baseURL := "https://gitlab.com/" + gitLabURL := baseURL + fmt.Sprintf("%s//.gitlab-ci.yml@refs/heads/%s", claims.ProjectPath, claims.Ref) gitLabURI, err := url.Parse(gitLabURL) if err != nil { t.Fatalf("failed to parse expected url") @@ -1059,18 +1064,20 @@ func TestAPIWithGitLab(t *testing.T) { if *leafCert.URIs[0] != *gitLabURI { t.Fatalf("URIs do not match: Expected %v, got %v", gitLabURI, leafCert.URIs[0]) } - url := "https://gitlab.com/" expectedExts := map[int]string{ - 9: url + claims.ProjectPath + "/-/jobs/" + claims.JobID, + 9: gitLabURL, + 10: claims.CiConfigSha, 11: claims.RunnerEnvironment, - 12: url + claims.ProjectPath, + 12: baseURL + claims.ProjectPath, 13: claims.Sha, 14: fmt.Sprintf("refs/heads/%s", claims.Ref), 15: claims.ProjectID, - 16: url + claims.NamespacePath, + 16: baseURL + claims.NamespacePath, 17: claims.NamespaceID, + 18: gitLabURL, + 19: claims.CiConfigSha, 20: claims.PipelineSource, - 21: url + claims.ProjectPath + "/-/pipelines/" + claims.PipelineID, + 21: baseURL + claims.ProjectPath + "/-/jobs/" + claims.JobID, } for o, value := range expectedExts { ext, found := findCustomExtension(leafCert, asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, o})