diff --git a/go/predicates/provenance/v1/provenance.go b/go/predicates/provenance/v1/provenance.go new file mode 100644 index 00000000..fede8050 --- /dev/null +++ b/go/predicates/provenance/v1/provenance.go @@ -0,0 +1,138 @@ +/* +Validator APIs for SLSA Provenance v1 protos. +*/ +package v1 + +import ( + "errors" + "fmt" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" +) + +// all of the following errors apply to SLSA Build L1 and above +var ( + ErrBuilderRequired = errors.New("runDetails.builder required") + ErrBuilderIdRequired = errors.New("runDetails.builder.id required") + ErrBuildDefinitionRequired = errors.New("buildDefinition required") + ErrBuildTypeRequired = errors.New("buildDefinition.buildType required") + ErrExternalParamsRequired = errors.New("buildDefinition.externalParameters required") + ErrRunDetailsRequired = errors.New("runDetails required") +) + +func (m *BuildMetadata) Validate() error { + // check valid timestamps + s := m.GetStartedOn() + if s != nil { + if err := s.CheckValid(); err != nil { + return fmt.Errorf("buildMetadata.startedOn error: %w", err) + } + } + + f := m.GetFinishedOn() + if f != nil { + if err := f.CheckValid(); err != nil { + return fmt.Errorf("buildMetadata.finishedOn error: %w", err) + } + } + + return nil +} + +func (b *Builder) Validate() error { + // the id field is required for SLSA Build L1 + if b.GetId() == "" { + return ErrBuilderIdRequired + } + + // check that all builderDependencies are valid RDs + builderDeps := b.GetBuilderDependencies() + for i, rd := range builderDeps { + if err := rd.Validate(); err != nil { + return fmt.Errorf("Invalid Builder.BuilderDependencies[%d]: %w", i, err) + } + } + + return nil +} + +func (b *BuildDefinition) Validate() error { + // the buildType field is required for SLSA Build L1 + if b.GetBuildType() == "" { + return ErrBuildTypeRequired + } + + // the externalParameters field is required for SLSA Build L1 + ext := b.GetExternalParameters() + if ext == nil || proto.Equal(ext, &structpb.Struct{}) { + return ErrExternalParamsRequired + } + + // check that all resolvedDependencies are valid RDs + resolvedDeps := b.GetResolvedDependencies() + for i, rd := range resolvedDeps { + if err := rd.Validate(); err != nil { + return fmt.Errorf("Invalid BuildDefinition.ResolvedDependencies[%d]: %w", i, err) + } + } + + return nil +} + +func (r *RunDetails) Validate() error { + // the builder field is required for SLSA Build L1 + builder := r.GetBuilder() + if builder == nil || proto.Equal(builder, &Builder{}) { + return ErrBuilderRequired + } + + // check the Builder + if err := builder.Validate(); err != nil { + return fmt.Errorf("runDetails.builder error: %w", err) + } + + // check the Metadata, if present + metadata := r.GetMetadata() + if metadata != nil && !proto.Equal(metadata, &BuildMetadata{}) { + if err := metadata.Validate(); err != nil { + return fmt.Errorf("Invalid RunDetails.Metadata: %w", err) + } + } + + // check that all byproducts are valid RDs + byproducts := r.GetByproducts() + for i, rd := range byproducts { + if err := rd.Validate(); err != nil { + return fmt.Errorf("Invalid RunDetails.Byproducts[%d]: %w", i, err) + } + } + + return nil +} + +func (p *Provenance) Validate() error { + // the buildDefinition field is required for SLSA Build L1 + buildDef := p.GetBuildDefinition() + if buildDef == nil || proto.Equal(buildDef, &BuildDefinition{}) { + return ErrBuildDefinitionRequired + } + + // check the BuildDefinition + if err := buildDef.Validate(); err != nil { + return fmt.Errorf("provenance.buildDefinition error: %w", err) + } + + // the runDetails field is required for SLSA Build L1 + runDetails := p.GetRunDetails() + if runDetails == nil || proto.Equal(runDetails, &RunDetails{}) { + return ErrRunDetailsRequired + } + + // check the RunDetails + if err := runDetails.Validate(); err != nil { + return fmt.Errorf("provenance.runDetails error: %w", err) + } + + return nil +} diff --git a/go/predicates/provenance/v1/provenance_test.go b/go/predicates/provenance/v1/provenance_test.go new file mode 100644 index 00000000..bf482b30 --- /dev/null +++ b/go/predicates/provenance/v1/provenance_test.go @@ -0,0 +1,139 @@ +/* +Tests for SLSA Provenance v1 protos. +*/ + +package v1 + +import ( + "fmt" + "testing" + + ita1 "github.com/in-toto/attestation/go/v1" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" +) + +func createTestProvenance(t *testing.T) *Provenance { + // Create a Provenance + + t.Helper() + + rd := &ita1.ResourceDescriptor{ + Name: "theResource", + Digest: map[string]string{"alg1": "abc123"}, + } + + builder := &Builder{ + Id: "theId", + Version: map[string]string{"theComponent": "v0.1"}, + BuilderDependencies: []*ita1.ResourceDescriptor{rd}, + } + + buildMeta := &BuildMetadata{ + InvocationId: "theInvocationId", + } + + runDetails := &RunDetails{ + Builder: builder, + Metadata: buildMeta, + Byproducts: []*ita1.ResourceDescriptor{rd}, + } + + externalParams, err := structpb.NewStruct(map[string]interface{}{ + "param1": map[string]interface{}{ + "subKey": "subVal"}}) + if err != nil { + t.Fatal(err) + } + + buildDef := &BuildDefinition{ + BuildType: "theBuildType", + ExternalParameters: externalParams, + ResolvedDependencies: []*ita1.ResourceDescriptor{rd}, + } + + return &Provenance{ + BuildDefinition: buildDef, + RunDetails: runDetails, + } +} + +func TestJsonUnmarshalProvenance(t *testing.T) { + var wantSt = `{"buildDefinition":{"buildType":"theBuildType","externalParameters":{"param1":{"subKey":"subVal"}},"resolvedDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"runDetails":{"builder":{"id":"theId","version":{"theComponent":"v0.1"},"builderDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"metadata":{"invocationId":"theInvocationId"},"byproducts":[{"name":"theResource","digest":{"alg1":"abc123"}}]}}` + + got := &Provenance{} + err := protojson.Unmarshal([]byte(wantSt), got) + assert.NoError(t, err, "error during JSON unmarshalling") + + want := createTestProvenance(t) + assert.NoError(t, err, "unexpected error during test Statement creation") + assert.True(t, proto.Equal(got, want), "protos do not match") +} + +func TestBadProvenanceBuildDefinition(t *testing.T) { + tests := map[string]struct { + input string + err error + noErrMessage string + }{ + "no buildDefinition": { + input: `{"buildDefinition":{},"runDetails":{"builder":{"id":"theId","version":{"theComponent":"v0.1"},"builderDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"metadata":{"invocationId":"theInvocationId"},"byproducts":[{"name":"theResource","digest":{"alg1":"abc123"}}]}}`, + err: ErrBuildDefinitionRequired, + noErrMessage: "created malformed Provenance (empty buildDefinition)", + }, + "buildDefinition missing buildType required field": { + input: `{"buildDefinition":{"externalParameters":{"param1":{"subKey":"subVal"}},"resolvedDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"runDetails":{"builder":{"id":"theId","version":{"theComponent":"v0.1"},"builderDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"metadata":{"invocationId":"theInvocationId"},"byproducts":[{"name":"theResource","digest":{"alg1":"abc123"}}]}}`, + err: ErrBuildTypeRequired, + noErrMessage: "created malformed Provenance (buildDefinition missing required buildType field)", + }, + "buildDefinition missing externalParameters required field": { + input: `{"buildDefinition":{"buildType":"theBuildType","resolvedDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"runDetails":{"builder":{"id":"theId","version":{"theComponent":"v0.1"},"builderDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"metadata":{"invocationId":"theInvocationId"},"byproducts":[{"name":"theResource","digest":{"alg1":"abc123"}}]}}`, + err: ErrExternalParamsRequired, + noErrMessage: "created malformed Provenance (buildDefinition missing requried externalParameters field)", + }, + } + + for name, test := range tests { + got := &Provenance{} + err := protojson.Unmarshal([]byte(test.input), got) + assert.NoError(t, err, fmt.Sprintf("error during JSON unmarshalling in test '%s'", name)) + + err = got.Validate() + assert.ErrorIs(t, err, test.err, fmt.Sprintf("%s in test '%s'", test.noErrMessage, name)) + } +} + +func TestBadProvenanceRunDetails(t *testing.T) { + tests := map[string]struct { + input string + err error + noErrMessage string + }{ + "no runDetails": { + input: `{"buildDefinition":{"buildType":"theBuildType","externalParameters":{"param1":{"subKey":"subVal"}},"resolvedDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"runDetails":{}}`, + err: ErrRunDetailsRequired, + noErrMessage: "created malformed Provenance (empty runDetails)", + }, + "runDetails missing builder required field": { + input: `{"buildDefinition":{"buildType":"theBuildType","externalParameters":{"param1":{"subKey":"subVal"}},"resolvedDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"runDetails":{"builder":{},"metadata":{"invocationId":"theInvocationId"},"byproducts":[{"name":"theResource","digest":{"alg1":"abc123"}}]}}`, + err: ErrBuilderRequired, + noErrMessage: "created malformed Provenance (runDetails missing required builder field)", + }, + "runDetails.builder missing id required field": { + input: `{"buildDefinition":{"buildType":"theBuildType","externalParameters":{"param1":{"subKey":"subVal"}},"resolvedDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"runDetails":{"builder":{"id":"","version":{"theComponent":"v0.1"},"builderDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"metadata":{"invocationId":"theInvocationId"},"byproducts":[{"name":"theResource","digest":{"alg1":"abc123"}}]}}`, + err: ErrBuilderIdRequired, + noErrMessage: "created malformed Provenance (runDetails.builder missing requried id field)", + }, + } + + for name, test := range tests { + got := &Provenance{} + err := protojson.Unmarshal([]byte(test.input), got) + assert.NoError(t, err, fmt.Sprintf("error during JSON unmarshalling in test '%s'", name)) + + err = got.Validate() + assert.ErrorIs(t, err, test.err, fmt.Sprintf("%s in test '%s'", test.noErrMessage, name)) + } +}