Skip to content

Commit

Permalink
feat: support builderID matching with or without semver for GHA (#257)
Browse files Browse the repository at this point in the history
* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update
  • Loading branch information
laurentsimon committed Sep 15, 2022
1 parent cddba70 commit 533d347
Show file tree
Hide file tree
Showing 16 changed files with 568 additions and 295 deletions.
300 changes: 190 additions & 110 deletions cli/slsa-verifier/main_test.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cli/slsa-verifier/verify/verify_artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type VerifyArtifactCommand struct {
PrintProvenance bool
}

func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (*utils.BuilderID, error) {
func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (*utils.TrustedBuilderID, error) {
artifactHash, err := getArtifactHash(artifacts[0])
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion cli/slsa-verifier/verify/verify_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type VerifyImageCommand struct {
DigestFn ComputeDigestFn
}

func (c *VerifyImageCommand) Exec(ctx context.Context, artifacts []string) (*utils.BuilderID, error) {
func (c *VerifyImageCommand) Exec(ctx context.Context, artifacts []string) (*utils.TrustedBuilderID, error) {
artifactImage := artifacts[0]
// Retrieve the image digest.
if c.DigestFn == nil {
Expand Down
1 change: 1 addition & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var (
ErrorUntrustedReusableWorkflow = errors.New("untrusted reusable workflow")
ErrorNoValidRekorEntries = errors.New("could not find a matching valid signature entry")
ErrorVerifierNotSupported = errors.New("no verifier support the builder")
ErrorInvalidOIDCIssuer = errors.New("invalid OIDC issuer")
ErrorNotSupported = errors.New("not supported")
ErrorInvalidFormat = errors.New("invalid format")
ErrorInvalidPEM = errors.New("invalid PEM")
Expand Down
4 changes: 2 additions & 2 deletions register/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ type SLSAVerifier interface {
provenance []byte, artifactHash string,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
) ([]byte, *utils.BuilderID, error)
) ([]byte, *utils.TrustedBuilderID, 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, *utils.BuilderID, error)
) ([]byte, *utils.TrustedBuilderID, error)
}

func RegisterVerifier(name string, verifier SLSAVerifier) {
Expand Down
10 changes: 5 additions & 5 deletions verifiers/internal/gcb/provenance.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ func isValidBuilderID(id string) error {
return serrors.ErrorInvalidBuilderID
}

func validateRecipeType(builderID utils.BuilderID, recipeType string) error {
func validateRecipeType(builderID utils.TrustedBuilderID, recipeType string) error {
var err error
v := builderID.Version()
switch v {
Expand Down Expand Up @@ -262,7 +262,7 @@ func validateRecipeType(builderID utils.BuilderID, recipeType string) error {
// - in the recipe type
// - the recipe argument type
// - the predicate builder ID.
func (self *Provenance) VerifyBuilder(builderOpts *options.BuilderOpts) (*utils.BuilderID, error) {
func (self *Provenance) VerifyBuilder(builderOpts *options.BuilderOpts) (*utils.TrustedBuilderID, error) {
if err := self.isVerified(); err != nil {
return nil, err
}
Expand All @@ -275,14 +275,14 @@ func (self *Provenance) VerifyBuilder(builderOpts *options.BuilderOpts) (*utils.
return nil, err
}

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

// Validate with user-provided value.
if builderOpts != nil && builderOpts.ExpectedID != nil {
if err := provBuilderID.Matches(*builderOpts.ExpectedID); err != nil {
if err := provBuilderID.Matches(*builderOpts.ExpectedID, false); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -346,7 +346,7 @@ func (self *Provenance) VerifySubjectDigest(expectedHash string) error {
}

// Verify source URI in provenance statement.
func (self *Provenance) VerifySourceURI(expectedSourceURI string, builderID utils.BuilderID) error {
func (self *Provenance) VerifySourceURI(expectedSourceURI string, builderID utils.TrustedBuilderID) error {
if err := self.isVerified(); err != nil {
return err
}
Expand Down
6 changes: 3 additions & 3 deletions verifiers/internal/gcb/provenance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ func Test_VerifyBuilder(t *testing.T) {
panic("outBuilderID is nil")
}

if err := outBuilderID.Matches(tt.builderID); err != nil {
if err := outBuilderID.Matches(tt.builderID, false); err != nil {
t.Errorf(fmt.Sprintf("matches failed: %v", err))
}
})
Expand Down Expand Up @@ -290,7 +290,7 @@ func Test_validateRecipeType(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

builderID, err := utils.BuilderIDNew(tt.builderID)
builderID, err := utils.TrustedBuilderIDNew(tt.builderID)
if err != nil {
panic(fmt.Errorf("BuilderIDNew: %w", err))
}
Expand Down Expand Up @@ -416,7 +416,7 @@ func Test_VerifySourceURI(t *testing.T) {
panic(fmt.Errorf("ProvenanceFromBytes: %w", err))
}

builderID, err := utils.BuilderIDNew(tt.builderID)
builderID, err := utils.TrustedBuilderIDNew(tt.builderID)
if err != nil {
panic(fmt.Errorf("BuilderIDNew: %w", err))
}
Expand Down
4 changes: 2 additions & 2 deletions verifiers/internal/gcb/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (v *GCBVerifier) VerifyArtifact(ctx context.Context,
provenance []byte, artifactHash string,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
) ([]byte, *utils.BuilderID, error) {
) ([]byte, *utils.TrustedBuilderID, error) {
return nil, nil, serrors.ErrorNotSupported
}

Expand All @@ -44,7 +44,7 @@ func (v *GCBVerifier) VerifyImage(ctx context.Context,
provenance []byte, artifactImage string,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
) ([]byte, *utils.BuilderID, error) {
) ([]byte, *utils.TrustedBuilderID, error) {
prov, err := ProvenanceFromBytes(provenance)
if err != nil {
return nil, nil, err
Expand Down
64 changes: 42 additions & 22 deletions verifiers/internal/gha/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

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

var (
Expand All @@ -33,63 +34,81 @@ var defaultContainerTrustedReusableWorkflows = map[string]bool{
func VerifyWorkflowIdentity(id *WorkflowIdentity,
builderOpts *options.BuilderOpts, source string,
defaultBuilders map[string]bool,
) (string, error) {
) (*utils.TrustedBuilderID, error) {
// cert URI path is /org/repo/path/to/workflow@ref
workflowPath := strings.SplitN(id.JobWobWorkflowRef, "@", 2)
if len(workflowPath) < 2 {
return "", fmt.Errorf("%w: workflow uri: %s", serrors.ErrorMalformedURI, id.JobWobWorkflowRef)
return nil, fmt.Errorf("%w: workflow uri: %s", serrors.ErrorMalformedURI, id.JobWobWorkflowRef)
}

// Trusted workflow verification by name.
// Verify trusted workflow.
reusableWorkflowPath := strings.Trim(workflowPath[0], "/")
builderID, err := verifyTrustedBuilderID(reusableWorkflowPath,
reusableWorkflowTag := strings.Trim(workflowPath[1], "/")
builderID, err := verifyTrustedBuilderID(reusableWorkflowPath, reusableWorkflowTag,
builderOpts.ExpectedID, defaultBuilders)
if err != nil {
return "", err
return nil, err
}

// Verify the ref.
if err := verifyTrustedBuilderRef(id, strings.Trim(workflowPath[1], "/")); err != nil {
return "", err
// Verify the ref is a full semantic version tag.
if err := verifyTrustedBuilderRef(id, reusableWorkflowTag); err != nil {
return nil, err
}

// Issuer verification.
if !strings.EqualFold(id.Issuer, certOidcIssuer) {
return "", fmt.Errorf("untrusted token issuer: %s", id.Issuer)
return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidOIDCIssuer, id.Issuer)
}

// The caller repository in the x509 extension is not fully qualified. It only contains
// {org}/{repository}.
expectedSource := strings.TrimPrefix(source, "git+https://")
expectedSource = strings.TrimPrefix(expectedSource, "github.com/")
if !strings.EqualFold(id.CallerRepository, expectedSource) {
return "", fmt.Errorf("%w: expected source '%s', got '%s'", serrors.ErrorMismatchSource,
return nil, fmt.Errorf("%w: expected source '%s', got '%s'", serrors.ErrorMismatchSource,
expectedSource, id.CallerRepository)
}

// Return the builder and its tag.
// Note: the tag has the format `refs/tags/v1.2.3`.
return builderID, nil
}

// Verifies the builder ID at path against an expected builderID.
// If an expected builderID is not provided, uses the defaultBuilders.
func verifyTrustedBuilderID(path string, builderID *string, defaultBuilders map[string]bool) (string, error) {
func verifyTrustedBuilderID(certPath, certTag string, expectedBuilderID *string, defaultBuilders map[string]bool) (*utils.TrustedBuilderID, error) {
var trustedBuilderID *utils.TrustedBuilderID
var err error
certBuilderName := "https://github.com/" + certPath
// WARNING: we don't validate the tag here, because we need to allow
// refs/heads/main for e2e tests. See verifyTrustedBuilderRef().
// No builder ID provided by user: use the default trusted workflows.
if builderID == nil || *builderID == "" {
if _, ok := defaultBuilders[path]; !ok {
return "", fmt.Errorf("%w: %s got %t", serrors.ErrorUntrustedReusableWorkflow, path, builderID == nil)
if expectedBuilderID == nil || *expectedBuilderID == "" {
if _, ok := defaultBuilders[certPath]; !ok {
return nil, fmt.Errorf("%w: %s got %t", serrors.ErrorUntrustedReusableWorkflow, certPath, expectedBuilderID == nil)
}
// Construct the builderID using the certificate's builder's name and tag.
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderName + "@" + certTag)
if err != nil {
return nil, err
}
} else {
// Verify the builderID.
// We only accept IDs on github.com.
url := "https://github.com/" + path
if url != *builderID {
return "", fmt.Errorf("%w: expected buildID '%s', got '%s'", serrors.ErrorUntrustedReusableWorkflow,
*builderID, url)
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderName + "@" + certTag)
if err != nil {
return nil, err
}

// BuilderID provided by user should match the certificate.
// Note: the certificate builderID has the form `name@refs/tags/v1.2.3`,
// so we pass `allowRef = true`.
if err := trustedBuilderID.Matches(*expectedBuilderID, true); err != nil {
return nil, fmt.Errorf("%w: %v", serrors.ErrorUntrustedReusableWorkflow, err)
}
}

return "https://github.com/" + path, nil
return trustedBuilderID, nil
}

// Only allow `@refs/heads/main` for the builder and the e2e tests that need to work at HEAD.
Expand All @@ -102,12 +121,13 @@ func verifyTrustedBuilderRef(id *WorkflowIdentity, ref string) error {
return nil
}

if !strings.HasPrefix(ref, "refs/tags/") {
return fmt.Errorf("%w: %s: not of the form 'refs/tags/name'", serrors.ErrorInvalidRef, ref)
// Extract the pin.
pin, err := utils.TagFromGitHubRef(ref)
if err != nil {
return err
}

// Valid semver of the form vX.Y.Z with no metadata.
pin := strings.TrimPrefix(ref, "refs/tags/")
if !(semver.IsValid(pin) &&
len(strings.Split(pin, ".")) == 3 &&
semver.Prerelease(pin) == "" &&
Expand Down
Loading

0 comments on commit 533d347

Please sign in to comment.