Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support builderID matching with or without semver for GCB #256

Merged
merged 20 commits into from
Sep 13, 2022
70 changes: 44 additions & 26 deletions cli/slsa-verifier/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (

"github.com/slsa-framework/slsa-verifier/cli/slsa-verifier/verify"
serrors "github.com/slsa-framework/slsa-verifier/errors"
"github.com/slsa-framework/slsa-verifier/verifiers/container"
"github.com/slsa-framework/slsa-verifier/verifiers/utils/container"
)

func errCmp(e1, e2 error) bool {
Expand Down Expand Up @@ -511,10 +511,12 @@ func Test_runVerifyArtifactPath(t *testing.T) {
return
}

if tt.outBuilderID != "" && outBuilderID != tt.outBuilderID {
t.Errorf(cmp.Diff(outBuilderID, tt.outBuilderID))
// Validate against test's expected builderID, if provided.
if tt.outBuilderID != "" && tt.outBuilderID != outBuilderID.String() {
t.Errorf(cmp.Diff(tt.outBuilderID, outBuilderID.String()))
}

// TODO: verify using Matches().
}
})
}
Expand Down Expand Up @@ -675,9 +677,12 @@ func Test_runVerifyGHAArtifactImage(t *testing.T) {
return
}

if tt.outBuilderID != "" && outBuilderID != tt.outBuilderID {
t.Errorf(cmp.Diff(outBuilderID, tt.outBuilderID))
// Validate against test's expected builderID, if provided.
if tt.outBuilderID != "" && tt.outBuilderID != outBuilderID.String() {
t.Errorf(cmp.Diff(tt.outBuilderID, outBuilderID.String()))
}

// TODO: verify using Matches().
}
})
}
Expand Down Expand Up @@ -858,7 +863,10 @@ func Test_runVerifyGCBArtifactImage(t *testing.T) {

for _, v := range checkVersions {
semver := path.Base(v)
builderID := pString(builder + "@" + semver)
// For each test, we run 2 sub-tests:
// 1. With the the full builderID including the semver.
// 2. With only the name of the builder.
builderIDs := []string{builder + "@" + semver, builder}
provenance := filepath.Clean(filepath.Join(TEST_DIR, v, tt.provenance))
image := tt.artifact
var fn verify.ComputeDigestFn
Expand All @@ -868,7 +876,7 @@ func Test_runVerifyGCBArtifactImage(t *testing.T) {
if !tt.noversion {
panic("builderID set but not noversion option")
}
builderID = tt.pBuilderID
builderIDs = []string{*tt.pBuilderID}
}

// Select the right image according to the builder version we are testing.
Expand All @@ -889,30 +897,40 @@ func Test_runVerifyGCBArtifactImage(t *testing.T) {
fn = localDigestComputeFn
}

cmd := verify.VerifyImageCommand{
SourceURI: tt.source,
SourceBranch: nil,
BuilderID: builderID,
SourceTag: nil,
SourceVersionTag: nil,
DigestFn: fn,
ProvenancePath: &provenance,
}
// We run the test for each builderID, in order to test
// a builderID provided by name and one containing both the name
// and semver.
for _, bid := range builderIDs {
cmd := verify.VerifyImageCommand{
SourceURI: tt.source,
SourceBranch: nil,
BuilderID: &bid,
SourceTag: nil,
SourceVersionTag: nil,
DigestFn: fn,
ProvenancePath: &provenance,
}

outBuilderID, err := cmd.Exec(context.Background(), []string{image})
outBuilderID, err := cmd.Exec(context.Background(), []string{image})

if !errCmp(err, tt.err) {
t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors()))
}
if !errCmp(err, tt.err) {
t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors()))
}

if err != nil {
return
}
if err != nil {
return
}

if tt.outBuilderID != "" && outBuilderID != tt.outBuilderID {
t.Errorf(cmp.Diff(outBuilderID, tt.outBuilderID))
}
// Validate against test's expected builderID, if provided.
if tt.outBuilderID != "" && tt.outBuilderID != outBuilderID.String() {
t.Errorf(cmp.Diff(tt.outBuilderID, outBuilderID.String()))
}

