Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
54 changes: 39 additions & 15 deletions verifiers/internal/gha/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,71 +67,95 @@ func VerifyCertficateSourceRepository(id *WorkflowIdentity,
func VerifyBuilderIdentity(id *WorkflowIdentity,
builderOpts *options.BuilderOpts,
defaultBuilders map[string]bool,
) (*utils.TrustedBuilderID, error) {
) (*utils.TrustedBuilderID, bool, error) {
// Issuer verification.
// NOTE: this is necessary before we do any further verification.
if id.Issuer != certOidcIssuer {
return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidOIDCIssuer, id.Issuer)
return nil, false, fmt.Errorf("%w: %s", serrors.ErrorInvalidOIDCIssuer, id.Issuer)
}

// cert URI path is /org/repo/path/to/workflow@ref
workflowPath := strings.SplitN(id.SubjectWorkflowRef, "@", 2)
if len(workflowPath) < 2 {
return nil, fmt.Errorf("%w: workflow uri: %s", serrors.ErrorMalformedURI, id.SubjectWorkflowRef)
return nil, false, fmt.Errorf("%w: workflow uri: %s", serrors.ErrorMalformedURI, id.SubjectWorkflowRef)
}

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

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

return builderID, nil
return builderID, byob, nil
}

// Verifies the builder ID at path against an expected builderID.
// If an expected builderID is not provided, uses the defaultBuilders.
func verifyTrustedBuilderID(certPath, certTag string, expectedBuilderID *string, defaultBuilders map[string]bool) (*utils.TrustedBuilderID, error) {
func verifyTrustedBuilderID(certPath, certTag string, expectedBuilderID *string, defaultTrustedBuilders map[string]bool) (*utils.TrustedBuilderID, bool, error) {
var trustedBuilderID *utils.TrustedBuilderID
var err error
certBuilderName := httpsGithubCom + 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 expectedBuilderID == nil || *expectedBuilderID == "" {
if _, ok := defaultBuilders[certPath]; !ok {
return nil, fmt.Errorf("%w: %s got %t", serrors.ErrorUntrustedReusableWorkflow, certPath, expectedBuilderID == nil)
if _, ok := defaultTrustedBuilders[certPath]; !ok {
return nil, false, 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, true)
if err != nil {
return nil, err
return nil, false, err
}
} else {
// Verify the builderID.
// We only accept IDs on github.com.
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderName+"@"+certTag, true)
if err != nil {
return nil, err
return nil, false, err
}

// Check if:
// - the builder in the cert is a BYOB builder
// - the caller trusts the BYOB builder
// If both are true, we don't match the user-provided builder ID
// against the certificate. Instead that will be done by the caller.
if isTrustedDelegatorBuilder(trustedBuilderID, defaultTrustedBuilders) {
return trustedBuilderID, true, nil
}

// BuilderID provided by user should match the certificate.
// Not a BYOB builder. 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.MatchesLoose(*expectedBuilderID, true); err != nil {
return nil, fmt.Errorf("%w: %v", serrors.ErrorUntrustedReusableWorkflow, err)
return nil, false, fmt.Errorf("%w: %v", serrors.ErrorUntrustedReusableWorkflow, err)
}
}

return trustedBuilderID, nil
return trustedBuilderID, false, nil
}

func isTrustedDelegatorBuilder(certBuilder *utils.TrustedBuilderID, trustedBuilders map[string]bool) bool {
for byobBuilder := range defaultBYOBReusableWorkflows {
// Check that the certificate builder is a BYOB workflow.
if err := certBuilder.MatchesLoose(httpsGithubCom+byobBuilder, true); err == nil {
// We found a delegator workflow that matches the certificate identity.
// Check that the BYOB builder is trusted by the caller.
if _, ok := trustedBuilders[byobBuilder]; !ok {
return false
}
return true
}
}
return false
}

// Only allow `@refs/heads/main` for the builder and the e2e tests that need to work at HEAD.
Expand Down
189 changes: 147 additions & 42 deletions verifiers/internal/gha/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
fulcio "github.com/sigstore/fulcio/pkg/certificate"
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
"github.com/slsa-framework/slsa-verifier/v2/options"
"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils"
)

