Skip to content

Commit

Permalink
[TEP-0091] add v1 support for VerifyResource
Browse files Browse the repository at this point in the history
This commit adds v1 task and pipeline verification for VerifyResource.
This is part of the v1 support for trusted resources.

Signed-off-by: Yongxuan Zhang [email protected]
  • Loading branch information
Yongxuanzhang authored and tekton-robot committed Jun 2, 2023
1 parent 5995b54 commit 8381939
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 23 deletions.
58 changes: 36 additions & 22 deletions pkg/trustedresources/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

"github.com/sigstore/sigstore/pkg/signature"
"github.com/tektoncd/pipeline/pkg/apis/config"
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/trustedresources/verifier"
Expand Down Expand Up @@ -63,14 +64,13 @@ type VerificationResult struct {
Err error
}

// VerifyResource verifies the signature and public key against resource (v1beta1 task and pipeline).
// VerifyResource verifies the signature and public key against resource (v1beta1 and v1 task and pipeline).
// VerificationResult is returned with different types for different cases:
// 1) Return VerificationResult with VerificationSkip type, when no policies are found and no-match-policy is set to ignore
// 2) Return VerificationResult with VerificationPass type when verification passed;
// 3) Return VerificationResult with VerificationWarn type, when no matching policies and feature flag "no-match-policy" is "warn", or only Warn mode verification policies fail. Err field is filled with the warning;
// 4) Return VerificationResult with VerificationError type when no policies are found and no-match-policy is set to fail, the resource fails to pass matched enforce verification policy, or there are errors during verification. Err is filled with the err.
// refSource contains the source information of the resource.
// TODO(#6729): Support v1 task and pipeline
func VerifyResource(ctx context.Context, resource metav1.Object, k8s kubernetes.Interface, refSource *v1beta1.RefSource, verificationpolicies []*v1alpha1.VerificationPolicy) VerificationResult {
var refSourceURI string
if refSource != nil {
Expand All @@ -92,36 +92,50 @@ func VerifyResource(ctx context.Context, resource metav1.Object, k8s kubernetes.
}
return VerificationResult{VerificationResultType: VerificationError, Err: fmt.Errorf("failed to get matched policies: %w", err)}
}

objectMeta, signature, err := prepareObjectMeta(resource)
if err != nil {
return VerificationResult{VerificationResultType: VerificationError, Err: err}
}
switch v := resource.(type) {
case *v1beta1.Task:
tm, signature, err := prepareObjectMeta(v.ObjectMeta)
if err != nil {
return VerificationResult{VerificationResultType: VerificationError, Err: err}
}
task := v1beta1.Task{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1beta1",
Kind: "Task"},
ObjectMeta: tm,
ObjectMeta: objectMeta,
Spec: v.TaskSpec(),
}
return verifyResource(ctx, &task, k8s, signature, matchedPolicies)
case *v1beta1.Pipeline:
pm, signature, err := prepareObjectMeta(v.ObjectMeta)
if err != nil {
return VerificationResult{VerificationResultType: VerificationError, Err: err}
case *v1.Task:
task := v1.Task{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1",
Kind: "Task"},
ObjectMeta: objectMeta,
Spec: v.Spec,
}
return verifyResource(ctx, &task, k8s, signature, matchedPolicies)
case *v1beta1.Pipeline:
pipeline := v1beta1.Pipeline{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1beta1",
Kind: "Pipeline"},
ObjectMeta: pm,
ObjectMeta: objectMeta,
Spec: v.PipelineSpec(),
}
return verifyResource(ctx, &pipeline, k8s, signature, matchedPolicies)
case *v1.Pipeline:
pipeline := v1.Pipeline{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1",
Kind: "Pipeline"},
ObjectMeta: objectMeta,
Spec: v.Spec,
}
return verifyResource(ctx, &pipeline, k8s, signature, matchedPolicies)
default:
return VerificationResult{VerificationResultType: VerificationError, Err: fmt.Errorf("%w: got resource %v but v1beta1.Task and v1beta1.Pipeline are currently supported", ErrResourceNotSupported, resource)}
}
return VerificationResult{VerificationResultType: VerificationError, Err: fmt.Errorf("%w: got resource %v but v1beta1.Task and v1beta1.Pipeline are currently supported", ErrResourceNotSupported, resource)}
}

