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

fix: Verify the TRW tag is a semver tag #619

Merged
merged 4 commits into from
May 26, 2023
Merged
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
34 changes: 2 additions & 32 deletions verifiers/internal/gha/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"fmt"
"strings"

"golang.org/x/mod/semver"

fulcio "github.com/sigstore/fulcio/pkg/certificate"
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
"github.com/slsa-framework/slsa-verifier/v2/options"
Expand Down Expand Up @@ -170,38 +168,10 @@ func verifyTrustedBuilderRef(id *WorkflowIdentity, ref string) error {
return nil
}

// Extract the tag.
pin, err := utils.TagFromGitHubRef(ref)
if err != nil {
return err
}

// Tags on trusted repositories should be a valid semver with version
// core including all three parts and no build identifier.
versionCore := strings.Split(pin, "-")[0]
if !semver.IsValid(pin) ||
len(strings.Split(versionCore, ".")) != 3 ||
semver.Build(pin) != "" {
return fmt.Errorf("%w: %s: version tag not valid", serrors.ErrorInvalidRef, pin)
}

return nil
return utils.IsValidBuilderTag(ref, true)
}

// Extract the pin.
pin, err := utils.TagFromGitHubRef(ref)
if err != nil {
return err
}

// Valid semver of the form vX.Y.Z with no metadata.
if !(semver.IsValid(pin) &&
len(strings.Split(pin, ".")) == 3 &&
semver.Prerelease(pin) == "" &&
semver.Build(pin) == "") {
return fmt.Errorf("%w: %s: not of the form vX.Y.Z", serrors.ErrorInvalidRef, pin)
}
return nil
return utils.IsValidBuilderTag(ref, false)
}

func getExtension(cert *x509.Certificate, oid asn1.ObjectIdentifier, encoded bool) (string, error) {
Expand Down
16 changes: 16 additions & 0 deletions verifiers/internal/gha/provenance.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,19 @@ func VerifyNpmPackageProvenance(env *dsselib.Envelope, workflow *WorkflowIdentit
return nil
}

func isValidDelegatorBuilderID(prov slsaprovenance.Provenance) error {
// Verify the TRW was referenced at a proper tag by the user.
id, err := prov.BuilderID()
if err != nil {
return err
}
parts := strings.Split(id, "@")
if len(parts) != 2 {
return fmt.Errorf("%w: %s", serrors.ErrorInvalidBuilderID, id)
}
return utils.IsValidBuilderTag(parts[1], false)
}

func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceOpts, byob bool,
) error {
prov, err := slsaprovenance.ProvenanceFromEnvelope(env)
Expand All @@ -286,6 +299,9 @@ func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceO

// Verify Builder ID.
if byob {
if err := isValidDelegatorBuilderID(prov); err != nil {
return err
}
// Note: `provenanceOpts.ExpectedBuilderID` is provided by the user.
if err := verifyBuilderIDLooseMatch(prov, provenanceOpts.ExpectedBuilderID); err != nil {
return err
Expand Down
95 changes: 95 additions & 0 deletions verifiers/internal/gha/provenance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,101 @@ func Test_verifySourceURI(t *testing.T) {
}
}

func Test_isValidDelegatorBuilderID(t *testing.T) {
t.Parallel()
tests := []struct {
name string
prov *intoto.ProvenanceStatement
err error
}{
{
name: "no @",
prov: &intoto.ProvenanceStatement{
Predicate: slsa02.ProvenancePredicate{
Builder: slsacommon.ProvenanceBuilder{
ID: "some/builderID",
},
},
},
err: serrors.ErrorInvalidBuilderID,
},
{
name: "invalid ref",
prov: &intoto.ProvenanceStatement{
Predicate: slsa02.ProvenancePredicate{
Builder: slsacommon.ProvenanceBuilder{
ID: "some/builderID@v1.2.3",
},
},
},
err: serrors.ErrorInvalidRef,
},
{
name: "invalid ref not tag",
prov: &intoto.ProvenanceStatement{
Predicate: slsa02.ProvenancePredicate{
Builder: slsacommon.ProvenanceBuilder{
ID: "some/builderID@refs/head/v1.2.3",
},
},
},
err: serrors.ErrorInvalidRef,
},
{
name: "invalid ref not full semver",
prov: &intoto.ProvenanceStatement{
Predicate: slsa02.ProvenancePredicate{
Builder: slsacommon.ProvenanceBuilder{
ID: "some/builderID@refs/heads/v1.2",
},
},
},
err: serrors.ErrorInvalidRef,
},
{
name: "valid builder",
prov: &intoto.ProvenanceStatement{
Predicate: slsa02.ProvenancePredicate{
Builder: slsacommon.ProvenanceBuilder{
ID: "some/builderID@refs/tags/v1.2.3",
},
},
},
},
}
for _, tt := range tests {
tt := tt // Re-initializing variable so it is not changed while executing the closure below
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

prov := &v02.ProvenanceV02{
ProvenanceStatement: tt.prov,
}

err := isValidDelegatorBuilderID(prov)
if !errCmp(err, tt.err) {
t.Errorf(cmp.Diff(err, tt.err))
}

// Update to v1 SLSA provenance.
prov1 := &v1.ProvenanceV1{
Predicate: slsa1.ProvenancePredicate{
RunDetails: slsa1.ProvenanceRunDetails{
Builder: slsa1.Builder{
ID: tt.prov.Predicate.Builder.ID,
},
},
},
}

err = isValidDelegatorBuilderID(prov1)
if !errCmp(err, tt.err) {
t.Errorf(cmp.Diff(err, tt.err))
}
})
}
}

func Test_verifyBuilderIDExactMatch(t *testing.T) {
t.Parallel()
tests := []struct {
Expand Down
30 changes: 30 additions & 0 deletions verifiers/utils/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"strings"

"golang.org/x/mod/semver"

serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
)

Expand Down Expand Up @@ -131,3 +133,31 @@ func TagFromGitHubRef(ref string) (string, error) {
}
return strings.TrimPrefix(ref, "refs/tags/"), nil
}

func IsValidBuilderTag(ref string, testing bool) error {
// Extract the pin.
pin, err := TagFromGitHubRef(ref)
if err != nil {
return err
}

if testing {
// Tags on trusted repositories should be a valid semver with version
// core including all three parts and no build identifier.
versionCore := strings.Split(pin, "-")[0]
if !semver.IsValid(pin) ||
len(strings.Split(versionCore, ".")) != 3 ||
semver.Build(pin) != "" {
return fmt.Errorf("%w: %s: version tag not valid", serrors.ErrorInvalidRef, pin)
}
}

// Valid semver of the form vX.Y.Z with no metadata.
if !semver.IsValid(pin) ||
len(strings.Split(pin, ".")) != 3 ||
semver.Prerelease(pin) != "" ||
semver.Build(pin) != "" {
return fmt.Errorf("%w: %s: not of the form vX.Y.Z", serrors.ErrorInvalidRef, pin)
}
return nil
}