diff --git a/.github/test_resource/example_provenance.json b/.github/test_resource/example_provenance.json index eee6a276..d9dfaeb3 100644 --- a/.github/test_resource/example_provenance.json +++ b/.github/test_resource/example_provenance.json @@ -1,42 +1,48 @@ { - "_type": "https://in-toto.io/Statement/v0.1", - "subject": [ - { - "name": "salsa.txt", - "digest": { - "sha256": "f8161d035cdf328c7bb124fce192cb90b603f34ca78d73e33b736b4f6bddf993" - } - } - ], - "predicateType": "https://slsa.dev/provenance/v0.1", - "predicate": { - "builder": { - "id": "https://github.com/philips-labs/slsa-provenance-action/Attestations/GitHubHostedActions@v1" - }, - "metadata": { - "buildInvocationId": "https://github.com/philips-labs/slsa-provenance-action/actions/runs/1303916967", - "completeness": { - "arguments": true, - "environment": false, - "materials": false - }, - "reproducible": false, - "buildFinishedOn": "2021-10-04T11:08:34Z" - }, - "recipe": { - "type": "https://github.com/Attestations/GitHubActionsWorkflow@v1", - "definedInMaterial": 0, - "entryPoint": "Integration test file provenance", - "arguments": null, - "environment": null - }, - "materials": [ - { - "uri": "git+https://github.com/philips-labs/slsa-provenance-action", - "digest": { - "sha1": "a3bc1c27230caa1cc3c27961f7e9cab43cd208dc" - } - } - ] + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "salsa.txt", + "digest": { + "sha256": "f8161d035cdf328c7bb124fce192cb90b603f34ca78d73e33b736b4f6bddf993" + } } -} \ No newline at end of file + ], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "builder": { + "id": "https://github.com/philips-labs/slsa-provenance-action/Attestations/GitHubHostedActions@v1" + }, + "buildType": "https://github.com/Attestations/GitHubActionsWorkflow@v1", + "invocation": { + "configSource": { + "entryPoint": "ci.yaml:build", + "uri": "git+https://github.com/philips-labs/slsa-provenance-action", + "digest": { + "sha1": "a3bc1c27230caa1cc3c27961f7e9cab43cd208dc" + } + }, + "parameters": null, + "environment": null + }, + "buildConfig": null, + "metadata": { + "buildInvocationId": "https://github.com/philips-labs/slsa-provenance-action/actions/runs/1303916967", + "buildFinishedOn": "2021-10-04T11:08:34Z", + "completeness": { + "parameters": true, + "environment": false, + "materials": false + }, + "reproducible": false + }, + "materials": [ + { + "uri": "git+https://github.com/philips-labs/slsa-provenance-action", + "digest": { + "sha1": "a3bc1c27230caa1cc3c27961f7e9cab43cd208dc" + } + } + ] + } +} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7dad33e0..f366cb29 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,7 +33,7 @@ jobs: - name: Lint run: | - result=$(make lint) + result="$(make lint)" echo "$result" [ -n "$(echo "$result" | grep 'diff -u')" ] && exit 1 || exit 0 diff --git a/lib/github/github.go b/lib/github/github.go index c695599e..ca5146cd 100644 --- a/lib/github/github.go +++ b/lib/github/github.go @@ -10,8 +10,8 @@ const ( HostedIDSuffix = "/Attestations/GitHubHostedActions@v1" // SelfHostedIDSuffix the GitHub self hosted attestation type SelfHostedIDSuffix = "/Attestations/SelfHostedActions@v1" - // RecipeType the attestion type for a recipe - RecipeType = "https://github.com/Attestations/GitHubActionsWorkflow@v1" + // BuildType URI indicating what type of build was performed. It determines the meaning of invocation, buildConfig and materials. + BuildType = "https://github.com/Attestations/GitHubActionsWorkflow@v1" // PayloadContentType used to define the Envelope content type // See: https://github.com/in-toto/attestation#provenance-example PayloadContentType = "application/vnd.in-toto+json" diff --git a/lib/github/provenance.go b/lib/github/provenance.go index 46faa76a..b769edc1 100644 --- a/lib/github/provenance.go +++ b/lib/github/provenance.go @@ -38,8 +38,8 @@ func (e *Environment) GenerateProvenanceStatement(ctx context.Context, artifactP intoto.WithMetadata(fmt.Sprintf("%s/actions/runs/%s", repoURI, e.Context.RunID)), // NOTE: This is inexact as multiple workflows in a repo can have the same name. // See https://github.com/github/feedback/discussions/4188 - intoto.WithRecipe( - RecipeType, + intoto.WithInvocation( + BuildType, e.Context.Workflow, nil, event.Inputs, diff --git a/lib/github/provenance_test.go b/lib/github/provenance_test.go index 062d7177..6e7f670f 100644 --- a/lib/github/provenance_test.go +++ b/lib/github/provenance_test.go @@ -271,12 +271,13 @@ func TestGenerateProvenance(t *testing.T) { assert.Equal(intoto.StatementType, stmt.Type) predicate := stmt.Predicate + assert.Equal(github.BuildType, predicate.BuildType) assert.Equal(fmt.Sprintf("%s%s", repoURL, github.HostedIDSuffix), predicate.ID) assert.Equal(materials, predicate.Materials) assert.Equal(fmt.Sprintf("%s%s", repoURL, github.HostedIDSuffix), predicate.Builder.ID) assertMetadata(assert, predicate.Metadata, gh, repoURL) - assertRecipe(assert, predicate.Recipe) + assertInvocation(assert, predicate.Invocation) } func TestGenerateProvenanceFromGitHubRelease(t *testing.T) { @@ -351,9 +352,10 @@ func TestGenerateProvenanceFromGitHubRelease(t *testing.T) { assert.Equal(fmt.Sprintf("%s%s", repoURL, github.HostedIDSuffix), predicate.ID) assert.Equal(materials, predicate.Materials) assert.Equal(fmt.Sprintf("%s%s", repoURL, github.HostedIDSuffix), predicate.Builder.ID) + assert.Equal(github.BuildType, predicate.BuildType) assertMetadata(assert, predicate.Metadata, ghContext, repoURL) - assertRecipe(assert, predicate.Recipe) + assertInvocation(assert, predicate.Invocation) stmtPath := path.Join(artifactPath, "build.provenance") @@ -393,12 +395,10 @@ func TestGenerateProvenanceFromGitHubReleaseErrors(t *testing.T) { assert.Nil(stmt) } -func assertRecipe(assert *assert.Assertions, recipe intoto.Recipe) { - assert.Equal(github.RecipeType, recipe.Type) - assert.Equal(0, recipe.DefinedInMaterial) - assert.Equal("", recipe.EntryPoint) +func assertInvocation(assert *assert.Assertions, recipe intoto.Invocation) { + assert.Equal("", recipe.ConfigSource.EntryPoint) assert.Nil(recipe.Environment) - assert.Nil(recipe.Arguments) + assert.Nil(recipe.Parameters) } func assertMetadata(assert *assert.Assertions, meta intoto.Metadata, gh github.Context, repoURL string) { @@ -406,7 +406,7 @@ func assertMetadata(assert *assert.Assertions, meta intoto.Metadata, gh github.C assert.NoError(err) assert.WithinDuration(time.Now().UTC(), bft, 1200*time.Millisecond) assert.Equal(fmt.Sprintf("%s/%s/%s", repoURL, "actions/runs", gh.RunID), meta.BuildInvocationID) - assert.Equal(true, meta.Completeness.Arguments) + assert.Equal(true, meta.Completeness.Parameters) assert.Equal(false, meta.Completeness.Environment) assert.Equal(false, meta.Completeness.Materials) assert.Equal(false, meta.Reproducible) diff --git a/lib/intoto/intoto.go b/lib/intoto/intoto.go index dd72bfac..6a82f76e 100644 --- a/lib/intoto/intoto.go +++ b/lib/intoto/intoto.go @@ -8,7 +8,7 @@ import ( const ( // SlsaPredicateType the predicate type for SLSA intoto statements - SlsaPredicateType = "https://slsa.dev/provenance/v0.1" + SlsaPredicateType = "https://slsa.dev/provenance/v0.2" // StatementType the type of the intoto statement StatementType = "https://in-toto.io/Statement/v0.1" ) @@ -57,7 +57,7 @@ func WithMetadata(buildInvocationID string) StatementOption { return func(s *Statement) { s.Predicate.Metadata = Metadata{ Completeness: Completeness{ - Arguments: true, + Parameters: true, Environment: false, Materials: false, }, @@ -68,19 +68,18 @@ func WithMetadata(buildInvocationID string) StatementOption { } } -// WithRecipe sets the Predicate Recipe and Materials -func WithRecipe(predicateType string, entryPoint string, environment json.RawMessage, arguments json.RawMessage, materials []Item) StatementOption { +// WithInvocation sets the Predicate Invocation and Materials +func WithInvocation(buildType, entryPoint string, environment json.RawMessage, parameters json.RawMessage, materials []Item) StatementOption { return func(s *Statement) { - s.Predicate.Recipe = Recipe{ - Type: predicateType, - EntryPoint: entryPoint, - Arguments: arguments, - // Subject to change and simplify https://github.com/slsa-framework/slsa/issues/178 - // Index in materials containing the recipe steps that are not implied by recipe.type. For example, if the recipe type were "make", then this would point to the source containing the Makefile, not the make program itself. - // Omit this field (or use null) if the recipe doesn't come from a material. - // TODO: What if there is more than one material? - DefinedInMaterial: 0, - Environment: environment, + s.Predicate.BuildType = buildType + s.Predicate.Invocation = Invocation{ + ConfigSource: ConfigSource{ + EntryPoint: entryPoint, + URI: materials[0].URI, + Digest: materials[0].Digest, + }, + Parameters: parameters, + Environment: environment, } s.Predicate.Materials = append(s.Predicate.Materials, materials...) } @@ -108,10 +107,17 @@ type Subject struct { // // A predicate has a required predicateType (TypeURI) identifying what the predicate means, plus an optional predicate (object) containing additional, type-dependent parameters. type Predicate struct { - Builder `json:"builder"` - Metadata `json:"metadata"` - Recipe `json:"recipe"` - Materials []Item `json:"materials"` + Builder `json:"builder"` + BuildType string `json:"buildType"` + Invocation `json:"invocation"` + BuildConfig *BuildConfig `json:"build_config,omitempty"` + Metadata `json:"metadata,omitempty"` + Materials []Item `json:"materials"` +} + +// BuildConfig Lists the steps in the build. +// If invocation.sourceConfig is not available, buildConfig can be used to verify information about the build. +type BuildConfig struct { } // Builder Identifies the entity that executed the recipe, which is trusted to have correctly performed the operation and populated this provenance. @@ -127,24 +133,30 @@ type Builder struct { // Metadata Other properties of the build. type Metadata struct { BuildInvocationID string `json:"buildInvocationId"` - Completeness `json:"completeness"` - Reproducible bool `json:"reproducible"` // BuildStartedOn not defined as it's not available from a GitHub Action. BuildFinishedOn string `json:"buildFinishedOn"` + Completeness `json:"completeness"` + Reproducible bool `json:"reproducible"` +} + +// Invocation Identifies the configuration used for the build. When combined with materials, this SHOULD fully describe the build, such that re-running this recipe results in bit-for-bit identical output (if the build is reproducible). +type Invocation struct { + ConfigSource ConfigSource `json:"configSource"` + Parameters json.RawMessage `json:"parameters"` + Environment json.RawMessage `json:"environment"` } -// Recipe Identifies the configuration used for the build. When combined with materials, this SHOULD fully describe the build, such that re-running this recipe results in bit-for-bit identical output (if the build is reproducible). -type Recipe struct { - Type string `json:"type"` - DefinedInMaterial int `json:"definedInMaterial"` - EntryPoint string `json:"entryPoint"` - Arguments json.RawMessage `json:"arguments"` - Environment json.RawMessage `json:"environment"` +// ConfigSource Describes where the config file that kicked off the build came from. +// This is effectively a pointer to the source where buildConfig came from. +type ConfigSource struct { + EntryPoint string `json:"entryPoint"` + URI string `json:"uri,omitempty"` + Digest DigestSet `json:"digest,omitempty"` } // Completeness Indicates that the builder claims certain fields in this message to be complete. type Completeness struct { - Arguments bool `json:"arguments"` + Parameters bool `json:"parameters"` Environment bool `json:"environment"` Materials bool `json:"materials"` } diff --git a/lib/intoto/intoto_test.go b/lib/intoto/intoto_test.go index ae7ed378..9a4551a0 100644 --- a/lib/intoto/intoto_test.go +++ b/lib/intoto/intoto_test.go @@ -1,20 +1,24 @@ package intoto import ( + "encoding/json" + "fmt" "testing" "time" "github.com/stretchr/testify/assert" ) +const ( + repoURI = "https://github.com/philips-labs/slsa-provenance-action" + builderID = repoURI + "/Attestations/GitHubHostedActions@v1" + buildInvocationID = repoURI + "/actions/runs/123498765" + buildType = "https://github.com/Attestations/GitHubActionsWorkflow@v1" +) + func TestSLSAProvenanceStatement(t *testing.T) { assert := assert.New(t) - repoURI := "https://github.com/philips-labs/slsa-provenance-action" - builderID := repoURI + "/Attestations/GitHubHostedActions@v1" - buildInvocationID := repoURI + "/actions/runs/123498765" - recipeType := "https://github.com/Attestations/GitHubActionsWorkflow@v1" - stmt := SLSAProvenanceStatement() assert.Equal(SlsaPredicateType, stmt.PredicateType) assert.Equal(StatementType, stmt.Type) @@ -49,7 +53,7 @@ func TestSLSAProvenanceStatement(t *testing.T) { bft, err := time.Parse(time.RFC3339, m.BuildFinishedOn) assert.NoError(err) assert.WithinDuration(time.Now().UTC(), bft, 1200*time.Millisecond) - assert.Equal(Completeness{Arguments: true, Environment: false, Materials: false}, stmt.Predicate.Metadata.Completeness) + assert.Equal(Completeness{Parameters: true, Environment: false, Materials: false}, stmt.Predicate.Metadata.Completeness) assert.False(m.Reproducible) provenanceActionMaterial := []Item{ @@ -60,24 +64,129 @@ func TestSLSAProvenanceStatement(t *testing.T) { } stmt = SLSAProvenanceStatement( - WithSubject(make([]Subject, 1)), + WithSubject([]Subject{{Name: "salsa.txt", Digest: DigestSet{"sha256": "f8161d035cdf328c7bb124fce192cb90b603f34ca78d73e33b736b4f6bddf993"}}}), WithBuilder(builderID), - WithRecipe( - recipeType, - "CI workflow", + WithMetadata("https://github.com/philips-labs/slsa-provenance-action/actions/runs/1303916967"), + WithInvocation( + buildType, + "ci.yaml:build", nil, nil, provenanceActionMaterial, ), ) - r := stmt.Predicate.Recipe + assertStatement(assert, stmt, builderID, buildType, provenanceActionMaterial, nil) +} + +func TestSLSAProvenanceStatementJSON(t *testing.T) { + assert := assert.New(t) + + materialJSON := `[ + { + "uri": "git+https://github.com/philips-labs/slsa-provenance-action", + "digest": { + "sha1": "a3bc1c27230caa1cc3c27961f7e9cab43cd208dc" + } + } + ]` + parametersJSON := `{ + "inputs": { + "skip_integration": true + } + }` + buildFinishedOn := time.Now().UTC().Format(time.RFC3339) + + var material []Item + err := json.Unmarshal([]byte(materialJSON), &material) + assert.NoError(err) + + jsonStatement := fmt.Sprintf(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "salsa.txt", + "digest": { + "sha256": "f8161d035cdf328c7bb124fce192cb90b603f34ca78d73e33b736b4f6bddf993" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "builder": { + "id": "%s" + }, + "buildType": "%s", + "invocation": { + "configSource": { + "entryPoint": "ci.yaml:build", + "uri": "git+https://github.com/philips-labs/slsa-provenance-action", + "digest": { + "sha1": "a3bc1c27230caa1cc3c27961f7e9cab43cd208dc" + } + }, + "parameters": %s, + "environment": null + }, + "metadata": { + "buildInvocationId": "https://github.com/philips-labs/slsa-provenance-action/actions/runs/1303916967", + "buildFinishedOn": "%s", + "completeness": { + "parameters": true, + "environment": false, + "materials": false + }, + "reproducible": false + }, + "materials": %s + } +}`, builderID, buildType, parametersJSON, buildFinishedOn, materialJSON) + + var stmt Statement + err = json.Unmarshal([]byte(jsonStatement), &stmt) + assert.NoError(err) + assertStatement(assert, &stmt, builderID, buildType, material, []byte(parametersJSON)) + + newStmt := SLSAProvenanceStatement( + WithSubject([]Subject{{Name: "salsa.txt", Digest: DigestSet{"sha256": "f8161d035cdf328c7bb124fce192cb90b603f34ca78d73e33b736b4f6bddf993"}}}), + WithBuilder(builderID), + WithMetadata("https://github.com/philips-labs/slsa-provenance-action/actions/runs/1303916967"), + WithInvocation(buildType, "ci.yaml:build", nil, []byte(parametersJSON), material), + ) + + newStmtJSON, err := json.MarshalIndent(newStmt, "", "\t") + assert.NoError(err) + + assert.Equal(jsonStatement, string(newStmtJSON)) +} + +func assertStatement(assert *assert.Assertions, stmt *Statement, builderID, buildType string, material []Item, parameters json.RawMessage) { + i := stmt.Predicate.Invocation assert.Equal(SlsaPredicateType, stmt.PredicateType) assert.Equal(StatementType, stmt.Type) assert.Len(stmt.Subject, 1) + assert.Equal(Subject{Name: "salsa.txt", Digest: DigestSet{"sha256": "f8161d035cdf328c7bb124fce192cb90b603f34ca78d73e33b736b4f6bddf993"}}, stmt.Subject[0]) assert.Equal(builderID, stmt.Predicate.Builder.ID) - assert.Equal(recipeType, r.Type) - assert.Equal("CI workflow", r.EntryPoint) - assert.Nil(r.Arguments) - assert.Equal(0, r.DefinedInMaterial) - assert.Equal(provenanceActionMaterial, stmt.Predicate.Materials) + assert.Equal(buildType, stmt.Predicate.BuildType) + assertConfigSource(assert, i.ConfigSource, stmt.Predicate.Materials) + assert.Nil(stmt.Predicate.BuildConfig) + assert.Equal(parameters, i.Parameters) + assert.Equal(material, stmt.Predicate.Materials) + assertMetadata(assert, stmt.Predicate.Metadata) +} + +func assertConfigSource(assert *assert.Assertions, cs ConfigSource, materials []Item) { + assert.Equal("ci.yaml:build", cs.EntryPoint) + assert.Equal(materials[0].URI, cs.URI) + assert.Equal(materials[0].Digest, cs.Digest) +} + +func assertMetadata(assert *assert.Assertions, md Metadata) { + assert.Equal("https://github.com/philips-labs/slsa-provenance-action/actions/runs/1303916967", md.BuildInvocationID) + bft, err := time.Parse(time.RFC3339, md.BuildFinishedOn) + assert.NoError(err) + assert.WithinDuration(time.Now().UTC(), bft, 1200*time.Millisecond) + assert.True(md.Completeness.Parameters) + assert.False(md.Completeness.Materials) + assert.False(md.Completeness.Environment) + assert.False(md.Reproducible) }