diff --git a/pkg/chains/formats/slsa/README.md b/pkg/chains/formats/slsa/README.md index f076517e69..3c50c07235 100644 --- a/pkg/chains/formats/slsa/README.md +++ b/pkg/chains/formats/slsa/README.md @@ -13,3 +13,4 @@ Shown below is the mapping between Tekton chains proveance and SLSA predicate. |:------------------------------------------|---------------:|------:| |**slsa/v1**| **slsa v0.2** | same as currently supported `in-toto` format| |**slsa/v2alpha1**| **slsa v0.2** | contains complete build instructions as in [TEP0122](https://github.com/tektoncd/community/pull/820). This is still a WIP and currently only available for taskrun level provenance. | +|**slsa/v2alpha2**| **slsa v1.0** | contains SLSAv1.0 predicate. The parameters are complete. Support still needs to be added for surfacing builder version and builder dependencies information.| diff --git a/pkg/config/config.go b/pkg/config/config.go index 9199cd8647..6427c5199d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -252,12 +252,12 @@ func NewConfigFromMap(data map[string]string) (*Config, error) { if err := cm.Parse(data, // Artifact-specific configs // TaskRuns - asString(taskrunFormatKey, &cfg.Artifacts.TaskRuns.Format, "in-toto", "slsa/v1", "slsa/v2alpha1"), + asString(taskrunFormatKey, &cfg.Artifacts.TaskRuns.Format, "in-toto", "slsa/v1", "slsa/v2alpha1", "slsa/v2alpha2"), asStringSet(taskrunStorageKey, &cfg.Artifacts.TaskRuns.StorageBackend, sets.New[string]("tekton", "oci", "gcs", "docdb", "grafeas", "kafka")), asString(taskrunSignerKey, &cfg.Artifacts.TaskRuns.Signer, "x509", "kms"), // PipelineRuns - asString(pipelinerunFormatKey, &cfg.Artifacts.PipelineRuns.Format, "in-toto", "slsa/v1"), + asString(pipelinerunFormatKey, &cfg.Artifacts.PipelineRuns.Format, "in-toto", "slsa/v1", "slsa/v2alpha2"), asStringSet(pipelinerunStorageKey, &cfg.Artifacts.PipelineRuns.StorageBackend, sets.New[string]("tekton", "oci", "grafeas")), asString(pipelinerunSignerKey, &cfg.Artifacts.PipelineRuns.Signer, "x509", "kms"), diff --git a/test/examples_test.go b/test/examples_test.go index 2ff174d2af..1f2d88a7b4 100644 --- a/test/examples_test.go +++ b/test/examples_test.go @@ -41,10 +41,10 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" intoto "github.com/in-toto/in-toto-golang/in_toto" slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" + slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/ghodss/yaml" - "github.com/tektoncd/chains/pkg/artifacts" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/test/tekton" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" @@ -64,6 +64,7 @@ type TestExample struct { payloadKey string signatureKey string outputLocation string + predicate string } // TestExamples copies the format in the tektoncd/pipelines repo @@ -80,6 +81,7 @@ func TestExamples(t *testing.T) { payloadKey: "chains.tekton.dev/payload-taskrun-%s", signatureKey: "chains.tekton.dev/signature-taskrun-%s", outputLocation: "slsa/v1", + predicate: "slsav0.1", }, { name: "taskrun-examples-slsa-v2", @@ -91,6 +93,19 @@ func TestExamples(t *testing.T) { payloadKey: "chains.tekton.dev/payload-taskrun-%s", signatureKey: "chains.tekton.dev/signature-taskrun-%s", outputLocation: "slsa/v2", + predicate: "slsav0.2", + }, + { + name: "taskrun-examples-slsa-v2alpha2", + cm: map[string]string{ + "artifacts.taskrun.format": "slsa/v2alpha2", + "artifacts.oci.storage": "tekton", + }, + getExampleObjects: getTaskRunExamples, + payloadKey: "chains.tekton.dev/payload-taskrun-%s", + signatureKey: "chains.tekton.dev/signature-taskrun-%s", + outputLocation: "slsa/v2alpha2", + predicate: "slsav1.0", }, { name: "pipelinerun-examples", @@ -102,6 +117,19 @@ func TestExamples(t *testing.T) { payloadKey: "chains.tekton.dev/payload-pipelinerun-%s", signatureKey: "chains.tekton.dev/signature-pipelinerun-%s", outputLocation: "slsa/v1", + predicate: "slsav1.0", + }, + { + name: "pipelinerun-examples-slsa-v2alpha2", + cm: map[string]string{ + "artifacts.taskrun.format": "slsa/v2alpha2", + "artifacts.oci.storage": "tekton", + }, + getExampleObjects: getPipelineRunExamples, + payloadKey: "chains.tekton.dev/payload-pipelinerun-%s", + signatureKey: "chains.tekton.dev/signature-pipelinerun-%s", + outputLocation: "slsa/v2alpha2", + predicate: "slsav1.0", }, } @@ -141,22 +169,41 @@ func runInTotoFormatterTests(ctx context.Context, t *testing.T, ns string, c *cl signature, _ := base64.StdEncoding.DecodeString(completed.GetAnnotations()[fmt.Sprintf(test.signatureKey, completed.GetUID())]) t.Logf("Got attestation: %s", string(payload)) - // make sure provenance is correct - var gotProvenance intoto.ProvenanceStatement - if err := json.Unmarshal(payload, &gotProvenance); err != nil { - t.Fatal(err) - } - expected := expectedProvenance(t, ctx, path, completed, test.outputLocation, ns, c) - - opts := []cmp.Option{ - // Annotations and labels may contain release specific information. Ignore - // those to avoid brittle tests. - cmpopts.IgnoreFields(slsa.ProvenanceInvocation{}, "Environment"), - cmpopts.IgnoreMapEntries(ignoreEnvironmentAnnotationsAndLabels), - } - - if diff := cmp.Diff(expected, gotProvenance, opts...); diff != "" { - t.Errorf("provenance dont match: -want +got: %s", diff) + if test.predicate == "slsav1.0" { + // make sure provenance is correct + var gotProvenance intoto.ProvenanceStatementSLSA1 + if err := json.Unmarshal(payload, &gotProvenance); err != nil { + t.Fatal(err) + } + expected := expectedProvenanceSLSA1(t, ctx, path, completed, test.outputLocation, ns, c) + + opts := []cmp.Option{ + // Annotations and labels may contain release specific information. Ignore + // those to avoid brittle tests. + cmpopts.IgnoreFields(slsa1.ProvenanceBuildDefinition{}, "InternalParameters"), + cmpopts.IgnoreMapEntries(ignoreEnvironmentAnnotationsAndLabels), + } + + if diff := cmp.Diff(expected, gotProvenance, opts...); diff != "" { + t.Errorf("provenance dont match: -want +got: %s", diff) + } + } else { + var gotProvenance intoto.ProvenanceStatement + if err := json.Unmarshal(payload, &gotProvenance); err != nil { + t.Fatal(err) + } + expected := expectedProvenance(t, ctx, path, completed, test.outputLocation, ns, c) + + opts := []cmp.Option{ + // Annotations and labels may contain release specific information. Ignore + // those to avoid brittle tests. + cmpopts.IgnoreFields(slsa.ProvenanceInvocation{}, "Environment"), + cmpopts.IgnoreMapEntries(ignoreEnvironmentAnnotationsAndLabels), + } + + if diff := cmp.Diff(expected, gotProvenance, opts...); diff != "" { + t.Errorf("provenance dont match: -want +got: %s", diff) + } } // verify signature @@ -203,12 +250,28 @@ func (v *verifier) Public() crypto.PublicKey { return v.pub } +func expectedProvenanceSLSA1(t *testing.T, ctx context.Context, example string, obj objects.TektonObject, outputLocation string, ns string, c *clients) intoto.ProvenanceStatementSLSA1 { + switch obj.(type) { + case *objects.TaskRunObject: + f := expectedTaskRunProvenanceFormat(t, example, obj, outputLocation) + return expectedAttestationSLSA1(t, example, f, outputLocation) + case *objects.PipelineRunObject: + f := expectedPipelineRunProvenanceFormat(t, ctx, example, obj, outputLocation, ns, c) + return expectedAttestationSLSA1(t, example, f, outputLocation) + default: + t.Error("Unexpected type trying to get provenance") + } + return intoto.ProvenanceStatementSLSA1{} +} + func expectedProvenance(t *testing.T, ctx context.Context, example string, obj objects.TektonObject, outputLocation string, ns string, c *clients) intoto.ProvenanceStatement { switch obj.(type) { case *objects.TaskRunObject: - return expectedTaskRunProvenance(t, example, obj, outputLocation) + f := expectedTaskRunProvenanceFormat(t, example, obj, outputLocation) + return expectedAttestation(t, example, f, outputLocation) case *objects.PipelineRunObject: - return expectedPipelineRunProvenance(t, ctx, example, obj, outputLocation, ns, c) + f := expectedPipelineRunProvenanceFormat(t, ctx, example, obj, outputLocation, ns, c) + return expectedAttestation(t, example, f, outputLocation) default: t.Error("Unexpected type trying to get provenance") } @@ -224,6 +287,7 @@ type Format struct { Entrypoint string PipelineStartedOn string PipelineFinishedOn string + UID string BuildStartTimes []string BuildFinishedTimes []string ContainerNames []string @@ -231,7 +295,7 @@ type Format struct { URIDigest []URIDigestPair } -func expectedTaskRunProvenance(t *testing.T, example string, obj objects.TektonObject, outputLocation string) intoto.ProvenanceStatement { +func expectedTaskRunProvenanceFormat(t *testing.T, example string, obj objects.TektonObject, outputLocation string) Format { tr := obj.GetObject().(*v1beta1.TaskRun) name := tr.Name @@ -247,7 +311,7 @@ func expectedTaskRunProvenance(t *testing.T, example string, obj objects.TektonO stepNames = append(stepNames, step.Name) images = append(images, step.ImageID) // append uri and digest that havent already been appended - uri := artifacts.OCIScheme + strings.Split(step.ImageID, "@")[0] + uri := fmt.Sprintf("oci://%s", strings.Split(step.ImageID, "@")[0]) digest := strings.Split(step.ImageID, ":")[1] uriDigest := fmt.Sprintf("%s %s", uri, digest) if _, ok := uriDigestSet[uriDigest]; !ok { @@ -256,8 +320,9 @@ func expectedTaskRunProvenance(t *testing.T, example string, obj objects.TektonO } } - f := Format{ + return Format{ Entrypoint: name, + UID: string(tr.ObjectMeta.UID), BuildStartTimes: []string{tr.Status.StartTime.Time.UTC().Format(time.RFC3339)}, BuildFinishedTimes: []string{tr.Status.CompletionTime.Time.UTC().Format(time.RFC3339)}, ContainerNames: stepNames, @@ -265,10 +330,9 @@ func expectedTaskRunProvenance(t *testing.T, example string, obj objects.TektonO URIDigest: uridigest, } - return readExpectedAttestation(t, example, f, outputLocation) } -func expectedPipelineRunProvenance(t *testing.T, ctx context.Context, example string, obj objects.TektonObject, outputLocation string, ns string, c *clients) intoto.ProvenanceStatement { +func expectedPipelineRunProvenanceFormat(t *testing.T, ctx context.Context, example string, obj objects.TektonObject, outputLocation string, ns string, c *clients) Format { pr := obj.GetObject().(*v1beta1.PipelineRun) buildStartTimes := []string{} @@ -276,7 +340,6 @@ func expectedPipelineRunProvenance(t *testing.T, ctx context.Context, example st var uridigest []URIDigestPair uriDigestSet := make(map[string]bool) - // TODO: Load TaskRun data from ChildReferences. for _, cr := range pr.Status.ChildReferences { taskRun, err := c.PipelineClient.TektonV1beta1().TaskRuns(ns).Get(ctx, cr.Name, metav1.GetOptions{}) if err != nil { @@ -286,7 +349,7 @@ func expectedPipelineRunProvenance(t *testing.T, ctx context.Context, example st buildFinishedTimes = append(buildFinishedTimes, taskRun.Status.CompletionTime.Time.UTC().Format(time.RFC3339)) for _, step := range taskRun.Status.Steps { // append uri and digest that havent already been appended - uri := artifacts.OCIScheme + strings.Split(step.ImageID, "@")[0] + uri := fmt.Sprintf("oci://%s", strings.Split(step.ImageID, "@")[0]) digest := strings.Split(step.ImageID, ":")[1] uriDigest := fmt.Sprintf("%s %s", uri, digest) if _, ok := uriDigestSet[uriDigest]; !ok { @@ -296,7 +359,7 @@ func expectedPipelineRunProvenance(t *testing.T, ctx context.Context, example st } for _, sidecar := range taskRun.Status.Sidecars { // append uri and digest that havent already been appended - uri := artifacts.OCIScheme + strings.Split(sidecar.ImageID, "@")[0] + uri := fmt.Sprintf("oci://%s", strings.Split(sidecar.ImageID, "@")[0]) digest := strings.Split(sidecar.ImageID, ":")[1] uriDigest := fmt.Sprintf("%s %s", uri, digest) if _, ok := uriDigestSet[uriDigest]; !ok { @@ -306,18 +369,27 @@ func expectedPipelineRunProvenance(t *testing.T, ctx context.Context, example st } } - f := Format{ + return Format{ + UID: string(pr.ObjectMeta.UID), PipelineStartedOn: pr.Status.StartTime.Time.UTC().Format(time.RFC3339), PipelineFinishedOn: pr.Status.CompletionTime.Time.UTC().Format(time.RFC3339), BuildStartTimes: buildStartTimes, BuildFinishedTimes: buildFinishedTimes, URIDigest: uridigest, } +} - return readExpectedAttestation(t, example, f, outputLocation) +func expectedAttestationSLSA1(t *testing.T, example string, f Format, outputLocation string) intoto.ProvenanceStatementSLSA1 { + b := readExpectedAttestationBytes(t, example, f, outputLocation) + return readExpectedAttestationSLSA1(t, b) } -func readExpectedAttestation(t *testing.T, example string, f Format, outputLocation string) intoto.ProvenanceStatement { +func expectedAttestation(t *testing.T, example string, f Format, outputLocation string) intoto.ProvenanceStatement { + b := readExpectedAttestationBytes(t, example, f, outputLocation) + return readExpectedAttestation(t, b) +} + +func readExpectedAttestationBytes(t *testing.T, example string, f Format, outputLocation string) *bytes.Buffer { path := filepath.Join("testdata", outputLocation, strings.Replace(filepath.Base(example), ".yaml", ".json", 1)) t.Logf("Reading expected provenance from %s", path) contents, err := ioutil.ReadFile(path) @@ -334,6 +406,18 @@ func readExpectedAttestation(t *testing.T, example string, f Format, outputLocat t.Fatal(err) } t.Logf("Expected attestation: %s", b.String()) + return b +} + +func readExpectedAttestationSLSA1(t *testing.T, b *bytes.Buffer) intoto.ProvenanceStatementSLSA1 { + var expected intoto.ProvenanceStatementSLSA1 + if err := json.Unmarshal(b.Bytes(), &expected); err != nil { + t.Fatal(err) + } + return expected +} + +func readExpectedAttestation(t *testing.T, b *bytes.Buffer) intoto.ProvenanceStatement { var expected intoto.ProvenanceStatement if err := json.Unmarshal(b.Bytes(), &expected); err != nil { t.Fatal(err) diff --git a/test/testdata/slsa/v2alpha2/pipeline-output-image.json b/test/testdata/slsa/v2alpha2/pipeline-output-image.json new file mode 100644 index 0000000000..b7da1c6273 --- /dev/null +++ b/test/testdata/slsa/v2alpha2/pipeline-output-image.json @@ -0,0 +1,122 @@ +{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v1", + "subject": [ + { + "name": "gcr.io/foo/bar", + "digest": { + "sha256": "05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5" + } + } + ], + "predicate": { + "buildDefinition": { + "buildType": "https://tekton.dev/chains/v2/slsa", + "externalParameters": { + "runSpec": { + "serviceAccountName": "default", + "params": [ + { + "name": "CHAINS-GIT_COMMIT", + "value": "my-git-commit" + },{ + "name": "CHAINS-GIT_URL", + "value": "https://my-git-url" + } + ], + "pipelineSpec": { + "results": [ + { + "name": "IMAGE_URL", + "description": "", + "value": "$(tasks.buildimage.results.IMAGE_URL)" + }, + { + "name": "IMAGE_DIGEST", + "description": "", + "value": "$(tasks.buildimage.results.IMAGE_DIGEST)" + } + ], + "tasks": [ + { + "name": "buildimage", + "taskSpec": { + "metadata": {}, + "steps": [ + { + "name": "create-dockerfile", + "image": "distroless.dev/busybox@sha256:186312fcf3f381b5fc1dd80b1afc0d316f3ed39fb4add8ff900d1f0c7c49a92c", + "resources": {}, + "script": "#!/usr/bin/env sh\necho 'gcr.io/foo/bar' | tee $(results.IMAGE_URL.path)\necho 'sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5' | tee $(results.IMAGE_DIGEST.path)", + "volumeMounts": [ + { + "mountPath": "/dockerfile", + "name": "dockerfile" + } + ] + } + ], + "spec": null, + "results": [ + { + "name": "IMAGE_URL", + "type": "string" + },{ + "name": "IMAGE_DIGEST", + "type": "string" + } + ], + "volumes": [ + { + "emptyDir": {}, + "name": "dockerfile" + } + ] + + } + } + ] + }, + "timeout": "1h0m0s" + } + }, + "resolvedDependencies": [ + {{range .URIDigest}} + { + "uri": "{{.URI}}", + "digest": { + "sha256": "{{.Digest}}" + } + }, + {{end}} + { + "uri": "git+https://my-git-url.git", + "digest": {"sha1": "my-git-commit"}, + "name": "inputs/result" + } + ] + }, + "runDetails": { + "builder": { + "id": "https://tekton.dev/chains/v2" + }, + "metadata": { + "invocationID": "{{.UID}}", + "startedOn": "{{.PipelineStartedOn}}", + "finishedOn": "{{.PipelineFinishedOn}}" + }, + "byproducts": [ + { + "name": "pipelineRunResults/IMAGE_URL", + "mediaType": "application/json", + "content": "Imdjci5pby9mb28vYmFyXG4i" + }, + { + "name": "pipelineRunResults/IMAGE_DIGEST", + "mediaType": "application/json", + "content": "InNoYTI1NjowNWY5NWIyNmVkMTA2NjhiNzE4M2MxZTJkYTk4NjEwZTkxMzcyZmE5ZjUxMDA0NmQ0Y2U1ODEyYWRkYWQ4NmI1XG4i" + } + ] + } + } +} diff --git a/test/testdata/slsa/v2alpha2/task-output-image.json b/test/testdata/slsa/v2alpha2/task-output-image.json new file mode 100644 index 0000000000..6eeb2167c1 --- /dev/null +++ b/test/testdata/slsa/v2alpha2/task-output-image.json @@ -0,0 +1,74 @@ +{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v1", + "subject": [ + { + "name": "gcr.io/foo/bar", + "digest": { + "sha256": "05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5" + } + } + ], + "predicate": { + "buildDefinition": { + "buildType": "https://tekton.dev/chains/v2/slsa", + "externalParameters": { + "runSpec": { + "serviceAccountName": "default", + "taskSpec": { + "steps": [ + { + "name": "create-image", + "image": "busybox", + "resources": {}, + "script": "#!/usr/bin/env sh\necho 'gcr.io/foo/bar' | tee $(results.IMAGE_URL.path)\necho 'sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5' | tee $(results.IMAGE_DIGEST.path)" + } + ], + "results": [ + { + "name": "IMAGE_URL", + "type": "string" + },{ + "name": "IMAGE_DIGEST", + "type": "string" + } + ] + }, + "timeout": "1h0m0s" + } + }, + "resolvedDependencies": [ + {{range .URIDigest}} + { + "uri": "{{.URI}}", + "digest": { + "sha256": "{{.Digest}}" + } + } + {{end}} + ] + }, + "runDetails": { + "builder": { + "id": "https://tekton.dev/chains/v2" + }, + "metadata": { + "invocationID": "{{.UID}}", + "startedOn": "{{index .BuildStartTimes 0}}", + "finishedOn": "{{index .BuildFinishedTimes 0}}" + }, + "byproducts": [ + { + "name": "taskRunResults/IMAGE_DIGEST", + "mediaType": "application/json", + "content": "InNoYTI1NjowNWY5NWIyNmVkMTA2NjhiNzE4M2MxZTJkYTk4NjEwZTkxMzcyZmE5ZjUxMDA0NmQ0Y2U1ODEyYWRkYWQ4NmI1XG4i" + }, + { + "name": "taskRunResults/IMAGE_URL", + "mediaType": "application/json", + "content": "Imdjci5pby9mb28vYmFyXG4i" + } + ] + } + } +}