mirror of
https://github.com/slsa-framework/slsa-verifier.git
synced 2026-08-18 02:56:31 +00:00
feat: Create interface for verifiers (#187)
* update * update * unit tests * update * comments * update * update * update * update * Use interface for builders * update * update * update * update * fix * update * update * update * update * update * update * update * update * update * update * update * update
This commit is contained in:
@@ -10,11 +10,13 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/slsa-framework/slsa-verifier/verification"
|
||||
"github.com/slsa-framework/slsa-verifier/options"
|
||||
"github.com/slsa-framework/slsa-verifier/verifiers"
|
||||
)
|
||||
|
||||
var (
|
||||
provenancePath string
|
||||
builderID string
|
||||
artifactPath string
|
||||
source string
|
||||
branch string
|
||||
@@ -24,6 +26,7 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&builderID, "builder-id", "", "EXPERIMENTAL: the unique builder ID who created the provenance")
|
||||
flag.StringVar(&provenancePath, "provenance", "", "path to a provenance file")
|
||||
flag.StringVar(&artifactPath, "artifact-path", "", "path to an artifact to verify")
|
||||
flag.StringVar(&source, "source", "",
|
||||
@@ -41,21 +44,26 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var ptag, pversiontag *string
|
||||
var pbuilderID, ptag, pversiontag *string
|
||||
|
||||
// Note: nil tag, version-tag and builder-id means we ignore them during verification.
|
||||
if isFlagPassed("tag") {
|
||||
ptag = &tag
|
||||
}
|
||||
if isFlagPassed("versioned-tag") {
|
||||
pversiontag = &versiontag
|
||||
}
|
||||
if isFlagPassed("builder-id") {
|
||||
pbuilderID = &builderID
|
||||
}
|
||||
|
||||
if ptag != nil && pversiontag != nil {
|
||||
fmt.Fprintf(os.Stderr, "'version' and 'tag' options cannot be used together\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
verifiedProvenance, err := runVerify(artifactPath, provenancePath, source, branch, ptag, pversiontag)
|
||||
verifiedProvenance, _, err := runVerify(artifactPath, provenancePath, source,
|
||||
branch, pbuilderID, ptag, pversiontag)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "FAILED: SLSA verification failed: %v\n", err)
|
||||
os.Exit(2)
|
||||
@@ -78,7 +86,9 @@ func isFlagPassed(name string) bool {
|
||||
return found
|
||||
}
|
||||
|
||||
func runVerify(artifactPath, provenancePath, source, branch string, ptag, pversiontag *string) ([]byte, error) {
|
||||
func runVerify(artifactPath, provenancePath, source, branch string,
|
||||
builderID, ptag, pversiontag *string,
|
||||
) ([]byte, string, error) {
|
||||
f, err := os.Open(artifactPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -87,7 +97,7 @@ func runVerify(artifactPath, provenancePath, source, branch string, ptag, pversi
|
||||
|
||||
provenance, err := os.ReadFile(provenancePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
@@ -96,15 +106,19 @@ func runVerify(artifactPath, provenancePath, source, branch string, ptag, pversi
|
||||
}
|
||||
artifactHash := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
provenanceOpts := &verification.ProvenanceOpts{
|
||||
provenanceOpts := &options.ProvenanceOpts{
|
||||
ExpectedSourceURI: source,
|
||||
ExpectedBranch: branch,
|
||||
ExpectedDigest: artifactHash,
|
||||
ExpectedVersionedTag: pversiontag,
|
||||
ExpectedTag: ptag,
|
||||
}
|
||||
|
||||
builderOpts := &options.BuilderOpts{
|
||||
ExpectedID: builderID,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
return verification.Verify(ctx, provenance,
|
||||
artifactHash,
|
||||
source, provenanceOpts)
|
||||
return verifiers.Verify(ctx, provenance,
|
||||
artifactHash, provenanceOpts, builderOpts)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
|
||||
"github.com/slsa-framework/slsa-verifier/verification"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
serrors "github.com/slsa-framework/slsa-verifier/errors"
|
||||
)
|
||||
|
||||
func errCmp(e1, e2 error) bool {
|
||||
@@ -42,6 +42,8 @@ func Test_runVerify(t *testing.T) {
|
||||
branch string
|
||||
ptag *string
|
||||
pversiontag *string
|
||||
pbuilderID *string
|
||||
builderID string
|
||||
err error
|
||||
// noversion is a special case where we are not testing all builder versions
|
||||
// for example, testdata for the builder at head in trusted repo workflows
|
||||
@@ -58,6 +60,18 @@ func Test_runVerify(t *testing.T) {
|
||||
artifact: "binary-linux-amd64-workflow_dispatch",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
},
|
||||
{
|
||||
name: "valid main branch default",
|
||||
artifact: "binary-linux-amd64-workflow_dispatch",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
},
|
||||
{
|
||||
name: "valid main branch default - invalid builderID",
|
||||
artifact: "binary-linux-amd64-workflow_dispatch",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pbuilderID: pString("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/not-trusted.yml"),
|
||||
err: serrors.ErrorUntrustedReusableWorkflow,
|
||||
},
|
||||
{
|
||||
name: "valid main branch set",
|
||||
artifact: "binary-linux-amd64-workflow_dispatch",
|
||||
@@ -69,39 +83,39 @@ func Test_runVerify(t *testing.T) {
|
||||
artifact: "binary-linux-amd64-workflow_dispatch",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
branch: "master",
|
||||
err: verification.ErrorMismatchBranch,
|
||||
err: serrors.ErrorMismatchBranch,
|
||||
},
|
||||
{
|
||||
name: "wrong source append A",
|
||||
artifact: "binary-linux-amd64-workflow_dispatch",
|
||||
source: "github.com/laurentsimon/slsa-verifier-test-genA",
|
||||
err: verification.ErrorMismatchRepository,
|
||||
err: serrors.ErrorMismatchSource,
|
||||
},
|
||||
{
|
||||
name: "wrong source prepend A",
|
||||
artifact: "binary-linux-amd64-workflow_dispatch",
|
||||
source: "github.com/laurentsimon/slsa-verifier-test-gen",
|
||||
err: verification.ErrorMismatchRepository,
|
||||
err: serrors.ErrorMismatchSource,
|
||||
},
|
||||
{
|
||||
name: "wrong source middle A",
|
||||
artifact: "binary-linux-amd64-workflow_dispatch",
|
||||
source: "github.com/Alaurentsimon/slsa-verifier-test-gen",
|
||||
err: verification.ErrorMismatchRepository,
|
||||
err: serrors.ErrorMismatchSource,
|
||||
},
|
||||
{
|
||||
name: "tag no match empty tag workflow_dispatch",
|
||||
artifact: "binary-linux-amd64-workflow_dispatch",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
ptag: pString("v1.2.3"),
|
||||
err: verification.ErrorMismatchTag,
|
||||
err: serrors.ErrorMismatchTag,
|
||||
},
|
||||
{
|
||||
name: "versioned tag no match empty tag workflow_dispatch",
|
||||
artifact: "binary-linux-amd64-workflow_dispatch",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v1"),
|
||||
err: verification.ErrorInvalidSemver,
|
||||
err: serrors.ErrorInvalidSemver,
|
||||
},
|
||||
// Provenance contains tag = v13.0.30.
|
||||
{
|
||||
@@ -109,21 +123,21 @@ func Test_runVerify(t *testing.T) {
|
||||
artifact: "binary-linux-amd64-push-v13.0.30",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
ptag: pString("v13.0.29"),
|
||||
err: verification.ErrorMismatchTag,
|
||||
err: serrors.ErrorMismatchTag,
|
||||
},
|
||||
{
|
||||
name: "tag v13.0 no match v13.0.30",
|
||||
artifact: "binary-linux-amd64-push-v13.0.30",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
ptag: pString("v13.0"),
|
||||
err: verification.ErrorMismatchTag,
|
||||
err: serrors.ErrorMismatchTag,
|
||||
},
|
||||
{
|
||||
name: "tag v13 no match v13.0.30",
|
||||
artifact: "binary-linux-amd64-push-v13.0.30",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
ptag: pString("v13"),
|
||||
err: verification.ErrorMismatchTag,
|
||||
err: serrors.ErrorMismatchTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v13.0.30 match push-v13.0.30",
|
||||
@@ -148,42 +162,42 @@ func Test_runVerify(t *testing.T) {
|
||||
artifact: "binary-linux-amd64-push-v13.0.30",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v2"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v0 no match push-v13.0.30",
|
||||
artifact: "binary-linux-amd64-push-v13.0.30",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v0"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v13.1 no match push-v13.0.30",
|
||||
artifact: "binary-linux-amd64-push-v13.0.30",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v13.1"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v12.9 no match push-v13.0.30",
|
||||
artifact: "binary-linux-amd64-push-v13.0.30",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v12.9"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v13.0.29 no match push-v13.0.30",
|
||||
artifact: "binary-linux-amd64-push-v13.0.30",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v13.0.29"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v13.0.31 no match push-v13.0.30",
|
||||
artifact: "binary-linux-amd64-push-v13.0.30",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v13.0.31"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
// Provenance contains tag = v14.
|
||||
{
|
||||
@@ -203,42 +217,42 @@ func Test_runVerify(t *testing.T) {
|
||||
artifact: "binary-linux-amd64-push-v14",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v14.1"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v13 no match push-v14",
|
||||
artifact: "binary-linux-amd64-push-v14",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v13"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v15 no match push-v14",
|
||||
artifact: "binary-linux-amd64-push-v14",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v15"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v13.2 no match push-v14",
|
||||
artifact: "binary-linux-amd64-push-v14",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v13.2"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v15 no match push-v14",
|
||||
artifact: "binary-linux-amd64-push-v14",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v15"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v0 no match push-v14",
|
||||
artifact: "binary-linux-amd64-push-v14",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v0"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
// Provenance contains tag = v14.2
|
||||
{
|
||||
@@ -252,14 +266,14 @@ func Test_runVerify(t *testing.T) {
|
||||
artifact: "binary-linux-amd64-push-v14.2",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v14.2.1"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v14.2.3 match push-v14.2",
|
||||
artifact: "binary-linux-amd64-push-v14.2",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v14.2.3"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v14 match push-v14.2",
|
||||
@@ -272,42 +286,42 @@ func Test_runVerify(t *testing.T) {
|
||||
artifact: "binary-linux-amd64-push-v14.2",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v14.1"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v14.1.1 no match push-v14.2",
|
||||
artifact: "binary-linux-amd64-push-v14.2",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v14.1.1"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v14.3.1 no match push-v14.2",
|
||||
artifact: "binary-linux-amd64-push-v14.2",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v14.3.1"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v13 no match push-v14.2",
|
||||
artifact: "binary-linux-amd64-push-v14.2",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v13"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v15 no match push-v14.2",
|
||||
artifact: "binary-linux-amd64-push-v14.2",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v15"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "versioned v15.1 no match push-v14.2",
|
||||
artifact: "binary-linux-amd64-push-v14.2",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
pversiontag: pString("v15.1"),
|
||||
err: verification.ErrorMismatchVersionedTag,
|
||||
err: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
// Multiple subjects in version v1.2.0+
|
||||
{
|
||||
@@ -324,6 +338,15 @@ func Test_runVerify(t *testing.T) {
|
||||
minversion: "v1.2.0",
|
||||
builders: []string{"generic"},
|
||||
},
|
||||
{
|
||||
name: "multiple subject second match - builderID",
|
||||
artifact: "binary-linux-amd64-multi-subject-second",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
minversion: "v1.2.0",
|
||||
builders: []string{"generic"},
|
||||
pbuilderID: pString("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml"),
|
||||
builderID: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml",
|
||||
},
|
||||
// Special case of the e2e test repository building builder from head.
|
||||
{
|
||||
name: "e2e test repository verified with builder at head",
|
||||
@@ -331,27 +354,28 @@ func Test_runVerify(t *testing.T) {
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
branch: "main",
|
||||
noversion: true,
|
||||
builderID: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml",
|
||||
},
|
||||
// Malicious builders and workflows.
|
||||
{
|
||||
name: "rekor upload bypassed",
|
||||
artifact: "binary-linux-amd64-no-tlog-upload",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
err: verification.ErrorNoValidRekorEntries,
|
||||
err: serrors.ErrorNoValidRekorEntries,
|
||||
noversion: true,
|
||||
},
|
||||
{
|
||||
name: "malicious: untrusted builder",
|
||||
artifact: "binary-linux-amd64-untrusted-builder",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
err: verification.ErrorUntrustedReusableWorkflow,
|
||||
err: serrors.ErrorUntrustedReusableWorkflow,
|
||||
noversion: true,
|
||||
},
|
||||
{
|
||||
name: "malicious: invalid signature expired certificate",
|
||||
artifact: "binary-linux-amd64-expired-cert",
|
||||
source: "github.com/slsa-framework/example-package",
|
||||
err: verification.ErrorNoValidRekorEntries,
|
||||
err: serrors.ErrorNoValidRekorEntries,
|
||||
noversion: true,
|
||||
},
|
||||
// Regression test of sharded UUID
|
||||
@@ -413,14 +437,22 @@ func Test_runVerify(t *testing.T) {
|
||||
artifactPath := filepath.Clean(filepath.Join(TEST_DIR, v, tt.artifact))
|
||||
provenancePath := fmt.Sprintf("%s.intoto.jsonl", artifactPath)
|
||||
|
||||
_, err := runVerify(artifactPath,
|
||||
_, builderID, err := runVerify(artifactPath,
|
||||
provenancePath,
|
||||
tt.source, branch,
|
||||
tt.source, branch, tt.pbuilderID,
|
||||
tt.ptag, tt.pversiontag)
|
||||
|
||||
if !errCmp(err, tt.err) {
|
||||
t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors()))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if tt.builderID != "" && builderID != tt.builderID {
|
||||
t.Errorf(cmp.Diff(builderID, tt.builderID))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,14 +5,17 @@ import "errors"
|
||||
var (
|
||||
ErrorInvalidDssePayload = errors.New("invalid DSSE envelope payload")
|
||||
ErrorMismatchBranch = errors.New("branch used to generate the binary does not match provenance")
|
||||
ErrorMismatchRepository = errors.New("repository used to generate the binary does not match provenance")
|
||||
ErrorMismatchBuilderID = errors.New("builderID does not match provenance")
|
||||
ErrorMismatchSource = errors.New("source used to generate the binary does not match provenance")
|
||||
ErrorMalformedURI = errors.New("URI is malformed")
|
||||
ErrorMismatchTag = errors.New("tag used to generate the binary does not match provenance")
|
||||
ErrorMismatchVersionedTag = errors.New("tag used to generate the binary does not match provenance")
|
||||
ErrorInvalidSemver = errors.New("invalid semantic version")
|
||||
ErrorRekorSearch = errors.New("error searching rekor entries")
|
||||
errorMismatchHash = errors.New("binary artifact hash does not match provenance subject")
|
||||
errorInvalidRef = errors.New("invalid ref")
|
||||
errorMalformedWorkflowURI = errors.New("malformed URI for workflow")
|
||||
ErrorMismatchHash = errors.New("binary artifact hash does not match provenance subject")
|
||||
ErrorInvalidRef = errors.New("invalid ref")
|
||||
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")
|
||||
ErrorNotSupported = errors.New("not supported")
|
||||
)
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/slsa-framework/slsa-verifier/verification"
|
||||
"github.com/slsa-framework/slsa-verifier/options"
|
||||
"github.com/slsa-framework/slsa-verifier/verifiers"
|
||||
)
|
||||
|
||||
var errInvalid = errors.New("invalid")
|
||||
@@ -20,6 +21,7 @@ type v1Query struct {
|
||||
ArtifactHash string `json:"artifactHash"`
|
||||
DsseEnvelope string `json:"provenanceContent"`
|
||||
// Optional fields.
|
||||
BuilderID *string `json:"builderID"`
|
||||
Tag *string `json:"tag"`
|
||||
Branch *string `json:"branch"`
|
||||
VersionedTag *string `json:"versionedTag"`
|
||||
@@ -37,6 +39,7 @@ type v1Result struct {
|
||||
Version uint `json:"version"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
Validation validation `json:"validation"`
|
||||
BuilderID string `json:"builderID"`
|
||||
IntotoStatement *string `json:"provenanceContent,omitempty"`
|
||||
}
|
||||
|
||||
@@ -81,6 +84,11 @@ func (r *v1Result) withValidation(v validation) *v1Result {
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *v1Result) withBuilderID(id string) *v1Result {
|
||||
r.BuilderID = id
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *v1Result) withIntotoStatement(c []byte) *v1Result {
|
||||
b := base64.StdEncoding.EncodeToString(c)
|
||||
r.IntotoStatement = &b
|
||||
@@ -112,16 +120,21 @@ func verifyHandlerV1(r *http.Request) *v1Result {
|
||||
if query.Branch != nil {
|
||||
branch = *query.Branch
|
||||
}
|
||||
provenanceOpts := &verification.ProvenanceOpts{
|
||||
provenanceOpts := &options.ProvenanceOpts{
|
||||
ExpectedSourceURI: query.Source,
|
||||
ExpectedBranch: branch,
|
||||
ExpectedDigest: query.ArtifactHash,
|
||||
ExpectedVersionedTag: query.VersionedTag,
|
||||
ExpectedTag: query.Tag,
|
||||
}
|
||||
|
||||
builderOpts := &options.BuilderOpts{
|
||||
ExpectedID: query.BuilderID,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
p, err := verification.Verify(ctx, []byte(query.DsseEnvelope),
|
||||
query.ArtifactHash, query.Source, provenanceOpts)
|
||||
p, builderID, err := verifiers.Verify(ctx, []byte(query.DsseEnvelope),
|
||||
query.ArtifactHash, provenanceOpts, builderOpts)
|
||||
if err != nil {
|
||||
return results.withError(err)
|
||||
}
|
||||
@@ -130,7 +143,7 @@ func verifyHandlerV1(r *http.Request) *v1Result {
|
||||
results = results.withIntotoStatement(p)
|
||||
}
|
||||
|
||||
return results.withValidation(validationSuccess)
|
||||
return results.withBuilderID(builderID).withValidation(validationSuccess)
|
||||
}
|
||||
|
||||
func queryFromString(content []byte) (*v1Query, error) {
|
||||
@@ -165,5 +178,7 @@ func (q *v1Query) validate() error {
|
||||
return fmt.Errorf("%w: tag and versionedTag are mutually exclusive", errInvalid)
|
||||
}
|
||||
|
||||
// BuilderID is optional, so not additional validation needed.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
package verification
|
||||
package options
|
||||
|
||||
// ProvenanceOpts are the options for checking provenance information.
|
||||
type ProvenanceOpts struct {
|
||||
// ExpectedDigest is the expected artifact sha included in the provenance
|
||||
// ExpectedDigest is the expected artifact sha included in the provenance.
|
||||
ExpectedDigest string
|
||||
|
||||
// ExpectedBranch is the expected branch (github_ref or github_base_ref) in
|
||||
// the invocation parameters.
|
||||
ExpectedBranch string
|
||||
|
||||
// ExpectedSourceURI is the expected source URI in the provenance.
|
||||
ExpectedSourceURI string
|
||||
|
||||
// ExpectedTag is the expected tag, github_ref, in the invocation parameters.
|
||||
ExpectedTag *string
|
||||
|
||||
// ExpectedVersionedTag is the expected versioned tag
|
||||
// ExpectedVersionedTag is the expected versioned tag.
|
||||
ExpectedVersionedTag *string
|
||||
|
||||
// ExpectedBuilderID is the expected builder ID.
|
||||
ExpectedBuilderID string
|
||||
}
|
||||
|
||||
// BuildOpts are the options for checking the builder.
|
||||
type BuilderOpts struct {
|
||||
// ExpectedID is the expected builder ID.
|
||||
ExpectedID *string
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package register
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/slsa-framework/slsa-verifier/options"
|
||||
)
|
||||
|
||||
var SLSAVerifiers = make(map[string]SLSAVerifier)
|
||||
|
||||
type SLSAVerifier interface {
|
||||
// IsAuthoritativeFor checks whether a verifier can
|
||||
// verify provenance for a given builder identified by its
|
||||
// `BuilderID`.
|
||||
IsAuthoritativeFor(builderID 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)
|
||||
|
||||
// VerifyImage verifies a provenance for a supplied OCI image.
|
||||
VerifyImage(ctx context.Context,
|
||||
provenance []byte, artifactHash string,
|
||||
provenanceOpts *options.ProvenanceOpts,
|
||||
builderOpts *options.BuilderOpts,
|
||||
) ([]byte, string, error)
|
||||
}
|
||||
|
||||
func RegisterVerifier(name string, verifier SLSAVerifier) {
|
||||
SLSAVerifiers[name] = verifier
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package verification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
|
||||
)
|
||||
|
||||
func Verify(ctx context.Context,
|
||||
provenance []byte, artifactHash, source string, provenanceOpts *ProvenanceOpts,
|
||||
) ([]byte, error) {
|
||||
rClient, err := rekor.NewClient(defaultRekorAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
/* Verify signature on the intoto attestation. */
|
||||
env, cert, err := VerifyProvenanceSignature(ctx, rClient, provenance, artifactHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
/* Verify properties of the signing identity. */
|
||||
// Get the workflow info given the certificate information.
|
||||
workflowInfo, err := GetWorkflowInfoFromCertificate(cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify the workflow identity.
|
||||
if err := VerifyWorkflowIdentity(workflowInfo, source); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
/* Verify properties of the SLSA provenance. */
|
||||
// Unpack and verify info in the provenance, including the Subject Digest.
|
||||
if err := VerifyProvenance(env, provenanceOpts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Verified build using builder https://github.com%s at commit %s\n",
|
||||
workflowInfo.JobWobWorkflowRef,
|
||||
workflowInfo.CallerHash)
|
||||
// Return verified provenance.
|
||||
return base64.StdEncoding.DecodeString(env.Payload)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package gha
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
serrors "github.com/slsa-framework/slsa-verifier/errors"
|
||||
"github.com/slsa-framework/slsa-verifier/options"
|
||||
register "github.com/slsa-framework/slsa-verifier/register"
|
||||
)
|
||||
|
||||
const VerifierName = "GCB"
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
register.RegisterVerifier(VerifierName, GCBVerifierNew())
|
||||
}
|
||||
|
||||
type GCBVerifier struct{}
|
||||
|
||||
func GCBVerifierNew() *GCBVerifier {
|
||||
return &GCBVerifier{}
|
||||
}
|
||||
|
||||
// IsAuthoritativeFor returns true of the verifier can verify provenance
|
||||
// generated by the builderID.
|
||||
func (v *GCBVerifier) IsAuthoritativeFor(builderID string) bool {
|
||||
// This verifier only supports the GCB builders.
|
||||
return strings.HasPrefix(builderID, "https://cloudbuild.googleapis.com/GoogleHostedWorker@")
|
||||
}
|
||||
|
||||
// VerifyArtifact verifies provenance for an artifact.
|
||||
func (v *GCBVerifier) VerifyArtifact(ctx context.Context,
|
||||
provenance []byte, artifactHash string,
|
||||
provenanceOpts *options.ProvenanceOpts,
|
||||
builderOpts *options.BuilderOpts,
|
||||
) ([]byte, string, error) {
|
||||
return nil, "todo", serrors.ErrorNotSupported
|
||||
}
|
||||
|
||||
// VerifyImage verifies provenance for an OCI image.
|
||||
func (v *GCBVerifier) VerifyImage(ctx context.Context,
|
||||
provenance []byte, artifactHash string,
|
||||
provenanceOpts *options.ProvenanceOpts,
|
||||
builderOpts *options.BuilderOpts,
|
||||
) ([]byte, string, error) {
|
||||
return nil, "todo", serrors.ErrorNotSupported
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package verification
|
||||
package gha
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
|
||||
serrors "github.com/slsa-framework/slsa-verifier/errors"
|
||||
"github.com/slsa-framework/slsa-verifier/options"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -15,44 +18,66 @@ var (
|
||||
certOidcIssuer = "https://token.actions.githubusercontent.com"
|
||||
)
|
||||
|
||||
var trustedReusableWorkflows = map[string]bool{
|
||||
var defaultTrustedReusableWorkflows = map[string]bool{
|
||||
trustedBuilderRepository + "/.github/workflows/generator_generic_slsa3.yml": true,
|
||||
trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml": true,
|
||||
}
|
||||
|
||||
// VerifyWorkflowIdentity verifies the signing certificate information
|
||||
func VerifyWorkflowIdentity(id *WorkflowIdentity, source string) error {
|
||||
func VerifyWorkflowIdentity(id *WorkflowIdentity, builderOpts *options.BuilderOpts, source string) (string, 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: %s", errorMalformedWorkflowURI, id.JobWobWorkflowRef)
|
||||
return "", fmt.Errorf("%w: workflow uri: %s", serrors.ErrorMalformedURI, id.JobWobWorkflowRef)
|
||||
}
|
||||
|
||||
// Trusted workflow verification by name.
|
||||
reusableWorkflowName := strings.Trim(workflowPath[0], "/")
|
||||
if _, ok := trustedReusableWorkflows[reusableWorkflowName]; !ok {
|
||||
return fmt.Errorf("%w: %s", ErrorUntrustedReusableWorkflow, reusableWorkflowName)
|
||||
reusableWorkflowPath := strings.Trim(workflowPath[0], "/")
|
||||
builderID, err := verifyTrustedBuilderID(reusableWorkflowPath, builderOpts.ExpectedID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Verify the ref.
|
||||
if err := verifyTrustedBuilderRef(id, strings.Trim(workflowPath[1], "/")); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Issuer verification.
|
||||
if !strings.EqualFold(id.Issuer, certOidcIssuer) {
|
||||
return fmt.Errorf("untrusted token issuer: %s", id.Issuer)
|
||||
return "", fmt.Errorf("untrusted token issuer: %s", id.Issuer)
|
||||
}
|
||||
|
||||
// The caller repository in the x509 extension is not fully qualified. It only contains
|
||||
// {org}/{repository}.
|
||||
expectedSource := strings.TrimPrefix(source, "github.com/")
|
||||
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'", ErrorMismatchRepository,
|
||||
return "", fmt.Errorf("%w: expected source '%s', got '%s'", serrors.ErrorMismatchSource,
|
||||
expectedSource, id.CallerRepository)
|
||||
}
|
||||
|
||||
return nil
|
||||
// Return the builder and its tag.
|
||||
return builderID, nil
|
||||
}
|
||||
|
||||
func verifyTrustedBuilderID(path string, builderID *string) (string, error) {
|
||||
// No builder ID provided by user: use the default trusted workflows.
|
||||
if builderID == nil || *builderID == "" {
|
||||
if _, ok := defaultTrustedReusableWorkflows[path]; !ok {
|
||||
return "", fmt.Errorf("%w: %s", serrors.ErrorUntrustedReusableWorkflow, path)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
return "https://github.com/" + path, nil
|
||||
}
|
||||
|
||||
// Only allow `@refs/heads/main` for the builder and the e2e tests that need to work at HEAD.
|
||||
@@ -66,7 +91,7 @@ func verifyTrustedBuilderRef(id *WorkflowIdentity, ref string) error {
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(ref, "refs/tags/") {
|
||||
return fmt.Errorf("%w: %s: not of the form 'refs/tags/name'", errorInvalidRef, ref)
|
||||
return fmt.Errorf("%w: %s: not of the form 'refs/tags/name'", serrors.ErrorInvalidRef, ref)
|
||||
}
|
||||
|
||||
// Valid semver of the form vX.Y.Z with no metadata.
|
||||
@@ -75,7 +100,7 @@ func verifyTrustedBuilderRef(id *WorkflowIdentity, ref string) error {
|
||||
len(strings.Split(pin, ".")) == 3 &&
|
||||
semver.Prerelease(pin) == "" &&
|
||||
semver.Build(pin) == "") {
|
||||
return fmt.Errorf("%w: %s: not of the form vX.Y.Z", errorInvalidRef, pin)
|
||||
return fmt.Errorf("%w: %s: not of the form vX.Y.Z", serrors.ErrorInvalidRef, pin)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
package verification
|
||||
package gha
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
serrors "github.com/slsa-framework/slsa-verifier/errors"
|
||||
"github.com/slsa-framework/slsa-verifier/options"
|
||||
)
|
||||
|
||||
func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
workflow *WorkflowIdentity
|
||||
source string
|
||||
err error
|
||||
name string
|
||||
workflow *WorkflowIdentity
|
||||
buildOpts *options.BuilderOpts
|
||||
builderID string
|
||||
source string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "invalid job workflow ref",
|
||||
@@ -25,7 +30,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Issuer: "https://token.actions.githubusercontent.com",
|
||||
},
|
||||
source: "asraa/slsa-on-github-test",
|
||||
err: errorMalformedWorkflowURI,
|
||||
err: serrors.ErrorMalformedURI,
|
||||
},
|
||||
{
|
||||
name: "untrusted job workflow ref",
|
||||
@@ -37,7 +42,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Issuer: "https://token.actions.githubusercontent.com",
|
||||
},
|
||||
source: "asraa/slsa-on-github-test",
|
||||
err: ErrorUntrustedReusableWorkflow,
|
||||
err: serrors.ErrorUntrustedReusableWorkflow,
|
||||
},
|
||||
{
|
||||
name: "untrusted job workflow ref for general repos",
|
||||
@@ -49,7 +54,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Issuer: "https://bad.issuer.com",
|
||||
},
|
||||
source: "asraa/slsa-on-github-test",
|
||||
err: errorInvalidRef,
|
||||
err: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "valid main ref for trusted builder",
|
||||
@@ -60,7 +65,8 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Trigger: "workflow_dispatch",
|
||||
Issuer: "https://token.actions.githubusercontent.com",
|
||||
},
|
||||
source: trustedBuilderRepository,
|
||||
source: trustedBuilderRepository,
|
||||
builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml",
|
||||
},
|
||||
{
|
||||
name: "valid main ref for e2e test",
|
||||
@@ -71,7 +77,38 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Trigger: "workflow_dispatch",
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: e2eTestRepository,
|
||||
builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml",
|
||||
},
|
||||
{
|
||||
name: "valid main ref for e2e test - match builderID",
|
||||
workflow: &WorkflowIdentity{
|
||||
CallerRepository: e2eTestRepository,
|
||||
CallerHash: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b",
|
||||
JobWobWorkflowRef: trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml@refs/heads/main",
|
||||
Trigger: "workflow_dispatch",
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: e2eTestRepository,
|
||||
buildOpts: &options.BuilderOpts{
|
||||
ExpectedID: asStringPointer("https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml"),
|
||||
},
|
||||
builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml",
|
||||
},
|
||||
{
|
||||
name: "valid main ref for e2e test - mismatch builderID",
|
||||
workflow: &WorkflowIdentity{
|
||||
CallerRepository: e2eTestRepository,
|
||||
CallerHash: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b",
|
||||
JobWobWorkflowRef: trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml@refs/heads/main",
|
||||
Trigger: "workflow_dispatch",
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: e2eTestRepository,
|
||||
buildOpts: &options.BuilderOpts{
|
||||
ExpectedID: asStringPointer("some-other-builderID"),
|
||||
},
|
||||
err: serrors.ErrorUntrustedReusableWorkflow,
|
||||
},
|
||||
{
|
||||
name: "unexpected source for e2e test",
|
||||
@@ -82,8 +119,9 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Trigger: "workflow_dispatch",
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: "malicious/source",
|
||||
err: ErrorMismatchRepository,
|
||||
source: "malicious/source",
|
||||
err: serrors.ErrorMismatchSource,
|
||||
builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml",
|
||||
},
|
||||
{
|
||||
name: "valid main ref for builder",
|
||||
@@ -94,7 +132,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: "malicious/source",
|
||||
err: ErrorMismatchRepository,
|
||||
err: serrors.ErrorMismatchSource,
|
||||
},
|
||||
{
|
||||
name: "unexpected source",
|
||||
@@ -106,7 +144,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: "asraa/slsa-on-github-test",
|
||||
err: ErrorMismatchRepository,
|
||||
err: serrors.ErrorMismatchSource,
|
||||
},
|
||||
{
|
||||
name: "valid workflow identity",
|
||||
@@ -117,7 +155,38 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Trigger: "workflow_dispatch",
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: "asraa/slsa-on-github-test",
|
||||
builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml",
|
||||
},
|
||||
{
|
||||
name: "valid workflow identity - match builderID",
|
||||
workflow: &WorkflowIdentity{
|
||||
CallerRepository: "asraa/slsa-on-github-test",
|
||||
CallerHash: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b",
|
||||
JobWobWorkflowRef: trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml@refs/tags/v1.2.3",
|
||||
Trigger: "workflow_dispatch",
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: "asraa/slsa-on-github-test",
|
||||
buildOpts: &options.BuilderOpts{
|
||||
ExpectedID: asStringPointer("https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml"),
|
||||
},
|
||||
builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml",
|
||||
},
|
||||
{
|
||||
name: "valid workflow identity - mismatch builderID",
|
||||
workflow: &WorkflowIdentity{
|
||||
CallerRepository: "asraa/slsa-on-github-test",
|
||||
CallerHash: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b",
|
||||
JobWobWorkflowRef: trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml@refs/tags/v1.2.3",
|
||||
Trigger: "workflow_dispatch",
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: "asraa/slsa-on-github-test",
|
||||
buildOpts: &options.BuilderOpts{
|
||||
ExpectedID: asStringPointer("some-other-builderID"),
|
||||
},
|
||||
err: serrors.ErrorUntrustedReusableWorkflow,
|
||||
},
|
||||
{
|
||||
name: "invalid workflow identity with prerelease",
|
||||
@@ -128,8 +197,9 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Trigger: "workflow_dispatch",
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: "asraa/slsa-on-github-test",
|
||||
err: errorInvalidRef,
|
||||
source: "asraa/slsa-on-github-test",
|
||||
err: serrors.ErrorInvalidRef,
|
||||
builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml",
|
||||
},
|
||||
{
|
||||
name: "invalid workflow identity with build",
|
||||
@@ -141,7 +211,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: "asraa/slsa-on-github-test",
|
||||
err: errorInvalidRef,
|
||||
err: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "invalid workflow identity with metadata",
|
||||
@@ -153,7 +223,7 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: "asraa/slsa-on-github-test",
|
||||
err: errorInvalidRef,
|
||||
err: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "valid workflow identity with fully qualified source",
|
||||
@@ -164,17 +234,124 @@ func Test_VerifyWorkflowIdentity(t *testing.T) {
|
||||
Trigger: "workflow_dispatch",
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: "github.com/asraa/slsa-on-github-test",
|
||||
builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml",
|
||||
},
|
||||
{
|
||||
name: "valid workflow identity with fully qualified source - match builderID",
|
||||
workflow: &WorkflowIdentity{
|
||||
CallerRepository: "asraa/slsa-on-github-test",
|
||||
CallerHash: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b",
|
||||
JobWobWorkflowRef: trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml@refs/tags/v1.2.3",
|
||||
Trigger: "workflow_dispatch",
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: "github.com/asraa/slsa-on-github-test",
|
||||
buildOpts: &options.BuilderOpts{
|
||||
ExpectedID: asStringPointer("https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml"),
|
||||
},
|
||||
builderID: "https://github.com/" + trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml",
|
||||
},
|
||||
{
|
||||
name: "valid workflow identity with fully qualified source - mismatch builderID",
|
||||
workflow: &WorkflowIdentity{
|
||||
CallerRepository: "asraa/slsa-on-github-test",
|
||||
CallerHash: "0dfcd24824432c4ce587f79c918eef8fc2c44d7b",
|
||||
JobWobWorkflowRef: trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml@refs/tags/v1.2.3",
|
||||
Trigger: "workflow_dispatch",
|
||||
Issuer: certOidcIssuer,
|
||||
},
|
||||
source: "github.com/asraa/slsa-on-github-test",
|
||||
buildOpts: &options.BuilderOpts{
|
||||
ExpectedID: asStringPointer("some-other-builderID"),
|
||||
},
|
||||
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()
|
||||
err := VerifyWorkflowIdentity(tt.workflow, tt.source)
|
||||
opts := tt.buildOpts
|
||||
if opts == nil {
|
||||
opts = &options.BuilderOpts{}
|
||||
}
|
||||
id, err := VerifyWorkflowIdentity(tt.workflow, opts, tt.source)
|
||||
if !errCmp(err, tt.err) {
|
||||
t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if id != tt.builderID {
|
||||
t.Errorf(cmp.Diff(id, tt.builderID))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func asStringPointer(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func Test_verifyTrustedBuilderID(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
id *string
|
||||
path string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
name: "default trusted",
|
||||
path: trustedBuilderRepository + "/.github/workflows/generator_generic_slsa3.yml",
|
||||
},
|
||||
{
|
||||
name: "valid ID for GitHub builder",
|
||||
path: "some/repo/someBuilderID",
|
||||
id: asStringPointer("https://github.com/some/repo/someBuilderID"),
|
||||
},
|
||||
{
|
||||
name: "non GitHub builder ID",
|
||||
path: "some/repo/someBuilderID",
|
||||
id: asStringPointer("https://not-github.com/some/repo/someBuilderID"),
|
||||
expected: serrors.ErrorUntrustedReusableWorkflow,
|
||||
},
|
||||
{
|
||||
name: "mismatch org GitHub",
|
||||
path: "some/repo/someBuilderID",
|
||||
id: asStringPointer("https://github.com/other/repo/someBuilderID"),
|
||||
expected: serrors.ErrorUntrustedReusableWorkflow,
|
||||
},
|
||||
{
|
||||
name: "mismatch name GitHub",
|
||||
path: "some/repo/someBuilderID",
|
||||
id: asStringPointer("https://github.com/some/other/someBuilderID"),
|
||||
expected: serrors.ErrorUntrustedReusableWorkflow,
|
||||
},
|
||||
{
|
||||
name: "mismatch id GitHub",
|
||||
path: "some/repo/someBuilderID",
|
||||
id: asStringPointer("https://github.com/some/repo/ID"),
|
||||
expected: 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.id)
|
||||
if !errCmp(err, tt.expected) {
|
||||
t.Errorf(cmp.Diff(err, tt.expected, cmpopts.EquateErrors()))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
expectedID := "https://github.com/" + tt.path
|
||||
if id != expectedID {
|
||||
t.Errorf(cmp.Diff(id, expectedID))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -202,31 +379,31 @@ func Test_verifyTrustedBuilderRef(t *testing.T) {
|
||||
name: "no patch semver for other builder",
|
||||
callerRepo: trustedBuilderRepository,
|
||||
builderRef: "refs/tags/v1.2",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "no min semver for builder",
|
||||
callerRepo: trustedBuilderRepository,
|
||||
builderRef: "refs/tags/v1",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "full semver with prerelease for builder",
|
||||
callerRepo: trustedBuilderRepository,
|
||||
builderRef: "refs/tags/v1.2.3-alpha",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "full semver with build for builder",
|
||||
callerRepo: trustedBuilderRepository,
|
||||
builderRef: "refs/tags/v1.2.3+123",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "full semver with build/prerelease for builder",
|
||||
callerRepo: trustedBuilderRepository,
|
||||
builderRef: "refs/tags/v1.2.3-alpha+123",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
// E2e tests repo.
|
||||
{
|
||||
@@ -243,38 +420,38 @@ func Test_verifyTrustedBuilderRef(t *testing.T) {
|
||||
name: "no patch semver for test repo",
|
||||
callerRepo: e2eTestRepository,
|
||||
builderRef: "refs/tags/v1.2",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "no min semver for test repo",
|
||||
callerRepo: e2eTestRepository,
|
||||
builderRef: "refs/tags/v1",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "full semver with prerelease for test repo",
|
||||
callerRepo: e2eTestRepository,
|
||||
builderRef: "refs/tags/v1.2.3-alpha",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "full semver with build for test repo",
|
||||
callerRepo: e2eTestRepository,
|
||||
builderRef: "refs/tags/v1.2.3+123",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "full semver with build/prerelease for test repo",
|
||||
callerRepo: e2eTestRepository,
|
||||
builderRef: "refs/tags/v1.2.3-alpha+123",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
// Other repos.
|
||||
{
|
||||
name: "main not allowed for other repos",
|
||||
callerRepo: "some/repo",
|
||||
builderRef: "refs/heads/main",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "full semver for other repos",
|
||||
@@ -285,31 +462,31 @@ func Test_verifyTrustedBuilderRef(t *testing.T) {
|
||||
name: "no patch semver for other repos",
|
||||
callerRepo: "some/repo",
|
||||
builderRef: "refs/tags/v1.2",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "no min semver for other repos",
|
||||
callerRepo: "some/repo",
|
||||
builderRef: "refs/tags/v1",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "full semver with prerelease for other repos",
|
||||
callerRepo: "some/repo",
|
||||
builderRef: "refs/tags/v1.2.3-alpha",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "full semver with build for other repos",
|
||||
callerRepo: "some/repo",
|
||||
builderRef: "refs/tags/v1.2.3+123",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
{
|
||||
name: "full semver with build/prerelease for other repos",
|
||||
callerRepo: "some/repo",
|
||||
builderRef: "refs/tags/v1.2.3-alpha+123",
|
||||
expected: errorInvalidRef,
|
||||
expected: serrors.ErrorInvalidRef,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -1,4 +1,4 @@
|
||||
package verification
|
||||
package gha
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -14,6 +14,9 @@ import (
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
"github.com/sigstore/rekor/pkg/generated/client"
|
||||
|
||||
serrors "github.com/slsa-framework/slsa-verifier/errors"
|
||||
"github.com/slsa-framework/slsa-verifier/options"
|
||||
)
|
||||
|
||||
func EnvelopeFromBytes(payload []byte) (env *dsselib.Envelope, err error) {
|
||||
@@ -25,34 +28,128 @@ func EnvelopeFromBytes(payload []byte) (env *dsselib.Envelope, err error) {
|
||||
func provenanceFromEnv(env *dsselib.Envelope) (prov *intoto.ProvenanceStatement, err error) {
|
||||
pyld, err := base64.StdEncoding.DecodeString(env.Payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "decoding payload")
|
||||
return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "decoding payload")
|
||||
}
|
||||
prov = &intoto.ProvenanceStatement{}
|
||||
if err := json.Unmarshal(pyld, prov); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "unmarshalling json")
|
||||
return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "unmarshalling json")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Verify Builder ID in provenance statement.
|
||||
func verifyBuilderID(prov *intoto.ProvenanceStatement, builderID string) error {
|
||||
// Check that the BuilderID is well-formed.
|
||||
provid, err := sourceFromURI(prov.Predicate.Builder.ID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Note: builderID does not contain the tag.
|
||||
// TODO(#189): support cases where user wants to match on the full builderID, including the tag.
|
||||
bid, err := sourceFromURI(builderID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if provid != bid {
|
||||
return fmt.Errorf("%w: expected '%s' in builder.id, got '%s'", serrors.ErrorMismatchBuilderID,
|
||||
bid, provid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func asURI(s string) string {
|
||||
source := s
|
||||
if !strings.HasPrefix(source, "https://") &&
|
||||
!strings.HasPrefix(source, "git+") {
|
||||
source = "git+https://" + source
|
||||
}
|
||||
if !strings.HasPrefix(source, "git+") {
|
||||
source = "git+" + source
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
// Verify source URI in provenance statement.
|
||||
func verifySourceURI(prov *intoto.ProvenanceStatement, expectedSourceURI string) error {
|
||||
source := asURI(expectedSourceURI)
|
||||
|
||||
// We expect github.com URIs only.
|
||||
if !strings.HasPrefix(source, "git+https://github.com/") {
|
||||
return fmt.Errorf("%w: expected source github.com repository '%s'", serrors.ErrorMalformedURI,
|
||||
source)
|
||||
}
|
||||
|
||||
// Verify source from ConfigSource field.
|
||||
configURI, err := sourceFromURI(prov.Predicate.Invocation.ConfigSource.URI, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if configURI != source {
|
||||
return fmt.Errorf("%w: expected source '%s' in configSource.uri, got '%s'", serrors.ErrorMismatchSource,
|
||||
source, prov.Predicate.Invocation.ConfigSource.URI)
|
||||
}
|
||||
|
||||
// Verify source from material section.
|
||||
if len(prov.Predicate.Materials) == 0 {
|
||||
return fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no material")
|
||||
}
|
||||
materialURI, err := sourceFromURI(prov.Predicate.Materials[0].URI, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if materialURI != source {
|
||||
return fmt.Errorf("%w: expected source '%s' in material section, got '%s'", serrors.ErrorMismatchSource,
|
||||
source, prov.Predicate.Materials[0].URI)
|
||||
}
|
||||
|
||||
// Last, verify that both fields match.
|
||||
// We use the full URI to match on the tag as well.
|
||||
if prov.Predicate.Invocation.ConfigSource.URI != prov.Predicate.Materials[0].URI {
|
||||
return fmt.Errorf("%w: material and config URIs do not match: '%s' != '%s'",
|
||||
serrors.ErrorInvalidDssePayload,
|
||||
prov.Predicate.Invocation.ConfigSource.URI, prov.Predicate.Materials[0].URI)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sourceFromURI(uri string, allowNotTag bool) (string, error) {
|
||||
if uri == "" {
|
||||
return "", fmt.Errorf("%w: empty uri", serrors.ErrorMalformedURI)
|
||||
}
|
||||
|
||||
r := strings.SplitN(uri, "@", 2)
|
||||
if len(r) < 2 && !allowNotTag {
|
||||
return "", fmt.Errorf("%w: %s", serrors.ErrorMalformedURI,
|
||||
uri)
|
||||
}
|
||||
if len(r) < 1 {
|
||||
return "", fmt.Errorf("%w: %s", serrors.ErrorMalformedURI,
|
||||
uri)
|
||||
}
|
||||
return r[0], nil
|
||||
}
|
||||
|
||||
// Verify SHA256 Subject Digest from the provenance statement.
|
||||
func verifySha256Digest(prov *intoto.ProvenanceStatement, expectedHash string) error {
|
||||
if len(prov.Subject) == 0 {
|
||||
return fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "no subjects")
|
||||
return fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no subjects")
|
||||
}
|
||||
|
||||
for _, subject := range prov.Subject {
|
||||
digestSet := subject.Digest
|
||||
hash, exists := digestSet["sha256"]
|
||||
if !exists {
|
||||
return fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "no sha256 subject digest")
|
||||
return fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "no sha256 subject digest")
|
||||
}
|
||||
|
||||
if strings.EqualFold(hash, expectedHash) {
|
||||
if hash == expectedHash {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("expected hash '%s' not found: %w", expectedHash, errorMismatchHash)
|
||||
return fmt.Errorf("expected hash '%s' not found: %w", expectedHash, serrors.ErrorMismatchHash)
|
||||
}
|
||||
|
||||
// VerifyProvenanceSignature returns the verified DSSE envelope containing the provenance
|
||||
@@ -85,32 +182,42 @@ func VerifyProvenanceSignature(ctx context.Context, rClient *client.Rekor, prove
|
||||
return env, cert, nil
|
||||
}
|
||||
|
||||
func VerifyProvenance(env *dsselib.Envelope, opts *ProvenanceOpts) error {
|
||||
func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceOpts) error {
|
||||
prov, err := provenanceFromEnv(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify Builder ID.
|
||||
if err := verifyBuilderID(prov, provenanceOpts.ExpectedBuilderID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify source.
|
||||
if err := verifySourceURI(prov, provenanceOpts.ExpectedSourceURI); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify subject digest.
|
||||
if err := verifySha256Digest(prov, opts.ExpectedDigest); err != nil {
|
||||
if err := verifySha256Digest(prov, provenanceOpts.ExpectedDigest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify the branch.
|
||||
if err := VerifyBranch(prov, opts.ExpectedBranch); err != nil {
|
||||
if err := VerifyBranch(prov, provenanceOpts.ExpectedBranch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify the tag.
|
||||
if opts.ExpectedTag != nil {
|
||||
if err := VerifyTag(prov, *opts.ExpectedTag); err != nil {
|
||||
if provenanceOpts.ExpectedTag != nil {
|
||||
if err := VerifyTag(prov, *provenanceOpts.ExpectedTag); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the versioned tag.
|
||||
if opts.ExpectedVersionedTag != nil {
|
||||
if err := VerifyVersionedTag(prov, *opts.ExpectedVersionedTag); err != nil {
|
||||
if provenanceOpts.ExpectedVersionedTag != nil {
|
||||
if err := VerifyVersionedTag(prov, *provenanceOpts.ExpectedVersionedTag); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -126,7 +233,7 @@ func VerifyBranch(prov *intoto.ProvenanceStatement, expectedBranch string) error
|
||||
|
||||
expectedBranch = "refs/heads/" + expectedBranch
|
||||
if !strings.EqualFold(branch, expectedBranch) {
|
||||
return fmt.Errorf("expected branch '%s', got '%s': %w", expectedBranch, branch, ErrorMismatchBranch)
|
||||
return fmt.Errorf("expected branch '%s', got '%s': %w", expectedBranch, branch, serrors.ErrorMismatchBranch)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -140,7 +247,7 @@ func VerifyTag(prov *intoto.ProvenanceStatement, expectedTag string) error {
|
||||
|
||||
expectedTag = "refs/tags/" + expectedTag
|
||||
if !strings.EqualFold(tag, expectedTag) {
|
||||
return fmt.Errorf("expected tag '%s', got '%s': %w", expectedTag, tag, ErrorMismatchTag)
|
||||
return fmt.Errorf("expected tag '%s', got '%s': %w", expectedTag, tag, serrors.ErrorMismatchTag)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -149,7 +256,7 @@ func VerifyTag(prov *intoto.ProvenanceStatement, expectedTag string) error {
|
||||
func VerifyVersionedTag(prov *intoto.ProvenanceStatement, expectedTag string) error {
|
||||
// Validate and canonicalize the provenance tag.
|
||||
if !semver.IsValid(expectedTag) {
|
||||
return fmt.Errorf("%s: %w", expectedTag, ErrorInvalidSemver)
|
||||
return fmt.Errorf("%s: %w", expectedTag, serrors.ErrorInvalidSemver)
|
||||
}
|
||||
|
||||
// Retrieve, validate and canonicalize the provenance tag.
|
||||
@@ -162,7 +269,7 @@ func VerifyVersionedTag(prov *intoto.ProvenanceStatement, expectedTag string) er
|
||||
}
|
||||
semTag := semver.Canonical(strings.TrimPrefix(tag, "refs/tags/"))
|
||||
if !semver.IsValid(semTag) {
|
||||
return fmt.Errorf("%s: %w", expectedTag, ErrorInvalidSemver)
|
||||
return fmt.Errorf("%s: %w", expectedTag, serrors.ErrorInvalidSemver)
|
||||
}
|
||||
|
||||
// Major should always be the same.
|
||||
@@ -170,7 +277,7 @@ func VerifyVersionedTag(prov *intoto.ProvenanceStatement, expectedTag string) er
|
||||
major := semver.Major(semTag)
|
||||
if major != expectedMajor {
|
||||
return fmt.Errorf("%w: major version expected '%s', got '%s'",
|
||||
ErrorMismatchVersionedTag, expectedMajor, major)
|
||||
serrors.ErrorMismatchVersionedTag, expectedMajor, major)
|
||||
}
|
||||
|
||||
expectedMinor, err := minorVersion(expectedTag)
|
||||
@@ -183,7 +290,7 @@ func VerifyVersionedTag(prov *intoto.ProvenanceStatement, expectedTag string) er
|
||||
|
||||
if minor != expectedMinor {
|
||||
return fmt.Errorf("%w: minor version expected '%s', got '%s'",
|
||||
ErrorMismatchVersionedTag, expectedMinor, minor)
|
||||
serrors.ErrorMismatchVersionedTag, expectedMinor, minor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +304,7 @@ func VerifyVersionedTag(prov *intoto.ProvenanceStatement, expectedTag string) er
|
||||
|
||||
if patch != expectedPatch {
|
||||
return fmt.Errorf("%w: patch version expected '%s', got '%s'",
|
||||
ErrorMismatchVersionedTag, expectedPatch, patch)
|
||||
serrors.ErrorMismatchVersionedTag, expectedPatch, patch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +327,7 @@ func patchVersion(v string) (string, error) {
|
||||
func extractFromVersion(v string, i int) (string, error) {
|
||||
parts := strings.Split(v, ".")
|
||||
if len(parts) <= i {
|
||||
return "", fmt.Errorf("%s: %w", v, ErrorInvalidSemver)
|
||||
return "", fmt.Errorf("%s: %w", v, serrors.ErrorInvalidSemver)
|
||||
}
|
||||
return parts[i], nil
|
||||
}
|
||||
@@ -228,13 +335,13 @@ func extractFromVersion(v string, i int) (string, error) {
|
||||
func getAsString(environment map[string]interface{}, field string) (string, error) {
|
||||
value, ok := environment[field]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload,
|
||||
return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload,
|
||||
fmt.Sprintf("environment type for %s", field))
|
||||
}
|
||||
|
||||
i, ok := value.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "environment type string")
|
||||
return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "environment type string")
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
@@ -242,12 +349,12 @@ func getAsString(environment map[string]interface{}, field string) (string, erro
|
||||
func getEventPayload(environment map[string]interface{}) (map[string]interface{}, error) {
|
||||
eventPayload, ok := environment["github_event_payload"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type event payload")
|
||||
return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type event payload")
|
||||
}
|
||||
|
||||
payload, ok := eventPayload.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type payload")
|
||||
return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type payload")
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
@@ -302,12 +409,12 @@ func getTargetCommittish(environment map[string]interface{}) (string, error) {
|
||||
// For a release event, we look for release.target_commitish.
|
||||
releasePayload, ok := payload["release"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "release absent from payload")
|
||||
return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "release absent from payload")
|
||||
}
|
||||
|
||||
release, ok := releasePayload.(map[string]interface{})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type releasePayload")
|
||||
return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type releasePayload")
|
||||
}
|
||||
|
||||
branch, err := getAsString(release, "target_commitish")
|
||||
@@ -333,7 +440,7 @@ func getBranchForTag(environment map[string]interface{}) (string, error) {
|
||||
func getTag(prov *intoto.ProvenanceStatement) (string, error) {
|
||||
environment, ok := prov.Predicate.Invocation.Environment.(map[string]interface{})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type")
|
||||
return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type")
|
||||
}
|
||||
|
||||
refType, err := getAsString(environment, "github_ref_type")
|
||||
@@ -347,7 +454,7 @@ func getTag(prov *intoto.ProvenanceStatement) (string, error) {
|
||||
case "tag":
|
||||
return getAsString(environment, "github_ref")
|
||||
default:
|
||||
return "", fmt.Errorf("%w: %s %s", ErrorInvalidDssePayload,
|
||||
return "", fmt.Errorf("%w: %s %s", serrors.ErrorInvalidDssePayload,
|
||||
"unknown ref type", refType)
|
||||
}
|
||||
}
|
||||
@@ -356,7 +463,7 @@ func getTag(prov *intoto.ProvenanceStatement) (string, error) {
|
||||
func getBranch(prov *intoto.ProvenanceStatement) (string, error) {
|
||||
environment, ok := prov.Predicate.Invocation.Environment.(map[string]interface{})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type")
|
||||
return "", fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type")
|
||||
}
|
||||
|
||||
refType, err := getAsString(environment, "github_ref_type")
|
||||
@@ -370,7 +477,7 @@ func getBranch(prov *intoto.ProvenanceStatement) (string, error) {
|
||||
case "tag":
|
||||
return getBranchForTag(environment)
|
||||
default:
|
||||
return "", fmt.Errorf("%w: %s %s", ErrorInvalidDssePayload,
|
||||
return "", fmt.Errorf("%w: %s %s", serrors.ErrorInvalidDssePayload,
|
||||
"unknown ref type", refType)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package verification
|
||||
package gha
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
||||
|
||||
serrors "github.com/slsa-framework/slsa-verifier/errors"
|
||||
)
|
||||
|
||||
func provenanceFromBytes(payload []byte) (*intoto.ProvenanceStatement, error) {
|
||||
@@ -29,25 +32,25 @@ func Test_VerifySha256Subject(t *testing.T) {
|
||||
name: "invalid dsse: not SLSA predicate",
|
||||
path: "./testdata/dsse-not-slsa.intoto.jsonl",
|
||||
artifactHash: "0ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e",
|
||||
expected: ErrorInvalidDssePayload,
|
||||
expected: serrors.ErrorInvalidDssePayload,
|
||||
},
|
||||
{
|
||||
name: "invalid dsse: nil subject",
|
||||
path: "./testdata/dsse-no-subject.intoto.jsonl",
|
||||
artifactHash: "0ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e",
|
||||
expected: ErrorInvalidDssePayload,
|
||||
expected: serrors.ErrorInvalidDssePayload,
|
||||
},
|
||||
{
|
||||
name: "invalid dsse: no sha256 subject digest",
|
||||
path: "./testdata/dsse-no-subject-hash.intoto.jsonl",
|
||||
artifactHash: "0ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e",
|
||||
expected: ErrorInvalidDssePayload,
|
||||
expected: serrors.ErrorInvalidDssePayload,
|
||||
},
|
||||
{
|
||||
name: "mismatched artifact hash with env",
|
||||
path: "./testdata/dsse-valid.intoto.jsonl",
|
||||
artifactHash: "1ae7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e",
|
||||
expected: errorMismatchHash,
|
||||
expected: serrors.ErrorMismatchHash,
|
||||
},
|
||||
{
|
||||
name: "valid entry",
|
||||
@@ -71,7 +74,7 @@ func Test_VerifySha256Subject(t *testing.T) {
|
||||
name: "multiple subjects invalid hash",
|
||||
path: "./testdata/dsse-valid-multi-subjects.intoto.jsonl",
|
||||
artifactHash: "04e7e4fa71686538440012ee36a2634dbaa19df2dd16a466f52411fb348bbc4e",
|
||||
expected: errorMismatchHash,
|
||||
expected: serrors.ErrorMismatchHash,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -96,6 +99,347 @@ func Test_VerifySha256Subject(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_verifySourceURI(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
prov *intoto.ProvenanceStatement
|
||||
sourceURI string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
name: "source has no @",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Invocation: slsa.ProvenanceInvocation{
|
||||
ConfigSource: slsa.ConfigSource{
|
||||
URI: "git+https://github.com/some/repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "git+https://github.com/some/repo",
|
||||
expected: serrors.ErrorMalformedURI,
|
||||
},
|
||||
{
|
||||
name: "empty materials",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Invocation: slsa.ProvenanceInvocation{
|
||||
ConfigSource: slsa.ConfigSource{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "git+https://github.com/some/repo",
|
||||
expected: serrors.ErrorInvalidDssePayload,
|
||||
},
|
||||
{
|
||||
name: "empty configSource",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Materials: []slsa.ProvenanceMaterial{
|
||||
{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "git+https://github.com/some/repo",
|
||||
expected: serrors.ErrorMalformedURI,
|
||||
},
|
||||
{
|
||||
name: "empty uri materials",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Materials: []slsa.ProvenanceMaterial{
|
||||
{
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "git+https://github.com/some/repo",
|
||||
expected: serrors.ErrorMalformedURI,
|
||||
},
|
||||
{
|
||||
name: "no tag uri materials",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Materials: []slsa.ProvenanceMaterial{
|
||||
{
|
||||
URI: "git+https://github.com/some/repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "git+https://github.com/some/repo",
|
||||
expected: serrors.ErrorMalformedURI,
|
||||
},
|
||||
{
|
||||
name: "no tag uri configSource",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Materials: []slsa.ProvenanceMaterial{
|
||||
{
|
||||
URI: "git+https://github.com/some/repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "git+https://github.com/some/repo",
|
||||
expected: serrors.ErrorMalformedURI,
|
||||
},
|
||||
{
|
||||
name: "match source",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Invocation: slsa.ProvenanceInvocation{
|
||||
ConfigSource: slsa.ConfigSource{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
Materials: []slsa.ProvenanceMaterial{
|
||||
{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "git+https://github.com/some/repo",
|
||||
},
|
||||
{
|
||||
name: "match source no git",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Invocation: slsa.ProvenanceInvocation{
|
||||
ConfigSource: slsa.ConfigSource{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
Materials: []slsa.ProvenanceMaterial{
|
||||
{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "https://github.com/some/repo",
|
||||
},
|
||||
{
|
||||
name: "match source no git+https",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Invocation: slsa.ProvenanceInvocation{
|
||||
ConfigSource: slsa.ConfigSource{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
Materials: []slsa.ProvenanceMaterial{
|
||||
{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "github.com/some/repo",
|
||||
},
|
||||
{
|
||||
name: "match source no repo",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Invocation: slsa.ProvenanceInvocation{
|
||||
ConfigSource: slsa.ConfigSource{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
Materials: []slsa.ProvenanceMaterial{
|
||||
{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "some/repo",
|
||||
expected: serrors.ErrorMalformedURI,
|
||||
},
|
||||
{
|
||||
name: "mismatch materials configSource tag",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Invocation: slsa.ProvenanceInvocation{
|
||||
ConfigSource: slsa.ConfigSource{
|
||||
URI: "git+https://github.com/some/repo@v1.2.4",
|
||||
},
|
||||
},
|
||||
Materials: []slsa.ProvenanceMaterial{
|
||||
{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "git+https://github.com/some/repo",
|
||||
expected: serrors.ErrorInvalidDssePayload,
|
||||
},
|
||||
{
|
||||
name: "mismatch materials configSource org",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Invocation: slsa.ProvenanceInvocation{
|
||||
ConfigSource: slsa.ConfigSource{
|
||||
URI: "git+https://github.com/other/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
Materials: []slsa.ProvenanceMaterial{
|
||||
{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "git+https://github.com/some/repo",
|
||||
expected: serrors.ErrorMismatchSource,
|
||||
},
|
||||
{
|
||||
name: "mismatch materials configSource name",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Invocation: slsa.ProvenanceInvocation{
|
||||
ConfigSource: slsa.ConfigSource{
|
||||
URI: "git+https://github.com/some/other@v1.2.3",
|
||||
},
|
||||
},
|
||||
Materials: []slsa.ProvenanceMaterial{
|
||||
{
|
||||
URI: "git+https://github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "git+https://github.com/some/repo",
|
||||
expected: serrors.ErrorMismatchSource,
|
||||
},
|
||||
{
|
||||
name: "not github.com repo",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Invocation: slsa.ProvenanceInvocation{
|
||||
ConfigSource: slsa.ConfigSource{
|
||||
URI: "git+https://not-github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
Materials: []slsa.ProvenanceMaterial{
|
||||
{
|
||||
URI: "git+https://not-github.com/some/repo@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourceURI: "git+https://not-github.com/some/repo",
|
||||
expected: serrors.ErrorMalformedURI,
|
||||
},
|
||||
}
|
||||
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()
|
||||
|
||||
err := verifySourceURI(tt.prov, tt.sourceURI)
|
||||
if !errCmp(err, tt.expected) {
|
||||
t.Errorf(cmp.Diff(err, tt.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_verifyBuilderID(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
prov *intoto.ProvenanceStatement
|
||||
id string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
name: "id has no @",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Builder: slsa.ProvenanceBuilder{
|
||||
ID: "some/builderID",
|
||||
},
|
||||
},
|
||||
},
|
||||
id: "some/builderID",
|
||||
expected: serrors.ErrorMalformedURI,
|
||||
},
|
||||
{
|
||||
name: "same builderID",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Builder: slsa.ProvenanceBuilder{
|
||||
ID: "some/builderID@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
id: "some/builderID",
|
||||
},
|
||||
{
|
||||
name: "same builderID full match",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Builder: slsa.ProvenanceBuilder{
|
||||
ID: "some/builderID@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
id: "some/builderID@v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "same builderID mismatch version",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Builder: slsa.ProvenanceBuilder{
|
||||
ID: "some/builderID@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
id: "some/builderID@v1.2.4",
|
||||
// TODO(#189): this should fail.
|
||||
},
|
||||
{
|
||||
name: "mismatch builderID",
|
||||
prov: &intoto.ProvenanceStatement{
|
||||
Predicate: slsa.ProvenancePredicate{
|
||||
Builder: slsa.ProvenanceBuilder{
|
||||
ID: "tome/builderID@v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
id: "some/builderID",
|
||||
expected: serrors.ErrorMismatchBuilderID,
|
||||
},
|
||||
{
|
||||
name: "empty builderID",
|
||||
prov: &intoto.ProvenanceStatement{},
|
||||
id: "some/builderID",
|
||||
expected: serrors.ErrorMalformedURI,
|
||||
},
|
||||
}
|
||||
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()
|
||||
|
||||
err := verifyBuilderID(tt.prov, tt.id)
|
||||
if !errCmp(err, tt.expected) {
|
||||
t.Errorf(cmp.Diff(err, tt.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_VerifyBranch(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
@@ -117,7 +461,7 @@ func Test_VerifyBranch(t *testing.T) {
|
||||
{
|
||||
name: "invalid ref type",
|
||||
path: "./testdata/dsse-invalid-ref-type.intoto.jsonl",
|
||||
expected: ErrorInvalidDssePayload,
|
||||
expected: serrors.ErrorInvalidDssePayload,
|
||||
},
|
||||
{
|
||||
name: "tag branch2 push trigger",
|
||||
@@ -163,17 +507,17 @@ func Test_VerifyTag(t *testing.T) {
|
||||
{
|
||||
name: "ref main",
|
||||
path: "./testdata/dsse-main-ref.intoto.jsonl",
|
||||
expected: ErrorMismatchTag,
|
||||
expected: serrors.ErrorMismatchTag,
|
||||
},
|
||||
{
|
||||
name: "ref branch3",
|
||||
path: "./testdata/dsse-branch3-ref.intoto.jsonl",
|
||||
expected: ErrorMismatchTag,
|
||||
expected: serrors.ErrorMismatchTag,
|
||||
},
|
||||
{
|
||||
name: "invalid ref type",
|
||||
path: "./testdata/dsse-invalid-ref-type.intoto.jsonl",
|
||||
expected: ErrorInvalidDssePayload,
|
||||
expected: serrors.ErrorInvalidDssePayload,
|
||||
},
|
||||
{
|
||||
name: "tag vslsa1",
|
||||
@@ -214,38 +558,38 @@ func Test_VerifyVersionedTag(t *testing.T) {
|
||||
{
|
||||
name: "ref main",
|
||||
path: "./testdata/dsse-main-ref.intoto.jsonl",
|
||||
expected: ErrorInvalidSemver,
|
||||
expected: serrors.ErrorInvalidSemver,
|
||||
tag: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "ref branch3",
|
||||
path: "./testdata/dsse-branch3-ref.intoto.jsonl",
|
||||
expected: ErrorInvalidSemver,
|
||||
expected: serrors.ErrorInvalidSemver,
|
||||
tag: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 invalid versioning",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "1.2",
|
||||
expected: ErrorInvalidSemver,
|
||||
expected: serrors.ErrorInvalidSemver,
|
||||
},
|
||||
{
|
||||
name: "invalid ref",
|
||||
path: "./testdata/dsse-invalid-ref-type.intoto.jsonl",
|
||||
expected: ErrorInvalidDssePayload,
|
||||
expected: serrors.ErrorInvalidDssePayload,
|
||||
tag: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "tag vslsa1 invalid",
|
||||
path: "./testdata/dsse-vslsa1-tag.intoto.jsonl",
|
||||
tag: "vslsa1",
|
||||
expected: ErrorInvalidSemver,
|
||||
expected: serrors.ErrorInvalidSemver,
|
||||
},
|
||||
{
|
||||
name: "tag vslsa1 invalid semver",
|
||||
path: "./testdata/dsse-vslsa1-tag.intoto.jsonl",
|
||||
tag: "v1.2.3",
|
||||
expected: ErrorInvalidSemver,
|
||||
expected: serrors.ErrorInvalidSemver,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3 exact match",
|
||||
@@ -266,25 +610,25 @@ func Test_VerifyVersionedTag(t *testing.T) {
|
||||
name: "tag v1.2.3 no match v2",
|
||||
path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl",
|
||||
tag: "v2",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3 no match v1.3",
|
||||
path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl",
|
||||
tag: "v1.3",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3 no match v1.2.4",
|
||||
path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl",
|
||||
tag: "v1.2.4",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3 no match v1.2.2",
|
||||
path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl",
|
||||
tag: "v1.2.2",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 exact v1.2",
|
||||
@@ -300,25 +644,25 @@ func Test_VerifyVersionedTag(t *testing.T) {
|
||||
name: "tag v1.1 no match v1.3",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1.1",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v0 no match v1.3",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v0",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 no match v1.3",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1.3",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 no match v1.2.3",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1.2.3",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 match v1.2.0",
|
||||
@@ -334,37 +678,37 @@ func Test_VerifyVersionedTag(t *testing.T) {
|
||||
name: "invalid v1.2+123",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1.2+123",
|
||||
expected: ErrorInvalidSemver,
|
||||
expected: serrors.ErrorInvalidSemver,
|
||||
},
|
||||
{
|
||||
name: "invalid v1.2-alpha",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1.2-alpha",
|
||||
expected: ErrorInvalidSemver,
|
||||
expected: serrors.ErrorInvalidSemver,
|
||||
},
|
||||
{
|
||||
name: "invalid v1-alpha",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1-alpha",
|
||||
expected: ErrorInvalidSemver,
|
||||
expected: serrors.ErrorInvalidSemver,
|
||||
},
|
||||
{
|
||||
name: "invalid v1+123",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1+123",
|
||||
expected: ErrorInvalidSemver,
|
||||
expected: serrors.ErrorInvalidSemver,
|
||||
},
|
||||
{
|
||||
name: "invalid v1-alpha+123",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1-alpha+123",
|
||||
expected: ErrorInvalidSemver,
|
||||
expected: serrors.ErrorInvalidSemver,
|
||||
},
|
||||
{
|
||||
name: "invalid v1.2-alpha+123",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1.2-alpha+123",
|
||||
expected: ErrorInvalidSemver,
|
||||
expected: serrors.ErrorInvalidSemver,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3-alpha match v1.2.3-alpha",
|
||||
@@ -375,7 +719,7 @@ func Test_VerifyVersionedTag(t *testing.T) {
|
||||
name: "tag v1.2.3-alpha no match v1.2.3",
|
||||
path: "./testdata/dsse-v1.2.3-alpha-tag.intoto.jsonl",
|
||||
tag: "v1.2.3",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3-alpha+123 match v1.2.3-alpha",
|
||||
@@ -401,19 +745,19 @@ func Test_VerifyVersionedTag(t *testing.T) {
|
||||
name: "tag v1.2.3-alpha no match v1.2.3-beta+123",
|
||||
path: "./testdata/dsse-v1.2.3-alpha-tag.intoto.jsonl",
|
||||
tag: "v1.2.3-beta+123",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3+123 no match v1.2.3-alpha+123",
|
||||
path: "./testdata/dsse-v1.2.3+123-tag.intoto.jsonl",
|
||||
tag: "v1.2.3-alpha+123",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3+123 no match v1.2.3-alpha",
|
||||
path: "./testdata/dsse-v1.2.3+123-tag.intoto.jsonl",
|
||||
tag: "v1.2.3-alpha",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3+123 match v1.2.3+123",
|
||||
@@ -434,19 +778,19 @@ func Test_VerifyVersionedTag(t *testing.T) {
|
||||
name: "tag v1.2.3 no match v1.2.3-aplha",
|
||||
path: "./testdata/dsse-v1.2.3-tag.intoto.jsonl",
|
||||
tag: "v1.2.3-alpha",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3-alpha no match v1.2.3-beta",
|
||||
path: "./testdata/dsse-v1.2.3-alpha-tag.intoto.jsonl",
|
||||
tag: "v1.2.3-beta",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 no match v1.2.3-beta",
|
||||
path: "./testdata/dsse-v1.2.3-alpha-tag.intoto.jsonl",
|
||||
tag: "v1.2.3-beta",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2.3 match v1.2.3+123",
|
||||
@@ -457,13 +801,13 @@ func Test_VerifyVersionedTag(t *testing.T) {
|
||||
name: "tag v1.2 no match v1.2.0-aplha+123",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v1.2.0-alpha+123",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1.2 no match v2",
|
||||
path: "./testdata/dsse-v1.2-tag.intoto.jsonl",
|
||||
tag: "v2",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1 exact match",
|
||||
@@ -474,25 +818,25 @@ func Test_VerifyVersionedTag(t *testing.T) {
|
||||
name: "tag v1 no match v2",
|
||||
path: "./testdata/dsse-v1-tag.intoto.jsonl",
|
||||
tag: "v2",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1 no match v1.2",
|
||||
path: "./testdata/dsse-v1-tag.intoto.jsonl",
|
||||
tag: "v1.2",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1 no match v0",
|
||||
path: "./testdata/dsse-v1-tag.intoto.jsonl",
|
||||
tag: "v0",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1 no match v1.2.3",
|
||||
path: "./testdata/dsse-v1-tag.intoto.jsonl",
|
||||
tag: "v1.2.3",
|
||||
expected: ErrorMismatchVersionedTag,
|
||||
expected: serrors.ErrorMismatchVersionedTag,
|
||||
},
|
||||
{
|
||||
name: "tag v1 match v1.0",
|
||||
@@ -508,7 +852,7 @@ func Test_VerifyVersionedTag(t *testing.T) {
|
||||
name: "invalid v1-alpha",
|
||||
path: "./testdata/dsse-v1-tag.intoto.jsonl",
|
||||
tag: "v1-alpha",
|
||||
expected: ErrorInvalidSemver,
|
||||
expected: serrors.ErrorInvalidSemver,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -1,4 +1,4 @@
|
||||
package verification
|
||||
package gha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -37,6 +37,8 @@ import (
|
||||
"github.com/sigstore/sigstore/pkg/signature/dsse"
|
||||
"github.com/slsa-framework/slsa-github-generator/signing/envelope"
|
||||
"github.com/transparency-dev/merkle/proof"
|
||||
|
||||
serrors "github.com/slsa-framework/slsa-verifier/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -265,11 +267,11 @@ func GetRekorEntries(rClient *client.Rekor, artifactHash string) ([]string, erro
|
||||
params.Query = &models.SearchIndex{Hash: fmt.Sprintf("sha256:%v", artifactHash)}
|
||||
resp, err := rClient.Index.SearchIndex(params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrorRekorSearch, err.Error())
|
||||
return nil, fmt.Errorf("%w: %s", serrors.ErrorRekorSearch, err.Error())
|
||||
}
|
||||
|
||||
if len(resp.Payload) == 0 {
|
||||
return nil, fmt.Errorf("%w: no matching entries found", ErrorRekorSearch)
|
||||
return nil, fmt.Errorf("%w: no matching entries found", serrors.ErrorRekorSearch)
|
||||
}
|
||||
|
||||
return resp.GetPayload(), nil
|
||||
@@ -300,11 +302,11 @@ func GetRekorEntriesWithCert(rClient *client.Rekor, provenance []byte) (*dsselib
|
||||
params.SetEntry(&searchLogQuery)
|
||||
resp, err := rClient.Entries.SearchLogQuery(params)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%w: %s", ErrorRekorSearch, err.Error())
|
||||
return nil, nil, fmt.Errorf("%w: %s", serrors.ErrorRekorSearch, err.Error())
|
||||
}
|
||||
|
||||
if len(resp.GetPayload()) != 1 {
|
||||
return nil, nil, fmt.Errorf("%w: %s", ErrorRekorSearch, "no matching rekor entries")
|
||||
return nil, nil, fmt.Errorf("%w: %s", serrors.ErrorRekorSearch, "no matching rekor entries")
|
||||
}
|
||||
|
||||
logEntry := resp.Payload[0]
|
||||
@@ -394,5 +396,5 @@ func FindSigningCertificate(ctx context.Context, uuids []string, dssePayload dss
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: got unexpected errors %s", ErrorNoValidRekorEntries, strings.Join(errs, ", "))
|
||||
return nil, fmt.Errorf("%w: got unexpected errors %s", serrors.ErrorNoValidRekorEntries, strings.Join(errs, ", "))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package verification
|
||||
package gha
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/sigstore/rekor/pkg/generated/client"
|
||||
"github.com/sigstore/rekor/pkg/generated/client/index"
|
||||
|
||||
serrors "github.com/slsa-framework/slsa-verifier/errors"
|
||||
)
|
||||
|
||||
type searchResult struct {
|
||||
@@ -46,7 +48,7 @@ func Test_GetRekorEntries(t *testing.T) {
|
||||
res: searchResult{
|
||||
err: index.NewSearchIndexDefault(500),
|
||||
},
|
||||
expected: ErrorRekorSearch,
|
||||
expected: serrors.ErrorRekorSearch,
|
||||
},
|
||||
{
|
||||
name: "no rekor entries found",
|
||||
@@ -57,7 +59,7 @@ func Test_GetRekorEntries(t *testing.T) {
|
||||
Payload: []string{},
|
||||
},
|
||||
},
|
||||
expected: ErrorRekorSearch,
|
||||
expected: serrors.ErrorRekorSearch,
|
||||
},
|
||||
{
|
||||
name: "valid rekor entries found",
|
||||
@@ -0,0 +1,90 @@
|
||||
package gha
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
|
||||
|
||||
serrors "github.com/slsa-framework/slsa-verifier/errors"
|
||||
"github.com/slsa-framework/slsa-verifier/options"
|
||||
"github.com/slsa-framework/slsa-verifier/register"
|
||||
)
|
||||
|
||||
const VerifierName = "GHA"
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
register.RegisterVerifier(VerifierName, GHAVerifierNew())
|
||||
}
|
||||
|
||||
type GHAVerifier struct{}
|
||||
|
||||
func GHAVerifierNew() *GHAVerifier {
|
||||
return &GHAVerifier{}
|
||||
}
|
||||
|
||||
// IsAuthoritativeFor returns true of the verifier can verify provenance
|
||||
// generated by the builderID.
|
||||
func (v *GHAVerifier) IsAuthoritativeFor(builderID string) bool {
|
||||
// This verifier only supports builders defined on GitHub.
|
||||
return strings.HasPrefix(builderID, "https://github.com/")
|
||||
}
|
||||
|
||||
// VerifyArtifact verifies provenance for an artifact.
|
||||
func (v *GHAVerifier) VerifyArtifact(ctx context.Context,
|
||||
provenance []byte, artifactHash string,
|
||||
provenanceOpts *options.ProvenanceOpts,
|
||||
builderOpts *options.BuilderOpts,
|
||||
) ([]byte, string, error) {
|
||||
rClient, err := rekor.NewClient(defaultRekorAddr)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
/* Verify signature on the intoto attestation. */
|
||||
env, cert, err := VerifyProvenanceSignature(ctx, rClient, provenance, artifactHash)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
/* Verify properties of the signing identity. */
|
||||
// Get the workflow info given the certificate information.
|
||||
workflowInfo, err := GetWorkflowInfoFromCertificate(cert)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Verify the workflow identity.
|
||||
builderID, err := VerifyWorkflowIdentity(workflowInfo, builderOpts,
|
||||
provenanceOpts.ExpectedSourceURI)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
/* Verify properties of the SLSA provenance. */
|
||||
// Unpack and verify info in the provenance, including the Subject Digest.
|
||||
provenanceOpts.ExpectedBuilderID = builderID
|
||||
if err := VerifyProvenance(env, provenanceOpts); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Verified build using builder https://github.com%s at commit %s\n",
|
||||
workflowInfo.JobWobWorkflowRef,
|
||||
workflowInfo.CallerHash)
|
||||
// Return verified provenance.
|
||||
r, err := base64.StdEncoding.DecodeString(env.Payload)
|
||||
return r, builderID, err
|
||||
}
|
||||
|
||||
// VerifyImage verifies provenance for an OCI image.
|
||||
func (v *GHAVerifier) VerifyImage(ctx context.Context,
|
||||
provenance []byte, artifactHash string,
|
||||
provenanceOpts *options.ProvenanceOpts,
|
||||
builderOpts *options.BuilderOpts,
|
||||
) ([]byte, string, error) {
|
||||
return nil, "todo", serrors.ErrorNotSupported
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package verifiers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
serrors "github.com/slsa-framework/slsa-verifier/errors"
|
||||
"github.com/slsa-framework/slsa-verifier/options"
|
||||
"github.com/slsa-framework/slsa-verifier/register"
|
||||
_ "github.com/slsa-framework/slsa-verifier/verifiers/internal/gcb"
|
||||
"github.com/slsa-framework/slsa-verifier/verifiers/internal/gha"
|
||||
)
|
||||
|
||||
func Verify(ctx context.Context,
|
||||
provenance []byte, artifactHash string,
|
||||
provenanceOpts *options.ProvenanceOpts,
|
||||
builderOpts *options.BuilderOpts,
|
||||
) ([]byte, string, error) {
|
||||
// If user provids a builderID, find the right verifier
|
||||
// based on its ID.
|
||||
if builderOpts.ExpectedID != nil &&
|
||||
*builderOpts.ExpectedID != "" {
|
||||
for _, v := range register.SLSAVerifiers {
|
||||
if v.IsAuthoritativeFor(*builderOpts.ExpectedID) {
|
||||
return v.VerifyArtifact(ctx, provenance, artifactHash,
|
||||
provenanceOpts, builderOpts)
|
||||
}
|
||||
}
|
||||
// No builder found.
|
||||
return nil, "", fmt.Errorf("%w: %s", serrors.ErrorVerifierNotSupported, *builderOpts.ExpectedID)
|
||||
}
|
||||
|
||||
// By default, try the GHA builders.
|
||||
return register.SLSAVerifiers[gha.VerifierName].VerifyArtifact(ctx, provenance, artifactHash,
|
||||
provenanceOpts, builderOpts)
|
||||
}
|
||||
Reference in New Issue
Block a user