mirror of
https://github.com/slsa-framework/slsa-verifier.git
synced 2026-08-18 02:56:31 +00:00
@@ -8,11 +8,17 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
serrors "github.com/slsa-framework/slsa-verifier/errors"
|
||||
"github.com/slsa-framework/slsa-verifier/options"
|
||||
"github.com/slsa-framework/slsa-verifier/verifiers"
|
||||
)
|
||||
|
||||
type workflowInputs struct {
|
||||
kv map[string]string
|
||||
}
|
||||
|
||||
var (
|
||||
provenancePath string
|
||||
builderID string
|
||||
@@ -21,6 +27,7 @@ var (
|
||||
branch string
|
||||
tag string
|
||||
versiontag string
|
||||
inputs workflowInputs
|
||||
printProvenance bool
|
||||
)
|
||||
|
||||
@@ -28,6 +35,23 @@ func experimentalEnabled() bool {
|
||||
return os.Getenv("SLSA_VERIFIER_EXPERIMENTAL") == "1"
|
||||
}
|
||||
|
||||
func (i *workflowInputs) String() string {
|
||||
return fmt.Sprintf("%v", i.kv)
|
||||
}
|
||||
|
||||
func (i *workflowInputs) Set(value string) error {
|
||||
l := strings.Split(value, "=")
|
||||
if len(l) != 2 {
|
||||
return fmt.Errorf("%w: expected 'key=value' format, got '%s'", serrors.ErrorInvalidFormat, value)
|
||||
}
|
||||
i.kv[l[0]] = l[1]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *workflowInputs) AsMap() map[string]string {
|
||||
return i.kv
|
||||
}
|
||||
|
||||
func main() {
|
||||
if experimentalEnabled() {
|
||||
flag.StringVar(&builderID, "builder-id", "", "EXPERIMENTAL: the unique builder ID who created the provenance")
|
||||
@@ -42,6 +66,9 @@ func main() {
|
||||
"[optional] expected version the binary was compiled from. Uses semantic version to match the tag")
|
||||
flag.BoolVar(&printProvenance, "print-provenance", false,
|
||||
"print the verified provenance to std out")
|
||||
inputs.kv = make(map[string]string)
|
||||
flag.Var(&inputs, "workflow-input",
|
||||
"[optional] a workflow input provided by a user at trigger time in the format 'key=value'. (Only for 'workflow_dispatch' events).")
|
||||
flag.Parse()
|
||||
|
||||
if provenancePath == "" || artifactPath == "" || source == "" {
|
||||
@@ -71,7 +98,7 @@ func main() {
|
||||
}
|
||||
|
||||
verifiedProvenance, _, err := runVerify(artifactPath, provenancePath, source,
|
||||
pbranch, pbuilderID, ptag, pversiontag)
|
||||
pbranch, pbuilderID, ptag, pversiontag, inputs.AsMap())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "FAILED: SLSA verification failed: %v\n", err)
|
||||
os.Exit(2)
|
||||
@@ -95,7 +122,7 @@ func isFlagPassed(name string) bool {
|
||||
}
|
||||
|
||||
func runVerify(artifactPath, provenancePath, source string,
|
||||
branch, builderID, ptag, pversiontag *string,
|
||||
branch, builderID, ptag, pversiontag *string, inputs map[string]string,
|
||||
) ([]byte, string, error) {
|
||||
f, err := os.Open(artifactPath)
|
||||
if err != nil {
|
||||
@@ -115,11 +142,12 @@ func runVerify(artifactPath, provenancePath, source string,
|
||||
artifactHash := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
provenanceOpts := &options.ProvenanceOpts{
|
||||
ExpectedSourceURI: source,
|
||||
ExpectedBranch: branch,
|
||||
ExpectedDigest: artifactHash,
|
||||
ExpectedVersionedTag: pversiontag,
|
||||
ExpectedTag: ptag,
|
||||
ExpectedSourceURI: source,
|
||||
ExpectedBranch: branch,
|
||||
ExpectedDigest: artifactHash,
|
||||
ExpectedVersionedTag: pversiontag,
|
||||
ExpectedTag: ptag,
|
||||
ExpectedWorkflowInputs: inputs,
|
||||
}
|
||||
|
||||
builderOpts := &options.BuilderOpts{
|
||||
|
||||
@@ -44,6 +44,7 @@ func Test_runVerify(t *testing.T) {
|
||||
pversiontag *string
|
||||
pbuilderID *string
|
||||
builderID string
|
||||
inputs map[string]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
|
||||
@@ -383,7 +384,7 @@ func Test_runVerify(t *testing.T) {
|
||||
err: serrors.ErrorNoValidRekorEntries,
|
||||
noversion: true,
|
||||
},
|
||||
// annotated tags.
|
||||
// Annotated tags.
|
||||
{
|
||||
name: "annotated tag",
|
||||
artifact: "annotated-tag",
|
||||
@@ -400,6 +401,42 @@ func Test_runVerify(t *testing.T) {
|
||||
err: serrors.ErrorMismatchBranch,
|
||||
noversion: true,
|
||||
},
|
||||
// Workflow inputs.
|
||||
{
|
||||
name: "workflow inputs match",
|
||||
artifact: "workflow-inputs",
|
||||
source: "github.com/laurentsimon/slsa-on-github-test",
|
||||
inputs: map[string]string{
|
||||
"release_version": "v1.2.3",
|
||||
"some_bool": "true",
|
||||
"some_integer": "123",
|
||||
},
|
||||
noversion: true,
|
||||
},
|
||||
{
|
||||
name: "workflow inputs missing field",
|
||||
artifact: "workflow-inputs",
|
||||
source: "github.com/laurentsimon/slsa-on-github-test",
|
||||
inputs: map[string]string{
|
||||
"release_version": "v1.2.3",
|
||||
"some_bool": "true",
|
||||
"missing_field": "123",
|
||||
},
|
||||
err: serrors.ErrorMismatchWorkflowInputs,
|
||||
noversion: true,
|
||||
},
|
||||
{
|
||||
name: "workflow inputs mismatch",
|
||||
artifact: "workflow-inputs",
|
||||
source: "github.com/laurentsimon/slsa-on-github-test",
|
||||
inputs: map[string]string{
|
||||
"release_version": "v1.2.3",
|
||||
"some_bool": "true",
|
||||
"some_integer": "321",
|
||||
},
|
||||
err: serrors.ErrorMismatchWorkflowInputs,
|
||||
noversion: true,
|
||||
},
|
||||
// Regression test of sharded UUID.
|
||||
{
|
||||
name: "regression: sharded uuids",
|
||||
@@ -458,7 +495,7 @@ func Test_runVerify(t *testing.T) {
|
||||
_, builderID, err := runVerify(artifactPath,
|
||||
provenancePath,
|
||||
tt.source, tt.pbranch, tt.pbuilderID,
|
||||
tt.ptag, tt.pversiontag)
|
||||
tt.ptag, tt.pversiontag, tt.inputs)
|
||||
|
||||
if !errCmp(err, tt.err) {
|
||||
t.Errorf(cmp.Diff(err, tt.err, cmpopts.EquateErrors()))
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
artifact1
|
||||
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@ var (
|
||||
ErrorMismatchBranch = errors.New("branch 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")
|
||||
ErrorMismatchWorkflowInputs = errors.New("workflow input does not match")
|
||||
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")
|
||||
@@ -18,4 +19,5 @@ var (
|
||||
ErrorNoValidRekorEntries = errors.New("could not find a matching valid signature entry")
|
||||
ErrorVerifierNotSupported = errors.New("no verifier support the builder")
|
||||
ErrorNotSupported = errors.New("not supported")
|
||||
ErrorInvalidFormat = errors.New("invalid format")
|
||||
)
|
||||
|
||||
@@ -20,6 +20,9 @@ type ProvenanceOpts struct {
|
||||
|
||||
// ExpectedBuilderID is the expected builder ID.
|
||||
ExpectedBuilderID string
|
||||
|
||||
// ExpectedWorkflowInputs is a map of key=value inputs.
|
||||
ExpectedWorkflowInputs map[string]string
|
||||
}
|
||||
|
||||
// BuildOpts are the options for checking the builder.
|
||||
|
||||
@@ -224,6 +224,61 @@ func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceO
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the workflow inputs.
|
||||
if len(provenanceOpts.ExpectedWorkflowInputs) > 0 {
|
||||
if err := VerifyWorkflowInputs(prov, provenanceOpts.ExpectedWorkflowInputs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func VerifyWorkflowInputs(prov *intoto.ProvenanceStatement, inputs map[string]string) error {
|
||||
environment, ok := prov.Predicate.Invocation.Environment.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type")
|
||||
}
|
||||
|
||||
// Verify it's a workflow_dispatch trigger.
|
||||
triggerName, err := getAsString(environment, "github_event_name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if triggerName != "workflow_dispatch" {
|
||||
return fmt.Errorf("%w: expected 'workflow_dispatch' trigger, got %s",
|
||||
serrors.ErrorMismatchWorkflowInputs, triggerName)
|
||||
}
|
||||
|
||||
// Assume no nested level.
|
||||
payload, err := getEventPayload(environment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payloadInputs, err := getAsAny(payload, "inputs")
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error retrieving 'inputs': %v", serrors.ErrorInvalidDssePayload, err)
|
||||
}
|
||||
|
||||
pyldInputs, ok := payloadInputs.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, "parameters type inputs")
|
||||
}
|
||||
|
||||
// Verify all inputs.
|
||||
for k, v := range inputs {
|
||||
value, err := getAsString(pyldInputs, k)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: cannot retrieve value of '%s'", serrors.ErrorMismatchWorkflowInputs, k)
|
||||
}
|
||||
|
||||
if v != value {
|
||||
return fmt.Errorf("%w: expected '%s=%s', got '%s=%s'",
|
||||
serrors.ErrorMismatchWorkflowInputs, k, v, k, value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -502,6 +502,91 @@ func Test_VerifyBranch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_VerifyWorkflowInputs(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
inputs map[string]string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
name: "match all",
|
||||
path: "./testdata/dsse-workflow-inputs.intoto.jsonl",
|
||||
inputs: map[string]string{
|
||||
"release_version": "v1.2.3",
|
||||
"some_bool": "true",
|
||||
"some_integer": "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "match subset",
|
||||
path: "./testdata/dsse-workflow-inputs.intoto.jsonl",
|
||||
inputs: map[string]string{
|
||||
"release_version": "v1.2.3",
|
||||
"some_integer": "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing field",
|
||||
path: "./testdata/dsse-workflow-inputs.intoto.jsonl",
|
||||
inputs: map[string]string{
|
||||
"release_version": "v1.2.3",
|
||||
"missing_field": "123",
|
||||
},
|
||||
expected: serrors.ErrorMismatchWorkflowInputs,
|
||||
},
|
||||
{
|
||||
name: "mismatch field release_version",
|
||||
path: "./testdata/dsse-workflow-inputs.intoto.jsonl",
|
||||
inputs: map[string]string{
|
||||
"release_version": "v1.2.4",
|
||||
"some_integer": "123",
|
||||
},
|
||||
expected: serrors.ErrorMismatchWorkflowInputs,
|
||||
},
|
||||
{
|
||||
name: "mismatch field some_integer",
|
||||
path: "./testdata/dsse-workflow-inputs.intoto.jsonl",
|
||||
inputs: map[string]string{
|
||||
"release_version": "v1.2.3",
|
||||
"some_integer": "124",
|
||||
},
|
||||
expected: serrors.ErrorMismatchWorkflowInputs,
|
||||
},
|
||||
{
|
||||
name: "not workflow_dispatch trigger",
|
||||
path: "./testdata/dsse-workflow-inputs-wrong-trigger.intoto.jsonl",
|
||||
inputs: map[string]string{
|
||||
"release_version": "v1.2.3",
|
||||
"some_bool": "true",
|
||||
"some_integer": "123",
|
||||
},
|
||||
expected: serrors.ErrorInvalidDssePayload,
|
||||
},
|
||||
}
|
||||
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()
|
||||
|
||||
content, err := os.ReadFile(tt.path)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("os.ReadFile: %w", err))
|
||||
}
|
||||
prov, err := provenanceFromBytes(content)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("provenanceFromBytes: %w", err))
|
||||
}
|
||||
|
||||
err = VerifyWorkflowInputs(prov, tt.inputs)
|
||||
if !errCmp(err, tt.expected) {
|
||||
t.Errorf(cmp.Diff(err, tt.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_VerifyTag(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -65,7 +65,7 @@ func (v *GHAVerifier) VerifyArtifact(ctx context.Context,
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
/* Verify properties of the SLSA provenance. */
|
||||
// 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 {
|
||||
|
||||
Reference in New Issue
Block a user