// VerifyTask is the deprecated, this is to keep backward compatibility
Expand Down Expand Up @@ -242,23 +256,23 @@ func verifyInterface(obj interface{}, verifier signature.Verifier, signature []b
// Returns a copy of the input object metadata with the annotations removed and the object's signature,
// if it is present in the metadata.
// Returns a non-nil error if the signature cannot be decoded.
func prepareObjectMeta(in metav1.ObjectMeta) (metav1.ObjectMeta, []byte, error) {
func prepareObjectMeta(in metav1.Object) (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
out.Name = in.GetName()
out.GenerateName = in.GetGenerateName()
out.Namespace = in.GetNamespace()

if in.Labels != nil {
if in.GetLabels() != nil {
out.Labels = make(map[string]string)
for k, v := range in.Labels {
for k, v := range in.GetLabels() {
out.Labels[k] = v
}
}

out.Annotations = make(map[string]string)
for k, v := range in.Annotations {
for k, v := range in.GetAnnotations() {
out.Annotations[k] = v
}

Expand All @@ -269,7 +283,7 @@ func prepareObjectMeta(in metav1.ObjectMeta) (metav1.ObjectMeta, []byte, error)
delete(out.Annotations, "kubectl.kubernetes.io/last-applied-configuration")

// signature should be contained in annotation
sig, ok := in.Annotations[SignatureAnnotation]
sig, ok := in.GetAnnotations()[SignatureAnnotation]
if !ok {
return out, nil, nil
}
Expand Down
138 changes: 137 additions & 1 deletion pkg/trustedresources/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,21 @@ limitations under the License.
package trustedresources

import (
"bytes"
"context"
"crypto"
"crypto/elliptic"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/tektoncd/pipeline/pkg/apis/config"
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/trustedresources/verifier"
Expand All @@ -41,6 +46,39 @@ const (
namespace = "trusted-resources"
)

var unsignedTask = v1.Task{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1",
Kind: "Task"},
ObjectMeta: metav1.ObjectMeta{
Name: "task",
Annotations: map[string]string{"foo": "bar"},
},
Spec: v1.TaskSpec{
Steps: []v1.Step{{
Image: "ubuntu",
Name: "echo",
}},
},
}

var unsignedPipeline = v1.Pipeline{
TypeMeta: metav1.TypeMeta{
APIVersion: "tekton.dev/v1",
Kind: "Pipeline"},
ObjectMeta: metav1.ObjectMeta{
Name: "pipeline",
Annotations: map[string]string{"foo": "bar"},
},
Spec: v1.PipelineSpec{
Tasks: []v1.PipelineTask{
{
Name: "task",
},
},
},
}

func TestVerifyInterface_Task_Success(t *testing.T) {
sv, _, err := signature.NewDefaultECDSASignerVerifier()
if err != nil {
Expand Down Expand Up @@ -502,6 +540,57 @@ func TestVerifyResource_Pipeline_Error(t *testing.T) {
}
}

func TestVerifyResource_V1Task_Success(t *testing.T) {
signer, _, k8sclient, vps := test.SetupVerificationPolicies(t)
signedTask, err := getSignedV1Task(unsignedTask.DeepCopy(), signer, "signed")
if err != nil {
t.Error(err)
}
vr := VerifyResource(context.Background(), signedTask, k8sclient, &v1beta1.RefSource{URI: "git+https://github.com/tektoncd/catalog.git"}, vps)
if vr.VerificationResultType != VerificationPass {
t.Errorf("VerificationResult mismatch: want %v, got %v", VerificationPass, vr.VerificationResultType)
}
}
func TestVerifyResource_V1Task_Error(t *testing.T) {
signer, _, k8sclient, vps := test.SetupVerificationPolicies(t)
signedTask, err := getSignedV1Task(unsignedTask.DeepCopy(), signer, "signed")
if err != nil {
t.Error(err)
}
modifiedTask := signedTask.DeepCopy()
modifiedTask.Annotations["foo"] = "modified"
vr := VerifyResource(context.Background(), modifiedTask, k8sclient, &v1beta1.RefSource{URI: "git+https://github.com/tektoncd/catalog.git"}, vps)
if vr.VerificationResultType != VerificationError && !errors.Is(vr.Err, ErrResourceVerificationFailed) {
t.Errorf("VerificationResult mismatch: want %v, got %v", VerificationResult{VerificationResultType: VerificationError, Err: ErrResourceVerificationFailed}, vr)
}
}

func TestVerifyResource_V1Pipeline_Success(t *testing.T) {
signer, _, k8sclient, vps := test.SetupVerificationPolicies(t)
signed, err := getSignedV1Pipeline(unsignedPipeline.DeepCopy(), signer, "signed")
if err != nil {
t.Error(err)
}
vr := VerifyResource(context.Background(), signed, k8sclient, &v1beta1.RefSource{URI: "git+https://github.com/tektoncd/catalog.git"}, vps)
if vr.VerificationResultType != VerificationPass {
t.Errorf("VerificationResult mismatch: want %v, got %v", VerificationPass, vr.VerificationResultType)
}
}

func TestVerifyResource_V1Pipeline_Error(t *testing.T) {
signer, _, k8sclient, vps := test.SetupVerificationPolicies(t)
signed, err := getSignedV1Pipeline(unsignedPipeline.DeepCopy(), signer, "signed")
if err != nil {
t.Error(err)
}
modifiedTask := signed.DeepCopy()
modifiedTask.Annotations["foo"] = "modified"
vr := VerifyResource(context.Background(), modifiedTask, k8sclient, &v1beta1.RefSource{URI: "git+https://github.com/tektoncd/catalog.git"}, vps)
if vr.VerificationResultType != VerificationError && !errors.Is(vr.Err, ErrResourceVerificationFailed) {
t.Errorf("VerificationResult mismatch: want %v, got %v", VerificationResult{VerificationResultType: VerificationError, Err: ErrResourceVerificationFailed}, vr)
}
}

func TestVerifyResource_TypeNotSupported(t *testing.T) {
resource := v1beta1.ClusterTask{}
refSource := &v1beta1.RefSource{URI: "git+https://github.com/tektoncd/catalog.git"}
Expand Down Expand Up @@ -572,7 +661,7 @@ func TestPrepareObjectMeta(t *testing.T) {

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
task, signature, err := prepareObjectMeta(*tc.objectmeta)
task, signature, err := prepareObjectMeta(tc.objectmeta)
if err != nil {
t.Fatalf("got unexpected err: %v", err)
}
Expand All @@ -586,3 +675,50 @@ func TestPrepareObjectMeta(t *testing.T) {
})
}
}

func signInterface(signer signature.Signer, i interface{}) ([]byte, error) {
if signer == nil {
return nil, fmt.Errorf("signer is nil")
}
b, err := json.Marshal(i)
if err != nil {
return nil, err
}
h := sha256.New()
h.Write(b)

sig, err := signer.SignMessage(bytes.NewReader(h.Sum(nil)))
if err != nil {
return nil, err
}

return sig, nil
}

func getSignedV1Task(unsigned *v1.Task, signer signature.Signer, name string) (*v1.Task, error) {
signed := unsigned.DeepCopy()
signed.Name = name
if signed.Annotations == nil {
signed.Annotations = map[string]string{}
}
signature, err := signInterface(signer, signed)
if err != nil {
return nil, err
}
signed.Annotations[SignatureAnnotation] = base64.StdEncoding.EncodeToString(signature)
return signed, nil
}

func getSignedV1Pipeline(unsigned *v1.Pipeline, signer signature.Signer, name string) (*v1.Pipeline, error) {
signed := unsigned.DeepCopy()
signed.Name = name
if signed.Annotations == nil {
signed.Annotations = map[string]string{}
}
signature, err := signInterface(signer, signed)
if err != nil {
return nil, err
}
signed.Annotations[SignatureAnnotation] = base64.StdEncoding.EncodeToString(signature)
return signed, nil
}

0 comments on commit 8381939

Please sign in to comment.