func Test_VerifyBuilderIdentity(t *testing.T) {
Expand All @@ -24,6 +25,7 @@ func Test_VerifyBuilderIdentity(t *testing.T) {
builderID string
defaults map[string]bool
err error
byob bool
}{
{
name: "invalid job workflow ref",
Expand Down Expand Up @@ -85,6 +87,32 @@ func Test_VerifyBuilderIdentity(t *testing.T) {
defaults: defaultArtifactTrustedReusableWorkflows,
builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml",
},
{
name: "valid generic delegator builder without tag",
workflow: &WorkflowIdentity{
SourceRepository: trustedBuilderRepository,
SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b",
SubjectWorkflowRef: trustedBuilderRepository + "/.github/workflows/delegator_generic_slsa3.yml@refs/tags/v1.2.3",
BuildTrigger: "workflow_dispatch",
Issuer: "https://token.actions.githubusercontent.com",
},
defaults: defaultBYOBReusableWorkflows,
builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/delegator_generic_slsa3.yml",
byob: true,
},
{
name: "valid low-perms delegator builder with short tag",
workflow: &WorkflowIdentity{
SourceRepository: trustedBuilderRepository,
SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b",
SubjectWorkflowRef: trustedBuilderRepository + "/.github/workflows/delegator_lowperms-generic_slsa3.yml@refs/tags/v1.2.3",
BuildTrigger: "workflow_dispatch",
Issuer: "https://token.actions.githubusercontent.com",
},
defaults: defaultBYOBReusableWorkflows,
builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/[email protected]",
byob: true,
},
{
name: "valid main ref for e2e test",
workflow: &WorkflowIdentity{
Expand Down Expand Up @@ -271,7 +299,14 @@ func Test_VerifyBuilderIdentity(t *testing.T) {
if opts == nil {
opts = &options.BuilderOpts{}
}
id, err := VerifyBuilderIdentity(tt.workflow, opts, tt.defaults)
if tt.builderID != "" {
opts.ExpectedID = &tt.builderID
}
id, byob, err := VerifyBuilderIdentity(tt.workflow, opts, tt.defaults)
if byob != tt.byob {
t.Errorf(cmp.Diff(byob, tt.byob))
}

if !errCmp(err, tt.err) {
t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors()))
}
Expand All @@ -286,6 +321,51 @@ func Test_VerifyBuilderIdentity(t *testing.T) {
}
}

func Test_isTrustedDelegatorBuilder(t *testing.T) {
t.Parallel()
tests := []struct {
name string
certBuilderID string
trustedBuilderIDs map[string]bool
result bool
}{
{
name: "match byob",
certBuilderID: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/delegator_lowperms-generic_slsa3.yml@refs/tags/v1.6.0",
trustedBuilderIDs: map[string]bool{
"slsa-framework/slsa-github-generator/.github/workflows/delegator_lowperms-generic_slsa3.yml": true,
"slsa-framework/slsa-github-generator/.github/workflows/some_delegator.yml": true,
},
result: true,
},
{
name: "match byob but not caller trusted",
certBuilderID: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/delegator_lowperms-generic_slsa3.yml@refs/tags/v1.6.0",
trustedBuilderIDs: map[string]bool{
"slsa-framework/slsa-github-generator/.github/workflows/some_other_delegator.yml": true,
"slsa-framework/slsa-github-generator/.github/workflows/some_delegator.yml": true,
},
result: false,
},
}
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()

trustedBuilderID, err := utils.TrustedBuilderIDNew(tt.certBuilderID, true)
if err != nil {
t.Fatalf(err.Error())
}

res := isTrustedDelegatorBuilder(trustedBuilderID, tt.trustedBuilderIDs)
if res != tt.result {
t.Errorf(cmp.Diff(res, tt.result))
}
})
}
}

