package gha import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "net/url" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" 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/internal/gha/slsaprovenance/common" "github.com/slsa-framework/slsa-verifier/v2/verifiers/utils" ) var ( refs123 = "@refs/tags/v1.2.3" githubWorkflows = "/.github/workflows/" builderGoSlsa3 = githubWorkflows + "builder_go_slsa3.yml" builderGoSlsa3GitURL = httpsGithubCom + trustedBuilderRepository + builderGoSlsa3 delegatorGenericSlsa3 = githubWorkflows + "delegator_generic_slsa3.yml" generatorGenericSlsa3 = githubWorkflows + "generator_generic_slsa3.yml" ) // Must checks the error and panics if not nil. func Must[T any](val T, err error) T { if err != nil { panic(err) } return val } func Test_VerifyBuilderIdentity(t *testing.T) { t.Parallel() tests := []struct { name string workflow *WorkflowIdentity buildOpts *options.BuilderOpts builderID string defaults map[string]bool err error byob bool }{ { name: "invalid job workflow ref", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse("https://github.com/random/workflow/ref")), BuildTrigger: "workflow_dispatch", Issuer: "https://token.actions.githubusercontent.com", }, defaults: defaultArtifactTrustedReusableWorkflows, err: serrors.ErrorMalformedURI, }, { name: "untrusted job workflow ref", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse("https://github.com/malicious/slsa-go/.github/workflows/builder.yml@refs/tags/v1.2.3")), BuildTrigger: "workflow_dispatch", Issuer: "https://token.actions.githubusercontent.com", }, defaults: defaultArtifactTrustedReusableWorkflows, err: serrors.ErrorUntrustedReusableWorkflow, }, { name: "untrusted job workflow ref for general repos", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + "@refs/heads/main")), BuildTrigger: "workflow_dispatch", Issuer: "https://token.actions.githubusercontent.com", }, defaults: defaultArtifactTrustedReusableWorkflows, err: serrors.ErrorInvalidRef, }, { name: "untrusted cert issuer for general repos", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: "https://bad.issuer.com", }, defaults: defaultArtifactTrustedReusableWorkflows, err: serrors.ErrorInvalidOIDCIssuer, }, { name: "valid trusted builder without tag", workflow: &WorkflowIdentity{ SourceRepository: trustedBuilderRepository, SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: "https://token.actions.githubusercontent.com", }, defaults: defaultArtifactTrustedReusableWorkflows, builderID: builderGoSlsa3GitURL, }, { name: "valid generic delegator builder without tag", workflow: &WorkflowIdentity{ SourceRepository: trustedBuilderRepository, SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GenericDelegatorBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: "https://token.actions.githubusercontent.com", }, defaults: defaultBYOBReusableWorkflows, builderID: httpsGithubCom + trustedBuilderRepository + delegatorGenericSlsa3, byob: true, }, { name: "valid low-perms delegator builder with short tag", workflow: &WorkflowIdentity{ SourceRepository: trustedBuilderRepository, SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GenericLowPermsDelegatorBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: "https://token.actions.githubusercontent.com", }, defaults: defaultBYOBReusableWorkflows, builderID: httpsGithubCom + trustedBuilderRepository + "/.github/workflows/delegator_lowperms-generic_slsa3.yml@v1.2.3", byob: true, }, { name: "valid main ref for e2e test", workflow: &WorkflowIdentity{ SourceRepository: e2eTestRepository, SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, defaults: defaultArtifactTrustedReusableWorkflows, builderID: builderGoSlsa3GitURL, }, { name: "valid main ref for e2e test - match builderID", workflow: &WorkflowIdentity{ SourceRepository: e2eTestRepository, SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, buildOpts: &options.BuilderOpts{ ExpectedID: asStringPointer(builderGoSlsa3GitURL), }, defaults: defaultArtifactTrustedReusableWorkflows, builderID: builderGoSlsa3GitURL, }, { name: "valid main ref for e2e test - mismatch builderID", workflow: &WorkflowIdentity{ SourceRepository: e2eTestRepository, SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, buildOpts: &options.BuilderOpts{ ExpectedID: asStringPointer("some-other-builderID"), }, defaults: defaultArtifactTrustedReusableWorkflows, err: serrors.ErrorUntrustedReusableWorkflow, }, { name: "valid workflow identity - match builderID", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, buildOpts: &options.BuilderOpts{ ExpectedID: asStringPointer(builderGoSlsa3GitURL), }, defaults: defaultArtifactTrustedReusableWorkflows, builderID: builderGoSlsa3GitURL, }, { name: "valid workflow identity - mismatch builderID", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, buildOpts: &options.BuilderOpts{ ExpectedID: asStringPointer("some-other-builderID"), }, defaults: defaultArtifactTrustedReusableWorkflows, err: serrors.ErrorUntrustedReusableWorkflow, }, { name: "invalid workflow identity with prerelease", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + "@refs/tags/v1.2.3-alpha")), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, err: serrors.ErrorInvalidRef, defaults: defaultArtifactTrustedReusableWorkflows, builderID: builderGoSlsa3GitURL, }, { name: "invalid workflow identity with build", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + "@refs/tags/v1.2.3+123")), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, defaults: defaultArtifactTrustedReusableWorkflows, err: serrors.ErrorInvalidRef, }, { name: "invalid workflow identity with metadata", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + "@refs/tags/v1.2.3-alpha+123")), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, defaults: defaultArtifactTrustedReusableWorkflows, err: serrors.ErrorInvalidRef, }, { name: "valid workflow identity with fully qualified source", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, defaults: defaultArtifactTrustedReusableWorkflows, builderID: builderGoSlsa3GitURL, }, { name: "valid workflow identity with fully qualified source - no default", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, buildOpts: &options.BuilderOpts{ ExpectedID: asStringPointer(builderGoSlsa3GitURL), }, builderID: builderGoSlsa3GitURL, }, { name: "valid workflow identity with fully qualified source - match builderID", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, buildOpts: &options.BuilderOpts{ ExpectedID: asStringPointer(builderGoSlsa3GitURL), }, defaults: defaultArtifactTrustedReusableWorkflows, builderID: builderGoSlsa3GitURL, }, { name: "valid workflow identity with fully qualified source - mismatch builderID", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, buildOpts: &options.BuilderOpts{ ExpectedID: asStringPointer("some-other-builderID"), }, defaults: defaultArtifactTrustedReusableWorkflows, err: serrors.ErrorUntrustedReusableWorkflow, }, { name: "valid workflow identity with fully qualified source - mismatch defaults", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, defaults: defaultContainerTrustedReusableWorkflows, err: serrors.ErrorUntrustedReusableWorkflow, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() opts := tt.buildOpts if opts == nil { opts = &options.BuilderOpts{} } if tt.builderID != "" { opts.ExpectedID = &tt.builderID } id, byob, err := VerifyBuilderIdentity(tt.workflow, opts, tt.defaults) if byob != tt.byob { t.Errorf("unexpected byob value:\n%s", cmp.Diff(tt.byob, byob)) } if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { t.Errorf("unexpected error (-want +got):\n%s", diff) } if err != nil { return } if err := id.MatchesLoose(tt.builderID, true); err != nil { t.Errorf("matches failed:%v", err) } }) } } func Test_isTrustedDelegatorBuilder(t *testing.T) { t.Parallel() tests := []struct { name string certBuilderID string trustedBuilderIDs map[string]bool result bool }{ { name: "match byob", certBuilderID: common.GenericLowPermsDelegatorBuilderID + "@refs/tags/v1.6.0", trustedBuilderIDs: map[string]bool{ common.GenericLowPermsDelegatorBuilderID: true, "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/some_delegator.yml": true, }, result: true, }, { name: "match byob but not caller trusted", certBuilderID: common.GenericLowPermsDelegatorBuilderID + "@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 { t.Run(tt.name, func(t *testing.T) { t.Parallel() trustedBuilderID, err := utils.TrustedBuilderIDNew(tt.certBuilderID, true) if err != nil { t.Fatal(err.Error()) } res := isTrustedDelegatorBuilder(trustedBuilderID, tt.trustedBuilderIDs) if res != tt.result { t.Error(cmp.Diff(res, tt.result)) } }) } } func Test_VerifyCertficateSourceRepository(t *testing.T) { t.Parallel() tests := []struct { name string workflow *WorkflowIdentity source string err error }{ { name: "repo match", workflow: &WorkflowIdentity{ SourceRepository: "asraa/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, source: "github.com/asraa/slsa-on-github-test", }, { name: "unexpected source for e2e test", workflow: &WorkflowIdentity{ SourceRepository: e2eTestRepository, SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, source: "malicious/source", err: serrors.ErrorMismatchSource, }, { name: "valid main ref for builder", workflow: &WorkflowIdentity{ SourceRepository: trustedBuilderRepository, SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, source: "malicious/source", err: serrors.ErrorMismatchSource, }, { name: "unexpected source", workflow: &WorkflowIdentity{ SourceRepository: "malicious/slsa-on-github-test", SourceSha1: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b", SubjectWorkflow: Must(url.Parse(common.GoBuilderID + refs123)), BuildTrigger: "workflow_dispatch", Issuer: certOidcIssuer, }, source: "asraa/slsa-on-github-test", err: serrors.ErrorMismatchSource, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() err := VerifyCertficateSourceRepository(tt.workflow, tt.source) if !errCmp(err, tt.err) { t.Error(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) } }) } } func asStringPointer(s string) *string { return &s } func Test_verifyTrustedBuilderID(t *testing.T) { t.Parallel() tests := []struct { name string id *string path string tag string defaults map[string]bool err error byob bool }{ { name: "default trusted short tag", path: trustedBuilderRepository + generatorGenericSlsa3, tag: "v1.2.3", defaults: defaultArtifactTrustedReusableWorkflows, }, { name: "default trusted long tag", path: trustedBuilderRepository + generatorGenericSlsa3, tag: "refs/tags/v1.2.3", defaults: defaultArtifactTrustedReusableWorkflows, }, { name: "generic delegator workflow long tag", path: trustedBuilderRepository + delegatorGenericSlsa3, id: asStringPointer(trustedBuilderRepository + "/.github/workflows/delegator_generic_slsa3.yml@refs/tags/v1.2.3"), tag: "refs/tags/v1.2.3", defaults: defaultBYOBReusableWorkflows, byob: true, }, { // This is a BYOB workflow without an id that tests non-compulsory builder-id // feature of slsa-verifier and expects byob to be true name: "generic delegator workflow no id", path: trustedBuilderRepository + delegatorGenericSlsa3, // NOTE: id is nil. id: nil, 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, }, { // This is a BYOB workflow without an id that tests non-compulsory builder-id // feature of slsa-verifier and expects byob to be true name: "low perms delegator workflow no ID provided", path: trustedBuilderRepository + "/.github/workflows/delegator_lowperms-generic_slsa3.yml", // NOTE: id is nil. id: nil, tag: "v1.2.3", defaults: defaultBYOBReusableWorkflows, byob: true, }, { name: "default mismatch against container defaults long tag", path: trustedBuilderRepository + generatorGenericSlsa3, tag: "refs/tags/v1.2.3", defaults: defaultContainerTrustedReusableWorkflows, err: serrors.ErrorUntrustedReusableWorkflow, }, { name: "valid ID for GitHub builder short tag", path: "some/repo/someBuilderID", tag: "v1.2.3", id: asStringPointer("https://github.com/some/repo/someBuilderID@v1.2.3"), }, { name: "valid ID for GitHub builder long tag", path: "some/repo/someBuilderID", tag: "refs/tags/v1.2.3", id: asStringPointer("https://github.com/some/repo/someBuilderID@refs/tags/v1.2.3"), }, { name: "valid short ID for GitHub builder long tag", path: "some/repo/someBuilderID", tag: "refs/tags/v1.2.3", id: asStringPointer("https://github.com/some/repo/someBuilderID@v1.2.3"), }, { 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", path: "some/repo/someBuilderID", tag: "refs/tags/v1.2.3", id: asStringPointer("https://github.com/some/repo/someBuilderID@refs/tags/v1.2.3"), }, { name: "valid ID for GitHub builder short tag", path: "some/repo/someBuilderID", tag: "v1.2.3", id: asStringPointer("https://github.com/some/repo/someBuilderID@v1.2.3"), }, { name: "valid short ID for GitHub builder long tag", path: "some/repo/someBuilderID", tag: "refs/tags/v1.2.3", id: asStringPointer("https://github.com/some/repo/someBuilderID@v1.2.3"), }, { 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.com/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"), 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"), 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"), 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"), err: serrors.ErrorUntrustedReusableWorkflow, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() id, byob, err := verifyTrustedBuilderID(httpsGithubCom+tt.path, tt.tag, tt.id, tt.defaults) if byob != tt.byob { t.Error(cmp.Diff(byob, tt.byob)) } if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" { t.Fatalf("unexpected error (-want +got):\n%s", diff) } if tt.err == nil { expectedID := httpsGithubCom + tt.path + "@" + tt.tag if err := id.MatchesLoose(expectedID, true); err != nil { t.Errorf("matches failed:%v", err) } } }) } } func Test_verifyTrustedBuilderRef(t *testing.T) { tests := []struct { name string callerRepo string builderRef string expected error testingEnabled bool }{ // Trusted repo. { name: "main not allowed for builder", callerRepo: trustedBuilderRepository, builderRef: "refs/heads/main", expected: serrors.ErrorInvalidRef, }, { name: "main allowed for builder w/ testing enabled", callerRepo: trustedBuilderRepository, builderRef: "refs/heads/main", testingEnabled: true, }, { name: "full semver for builder", callerRepo: trustedBuilderRepository, builderRef: "refs/tags/v1.2.3", }, { name: "no patch semver for other builder", callerRepo: trustedBuilderRepository, builderRef: "refs/tags/v1.2", expected: serrors.ErrorInvalidRef, }, { name: "no min semver for builder", callerRepo: trustedBuilderRepository, builderRef: "refs/tags/v1", expected: serrors.ErrorInvalidRef, }, { name: "full semver with prerelease for builder", callerRepo: trustedBuilderRepository, builderRef: "refs/tags/v1.2.3-alpha", expected: serrors.ErrorInvalidRef, }, { name: "full semver with build for builder", callerRepo: trustedBuilderRepository, builderRef: "refs/tags/v1.2.3+123", expected: serrors.ErrorInvalidRef, }, { name: "full semver with build/prerelease for builder", callerRepo: trustedBuilderRepository, builderRef: "refs/tags/v1.2.3-alpha+123", expected: serrors.ErrorInvalidRef, }, // E2e tests repo. { name: "main not allowed for test repo", callerRepo: e2eTestRepository, builderRef: "refs/heads/main", expected: serrors.ErrorInvalidRef, }, { name: "main allowed for test repo w/ testing enabled", callerRepo: e2eTestRepository, builderRef: "refs/heads/main", testingEnabled: true, }, { name: "full semver for test repo", callerRepo: e2eTestRepository, builderRef: "refs/tags/v1.2.3", }, { name: "no patch semver for test repo", callerRepo: e2eTestRepository, builderRef: "refs/tags/v1.2", expected: serrors.ErrorInvalidRef, }, { name: "no min semver for test repo", callerRepo: e2eTestRepository, builderRef: "refs/tags/v1", expected: serrors.ErrorInvalidRef, }, { name: "full semver with prerelease for test repo", callerRepo: e2eTestRepository, builderRef: "refs/tags/v1.2.3-alpha", expected: serrors.ErrorInvalidRef, }, { name: "full semver with build for test repo", callerRepo: e2eTestRepository, builderRef: "refs/tags/v1.2.3+123", expected: serrors.ErrorInvalidRef, }, { name: "full semver with build/prerelease for test repo", callerRepo: e2eTestRepository, builderRef: "refs/tags/v1.2.3-alpha+123", expected: serrors.ErrorInvalidRef, }, // Other repos. { name: "main not allowed for other repos", callerRepo: "some/repo", builderRef: "refs/heads/main", expected: serrors.ErrorInvalidRef, }, { name: "main not allowed for other repos w/ testing enabled", callerRepo: "some/repo", builderRef: "refs/heads/main", testingEnabled: true, expected: serrors.ErrorInvalidRef, }, { name: "full semver for other repos", callerRepo: "some/repo", builderRef: "refs/tags/v1.2.3", }, { name: "no patch semver for other repos", callerRepo: "some/repo", builderRef: "refs/tags/v1.2", expected: serrors.ErrorInvalidRef, }, { name: "no min semver for other repos", callerRepo: "some/repo", builderRef: "refs/tags/v1", expected: serrors.ErrorInvalidRef, }, { name: "full semver with prerelease for other repos", callerRepo: "some/repo", builderRef: "refs/tags/v1.2.3-alpha", expected: serrors.ErrorInvalidRef, }, { name: "full semver with prerelease for other repos w/ testing enabled", callerRepo: "some/repo", builderRef: "refs/tags/v1.2.3-alpha", testingEnabled: true, expected: serrors.ErrorInvalidRef, }, { name: "full semver with build for other repos", callerRepo: "some/repo", builderRef: "refs/tags/v1.2.3+123", expected: serrors.ErrorInvalidRef, }, { name: "full semver with build for other repos w/ testing enabled", callerRepo: "some/repo", builderRef: "refs/tags/v1.2.3+123", testingEnabled: true, expected: serrors.ErrorInvalidRef, }, { name: "full semver with build/prerelease for other repos", callerRepo: "some/repo", builderRef: "refs/tags/v1.2.3-alpha+123", expected: serrors.ErrorInvalidRef, }, { name: "full semver with build/prerelease for other repos w/ testing enabled", callerRepo: "some/repo", builderRef: "refs/tags/v1.2.3-alpha+123", testingEnabled: true, expected: serrors.ErrorInvalidRef, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { wf := WorkflowIdentity{ SourceRepository: tt.callerRepo, } if tt.testingEnabled { t.Setenv("SLSA_VERIFIER_TESTING", "1") } else { // Ensure that the variable is not set. t.Setenv("SLSA_VERIFIER_TESTING", "") } err := verifyTrustedBuilderRef(&wf, tt.builderRef) if !errCmp(err, tt.expected) { t.Error(cmp.Diff(err, tt.expected, cmpopts.EquateErrors())) } }) } } func Test_GetWorkflowInfoFromCertificate(t *testing.T) { t.Parallel() // See https://github.com/sigstore/fulcio/blob/e763d76e3f7786b52db4b27ab87dc446da24895a/pkg/certificate/extensions.go. trigger := "workflow_dispatch" encodedTrigger, err := asn1.MarshalWithParams(trigger, "utf8") if err != nil { t.Error(err.Error()) } repo := "org/repo" encodedRepoURI, err := asn1.MarshalWithParams(httpsGithubCom+repo, "utf8") if err != nil { t.Error(err.Error()) } issuer := "the-issuer" encodedIssuer, err := asn1.MarshalWithParams(issuer, "utf8") if err != nil { t.Error(err.Error()) } digest := "abcdef" encodedDigest, err := asn1.MarshalWithParams(digest, "utf8") if err != nil { t.Error(err.Error()) } encodedHosted, err := asn1.MarshalWithParams("github-hosted", "utf8") if err != nil { t.Error(err.Error()) } hosted := HostedGitHub ref := "refs/tags/v1.2.3" encodedRef, err := asn1.MarshalWithParams(ref, "utf8") if err != nil { t.Error(err.Error()) } sourceID := "12345" encodedSourceID, err := asn1.MarshalWithParams(sourceID, "utf8") if err != nil { t.Error(err.Error()) } sourceOwnerID := "12345" encodedSourceOwnerID, err := asn1.MarshalWithParams(sourceOwnerID, "utf8") if err != nil { t.Error(err.Error()) } buildConfigSha1 := "abcdef" encodedBuildConfigSha1, err := asn1.MarshalWithParams(buildConfigSha1, "utf8") if err != nil { t.Error(err.Error()) } buildConfigPath := "path/to/workflow" encodedBuildConfigURI, err := asn1.MarshalWithParams(httpsGithubCom+repo+"/"+buildConfigPath+"@"+ref, "utf8") if err != nil { t.Error(err.Error()) } invocationID := "9207262" encodedInvocationURI, err := asn1.MarshalWithParams(httpsGithubCom+repo+"/actions/runs/"+invocationID, "utf8") if err != nil { t.Error(err.Error()) } subjectSha1 := "subjectSha1" encodedSubjectSha1, err := asn1.MarshalWithParams(subjectSha1, "utf8") if err != nil { t.Error(err.Error()) } tests := []struct { name string cert x509.Certificate workflow WorkflowIdentity err error }{ { name: "old cert", cert: x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", Host: "github.com", Path: "/" + repo + "/" + buildConfigPath, }, }, Extensions: []pkix.Extension{ { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDIssuer, Value: []byte(issuer), }, { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDGitHubWorkflowTrigger, Value: []byte(trigger), }, { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDGitHubWorkflowSHA, Value: []byte(digest), }, { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDGitHubWorkflowRepository, Value: []byte(repo), }, }, }, workflow: WorkflowIdentity{ Issuer: issuer, SubjectWorkflow: Must(url.Parse(httpsGithubCom + repo + "/" + buildConfigPath)), SourceRepository: repo, SourceSha1: digest, BuildTrigger: trigger, }, }, { name: "old cert empty URIs", cert: x509.Certificate{ Extensions: []pkix.Extension{ { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDIssuer, Value: []byte(issuer), }, { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDGitHubWorkflowTrigger, Value: []byte(trigger), }, { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDGitHubWorkflowSHA, Value: []byte(digest), }, { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDGitHubWorkflowRepository, Value: []byte(repo), }, }, }, err: serrors.ErrorInvalidFormat, }, { name: "new cert", cert: x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", Host: "github.com", Path: "/" + repo + "/" + buildConfigPath, }, }, Extensions: []pkix.Extension{ // Deprecated claims. { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDIssuer, Value: []byte(issuer), }, { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDGitHubWorkflowTrigger, Value: []byte(trigger), }, { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDGitHubWorkflowSHA, Value: []byte(digest), }, { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDGitHubWorkflowRepository, Value: []byte(repo), }, // New claims. { Id: fulcio.OIDBuildTrigger, Value: encodedTrigger, }, { Id: fulcio.OIDSourceRepositoryURI, Value: encodedRepoURI, }, { Id: fulcio.OIDIssuerV2, Value: encodedIssuer, }, { Id: fulcio.OIDSourceRepositoryDigest, Value: encodedDigest, }, { Id: fulcio.OIDRunnerEnvironment, Value: encodedHosted, }, { Id: fulcio.OIDSourceRepositoryRef, Value: encodedRef, }, { Id: fulcio.OIDSourceRepositoryIdentifier, Value: encodedSourceID, }, { Id: fulcio.OIDSourceRepositoryOwnerIdentifier, Value: encodedSourceOwnerID, }, { Id: fulcio.OIDBuildConfigDigest, Value: encodedBuildConfigSha1, }, { Id: fulcio.OIDBuildConfigURI, Value: encodedBuildConfigURI, }, { Id: fulcio.OIDRunInvocationURI, Value: encodedInvocationURI, }, { Id: fulcio.OIDBuildSignerDigest, Value: encodedSubjectSha1, }, }, }, workflow: WorkflowIdentity{ Issuer: issuer, SubjectSha1: &subjectSha1, SubjectHosted: &hosted, SubjectWorkflow: Must(url.Parse(httpsGithubCom + repo + "/" + buildConfigPath)), SourceRepository: repo, SourceSha1: digest, SourceRef: &ref, SourceID: &sourceID, SourceOwnerID: &sourceOwnerID, BuildTrigger: trigger, BuildConfigPath: &buildConfigPath, RunID: &invocationID, }, }, { name: "new cert empty URIs", cert: x509.Certificate{ Extensions: []pkix.Extension{ // Deprecated claims. { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDIssuer, Value: []byte(issuer), }, { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDGitHubWorkflowTrigger, Value: []byte(trigger), }, { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDGitHubWorkflowSHA, Value: []byte(digest), }, { //nolint: staticcheck // SA1019: Need to support older signatures. Id: fulcio.OIDGitHubWorkflowRepository, Value: []byte(repo), }, // New claims. { Id: fulcio.OIDBuildTrigger, Value: encodedTrigger, }, { Id: fulcio.OIDSourceRepositoryURI, Value: encodedRepoURI, }, { Id: fulcio.OIDIssuerV2, Value: encodedIssuer, }, { Id: fulcio.OIDSourceRepositoryDigest, Value: encodedDigest, }, { Id: fulcio.OIDRunnerEnvironment, Value: encodedHosted, }, { Id: fulcio.OIDSourceRepositoryRef, Value: encodedRef, }, { Id: fulcio.OIDSourceRepositoryIdentifier, Value: encodedSourceID, }, { Id: fulcio.OIDSourceRepositoryOwnerIdentifier, Value: encodedSourceOwnerID, }, { Id: fulcio.OIDBuildConfigDigest, Value: encodedBuildConfigSha1, }, { Id: fulcio.OIDBuildConfigURI, Value: encodedBuildConfigURI, }, { Id: fulcio.OIDRunInvocationURI, Value: encodedInvocationURI, }, }, }, err: serrors.ErrorInvalidFormat, }, { name: "new cert no deprecated claims", cert: x509.Certificate{ URIs: []*url.URL{ { Scheme: "https", Host: "github.com", Path: "/" + repo + "/" + buildConfigPath, }, }, Extensions: []pkix.Extension{ // New claims. { Id: fulcio.OIDBuildTrigger, Value: encodedTrigger, }, { Id: fulcio.OIDSourceRepositoryURI, Value: encodedRepoURI, }, { Id: fulcio.OIDIssuerV2, Value: encodedIssuer, }, { Id: fulcio.OIDSourceRepositoryDigest, Value: encodedDigest, }, { Id: fulcio.OIDRunnerEnvironment, Value: encodedHosted, }, { Id: fulcio.OIDSourceRepositoryRef, Value: encodedRef, }, { Id: fulcio.OIDSourceRepositoryIdentifier, Value: encodedSourceID, }, { Id: fulcio.OIDSourceRepositoryOwnerIdentifier, Value: encodedSourceOwnerID, }, { Id: fulcio.OIDBuildConfigDigest, Value: encodedBuildConfigSha1, }, { Id: fulcio.OIDBuildConfigURI, Value: encodedBuildConfigURI, }, { Id: fulcio.OIDRunInvocationURI, Value: encodedInvocationURI, }, }, }, workflow: WorkflowIdentity{ Issuer: issuer, SubjectWorkflow: Must(url.Parse(httpsGithubCom + repo + "/" + buildConfigPath)), SourceRepository: repo, SourceSha1: digest, BuildTrigger: trigger, SubjectHosted: &hosted, SourceRef: &ref, SourceID: &sourceID, SourceOwnerID: &sourceOwnerID, BuildConfigPath: &buildConfigPath, RunID: &invocationID, }, }, { name: "new cert no deprecated claims empty URIs", cert: x509.Certificate{ Extensions: []pkix.Extension{ // New claims. { Id: fulcio.OIDBuildTrigger, Value: encodedTrigger, }, { Id: fulcio.OIDSourceRepositoryURI, Value: encodedRepoURI, }, { Id: fulcio.OIDIssuerV2, Value: encodedIssuer, }, { Id: fulcio.OIDSourceRepositoryDigest, Value: encodedDigest, }, { Id: fulcio.OIDRunnerEnvironment, Value: encodedHosted, }, { Id: fulcio.OIDSourceRepositoryRef, Value: encodedRef, }, { Id: fulcio.OIDSourceRepositoryIdentifier, Value: encodedSourceID, }, { Id: fulcio.OIDSourceRepositoryOwnerIdentifier, Value: encodedSourceOwnerID, }, { Id: fulcio.OIDBuildConfigDigest, Value: encodedBuildConfigSha1, }, { Id: fulcio.OIDBuildConfigURI, Value: encodedBuildConfigURI, }, { Id: fulcio.OIDRunInvocationURI, Value: encodedInvocationURI, }, }, }, err: serrors.ErrorInvalidFormat, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() workflow, err := GetWorkflowInfoFromCertificate(&tt.cert) if !errCmp(err, tt.err) { t.Error(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) } if err != nil { return } if !cmp.Equal(*workflow, tt.workflow) { t.Error(cmp.Diff(*workflow, tt.workflow)) } }) } } func TestWorkflowIdentity(t *testing.T) { t.Parallel() tests := []struct { name string workflow WorkflowIdentity workflowName string workflowPath string workflowRef string }{ { name: "no ref", workflow: WorkflowIdentity{ SubjectWorkflow: Must(url.Parse("https://github.com/random/workflow/ref")), }, workflowName: "https://github.com/random/workflow/ref", workflowPath: "/random/workflow/ref", workflowRef: "", }, { name: "with ref", workflow: WorkflowIdentity{ SubjectWorkflow: Must(url.Parse("https://github.com/random/workflow/ref@refs/heads/foo")), }, workflowName: "https://github.com/random/workflow/ref", workflowPath: "/random/workflow/ref", workflowRef: "refs/heads/foo", }, { name: "multiple (at) symbols", workflow: WorkflowIdentity{ SubjectWorkflow: Must(url.Parse("https://github.com/random/work@flow/ref@refs/heads/foo")), }, workflowName: "https://github.com/random/work@flow/ref", workflowPath: "/random/work@flow/ref", workflowRef: "refs/heads/foo", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got, want := tt.workflow.SubjectWorkflowName(), tt.workflowName; got != want { t.Errorf("unexpected subject workflow name, got %q, want %q", got, want) } if got, want := tt.workflow.SubjectWorkflowPath(), tt.workflowPath; got != want { t.Errorf("unexpected subject workflow path, got %q, want %q", got, want) } if got, want := tt.workflow.SubjectWorkflowRef(), tt.workflowRef; got != want { t.Errorf("unexpected subject workflow ref, got %q, want %q", got, want) } }) } }