Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cli/slsa-verifier/verify/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"hash"
"io"
"os"

"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils"
)

func computeFileHash(filePath string, h hash.Hash) (string, error) {
Expand All @@ -33,3 +35,11 @@ func computeFileHash(filePath string, h hash.Hash) (string, error) {
}
return hex.EncodeToString(h.Sum(nil)), nil
}

// newTagResolver creates a GitHubTagResolver, optionally authenticated via
// the GITHUB_TOKEN environment variable.
func newTagResolver() utils.TagResolver {
return &utils.GitHubTagResolver{
Token: os.Getenv("GITHUB_TOKEN"),
}
}
3 changes: 2 additions & 1 deletion cli/slsa-verifier/verify/verify_artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (*
}

builderOpts := &options.BuilderOpts{
ExpectedID: c.BuilderID,
ExpectedID: c.BuilderID,
TagResolver: newTagResolver(),
}

provenance, err := os.ReadFile(c.ProvenancePath)
Expand Down
3 changes: 2 additions & 1 deletion cli/slsa-verifier/verify/verify_github_attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ func (c *VerifyGithubAttestationCommand) Exec(ctx context.Context, artifact stri
}

builderOpts := &options.BuilderOpts{
ExpectedID: c.BuilderID,
ExpectedID: c.BuilderID,
TagResolver: newTagResolver(),
}

attestation, err := os.ReadFile(c.AttestationPath)
Expand Down
3 changes: 2 additions & 1 deletion cli/slsa-verifier/verify/verify_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ func (c *VerifyImageCommand) Exec(ctx context.Context, artifacts []string) (*uti
}

builderOpts := &options.BuilderOpts{
ExpectedID: c.BuilderID,
ExpectedID: c.BuilderID,
TagResolver: newTagResolver(),
}

var provenance []byte
Expand Down
3 changes: 2 additions & 1 deletion cli/slsa-verifier/verify/verify_npm_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ func (c *VerifyNpmPackageCommand) Exec(ctx context.Context, tarballs []string) (
}

builderOpts := &options.BuilderOpts{
ExpectedID: c.BuilderID,
ExpectedID: c.BuilderID,
TagResolver: newTagResolver(),
}

attestations, err := os.ReadFile(c.AttestationsPath)
Expand Down
14 changes: 11 additions & 3 deletions options/options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package options

import "crypto"
import (
"crypto"

"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils"
)

