-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
verify.go
199 lines (173 loc) · 7.13 KB
/
verify.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
/*
Copyright 2022 The Tekton Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package trustedresources
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"regexp"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/trustedresources/verifier"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
const (
// SignatureAnnotation is the key of signature in annotation map
SignatureAnnotation = "tekton.dev/signature"
)
// VerifyTask verifies the signature and public key against task.
// source is from ConfigSource.URI, which will be used to match policy patterns. k8s is used to fetch secret from cluster
func VerifyTask(ctx context.Context, taskObj v1beta1.TaskObject, k8s kubernetes.Interface, source string, policies []*v1alpha1.VerificationPolicy) error {
tm, signature, err := prepareObjectMeta(taskObj.TaskMetadata())
if err != nil {
return err
}
task := v1beta1.Task{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1beta1",
Kind: "Task"},
ObjectMeta: tm,
Spec: taskObj.TaskSpec(),
}
return verifyResource(ctx, &task, k8s, signature, source, policies)
}
// VerifyPipeline verifies the signature and public key against pipeline.
// source is from ConfigSource.URI, which will be used to match policy patterns, k8s is used to fetch secret from cluster
func VerifyPipeline(ctx context.Context, pipelineObj v1beta1.PipelineObject, k8s kubernetes.Interface, source string, policies []*v1alpha1.VerificationPolicy) error {
pm, signature, err := prepareObjectMeta(pipelineObj.PipelineMetadata())
if err != nil {
return err
}
pipeline := v1beta1.Pipeline{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1beta1",
Kind: "Pipeline"},
ObjectMeta: pm,
Spec: pipelineObj.PipelineSpec(),
}
return verifyResource(ctx, &pipeline, k8s, signature, source, policies)
}
// verifyResource verifies resource which implements metav1.Object by provided signature and public keys from configmap or policies.
// It will fetch public key from configmap first, if no keys are found then try to fetch keys from VerificationPolicy
// For verificationPolicies verifyResource will adopt the following rules to do verification:
// 1. For each policy, check if the resource url is matching any of the `patterns` in the `resources` list. If matched then this policy will be used for verification.
// 2. If multiple policies are matched, the resource needs to pass all of them to pass verification.
// 3. To pass one policy, the resource can pass any public keys in the policy.
func verifyResource(ctx context.Context, resource metav1.Object, k8s kubernetes.Interface, signature []byte, source string, policies []*v1alpha1.VerificationPolicy) error {
verifiers, err := verifier.FromConfigMap(ctx, k8s)
if err != nil && !errors.Is(err, verifier.ErrorEmptyPublicKeys) {
return fmt.Errorf("failed to get verifiers from configmap: %w", err)
}
if len(verifiers) != 0 {
for _, verifier := range verifiers {
// if one of the verifier passes verification, then this resource passes verification
if err := verifyInterface(resource, verifier, signature); err == nil {
return nil
}
}
return fmt.Errorf("%w: resource %s in namespace %s fails verification", ErrorResourceVerificationFailed, resource.GetName(), resource.GetNamespace())
}
if len(policies) == 0 {
return ErrorEmptyVerificationConfig
}
matchedPolicies := []*v1alpha1.VerificationPolicy{}
for _, p := range policies {
for _, r := range p.Spec.Resources {
matching, err := regexp.MatchString(r.Pattern, source)
if err != nil {
return fmt.Errorf("%v: %w", err, ErrorRegexMatch)
}
if matching {
matchedPolicies = append(matchedPolicies, p)
break
}
}
}
if len(matchedPolicies) == 0 {
return fmt.Errorf("%w: no matching policies are found for resource: %s against source: %s", ErrorNoMatchedPolicies, resource.GetName(), source)
}
for _, p := range matchedPolicies {
passVerification := false
verifiers, err := verifier.FromPolicy(ctx, k8s, p)
if err != nil {
return fmt.Errorf("failed to get verifiers from policy: %w", err)
}
for _, verifier := range verifiers {
// if one of the verifier passes verification, then this policy passes verification
if err := verifyInterface(resource, verifier, signature); err == nil {
passVerification = true
break
}
}
// if this policy fails the verification, should return error directly. No need to check other policies
if passVerification == false {
return fmt.Errorf("%w: resource %s in namespace %s fails verification", ErrorResourceVerificationFailed, resource.GetName(), resource.GetNamespace())
}
}
return nil
}
// verifyInterface get the checksum of json marshalled object and verify it.
func verifyInterface(obj interface{}, verifier signature.Verifier, signature []byte) error {
ts, err := json.Marshal(obj)
if err != nil {
return fmt.Errorf("failed to marshal the object: %w", err)
}
h := sha256.New()
h.Write(ts)
if err := verifier.VerifySignature(bytes.NewReader(signature), bytes.NewReader(h.Sum(nil))); err != nil {
return fmt.Errorf("%w:%v", ErrorResourceVerificationFailed, err.Error())
}
return nil
}
// prepareObjectMeta will remove annotations not configured from user side -- "kubectl-client-side-apply" and "kubectl.kubernetes.io/last-applied-configuration"
// to avoid verification failure and extract the signature.
func prepareObjectMeta(in metav1.ObjectMeta) (metav1.ObjectMeta, []byte, error) {
out := metav1.ObjectMeta{}
// exclude the fields populated by system.
out.Name = in.Name
out.GenerateName = in.GenerateName
out.Namespace = in.Namespace
if in.Labels != nil {
out.Labels = make(map[string]string)
for k, v := range in.Labels {
out.Labels[k] = v
}
}
out.Annotations = make(map[string]string)
for k, v := range in.Annotations {
out.Annotations[k] = v
}
// exclude the annotations added by other components
// Task annotations are unlikely to be changed, we need to make sure other components
// like resolver doesn't modify the annotations, otherwise the verification will fail
delete(out.Annotations, "kubectl-client-side-apply")
delete(out.Annotations, "kubectl.kubernetes.io/last-applied-configuration")
// signature should be contained in annotation
sig, ok := in.Annotations[SignatureAnnotation]
if !ok {
return out, nil, ErrorSignatureMissing
}
// extract signature
signature, err := base64.StdEncoding.DecodeString(sig)
if err != nil {
return out, nil, err
}
delete(out.Annotations, SignatureAnnotation)
return out, signature, nil
}