Skip to content

Commit

Permalink
Update GitLab claim mappings for build configs
Browse files Browse the repository at this point in the history
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 sigstore#1182
  • Loading branch information
marshall007 committed Jun 2, 2023
1 parent d2286c6 commit e4ac946
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 14 deletions.
1 change: 1 addition & 0 deletions pkg/identity/gitlabcom/issuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func TestIssuer(t *testing.T) {
"user_email": "[email protected]",
"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,
Expand Down
38 changes: 35 additions & 3 deletions pkg/identity/gitlabcom/principal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"`
Expand All @@ -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")
}
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
Expand Down
93 changes: 82 additions & 11 deletions pkg/identity/gitlabcom/principal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package gitlabcom

import (
"bytes"
"context"
"crypto/x509"
"encoding/asn1"
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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`: {
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit e4ac946

Please sign in to comment.