// Validate against builderID we generated automatically.
if err := outBuilderID.Matches(bid); err != nil {
t.Errorf(fmt.Sprintf("matches failed: %v", err))
}
}
}
})
}
Expand Down
9 changes: 5 additions & 4 deletions cli/slsa-verifier/verify/verify_artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

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

// Note: nil branch, tag, version-tag and builder-id means we ignore them during verification.
Expand All @@ -38,10 +39,10 @@ type VerifyArtifactCommand struct {
PrintProvenance bool
}

func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (string, error) {
func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (*utils.BuilderID, error) {
artifactHash, err := getArtifactHash(artifacts[0])
if err != nil {
return "", err
return nil, err
}

provenanceOpts := &options.ProvenanceOpts{
Expand All @@ -59,12 +60,12 @@ func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (s

provenance, err := os.ReadFile(c.ProvenancePath)
if err != nil {
return "", err
return nil, err
}

verifiedProvenance, outBuilderID, err := verifiers.VerifyArtifact(ctx, provenance, artifactHash, provenanceOpts, builderOpts)
if err != nil {
return "", err
return nil, err
}

if c.PrintProvenance {
Expand Down
13 changes: 7 additions & 6 deletions cli/slsa-verifier/verify/verify_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import (

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

type ComputeDigestFn func(string) (string, error)
Expand All @@ -40,20 +41,20 @@ type VerifyImageCommand struct {
DigestFn ComputeDigestFn
}

func (c *VerifyImageCommand) Exec(ctx context.Context, artifacts []string) (string, error) {
func (c *VerifyImageCommand) Exec(ctx context.Context, artifacts []string) (*utils.BuilderID, error) {
artifactImage := artifacts[0]
// Retrieve the image digest.
if c.DigestFn == nil {
c.DigestFn = container.GetImageDigest
}
digest, err := c.DigestFn(artifactImage)
if err != nil {
return "", err
return nil, err
}

// Verify that the reference is immutable.
if err := container.ValidateArtifactReference(artifactImage, digest); err != nil {
return "", err
return nil, err
}

provenanceOpts := &options.ProvenanceOpts{
Expand All @@ -73,13 +74,13 @@ func (c *VerifyImageCommand) Exec(ctx context.Context, artifacts []string) (stri
if c.ProvenancePath != nil {
provenance, err = os.ReadFile(*c.ProvenancePath)
if err != nil {
return "", err
return nil, err
}
}

verifiedProvenance, outBuilderID, err := verifiers.VerifyImage(ctx, artifacts[0], provenance, provenanceOpts, builderOpts)
if err != nil {
return "", err
return nil, err
}

if c.PrintProvenance {
Expand Down
7 changes: 4 additions & 3 deletions register/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

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

var SLSAVerifiers = make(map[string]SLSAVerifier)
Expand All @@ -12,21 +13,21 @@ type SLSAVerifier interface {
// IsAuthoritativeFor checks whether a verifier can
// verify provenance for a given builder identified by its
// `BuilderID`.
IsAuthoritativeFor(builderID string) bool
IsAuthoritativeFor(builderIDName string) bool

// VerifyArtifact verifies a provenance for a supplied artifact.
VerifyArtifact(ctx context.Context,
provenance []byte, artifactHash string,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
) ([]byte, string, error)
) ([]byte, *utils.BuilderID, error)

// VerifyImage verifies a provenance for a supplied OCI image.
VerifyImage(ctx context.Context,
provenance []byte, artifactImage string,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
) ([]byte, string, error)
) ([]byte, *utils.BuilderID, error)
}

func RegisterVerifier(name string, verifier SLSAVerifier) {
Expand Down
60 changes: 26 additions & 34 deletions verifiers/internal/gcb/provenance.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
serrors "github.com/slsa-framework/slsa-verifier/errors"
"github.com/slsa-framework/slsa-verifier/options"
"github.com/slsa-framework/slsa-verifier/verifiers/internal/gcb/keys"
"github.com/slsa-framework/slsa-verifier/verifiers/utils"
)

var GCBBuilderIDs = []string{
Expand Down Expand Up @@ -221,28 +222,18 @@ func isValidBuilderID(id string) error {
return serrors.ErrorInvalidBuilderID
}

func getBuilderVersion(builderID string) (string, error) {
parts := strings.Split(builderID, "@")
if len(parts) != 2 {
return "", fmt.Errorf("%w: '%s'", serrors.ErrorInvalidFormat, parts)
}
return parts[1], nil
}

func validateRecipeType(builderID, recipeType string) error {
v, err := getBuilderVersion(builderID)
if err != nil {
return err
}
func validateRecipeType(builderID utils.BuilderID, recipeType string) error {
var err error
v := builderID.Version()
switch v {
case "v0.2":
// In this version, the recipe type should be the same as
// the builder ID.
if builderID == recipeType {
if builderID.String() == recipeType {
return nil
}
err = fmt.Errorf("%w: expected '%s', got '%s'",
serrors.ErrorInvalidRecipe, builderID, recipeType)
serrors.ErrorInvalidRecipe, builderID.String(), recipeType)

case "v0.3":
// In this version, two recipe types are allowed, depending how the
Expand Down Expand Up @@ -270,50 +261,54 @@ func validateRecipeType(builderID, recipeType string) error {
// VerifyBuilder verifies the builder in the DSSE payload:
// - in the recipe type
// - the recipe argument type
// - the predicate builder ID
func (self *Provenance) VerifyBuilder(builderOpts *options.BuilderOpts) (string, error) {
// - the predicate builder ID.
func (self *Provenance) VerifyBuilder(builderOpts *options.BuilderOpts) (*utils.BuilderID, error) {
if err := self.isVerified(); err != nil {
return "", err
return nil, err
}

statement := self.verifiedIntotoStatement
predicateBuilderID := statement.Predicate.Builder.ID

// Sanity check the builderID.
if err := isValidBuilderID(predicateBuilderID); err != nil {
return "", err
return nil, err
}

provBuilderID, err := utils.BuilderIDNew(predicateBuilderID)
if err != nil {
return nil, err
}

// Validate with user-provided value.
if builderOpts != nil && builderOpts.ExpectedID != nil {
if *builderOpts.ExpectedID != predicateBuilderID {
return "", fmt.Errorf("%w: expected '%s', got '%s'", serrors.ErrorMismatchBuilderID,
*builderOpts.ExpectedID, predicateBuilderID)
if err := provBuilderID.Matches(*builderOpts.ExpectedID); err != nil {
return nil, err
}
}

// Valiate the recipe type.
if err := validateRecipeType(predicateBuilderID, statement.Predicate.Recipe.Type); err != nil {
return "", err
if err := validateRecipeType(*provBuilderID, statement.Predicate.Recipe.Type); err != nil {
return nil, err
}

// Validate the recipe argument type.
expectedType := "type.googleapis.com/google.devtools.cloudbuild.v1.Build"
args, ok := statement.Predicate.Recipe.Arguments.(map[string]interface{})
if !ok {
return "", fmt.Errorf("%w: recipe arguments is not a map", serrors.ErrorInvalidDssePayload)
return nil, fmt.Errorf("%w: recipe arguments is not a map", serrors.ErrorInvalidDssePayload)
}
ts, err := getAsString(args, "@type")
if err != nil {
return "", err
return nil, err
}

if ts != expectedType {
return "", fmt.Errorf("%w: expected '%s', got '%s'", serrors.ErrorMismatchBuilderID,
return nil, fmt.Errorf("%w: expected '%s', got '%s'", serrors.ErrorMismatchBuilderID,
expectedType, ts)
}

return predicateBuilderID, nil
return provBuilderID, nil
}

func getAsString(m map[string]interface{}, key string) (string, error) {
Expand Down Expand Up @@ -351,7 +346,7 @@ func (self *Provenance) VerifySubjectDigest(expectedHash string) error {
}

// Verify source URI in provenance statement.
func (self *Provenance) VerifySourceURI(expectedSourceURI, builderID string) error {
func (self *Provenance) VerifySourceURI(expectedSourceURI string, builderID utils.BuilderID) error {
if err := self.isVerified(); err != nil {
return err
}
Expand All @@ -366,11 +361,8 @@ func (self *Provenance) VerifySourceURI(expectedSourceURI, builderID string) err
expectedSourceURI = "https://" + expectedSourceURI
}

v, err := getBuilderVersion(builderID)
if err != nil {
return err
}

var err error
v := builderID.Version()
switch v {
case "v0.2":
// In v0.2, it uses format
Expand Down
Loading