// ProvenanceOpts are the options for checking provenance information.
type ProvenanceOpts struct {
Expand Down Expand Up @@ -34,10 +38,14 @@ type ProvenanceOpts struct {
ExpectedProvenanceRepository *string
}

// BuildOpts are the options for checking the builder.
// BuilderOpts are the options for checking the builder.
type BuilderOpts struct {
// ExpectedBuilderID is the builderID passed in from the user.
// ExpectedID is the builderID passed in from the user.
ExpectedID *string

// TagResolver is used to resolve a commit SHA to its associated tags.
// If nil, SHA pinning is not supported and only tag refs are accepted.
TagResolver utils.TagResolver
}

// VSAOpts are the options for checking the VSA.
Expand Down
32 changes: 20 additions & 12 deletions verifiers/internal/gha/builder.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gha

import (
"context"
"crypto/x509"
"encoding/asn1"
"fmt"
Expand Down Expand Up @@ -63,7 +64,7 @@ func VerifyCertficateSourceRepository(id *WorkflowIdentity,
// Builder IDs are verified against an expected builder ID provided in the
// builerOpts, or against the set of defaultBuilders provided. The identiy
// in the certificate corresponds to a GitHub workflow's path.
func VerifyBuilderIdentity(id *WorkflowIdentity,
func VerifyBuilderIdentity(ctx context.Context, id *WorkflowIdentity,
builderOpts *options.BuilderOpts,
defaultBuilders map[string]bool,
) (*utils.TrustedBuilderID, bool, error) {
Expand All @@ -90,7 +91,7 @@ func VerifyBuilderIdentity(id *WorkflowIdentity,
}

// Verify the ref is a full semantic version tag.
if err := verifyTrustedBuilderRef(id, workflowTag); err != nil {
if err := verifyTrustedBuilderRef(ctx, id, workflowTag, builderOpts.TagResolver); err != nil {
return nil, byob, err
}

Expand Down Expand Up @@ -171,19 +172,26 @@ func isTrustedDelegatorBuilder(certBuilder *utils.TrustedBuilderID, trustedBuild
// Only allow `@refs/heads/main` for the builder and the e2e tests that need to work at HEAD.
// This lets us use the pre-build builder binary generated during release (release happen at main).
// For other projects, we only allow semantic versions that map to a release.
func verifyTrustedBuilderRef(id *WorkflowIdentity, ref string) error {
if (id.SourceRepository == trustedBuilderRepository ||
id.SourceRepository == e2eTestRepository) &&
options.TestingEnabled() {
// Allow verification on the main branch to support e2e tests.
if ref == "refs/heads/main" {
return nil
}
func verifyTrustedBuilderRef(ctx context.Context, id *WorkflowIdentity, ref string, resolver utils.TagResolver) error {
testing := (id.SourceRepository == trustedBuilderRepository ||
id.SourceRepository == e2eTestRepository) && options.TestingEnabled()

if testing && ref == "refs/heads/main" {
return nil
}

return utils.IsValidBuilderTag(ref, true)
if utils.IsSHA(ref) {
if resolver == nil {
return fmt.Errorf("%w: SHA pinning requires a tag resolver; set GITHUB_TOKEN or provide a resolver", serrors.ErrorInvalidRef)
}
parts := strings.SplitN(id.SourceRepository, "/", 2)
if len(parts) != 2 {
return fmt.Errorf("%w: cannot parse repository %q", serrors.ErrorInvalidRef, id.SourceRepository)
}
return utils.IsValidBuilderSHARef(ctx, ref, testing, resolver, parts[0], parts[1])
}

return utils.IsValidBuilderTag(ref, false)
return utils.IsValidBuilderTag(ref, testing)
}

func getExtension(cert *x509.Certificate, oid asn1.ObjectIdentifier, encoded bool) (string, error) {
Expand Down
84 changes: 82 additions & 2 deletions verifiers/internal/gha/builder_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gha

import (
"context"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
Expand Down Expand Up @@ -319,7 +320,7 @@ func Test_VerifyBuilderIdentity(t *testing.T) {
if tt.builderID != "" {
opts.ExpectedID = &tt.builderID
}
id, byob, err := VerifyBuilderIdentity(tt.workflow, opts, tt.defaults)
id, byob, err := VerifyBuilderIdentity(context.Background(), tt.workflow, opts, tt.defaults)
if byob != tt.byob {
t.Errorf("unexpected byob value:\n%s", cmp.Diff(tt.byob, byob))
}
Expand Down Expand Up @@ -452,6 +453,85 @@ func asStringPointer(s string) *string {
return &s
}

// fakeTagResolver is a TagResolver for testing.
type fakeTagResolver struct {
tags map[string][]string // sha → tag names
}

func (f *fakeTagResolver) TagsForCommitSHA(_ context.Context, _, _, sha string) ([]string, error) {
return f.tags[sha], nil
}

func Test_verifyTrustedBuilderRefSHA(t *testing.T) {
t.Parallel()

const validSHA = "abc0123456789abcdef0123456789abcdef01234"

tests := []struct {
name string
workflow *WorkflowIdentity
ref string
resolver utils.TagResolver
err error
}{
{
name: "SHA pinned to valid semver tag - pass",
workflow: &WorkflowIdentity{
SourceRepository: "slsa-framework/slsa-github-generator",
Issuer: certOidcIssuer,
},
ref: validSHA,
resolver: &fakeTagResolver{
tags: map[string][]string{validSHA: {"v1.2.3"}},
},
},
{
name: "SHA pinned to no tags - fail",
workflow: &WorkflowIdentity{
SourceRepository: "slsa-framework/slsa-github-generator",
Issuer: certOidcIssuer,
},
ref: validSHA,
resolver: &fakeTagResolver{
tags: map[string][]string{},
},
err: serrors.ErrorInvalidRef,
},
{
name: "SHA pinned to non-semver tag only - fail",
workflow: &WorkflowIdentity{
SourceRepository: "slsa-framework/slsa-github-generator",
Issuer: certOidcIssuer,
},
ref: validSHA,
resolver: &fakeTagResolver{
tags: map[string][]string{validSHA: {"main-build-123"}},
},
err: serrors.ErrorInvalidRef,
},
{
name: "SHA with nil resolver - fail",
workflow: &WorkflowIdentity{
SourceRepository: "slsa-framework/slsa-github-generator",
Issuer: certOidcIssuer,
},
ref: validSHA,
resolver: nil,
err: serrors.ErrorInvalidRef,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := verifyTrustedBuilderRef(context.Background(), tt.workflow, tt.ref, tt.resolver)
if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" {
t.Errorf("unexpected error (-want +got):\n%s", diff)
}
})
}
}

func Test_verifyTrustedBuilderID(t *testing.T) {
t.Parallel()
tests := []struct {
Expand Down Expand Up @@ -815,7 +895,7 @@ func Test_verifyTrustedBuilderRef(t *testing.T) {
t.Setenv("SLSA_VERIFIER_TESTING", "")
}

err := verifyTrustedBuilderRef(&wf, tt.builderRef)
err := verifyTrustedBuilderRef(context.Background(), &wf, tt.builderRef, nil)
if !errCmp(err, tt.expected) {
t.Error(cmp.Diff(err, tt.expected, cmpopts.EquateErrors()))
}
Expand Down
4 changes: 2 additions & 2 deletions verifiers/internal/gha/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,13 +294,13 @@ func (n *Npm) verifyPackageVersion(version *string) error {
return nil
}

func (n *Npm) verifyBuilderID(
func (n *Npm) verifyBuilderID(ctx context.Context,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
defaultBuilders map[string]bool,
) (*utils.TrustedBuilderID, error) {
// Verify certificate information.
builder, err := verifyNpmEnvAndCert(
builder, err := verifyNpmEnvAndCert(ctx,
n.ProvenanceEnvelope(),
n.ProvenanceLeafCertificate(),
provenanceOpts, builderOpts,
Expand Down
16 changes: 8 additions & 8 deletions verifiers/internal/gha/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (v *GHAVerifier) IsAuthoritativeFor(builderID string) bool {
return strings.HasPrefix(builderID, httpsGithubCom)
}

func verifyEnvAndCert(env *dsse.Envelope,
func verifyEnvAndCert(ctx context.Context, env *dsse.Envelope,
cert *x509.Certificate,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
Expand All @@ -56,7 +56,7 @@ func verifyEnvAndCert(env *dsse.Envelope,
}

// Verify the builder identity.
verifiedBuilderID, byob, err := VerifyBuilderIdentity(workflowInfo, builderOpts, defaultBuilders)
verifiedBuilderID, byob, err := VerifyBuilderIdentity(ctx, workflowInfo, builderOpts, defaultBuilders)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -97,7 +97,7 @@ func verifyEnvAndCert(env *dsse.Envelope,
return r, verifiedBuilderID, nil
}

func verifyNpmEnvAndCert(env *dsse.Envelope,
func verifyNpmEnvAndCert(ctx context.Context, env *dsse.Envelope,
cert *x509.Certificate,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
Expand All @@ -118,7 +118,7 @@ func verifyNpmEnvAndCert(env *dsse.Envelope,
delegatorBuilderOpts := options.BuilderOpts{
ExpectedID: &expectedDelegatorWorkflow,
}
trustedBuilderID, byob, err := VerifyBuilderIdentity(workflowInfo, &delegatorBuilderOpts, defaultBuilders)
trustedBuilderID, byob, err := VerifyBuilderIdentity(ctx, workflowInfo, &delegatorBuilderOpts, defaultBuilders)
// We accept a non-trusted builder for the default npm builder
// that uses npm CLI.
if err != nil && !errors.Is(err, serrors.ErrorUntrustedReusableWorkflow) {
Expand Down Expand Up @@ -237,7 +237,7 @@ func (v *GHAVerifier) VerifyArtifact(ctx context.Context,
return nil, nil, err
}

return verifyEnvAndCert(signedAtt.Envelope, signedAtt.SigningCert,
return verifyEnvAndCert(ctx, signedAtt.Envelope, signedAtt.SigningCert,
provenanceOpts, builderOpts,
utils.MergeMaps(defaultArtifactTrustedReusableWorkflows, defaultBYOBReusableWorkflows))
}
Expand All @@ -263,7 +263,7 @@ func (v *GHAVerifier) VerifyGithubAttestation(ctx context.Context,
return nil, nil, err
}

return verifyEnvAndCert(signedAtt.Envelope, signedAtt.SigningCert,
return verifyEnvAndCert(ctx, signedAtt.Envelope, signedAtt.SigningCert,
provenanceOpts, builderOpts, map[string]bool{})
}

Expand Down Expand Up @@ -323,7 +323,7 @@ func (v *GHAVerifier) VerifyImage(ctx context.Context,
fmt.Fprintf(os.Stderr, "unexpected error getting certificate from OCI registry %s", err)
continue
}
verifiedProvenance, builderID, err = verifyEnvAndCert(env,
verifiedProvenance, builderID, err = verifyEnvAndCert(ctx, env,
cert, provenanceOpts, builderOpts,
defaultContainerTrustedReusableWorkflows)
if err == nil {
Expand Down Expand Up @@ -365,7 +365,7 @@ func (v *GHAVerifier) VerifyNpmPackage(ctx context.Context,
}

// Verify provenance builder information.
builder, err := npm.verifyBuilderID(
builder, err := npm.verifyBuilderID(ctx,
provenanceOpts, builderOpts,
defaultBYOBReusableWorkflows)
if err != nil {
Expand Down
26 changes: 26 additions & 0 deletions verifiers/utils/builder.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package utils

import (
"context"
"fmt"
"strings"

Expand Down Expand Up @@ -151,6 +152,31 @@ func IsValidBuilderTag(ref string, testing bool) error {
return nil
}

// IsValidBuilderSHARef validates a commit SHA by resolving it to a semver tag
// via the GitHub API. owner and repo identify the builder repository.
func IsValidBuilderSHARef(ctx context.Context, sha string, testing bool,
resolver TagResolver, owner, repo string,
) error {
if !IsSHA(sha) {
return fmt.Errorf("%w: %q is not a valid commit SHA", serrors.ErrorInvalidRef, sha)
}
tags, err := resolver.TagsForCommitSHA(ctx, owner, repo, sha)
if err != nil {
return fmt.Errorf("%w: resolving SHA %s: %w", serrors.ErrorInvalidRef, sha, err)
}
if len(tags) == 0 {
return fmt.Errorf("%w: SHA %s has no associated tags in %s/%s",
serrors.ErrorInvalidRef, sha, owner, repo)
}
for _, tag := range tags {
if err := IsValidBuilderTag("refs/tags/"+tag, testing); err == nil {
return nil
}
}
return fmt.Errorf("%w: SHA %s has no valid semver tag in %s/%s",
serrors.ErrorInvalidRef, sha, owner, repo)
}

func IsValidJreleaserBuilderTag(ref string) error {
// Extract the pin.
pin, err := TagFromGitRef(ref)
Expand Down
Loading