Skip to content

Commit

Permalink
[TEP-0089] Enable the signing and verification of TR results and the …
Browse files Browse the repository at this point in the history
…TR status.

This PR enables the signing and verification of TR results and TR status.

Before this change the spireAPIController object was injected into the TR reconciler but it was not used.

After this change,
- At the start of every reconcile run, the reconciler will verify if the signature on the status can be verified, else it will error out.
- At the end of every reconcile run, the reconciler will sign the status and add it as an annotation.
- When TR results are read from the termination message and converted into TR results, they will be verified.

This commit is part of a series of PRs to implement TEP-0089. The implementation of TEP-0089 is tracked in the issue tektoncd#6597 SPIRE for non-falsifiable provenance.
  • Loading branch information
jagathprakash committed Jun 13, 2023
1 parent 61d0008 commit df6700e
Show file tree
Hide file tree
Showing 3 changed files with 325 additions and 4 deletions.
254 changes: 250 additions & 4 deletions docs/spire.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,58 @@ weight: 1660
-->
⚠️ This is a work in progress: SPIRE support is not yet functional

TaskRun result attestations is currently an alpha experimental feature. Currently all that is implemented is support for configuring Tekton to connect to SPIRE. See TEP-0089 for details on the overall design and feature set.
TaskRun result attestations is currently an alpha experimental feature. Currently all that is implemented is support for configuring Tekton to connect to SPIRE and enabling TaskRun to sign and verify the TaskRunStatus. See [TEP-0089](https://github.com/tektoncd/community/blob/main/teps/0089-nonfalsifiable-provenance-support.md) for details on the overall design and feature set.

This being a large feature, this will be implemented in the following phases. This document will be updated as we implement new phases.
1. Add a client for SPIRE (done).
2. Add a configMap which initializes SPIRE (in progress).
3. Modify TaskRun to sign and verify TaskRun Results using SPIRE.
4. Modify Tekton Chains to verify the TaskRun Results.
2. Add a configMap which initializes SPIRE (done).
3. Modify TaskRun to sign and verify TaskRunStatus using SPIRE (done).
4. Enabling Chains to verify the TaskRun Results.

When the TaskRun result attestations feature is [enabled](./spire.md#enabling-taskrun-result-attestations) all TaskRuns will produce a signature alongside its results, which can then be used to validate its provenance. For example, a TaskRun result that creates user-specified results `commit` and `url` would look like the following. `SVID`, `RESULT_MANIFEST`, `RESULT_MANIFEST.sig`, `commit.sig` and `url.sig` are generated attestations by the integration of SPIRE and Tekton Controller.

Parsed, the fields would be:
```
...
<truncated>
...
📝 Results
NAME VALUE
∙ RESULT_MANIFEST commit,url,SVID,commit.sig,url.sig
∙ RESULT_MANIFEST.sig MEUCIQD55MMII9SEk/esQvwNLGC43y7efNGZ+7fsTdq+9vXYFAIgNoRW7cV9WKriZkcHETIaAKqfcZVJfsKbEmaDyohDSm4=
∙ SVID -----BEGIN CERTIFICATE-----
MIICGzCCAcGgAwIBAgIQH9VkLxKkYMidPIsofckRQTAKBggqhkjOPQQDAjAeMQsw
CQYDVQQGEwJVUzEPMA0GA1UEChMGU1BJRkZFMB4XDTIyMDIxMTE2MzM1MFoXDTIy
MDIxMTE3MzQwMFowHTELMAkGA1UEBhMCVVMxDjAMBgNVBAoTBVNQSVJFMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEBRdg3LdxVAELeH+lq8wzdEJd4Gnt+m9G0Qhy
NyWoPmFUaj9vPpvOyRgzxChYnW0xpcDWihJBkq/EbusPvQB8CKOB4TCB3jAOBgNV
HQ8BAf8EBAMCA6gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1Ud
EwEB/wQCMAAwHQYDVR0OBBYEFID7ARM5+vwzvnLPMO7Icfnj7l7hMB8GA1UdIwQY
MBaAFES3IzpGDqgV3QcQNgX8b/MBwyAtMF8GA1UdEQRYMFaGVHNwaWZmZTovL2V4
YW1wbGUub3JnL25zL2RlZmF1bHQvdGFza3J1bi9jYWNoZS1pbWFnZS1waXBlbGlu
ZXJ1bi04ZHE5Yy1mZXRjaC1mcm9tLWdpdDAKBggqhkjOPQQDAgNIADBFAiEAi+LR
JkrZn93PZPslaFmcrQw3rVcEa4xKmPleSvQaBoACIF1QB+q1uwH6cNvWdbLK9g+W
T9Np18bK0xc6p5SuTM2C
-----END CERTIFICATE-----
∙ commit aa79de59c4bae24e32f15fda467d02ae9cd94b01
∙ commit.sig MEQCIEJHk+8B+mCFozp0F52TQ1AadlhEo1lZNOiOnb/ht71aAiBCE0otKB1R0BktlPvweFPldfZfjG0F+NUSc2gPzhErzg==
∙ url https://github.com/buildpacks/samples
∙ url.sig MEUCIF0Fuxr6lv1MmkreqDKcPH3m+eXp+gY++VcxWgGCx7T1AiEA9U/tROrKuCGfKApLq2A9EModbdoGXyQXFOpAa0aMpOg=
```

However, the verification materials are removed from the final results as part of the TaskRun status. It is stored in the termination messages (more details below):

```
$ tkn tr describe cache-image-pipelinerun-8dq9c-fetch-from-git
...
<truncated>
...
📝 Results
NAME VALUE
∙ commit aa79de59c4bae24e32f15fda467d02ae9cd94b01
∙ url https://github.com/buildpacks/samples
```

## Architecture Overview

Expand Down Expand Up @@ -127,3 +172,204 @@ To enable TaskRun attestations:
# spire-node-alias-prefix specifies the SPIRE node alias prefix to use.
spire-node-alias-prefix: "/tekton-node/"
```

## Sample TaskRun attestation

The following example shows how this feature works:

```yaml
kind: TaskRun
apiVersion: tekton.dev/v1beta1
metadata:
name: non-falsifiable-provenance
spec:
timeout: 60s
taskSpec:
steps:
- name: non-falsifiable
image: ubuntu
script: |
#!/usr/bin/env bash
printf "%s" "hello" > "$(results.foo.path)"
printf "%s" "world" > "$(results.bar.path)"
results:
- name: foo
- name: bar
```


The termination message is:
```
message: '[{"key":"RESULT_MANIFEST","value":"foo,bar","type":1},{"key":"RESULT_MANIFEST.sig","value":"MEQCIB4grfqBkcsGuVyoQd9KUVzNZaFGN6jQOKK90p5HWHqeAiB7yZerDA+YE3Af/ALG43DQzygiBpKhTt8gzWGmpvXJFw==","type":1},{"key":"SVID","value":"-----BEGIN
CERTIFICATE-----\nMIICCjCCAbCgAwIBAgIRALH94zAZZXdtPg97O5vG5M0wCgYIKoZIzj0EAwIwHjEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTAeFw0yMjAzMTQxNTUzNTlaFw0y\nMjAzMTQxNjU0MDlaMB0xCzAJBgNVBAYTAlVTMQ4wDAYDVQQKEwVTUElSRTBZMBMG\nByqGSM49AgEGCCqGSM49AwEHA0IABPLzFTDY0RDpjKb+eZCIWgUw9DViu8/pM8q7\nHMTKCzlyGqhaU80sASZfpkZvmi72w+gLszzwVI1ZNU5e7aCzbtSjgc8wgcwwDgYD\nVR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV\nHRMBAf8EAjAAMB0GA1UdDgQWBBSsUvspy+/Dl24pA1f+JuNVJrjgmTAfBgNVHSME\nGDAWgBSOMyOHnyLLGxPSD9RRFL+Yhm/6qzBNBgNVHREERjBEhkJzcGlmZmU6Ly9l\neGFtcGxlLm9yZy9ucy9kZWZhdWx0L3Rhc2tydW4vbm9uLWZhbHNpZmlhYmxlLXBy\nb3ZlbmFuY2UwCgYIKoZIzj0EAwIDSAAwRQIhAM4/bPAH9dyhBEj3DbwtJKMyEI56\n4DVrP97ps9QYQb23AiBiXWrQkvRYl0h4CX0lveND2yfqLrGdVL405O5NzCcUrA==\n-----END
CERTIFICATE-----\n","type":1},{"key":"bar","value":"world","type":1},{"key":"bar.sig","value":"MEUCIQDOtg+aEP1FCr6/FsHX+bY1d5abSQn2kTiUMg4Uic2lVQIgTVF5bbT/O77VxESSMtQlpBreMyw2GmKX2hYJlaOEH1M=","type":1},{"key":"foo","value":"hello","type":1},{"key":"foo.sig","value":"MEQCIBr+k0i7SRSyb4h96vQE9hhxBZiZb/2PXQqReOKJDl/rAiBrjgSsalwOvN0zgQay0xQ7PRbm5YSmI8tvKseLR8Ryww==","type":1}]'
```

Parsed, the fields are:
- `RESULT_MANIFEST`: List of results that should be present, to prevent pick and choose attacks
- `RESULT_MANIFEST.sig`: The signature of the result manifest
- `SVID`: The x509 certificate that will be used to verify the signature trust chain to the authority
- `*.sig`: The signature of each individual result output
```
∙ RESULT_MANIFEST foo,bar
∙ RESULT_MANIFEST.sig MEQCIB4grfqBkcsGuVyoQd9KUVzNZaFGN6jQOKK90p5HWHqeAiB7yZerDA+YE3Af/ALG43DQzygiBpKhTt8gzWGmpvXJFw==
∙ SVID -----BEGIN CERTIFICATE-----
MIICCjCCAbCgAwIBAgIRALH94zAZZXdtPg97O5vG5M0wCgYIKoZIzj0EAwIwHjEL
MAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTAeFw0yMjAzMTQxNTUzNTlaFw0y
MjAzMTQxNjU0MDlaMB0xCzAJBgNVBAYTAlVTMQ4wDAYDVQQKEwVTUElSRTBZMBMG
ByqGSM49AgEGCCqGSM49AwEHA0IABPLzFTDY0RDpjKb+eZCIWgUw9DViu8/pM8q7
HMTKCzlyGqhaU80sASZfpkZvmi72w+gLszzwVI1ZNU5e7aCzbtSjgc8wgcwwDgYD
VR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV
HRMBAf8EAjAAMB0GA1UdDgQWBBSsUvspy+/Dl24pA1f+JuNVJrjgmTAfBgNVHSME
GDAWgBSOMyOHnyLLGxPSD9RRFL+Yhm/6qzBNBgNVHREERjBEhkJzcGlmZmU6Ly9l
eGFtcGxlLm9yZy9ucy9kZWZhdWx0L3Rhc2tydW4vbm9uLWZhbHNpZmlhYmxlLXBy
b3ZlbmFuY2UwCgYIKoZIzj0EAwIDSAAwRQIhAM4/bPAH9dyhBEj3DbwtJKMyEI56
4DVrP97ps9QYQb23AiBiXWrQkvRYl0h4CX0lveND2yfqLrGdVL405O5NzCcUrA==
-----END CERTIFICATE-----
∙ bar world
∙ bar.sig MEUCIQDOtg+aEP1FCr6/FsHX+bY1d5abSQn2kTiUMg4Uic2lVQIgTVF5bbT/O77VxESSMtQlpBreMyw2GmKX2hYJlaOEH1M=
∙ foo hello
∙ foo.sig MEQCIBr+k0i7SRSyb4h96vQE9hhxBZiZb/2PXQqReOKJDl/rAiBrjgSsalwOvN0zgQay0xQ7PRbm5YSmI8tvKseLR8Ryww==
```


However, the verification materials are removed from the results as part of the TaskRun status:
```console
$ tkn tr describe non-falsifiable-provenance
Name: non-falsifiable-provenance
Namespace: default
Service Account: default
Timeout: 1m0s
Labels:
app.kubernetes.io/managed-by=tekton-pipelines
🌡️ Status
STARTED DURATION STATUS
38 seconds ago 36 seconds Succeeded
📝 Results
NAME VALUE
∙ bar world
∙ foo hello
🦶 Steps
NAME STATUS
∙ non-falsifiable Completed
```

## How is the result being verified

The signatures are being verified by the Tekton controller, the process of verification is as follows:

- Verifying the SVID
- Obtain the trust bundle from the SPIRE server
- Verify the SVID with the trust bundle
- Verify that the SVID spiffe ID is for the correct TaskRun
- Verifying the result manifest
- Verify the content of `RESULT_MANIFEST` with the field `RESULT_MANIFEST.sig` with the SVID public key
- Verify that there is a corresponding field for all items listed in `RESULT_MANIFEST` (besides SVID and `*.sig` fields)
- Verify individual result fields
- For each of the items in the results, verify its content against its associated `.sig` field


# TaskRun Status attestations

Each TaskRun status that is written by the tekton-pipelines-controller will be signed to ensure that there is no external
tampering of the TaskRun status. Upon each retrieval of the TaskRun, the tekton-pipelines-controller checks if the status is initialized,
and that the signature validates the current status.
The signature and SVID will be stored as annotations on the TaskRun Status field, and can be verified by a client.

The verification is done on every consumption of the TaskRun except when the TaskRun is uninitialized. When uninitialized, the
tekton-pipelines-controller is not influenced by fields in the status and thus will not sign incorrect reflections of the TaskRun.

The spec and TaskRun annotations/labels are not signed when there are valid interactions from other controllers or users (i.e. cancelling taskrun).
Editing the object annotations/labels or spec will not result in any unverifiable outcome of the status field.

As the TaskRun progresses, the Pipeline Controller will reconcile the TaskRun object and continually verify the current hash against the `tekton.dev/status-hash-sig` before updating the hash to match the new status and creating a new signature.

An example TaskRun annotations would be:

```console
$ tkn tr describe non-falsifiable-provenance -oyaml
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
annotations:
pipeline.tekton.dev/release: 3ee99ec
creationTimestamp: "2022-03-04T19:10:46Z"
generation: 1
labels:
app.kubernetes.io/managed-by: tekton-pipelines
name: non-falsifiable-provenance
namespace: default
resourceVersion: "23088242"
uid: 548ebe99-d40b-4580-a9bc-afe80915e22e
spec:
serviceAccountName: default
taskSpec:
results:
- description: ""
name: foo
- description: ""
name: bar
steps:
- image: ubuntu
name: non-falsifiable
resources: {}
script: |
#!/usr/bin/env bash
sleep 30
printf "%s" "hello" > "$(results.foo.path)"
printf "%s" "world" > "$(results.bar.path)"
timeout: 1m0s
status:
annotations:
tekton.dev/controller-svid: |
-----BEGIN CERTIFICATE-----
MIIB7jCCAZSgAwIBAgIRAI8/08uXSn9tyv7cRN87uvgwCgYIKoZIzj0EAwIwHjEL
MAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTAeFw0yMjAzMDQxODU0NTlaFw0y
MjAzMDQxOTU1MDlaMB0xCzAJBgNVBAYTAlVTMQ4wDAYDVQQKEwVTUElSRTBZMBMG
ByqGSM49AgEGCCqGSM49AwEHA0IABL+e9OjkMv+7XgMWYtrzq0ESzJi+znA/Pm8D
nvApAHg3/rEcNS8c5LgFFRzDfcs9fxGSSkL1JrELzoYul1Q13XejgbMwgbAwDgYD
VR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV
HRMBAf8EAjAAMB0GA1UdDgQWBBR+ma+yZfo092FKIM4F3yhEY8jgDDAfBgNVHSME
GDAWgBRKiCg5+YdTaQ+5gJmvt2QcDkQ6KjAxBgNVHREEKjAohiZzcGlmZmU6Ly9l
eGFtcGxlLm9yZy90ZWt0b24vY29udHJvbGxlcjAKBggqhkjOPQQDAgNIADBFAiEA
8xVWrQr8+i6yMLDm9IUjtvTbz9ofjSsWL6c/+rxmmRYCIBTiJ/HW7di3inSfxwqK
5DKyPrKoR8sq8Ne7flkhgbkg
-----END CERTIFICATE-----
tekton.dev/status-hash: 76692c9dcd362f8a6e4bda8ccb4c0937ad16b0d23149ae256049433192892511
tekton.dev/status-hash-sig: MEQCIFv2bW0k4g0Azx+qaeZjUulPD8Ma3uCUn0tXQuuR1FaEAiBHQwN4XobOXmC2nddYm04AZ74YubUyNl49/vnbnR/HcQ==
completionTime: "2022-03-04T19:11:22Z"
conditions:
- lastTransitionTime: "2022-03-04T19:11:22Z"
message: All Steps have completed executing
reason: Succeeded
status: "True"
type: Succeeded
- lastTransitionTime: "2022-03-04T19:11:22Z"
message: Spire verified
reason: TaskRunResultsVerified
status: "True"
type: SignedResultsVerified
podName: non-falsifiable-provenance-pod
startTime: "2022-03-04T19:10:46Z"
steps:
...
<TRUNCATED>
```

## How is the status being verified

The signature are being verified by the Tekton controller, the process of verification is as follows:

- Verify status-hash fields
- verify `tekton.dev/status-hash` content against its associated `tekton.dev/status-hash-sig` field. If status hash does
not match invalidate the `tekton.dev/verified = no` annotation will be added

## Further Details

To learn more about SPIRE attestations, check out the [TEP](https://github.com/tektoncd/community/blob/main/teps/0089-nonfalsifiable-provenance-support.md).
33 changes: 33 additions & 0 deletions test/featureflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,39 @@ import (
"knative.dev/pkg/system"
)

var spireFeatureGates = map[string]string{
"enable-nonfalsifiability": "spire",
"enable-api-fields": "alpha",
}

func isSpireEnabled(ctx context.Context, t *testing.T, c *clients, namespace string) bool {
t.Helper()
isSpireEnabled, _ := hasAllFeatureGates(ctx, t, spireFeatureGates, c, namespace)
return isSpireEnabled
}

func hasAllFeatureGates(ctx context.Context, t *testing.T, gates map[string]string, c *clients, namespace string) (bool, string) {
t.Helper()
featureFlagsCM, err := c.KubeClient.CoreV1().ConfigMaps(system.Namespace()).Get(ctx, config.GetFeatureFlagsConfigName(), metav1.GetOptions{})
if err != nil {
t.Fatalf("Failed to get ConfigMap `%s`: %s", config.GetFeatureFlagsConfigName(), err)
}
pairs := []string{}
for name, value := range gates {
actual, ok := featureFlagsCM.Data[name]
if !ok || value != actual {
pairs = append(pairs, fmt.Sprintf("%q is %q, want %s", name, actual, value))
}
}
if len(pairs) > 0 {
status := fmt.Sprintf(
"Some feature flags in namespace %q not matching %s\nExisting feature flag: %#v\n",
system.Namespace(), strings.Join(pairs, " and "), featureFlagsCM.Data)
return false, status
}
return true, ""
}

// requireAnyGate returns a setup func that will skip the current
// test if none of the feature-flags in the given map match
// what's in the feature-flags ConfigMap. It will fatally fail
Expand Down
42 changes: 42 additions & 0 deletions test/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import (
"testing"

"github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/names"
"github.com/tektoncd/pipeline/pkg/spire"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -315,3 +317,43 @@ func getCRDYaml(ctx context.Context, cs *clients, ns string) ([]byte, error) {

return output, nil
}

// Verifies if the taskrun results should not be verified by spire
func spireShouldFailTaskRunResultsVerify(t *testing.T, tr *v1beta1.TaskRun) {
t.Helper()
if tr.IsTaskRunResultVerified() {
t.Errorf("Taskrun `%s` status condition should not be verified as taskrun failed", tr.Name)
}
t.Logf("Taskrun `%s` status results condition verified by spire as false, which is valid", tr.Name)
}

// Verifies if the taskrun results are verified by spire
func spireShouldPassTaskRunResultsVerify(t *testing.T, tr *v1beta1.TaskRun) {
t.Helper()
if !tr.IsTaskRunResultVerified() {
t.Errorf("Taskrun `%s` status condition not verified. Spire taskrun results verification failure", tr.Name)
} else {
t.Logf("Taskrun `%s` status results condition verified by spire as true, which is valid", tr.Name)
}
t.Logf("Taskrun `%s` status results condition verified by spire as true, which is valid", tr.Name)
}

// Verifies if the taskrun status annotation does not contain "not-verified"
func spireShouldPassSpireAnnotation(t *testing.T, tr *v1beta1.TaskRun) {
t.Helper()
if _, notVerified := tr.Status.Annotations[spire.VerifiedAnnotation]; notVerified {
t.Errorf("Taskrun `%s` status not verified. Spire annotation tekton.dev/spire-verified = no. Failed spire verification", tr.Name)
}
t.Logf("Taskrun `%s` status spire annotation verified", tr.Name)
}

// Verifies if the taskrun status annotation does contain "not-verified"
func spireShouldFailSpireAnnotation(t *testing.T, tr *v1beta1.TaskRun) {
t.Helper()
_, notVerified := tr.Status.Annotations[spire.VerifiedAnnotation]
_, hash := tr.Status.Annotations[spire.TaskRunStatusHashAnnotation]
if !notVerified && hash {
t.Errorf("Taskrun `%s` status should be not verified missing spire Annotation tekton.dev/not-verified = yes", tr.Name)
}
t.Logf("Taskrun `%s` status spire annotation not verified, which is valid", tr.Name)
}

0 comments on commit df6700e

Please sign in to comment.