feat: add a -workflow-input option (#216)

* update
This commit is contained in:
laurentsimon
2022-08-15 19:38:15 +00:00
committed by GitHub
parent fc6a8e0499
commit 5bb13ef508
11 changed files with 224 additions and 10 deletions
+35 -7
View File
@@ -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{
+39 -2
View File
@@ -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
View File
@@ -0,0 +1 @@
artifact1
File diff suppressed because one or more lines are too long
+2
View File
@@ -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")
)
+3
View File
@@ -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.
+55
View File
@@ -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
}
+85
View File
@@ -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 {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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 {