From c8ab77875d4b9b5919320e7fb1cb331f80c6207b Mon Sep 17 00:00:00 2001 From: Luiz Carvalho Date: Mon, 27 Feb 2023 16:40:25 -0500 Subject: [PATCH] Add support for signing SBOMs The idea is a TaskRun produces an unsigned SBOM and pushes it to the OCI registry. The reference to the SBOM is then added to the result IMAGE_SBOM_URL. When Chains sees this result, it downloads the SBOM from the OCI registry, signs it, then adds to the image attestations with an SBOM-specific predicate. IMAGE_SBOM_FORMAT is used to specify the predicate to list in the attestation. This is also possible when using the IMAGES result, and the corresponding SBOMS result. Both must have the exact number of items. When using the SBOMS result, SBOMS_FORMAT is also required to specify the SBOM format for all the items in SBOMS. Signed-off-by: Luiz Carvalho --- pkg/artifacts/signable.go | 154 +++++++++++++++++- pkg/chains/formats/format.go | 3 +- pkg/chains/formats/sbom/sbom.go | 107 ++++++++++++ pkg/chains/formats/simple/simple.go | 3 +- pkg/chains/formats/simple/simple_test.go | 4 +- pkg/chains/formats/slsa/v1/intotoite6.go | 8 +- pkg/chains/formats/slsa/v1/intotoite6_test.go | 12 +- pkg/chains/formats/slsa/v2/slsav2.go | 3 +- pkg/chains/formats/slsa/v2/slsav2_test.go | 8 +- pkg/chains/objects/objects.go | 50 ++++++ pkg/chains/signing.go | 6 +- pkg/config/config.go | 15 ++ pkg/config/store_test.go | 147 +++++++++++++++++ pkg/reconciler/pipelinerun/controller.go | 1 + pkg/reconciler/taskrun/controller.go | 1 + 15 files changed, 499 insertions(+), 23 deletions(-) create mode 100644 pkg/chains/formats/sbom/sbom.go diff --git a/pkg/artifacts/signable.go b/pkg/artifacts/signable.go index 3beccb123b..a88c932340 100644 --- a/pkg/artifacts/signable.go +++ b/pkg/artifacts/signable.go @@ -154,8 +154,10 @@ type image struct { // URI is the resource uri for the target needed iff the target is a material. // Digest is the target's SHA digest. type StructuredSignable struct { - URI string - Digest string + URI string + Digest string + SBOMURI string + SBOMFormat string } func (oa *OCIArtifact) ExtractObjects(obj objects.TektonObject) []interface{} { @@ -204,7 +206,7 @@ func (oa *OCIArtifact) ExtractObjects(obj objects.TektonObject) []interface{} { func ExtractOCIImagesFromResults(obj objects.TektonObject, logger *zap.SugaredLogger) []interface{} { objs := []interface{}{} - ss := extractTargetFromResults(obj, "IMAGE_URL", "IMAGE_DIGEST", logger) + ss := extractTargetFromResults(obj, "IMAGE_URL", "IMAGE_DIGEST", "", "", logger) for _, s := range ss { if s == nil || s.Digest == "" || s.URI == "" { continue @@ -244,7 +246,7 @@ func ExtractOCIImagesFromResults(obj objects.TektonObject, logger *zap.SugaredLo // ExtractSignableTargetFromResults extracts signable targets that aim to generate intoto provenance as materials within TaskRun results and store them as StructuredSignable. func ExtractSignableTargetFromResults(obj objects.TektonObject, logger *zap.SugaredLogger) []*StructuredSignable { objs := []*StructuredSignable{} - ss := extractTargetFromResults(obj, "ARTIFACT_URI", "ARTIFACT_DIGEST", logger) + ss := extractTargetFromResults(obj, "ARTIFACT_URI", "ARTIFACT_DIGEST", "", "", logger) // Only add it if we got both the signable URI and digest. for _, s := range ss { if s == nil || s.Digest == "" || s.URI == "" { @@ -266,7 +268,7 @@ func (s *StructuredSignable) FullRef() string { return fmt.Sprintf("%s@%s", s.URI, s.Digest) } -func extractTargetFromResults(obj objects.TektonObject, identifierSuffix string, digestSuffix string, logger *zap.SugaredLogger) map[string]*StructuredSignable { +func extractTargetFromResults(obj objects.TektonObject, identifierSuffix, digestSuffix, sbomSuffix, sbomFormatSuffix string, logger *zap.SugaredLogger) map[string]*StructuredSignable { ss := map[string]*StructuredSignable{} for _, res := range obj.GetResults() { @@ -296,6 +298,31 @@ func extractTargetFromResults(obj objects.TektonObject, identifierSuffix string, ss[marker] = &StructuredSignable{Digest: strings.TrimSpace(res.Value.StringVal)} } } + if sbomSuffix != "" && strings.HasSuffix(res.Name, sbomSuffix) { + if res.Value.StringVal == "" { + logger.Debugf("error getting string value for %s", res.Name) + continue + } + marker := strings.TrimSuffix(res.Name, sbomSuffix) + sbomURI := strings.TrimSpace(res.Value.StringVal) + if v, ok := ss[marker]; ok { + v.SBOMURI = sbomURI + } else { + ss[marker] = &StructuredSignable{SBOMURI: sbomURI} + } + } + if sbomFormatSuffix != "" && strings.HasSuffix(res.Name, sbomFormatSuffix) { + if res.Value.StringVal == "" { + logger.Debugf("error getting string value for %s", res.Name) + } + marker := strings.TrimSuffix(res.Name, sbomFormatSuffix) + sbomFormat := strings.TrimSpace(res.Value.StringVal) + if v, ok := ss[marker]; ok { + v.SBOMFormat = sbomFormat + } else { + ss[marker] = &StructuredSignable{SBOMFormat: sbomFormat} + } + } } return ss @@ -434,3 +461,120 @@ func (oa *OCIArtifact) FullKey(obj interface{}) string { func (oa *OCIArtifact) Enabled(cfg config.Config) bool { return cfg.Artifacts.OCI.Enabled() } + +type SBOMArtifact struct { + Logger *zap.SugaredLogger +} + +var _ Signable = &SBOMArtifact{} + +func (sa *SBOMArtifact) ExtractObjects(tektonObject objects.TektonObject) []interface{} { + var objs []interface{} + for _, obj := range extractSBOMFromResults(tektonObject, sa.Logger) { + objs = append(objs, objects.NewSBOMObject(obj.SBOMURI, obj.SBOMFormat, obj.URI, obj.Digest, tektonObject)) + } + return objs +} + +func (sa *SBOMArtifact) StorageBackend(cfg config.Config) sets.String { + return cfg.Artifacts.SBOM.StorageBackend +} +func (sa *SBOMArtifact) Signer(cfg config.Config) string { + return cfg.Artifacts.SBOM.Signer +} +func (sa *SBOMArtifact) PayloadFormat(cfg config.Config) config.PayloadType { + return config.PayloadType(cfg.Artifacts.SBOM.Format) +} + +func (sa *SBOMArtifact) FullKey(obj interface{}) string { + v := obj.(*objects.SBOMObject) + return v.GetSBOMURL() +} + +func (sa *SBOMArtifact) ShortKey(obj interface{}) string { + v := obj.(*objects.SBOMObject) + return v.GetSBOMURL() +} +func (sa *SBOMArtifact) Type() string { + return "sbom" +} +func (sa *SBOMArtifact) Enabled(cfg config.Config) bool { + return cfg.Artifacts.SBOM.Enabled() +} + +func extractSBOMFromResults(tektonObject objects.TektonObject, logger *zap.SugaredLogger) []*StructuredSignable { + var objs []*StructuredSignable + + ss := extractTargetFromResults(tektonObject, "IMAGE_URL", "IMAGE_DIGEST", "IMAGE_SBOM_URL", "IMAGE_SBOM_FORMAT", logger) + for _, s := range ss { + if s == nil || s.Digest == "" || s.URI == "" || s.SBOMURI == "" || s.SBOMFormat == "" { + continue + } + if _, err := name.NewDigest(s.SBOMURI); err != nil { + logger.Errorf("error getting digest for SBOM image %s: %v", s.SBOMURI, err) + continue + } + objs = append(objs, s) + } + + var images []name.Digest + var sboms []string + var sbomsFormat string + // look for a comma separated list of images and their SBOMs + for _, key := range tektonObject.GetResults() { + switch key.Name { + case "IMAGES": + for _, img := range strings.FieldsFunc(key.Value.StringVal, split) { + trimmed := strings.TrimSpace(img) + if trimmed == "" { + continue + } + + dgst, err := name.NewDigest(trimmed) + if err != nil { + logger.Errorf("error getting digest for img %s: %v", trimmed, err) + continue + } + images = append(images, dgst) + } + case "SBOMS": + for _, img := range strings.FieldsFunc(key.Value.StringVal, split) { + trimmed := strings.TrimSpace(img) + if trimmed == "" { + continue + } + if _, err := name.NewDigest(trimmed); err != nil { + logger.Errorf("error getting digest for SBOM image %s: %v", trimmed, err) + continue + } + sboms = append(sboms, trimmed) + } + case "SBOMS_FORMAT": + f := strings.TrimSpace(key.Value.StringVal) + if f != "" { + sbomsFormat = f + } + } + } + + if len(images) != len(sboms) { + logger.Warnf("IMAGES and SBOMS do not contain the same amount of entries") + return objs + } + + if len(sboms) > 0 && sbomsFormat == "" { + logger.Warnf("SBOMS_FORMAT not specified") + return objs + } + + for i, sbom := range sboms { + img := images[i] + objs = append(objs, &StructuredSignable{ + URI: img.Name(), + Digest: img.Identifier(), + SBOMURI: sbom, + SBOMFormat: sbomsFormat, + }) + } + return objs +} diff --git a/pkg/chains/formats/format.go b/pkg/chains/formats/format.go index 690baa7b2e..2585b1d869 100644 --- a/pkg/chains/formats/format.go +++ b/pkg/chains/formats/format.go @@ -18,11 +18,12 @@ import ( "fmt" "github.com/tektoncd/chains/pkg/config" + "k8s.io/client-go/kubernetes" ) // Payloader is an interface to generate a chains Payload from a TaskRun type Payloader interface { - CreatePayload(ctx context.Context, obj interface{}) (interface{}, error) + CreatePayload(ctx context.Context, kc kubernetes.Interface, obj interface{}) (interface{}, error) Type() config.PayloadType Wrap() bool } diff --git a/pkg/chains/formats/sbom/sbom.go b/pkg/chains/formats/sbom/sbom.go new file mode 100644 index 0000000000..5fca1e8f3f --- /dev/null +++ b/pkg/chains/formats/sbom/sbom.go @@ -0,0 +1,107 @@ +/* +Copyright 2023 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 sbom + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + intoto "github.com/in-toto/in-toto-golang/in_toto" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" + "github.com/tektoncd/chains/pkg/chains/objects" + "go.uber.org/zap" + "k8s.io/client-go/kubernetes" +) + +const maxSBOMBytes = 5 * 1024 * 1024 // 5 Megabytes + +func GenerateAttestation(ctx context.Context, kc kubernetes.Interface, builderID string, sbom *objects.SBOMObject, logger *zap.SugaredLogger) (interface{}, error) { + subject := []intoto.Subject{ + {Name: sbom.GetImageURL(), Digest: toDigestSet(sbom.GetImageDigest())}, + } + + data, err := getData(ctx, kc, sbom) + if err != nil { + return nil, err + } + + att := intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + Type: intoto.StatementInTotoV01, + PredicateType: sbom.GetSBOMFormat(), + Subject: subject, + }, + Predicate: data, + } + return att, nil +} + +func getData(ctx context.Context, kc kubernetes.Interface, sbom *objects.SBOMObject) (json.RawMessage, error) { + opt, err := sbom.OCIRemoteOption(ctx, kc) + if err != nil { + return nil, err + } + + uri := sbom.GetSBOMURL() + ref, err := name.ParseReference(uri) + if err != nil { + return nil, err + } + + image, err := remote.Image(ref, opt) + if err != nil { + return nil, err + } + + layers, err := image.Layers() + if err != nil { + return nil, err + } + + if len(layers) != 1 { + return nil, fmt.Errorf("expected exactly 1 layer in sbom image %s, found %d", uri, len(layers)) + } + + layer, err := layers[0].Uncompressed() + if err != nil { + return nil, err + } + defer layer.Close() + + var blob bytes.Buffer + if _, err := io.Copy(&blob, io.LimitReader(layer, maxSBOMBytes)); err != nil { + return nil, err + } + + var data json.RawMessage + if err := json.Unmarshal(blob.Bytes(), &data); err != nil { + return nil, err + } + return data, nil +} + +func toDigestSet(digest string) slsa.DigestSet { + algo, value, found := strings.Cut(digest, ":") + if !found { + value = algo + algo = "sha256" + } + return slsa.DigestSet{algo: value} +} diff --git a/pkg/chains/formats/simple/simple.go b/pkg/chains/formats/simple/simple.go index 10c464f96a..43f46f53fd 100644 --- a/pkg/chains/formats/simple/simple.go +++ b/pkg/chains/formats/simple/simple.go @@ -20,6 +20,7 @@ import ( "github.com/sigstore/sigstore/pkg/signature/payload" "github.com/tektoncd/chains/pkg/chains/formats" "github.com/tektoncd/chains/pkg/config" + "k8s.io/client-go/kubernetes" "github.com/google/go-containerregistry/pkg/name" ) @@ -39,7 +40,7 @@ type SimpleSigning struct{} type SimpleContainerImage payload.SimpleContainerImage // CreatePayload implements the Payloader interface. -func (i *SimpleSigning) CreatePayload(ctx context.Context, obj interface{}) (interface{}, error) { +func (i *SimpleSigning) CreatePayload(ctx context.Context, _ kubernetes.Interface, obj interface{}) (interface{}, error) { switch v := obj.(type) { case name.Digest: format := NewSimpleStruct(v) diff --git a/pkg/chains/formats/simple/simple_test.go b/pkg/chains/formats/simple/simple_test.go index a3be8461a0..fc091210f7 100644 --- a/pkg/chains/formats/simple/simple_test.go +++ b/pkg/chains/formats/simple/simple_test.go @@ -62,7 +62,7 @@ func TestSimpleSigning_CreatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { i := &SimpleSigning{} - got, err := i.CreatePayload(context.Background(), tt.obj) + got, err := i.CreatePayload(context.Background(), nil, tt.obj) if (err != nil) != tt.wantErr { t.Errorf("SimpleSigning.CreatePayload() error = %v, wantErr %v", err, tt.wantErr) return @@ -82,7 +82,7 @@ func TestImageName(t *testing.T) { obj := makeDigest(t, img) i := &SimpleSigning{} - format, err := i.CreatePayload(context.Background(), obj) + format, err := i.CreatePayload(context.Background(), nil, obj) if err != nil { t.Fatal(err) } diff --git a/pkg/chains/formats/slsa/v1/intotoite6.go b/pkg/chains/formats/slsa/v1/intotoite6.go index 1e10de7316..60d86b1b8d 100644 --- a/pkg/chains/formats/slsa/v1/intotoite6.go +++ b/pkg/chains/formats/slsa/v1/intotoite6.go @@ -21,10 +21,12 @@ import ( "fmt" "github.com/tektoncd/chains/pkg/chains/formats" + "github.com/tektoncd/chains/pkg/chains/formats/sbom" "github.com/tektoncd/chains/pkg/chains/formats/slsa/v1/pipelinerun" "github.com/tektoncd/chains/pkg/chains/formats/slsa/v1/taskrun" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/config" + "k8s.io/client-go/kubernetes" "knative.dev/pkg/logging" ) @@ -52,13 +54,17 @@ func (i *InTotoIte6) Wrap() bool { return true } -func (i *InTotoIte6) CreatePayload(ctx context.Context, obj interface{}) (interface{}, error) { +func (i *InTotoIte6) CreatePayload(ctx context.Context, kc kubernetes.Interface, obj interface{}) (interface{}, error) { logger := logging.FromContext(ctx) switch v := obj.(type) { case *objects.TaskRunObject: return taskrun.GenerateAttestation(i.builderID, v, logger) case *objects.PipelineRunObject: return pipelinerun.GenerateAttestation(i.builderID, v, logger) + case *objects.SBOMObject: + // TODO: It is odd that the slsa package has a dependency on the sbom package. But, + // this is required for now since intotoite6 is currently a part of the slsa package. + return sbom.GenerateAttestation(ctx, kc, i.builderID, v, logger) default: return nil, fmt.Errorf("intoto does not support type: %s", v) } diff --git a/pkg/chains/formats/slsa/v1/intotoite6_test.go b/pkg/chains/formats/slsa/v1/intotoite6_test.go index db6504ce6b..b036923ee7 100644 --- a/pkg/chains/formats/slsa/v1/intotoite6_test.go +++ b/pkg/chains/formats/slsa/v1/intotoite6_test.go @@ -132,7 +132,7 @@ func TestTaskRunCreatePayload1(t *testing.T) { } i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, objects.NewTaskRunObject(tr)) + got, err := i.CreatePayload(ctx, nil, objects.NewTaskRunObject(tr)) if err != nil { t.Errorf("unexpected error: %s", err.Error()) @@ -358,7 +358,7 @@ func TestPipelineRunCreatePayload(t *testing.T) { i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, pro) + got, err := i.CreatePayload(ctx, nil, pro) if err != nil { t.Errorf("unexpected error: %s", err.Error()) } @@ -575,7 +575,7 @@ func TestPipelineRunCreatePayloadChildRefs(t *testing.T) { pro.AppendTaskRun(tr2) i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, pro) + got, err := i.CreatePayload(ctx, nil, pro) if err != nil { t.Errorf("unexpected error: %s", err.Error()) } @@ -650,7 +650,7 @@ func TestTaskRunCreatePayload2(t *testing.T) { }, } i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, objects.NewTaskRunObject(tr)) + got, err := i.CreatePayload(ctx, nil, objects.NewTaskRunObject(tr)) if err != nil { t.Errorf("unexpected error: %s", err.Error()) @@ -721,7 +721,7 @@ func TestMultipleSubjects(t *testing.T) { } i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, objects.NewTaskRunObject(tr)) + got, err := i.CreatePayload(ctx, nil, objects.NewTaskRunObject(tr)) if err != nil { t.Errorf("unexpected error: %s", err.Error()) } @@ -758,7 +758,7 @@ func TestCreatePayloadError(t *testing.T) { f, _ := NewFormatter(cfg) t.Run("Invalid type", func(t *testing.T) { - p, err := f.CreatePayload(ctx, "not a task ref") + p, err := f.CreatePayload(ctx, nil, "not a task ref") if p != nil { t.Errorf("Unexpected payload") diff --git a/pkg/chains/formats/slsa/v2/slsav2.go b/pkg/chains/formats/slsa/v2/slsav2.go index 0006cee58e..9d486ab85f 100644 --- a/pkg/chains/formats/slsa/v2/slsav2.go +++ b/pkg/chains/formats/slsa/v2/slsav2.go @@ -24,6 +24,7 @@ import ( "github.com/tektoncd/chains/pkg/chains/formats/slsa/v2/taskrun" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/config" + "k8s.io/client-go/kubernetes" ) const ( @@ -48,7 +49,7 @@ func (s *Slsa) Wrap() bool { return true } -func (s *Slsa) CreatePayload(ctx context.Context, obj interface{}) (interface{}, error) { +func (s *Slsa) CreatePayload(ctx context.Context, kc kubernetes.Interface, obj interface{}) (interface{}, error) { switch v := obj.(type) { case *objects.TaskRunObject: return taskrun.GenerateAttestation(s.builderID, s.Type(), v, ctx) diff --git a/pkg/chains/formats/slsa/v2/slsav2_test.go b/pkg/chains/formats/slsa/v2/slsav2_test.go index 9d42d20111..6651cb337b 100644 --- a/pkg/chains/formats/slsa/v2/slsav2_test.go +++ b/pkg/chains/formats/slsa/v2/slsav2_test.go @@ -157,7 +157,7 @@ func TestTaskRunCreatePayload1(t *testing.T) { } i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, objects.NewTaskRunObject(tr)) + got, err := i.CreatePayload(ctx, nil, objects.NewTaskRunObject(tr)) if err != nil { t.Errorf("unexpected error: %s", err.Error()) @@ -255,7 +255,7 @@ func TestTaskRunCreatePayload2(t *testing.T) { }, } i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, objects.NewTaskRunObject(tr)) + got, err := i.CreatePayload(ctx, nil, objects.NewTaskRunObject(tr)) if err != nil { t.Errorf("unexpected error: %s", err.Error()) @@ -349,7 +349,7 @@ func TestMultipleSubjects(t *testing.T) { } i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, objects.NewTaskRunObject(tr)) + got, err := i.CreatePayload(ctx, nil, objects.NewTaskRunObject(tr)) if err != nil { t.Errorf("unexpected error: %s", err.Error()) } @@ -386,7 +386,7 @@ func TestCreatePayloadError(t *testing.T) { f, _ := NewFormatter(cfg) t.Run("Invalid type", func(t *testing.T) { - p, err := f.CreatePayload(ctx, "not a task ref") + p, err := f.CreatePayload(ctx, nil, "not a task ref") if p != nil { t.Errorf("Unexpected payload") diff --git a/pkg/chains/objects/objects.go b/pkg/chains/objects/objects.go index dc96b620d4..f2d702ecbd 100644 --- a/pkg/chains/objects/objects.go +++ b/pkg/chains/objects/objects.go @@ -18,6 +18,8 @@ import ( "errors" "fmt" + "github.com/google/go-containerregistry/pkg/authn/k8schain" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" @@ -25,6 +27,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" "knative.dev/pkg/apis" ) @@ -224,3 +227,50 @@ func getPodPullSecrets(podTemplate *pod.Template) []string { } return imgPullSecrets } + +type SBOMObject struct { + sbomURL string + sbomFormat string + imageURL string + imageDigest string + tektonObject TektonObject +} + +func NewSBOMObject(sbomURL, sbomFormat, imageURL, imageDigest string, tektonObject TektonObject) *SBOMObject { + return &SBOMObject{ + sbomURL: sbomURL, + sbomFormat: sbomFormat, + imageURL: imageURL, + imageDigest: imageDigest, + tektonObject: tektonObject, + } +} + +func (so *SBOMObject) GetSBOMURL() string { + return so.sbomURL +} + +func (so *SBOMObject) GetSBOMFormat() string { + return so.sbomFormat +} + +func (so *SBOMObject) GetImageURL() string { + return so.imageURL +} + +func (so *SBOMObject) GetImageDigest() string { + return so.imageDigest +} + +func (so *SBOMObject) OCIRemoteOption(ctx context.Context, client kubernetes.Interface) (remote.Option, error) { + keychain, err := k8schain.New(ctx, client, + k8schain.Options{ + Namespace: so.tektonObject.GetNamespace(), + ServiceAccountName: so.tektonObject.GetServiceAccountName(), + ImagePullSecrets: so.tektonObject.GetPullSecrets(), + }) + if err != nil { + return nil, err + } + return remote.WithAuthFromKeychain(keychain), nil +} diff --git a/pkg/chains/signing.go b/pkg/chains/signing.go index d369422e55..f3ebe14ebe 100644 --- a/pkg/chains/signing.go +++ b/pkg/chains/signing.go @@ -31,6 +31,7 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" versioned "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" "go.uber.org/zap" + "k8s.io/client-go/kubernetes" "knative.dev/pkg/logging" ) @@ -45,6 +46,7 @@ type ObjectSigner struct { Backends map[string]storage.Backend SecretPath string Pipelineclientset versioned.Interface + KubeClient kubernetes.Interface } func allSigners(ctx context.Context, sp string, cfg config.Config, l *zap.SugaredLogger) map[string]signing.Signer { @@ -89,6 +91,7 @@ func getSignableTypes(obj objects.TektonObject, logger *zap.SugaredLogger) ([]ar return []artifacts.Signable{ &artifacts.TaskRunArtifact{Logger: logger}, &artifacts.OCIArtifact{Logger: logger}, + &artifacts.SBOMArtifact{Logger: logger}, }, nil case *v1beta1.PipelineRun: return []artifacts.Signable{ @@ -132,8 +135,7 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject) // Go through each object one at a time. for _, obj := range objects { - - payload, err := payloader.CreatePayload(ctx, obj) + payload, err := payloader.CreatePayload(ctx, o.KubeClient, obj) if err != nil { logger.Error(err) continue diff --git a/pkg/config/config.go b/pkg/config/config.go index a5e85c724f..9452c5cbe7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -40,6 +40,7 @@ type ArtifactConfigs struct { OCI Artifact PipelineRuns Artifact TaskRuns Artifact + SBOM Artifact } // Artifact contains the configuration for how to sign/store/format the signatures for a single artifact @@ -158,6 +159,10 @@ const ( ociStorageKey = "artifacts.oci.storage" ociSignerKey = "artifacts.oci.signer" + sbomFormatKey = "artifacts.sbom.format" + sbomStorageKey = "artifacts.sbom.storage" + sbomSignerKey = "artifacts.sbom.signer" + gcsBucketKey = "storage.gcs.bucket" ociRepositoryKey = "storage.oci.repository" ociRepositoryInsecureKey = "storage.oci.repository.insecure" @@ -223,6 +228,11 @@ func defaultConfig() *Config { StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Transparency: TransparencyConfig{ URL: "https://rekor.sigstore.dev", @@ -266,6 +276,11 @@ func NewConfigFromMap(data map[string]string) (*Config, error) { asStringSet(ociStorageKey, &cfg.Artifacts.OCI.StorageBackend, sets.NewString("tekton", "oci", "gcs", "docdb", "grafeas", "kafka")), asString(ociSignerKey, &cfg.Artifacts.OCI.Signer, "x509", "kms"), + // SBOM + asString(sbomFormatKey, &cfg.Artifacts.SBOM.Format, "in-toto"), + asStringSet(sbomStorageKey, &cfg.Artifacts.SBOM.StorageBackend, sets.NewString("tekton", "oci", "gcs", "docdb", "grafeas", "kafka")), + asString(sbomSignerKey, &cfg.Artifacts.SBOM.Signer, "x509", "kms"), + // PubSub - General asString(pubsubProvider, &cfg.Storage.PubSub.Provider, "inmemory", "kafka"), asString(pubsubTopic, &cfg.Storage.PubSub.Topic), diff --git a/pkg/config/store_test.go b/pkg/config/store_test.go index 5e1f70be57..0b0bfdf1b5 100644 --- a/pkg/config/store_test.go +++ b/pkg/config/store_test.go @@ -112,6 +112,11 @@ var defaultArtifacts = ArtifactConfigs{ StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, } var defaultStorage = StorageConfigs{ @@ -129,6 +134,7 @@ func TestParse(t *testing.T) { name string data map[string]string taskrunEnabled bool + sbomEnabled bool ociEnbaled bool want Config }{ @@ -137,6 +143,7 @@ func TestParse(t *testing.T) { data: map[string]string{}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -151,6 +158,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: BuilderConfig{ "builder-id-test", @@ -167,6 +175,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -184,6 +193,7 @@ func TestParse(t *testing.T) { data: map[string]string{taskrunStorageKey: "tekton,oci"}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -202,6 +212,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -213,6 +228,7 @@ func TestParse(t *testing.T) { data: map[string]string{taskrunStorageKey: ""}, taskrunEnabled: false, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -231,6 +247,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -242,6 +263,7 @@ func TestParse(t *testing.T) { data: map[string]string{ociStorageKey: "oci,tekton"}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -260,6 +282,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci", "tekton"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -271,6 +298,7 @@ func TestParse(t *testing.T) { data: map[string]string{ociStorageKey: ""}, taskrunEnabled: true, ociEnbaled: false, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -289,6 +317,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString(""), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -303,6 +336,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: false, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -321,6 +355,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString(""), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -335,6 +374,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: false, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -353,6 +393,89 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci", "tekton"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, + }, + Signers: defaultSigners, + Storage: defaultStorage, + Transparency: defaultTransparency, + }, + }, + { + name: "sbom", + data: map[string]string{ + sbomStorageKey: "oci,tekton", + sbomFormatKey: "in-toto", + sbomSignerKey: "kms", + }, + taskrunEnabled: true, + ociEnbaled: true, + sbomEnabled: true, + want: Config{ + Builder: defaultBuilder, + Artifacts: ArtifactConfigs{ + TaskRuns: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("tekton"), + Signer: "x509", + }, + PipelineRuns: Artifact{ + Format: "in-toto", + Signer: "x509", + StorageBackend: sets.NewString("tekton"), + }, + OCI: Artifact{ + Format: "simplesigning", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci", "tekton"), + Signer: "kms", + }, + }, + Signers: defaultSigners, + Storage: defaultStorage, + Transparency: defaultTransparency, + }, + }, + { + name: "sbom disabled", + data: map[string]string{ + sbomStorageKey: "", + sbomFormatKey: "in-toto", + sbomSignerKey: "kms", + }, + taskrunEnabled: true, + ociEnbaled: true, + sbomEnabled: false, + want: Config{ + Builder: defaultBuilder, + Artifacts: ArtifactConfigs{ + TaskRuns: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("tekton"), + Signer: "x509", + }, + PipelineRuns: Artifact{ + Format: "in-toto", + Signer: "x509", + StorageBackend: sets.NewString("tekton"), + }, + OCI: Artifact{ + Format: "simplesigning", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString(""), + Signer: "kms", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -364,6 +487,7 @@ func TestParse(t *testing.T) { data: map[string]string{taskrunSignerKey: "x509"}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -382,6 +506,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -393,6 +522,7 @@ func TestParse(t *testing.T) { data: map[string]string{transparencyEnabledKey: "manual"}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -413,6 +543,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -431,6 +562,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -445,6 +581,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -463,6 +600,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: SignerConfigs{ X509: X509Signer{ @@ -482,6 +624,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -505,6 +648,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -536,6 +680,9 @@ func TestParse(t *testing.T) { if got.Artifacts.TaskRuns.Enabled() != tt.taskrunEnabled { t.Errorf("Taskrun artifact enable mismatch") } + if got.Artifacts.SBOM.Enabled() != tt.sbomEnabled { + t.Errorf("SBOM artifact enable mismatch") + } if diff := cmp.Diff(*got, tt.want); diff != "" { t.Errorf("parse() = %v", diff) } diff --git a/pkg/reconciler/pipelinerun/controller.go b/pkg/reconciler/pipelinerun/controller.go index 8813314770..40a193f7e0 100644 --- a/pkg/reconciler/pipelinerun/controller.go +++ b/pkg/reconciler/pipelinerun/controller.go @@ -44,6 +44,7 @@ func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl psSigner := &chains.ObjectSigner{ SecretPath: SecretPath, Pipelineclientset: pipelineClient, + KubeClient: kubeClient, } c := &Reconciler{ diff --git a/pkg/reconciler/taskrun/controller.go b/pkg/reconciler/taskrun/controller.go index d491529e45..759dc3c7a5 100644 --- a/pkg/reconciler/taskrun/controller.go +++ b/pkg/reconciler/taskrun/controller.go @@ -40,6 +40,7 @@ func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl tsSigner := &chains.ObjectSigner{ SecretPath: SecretPath, Pipelineclientset: pipelineClient, + KubeClient: kubeClient, } c := &Reconciler{