func Test_VerifyCertficateSourceRepository(t *testing.T) {
t.Parallel()
tests := []struct {
Expand Down Expand Up @@ -365,7 +445,8 @@ func Test_verifyTrustedBuilderID(t *testing.T) {
path string
tag string
defaults map[string]bool
expected error
err error
byob bool
}{
{
name: "default trusted short tag",
Expand All @@ -379,12 +460,34 @@ func Test_verifyTrustedBuilderID(t *testing.T) {
tag: "refs/tags/v1.2.3",
defaults: defaultArtifactTrustedReusableWorkflows,
},
{
name: "generic delegator workflow long tag",
path: trustedBuilderRepository + "/.github/workflows/delegator_generic_slsa3.yml",
id: asStringPointer(trustedBuilderRepository + "/.github/workflows/delegator_generic_slsa3.yml@refs/tags/v1.2.3"),
tag: "refs/tags/v1.2.3",
defaults: defaultBYOBReusableWorkflows,
byob: true,
},
{
name: "low perms delegator workflow short tag",
path: trustedBuilderRepository + "/.github/workflows/delegator_lowperms-generic_slsa3.yml",
id: asStringPointer(trustedBuilderRepository + "/.github/workflows/delegator_lowperms-generic_slsa3.yml@refs/tags/v1.2.3"),
tag: "v1.2.3",
defaults: defaultBYOBReusableWorkflows,
byob: true,
},
{
name: "low perms delegator workflow no ID provided",
path: trustedBuilderRepository + "/.github/workflows/delegator_lowperms-generic_slsa3.yml",
tag: "v1.2.3",
defaults: defaultBYOBReusableWorkflows,
},
{
name: "default mismatch against container defaults long tag",
path: trustedBuilderRepository + "/.github/workflows/generator_generic_slsa3.yml",
tag: "refs/tags/v1.2.3",
defaults: defaultContainerTrustedReusableWorkflows,
expected: serrors.ErrorUntrustedReusableWorkflow,
err: serrors.ErrorUntrustedReusableWorkflow,
},
{
name: "valid ID for GitHub builder short tag",
Expand All @@ -405,11 +508,11 @@ func Test_verifyTrustedBuilderID(t *testing.T) {
id: asStringPointer("https://github.com/some/repo/[email protected]"),
},
{
name: "valid long ID for GitHub builder short tag",
path: "some/repo/someBuilderID",
tag: "v1.2.3",
id: asStringPointer("https://github.com/some/repo/someBuilderID@refs/tags/v1.2.3"),
expected: serrors.ErrorUntrustedReusableWorkflow,
name: "valid long ID for GitHub builder short tag",
path: "some/repo/someBuilderID",
tag: "v1.2.3",
id: asStringPointer("https://github.com/some/repo/someBuilderID@refs/tags/v1.2.3"),
err: serrors.ErrorUntrustedReusableWorkflow,
},
{
name: "valid ID for GitHub builder long tag",
Expand All @@ -430,56 +533,58 @@ func Test_verifyTrustedBuilderID(t *testing.T) {
id: asStringPointer("https://github.com/some/repo/[email protected]"),
},
{
name: "valid long ID for GitHub builder short tag",
path: "some/repo/someBuilderID",
tag: "v1.2.3",
id: asStringPointer("https://github.com/some/repo/someBuilderID@refs/tags/v1.2.3"),
expected: serrors.ErrorUntrustedReusableWorkflow,
name: "valid long ID for GitHub builder short tag",
path: "some/repo/someBuilderID",
tag: "v1.2.3",
id: asStringPointer("https://github.com/some/repo/someBuilderID@refs/tags/v1.2.3"),
err: serrors.ErrorUntrustedReusableWorkflow,
},
{
name: "non GitHub builder ID long builder tag",
path: "some/repo/someBuilderID",
tag: "refs/tags/v1.2.3",
id: asStringPointer("https://not-github.zerozr99.workers.dev/some/repo/someBuilderID"),
expected: serrors.ErrorUntrustedReusableWorkflow,
name: "non GitHub builder ID long builder tag",
path: "some/repo/someBuilderID",
tag: "refs/tags/v1.2.3",
id: asStringPointer("https://not-github.zerozr99.workers.dev/some/repo/someBuilderID"),
err: serrors.ErrorUntrustedReusableWorkflow,
},
{
name: "mismatch org GitHub short builder tag",
path: "some/repo/someBuilderID",
tag: "v1.2.3",
id: asStringPointer("https://github.com/other/repo/someBuilderID"),
expected: serrors.ErrorUntrustedReusableWorkflow,
name: "mismatch org GitHub short builder tag",
path: "some/repo/someBuilderID",
tag: "v1.2.3",
id: asStringPointer("https://github.com/other/repo/someBuilderID"),
err: serrors.ErrorUntrustedReusableWorkflow,
},
{
name: "mismatch org GitHub long builder tag",
path: "some/repo/someBuilderID",
tag: "refs/tags/v1.2.3",
id: asStringPointer("https://github.com/other/repo/someBuilderID"),
expected: serrors.ErrorUntrustedReusableWorkflow,
name: "mismatch org GitHub long builder tag",
path: "some/repo/someBuilderID",
tag: "refs/tags/v1.2.3",
id: asStringPointer("https://github.com/other/repo/someBuilderID"),
err: serrors.ErrorUntrustedReusableWorkflow,
},
{
name: "mismatch name GitHub long builder tag",
path: "some/repo/someBuilderID",
tag: "refs/tags/v1.2.3",
id: asStringPointer("https://github.com/some/other/someBuilderID"),
expected: serrors.ErrorUntrustedReusableWorkflow,
name: "mismatch name GitHub long builder tag",
path: "some/repo/someBuilderID",
tag: "refs/tags/v1.2.3",
id: asStringPointer("https://github.com/some/other/someBuilderID"),
err: serrors.ErrorUntrustedReusableWorkflow,
},
{
name: "mismatch id GitHub long builder tag",
path: "some/repo/someBuilderID",
tag: "refs/tags/v1.2.3",
id: asStringPointer("https://github.com/some/repo/ID"),
expected: serrors.ErrorUntrustedReusableWorkflow,
name: "mismatch id GitHub long builder tag",
path: "some/repo/someBuilderID",
tag: "refs/tags/v1.2.3",
id: asStringPointer("https://github.com/some/repo/ID"),
err: serrors.ErrorUntrustedReusableWorkflow,
},
}
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()

id, err := verifyTrustedBuilderID(tt.path, tt.tag, tt.id, tt.defaults)
if !errCmp(err, tt.expected) {
t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors()))
id, byob, err := verifyTrustedBuilderID(tt.path, tt.tag, tt.id, tt.defaults)
if byob != tt.byob {
t.Errorf(cmp.Diff(byob, tt.byob))
}
if !errCmp(err, tt.err) {
t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors()))
}
if err != nil {
return
Expand Down
Loading