Skip to content

Commit

Permalink
Adds initial Attestor implementation.
Browse files Browse the repository at this point in the history
This is the initial implementation of Attestors, which uses generics to
link chains components together with strict typing.

To start, this adds Attestor implementations of OCI signing and v1 SLSA
attestations. These Attestors are NOT wired up to the controller yet,
since they don't yet support the full range of config options (and
there's likely a few tweaks we need to make in order to help reuse
components like signers between Attestors).
  • Loading branch information
wlynch committed Nov 23, 2023
1 parent dd3620e commit bac7923
Show file tree
Hide file tree
Showing 18 changed files with 579 additions and 158 deletions.
55 changes: 44 additions & 11 deletions pkg/artifacts/signable.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ type Signable interface {
Enabled(cfg config.Config) bool
}

// Extractor extracts a given type T from a Tekton object.
type Extractor[T any] interface {
Extract(ctx context.Context, obj objects.TektonObject) ([]T, error)
}

type TaskRunArtifact struct{}

var _ Signable = &TaskRunArtifact{}
Expand Down Expand Up @@ -150,7 +155,32 @@ type image struct {

func (oa *OCIArtifact) ExtractObjects(ctx context.Context, obj objects.TektonObject) []interface{} {
log := logging.FromContext(ctx)
objs := []interface{}{}
digests, err := oa.Extract(ctx, obj)
if err != nil {
log.Error(err)
return nil
}

// Convert to interface
objs := []any{}
for _, d := range digests {
objs = append(objs, d)
}
return objs
}

var (
defaultOCI = OCIArtifact{}
)

func ExtractOCI(ctx context.Context, obj objects.TektonObject) ([]name.Digest, error) {
return defaultOCI.Extract(ctx, obj)
}

func (OCIArtifact) Extract(ctx context.Context, obj objects.TektonObject) ([]name.Digest, error) {
log := logging.FromContext(ctx)

var out []name.Digest

// TODO: Not applicable to PipelineRuns, should look into a better way to separate this out
if tr, ok := obj.GetObject().(*v1beta1.TaskRun); ok {
Expand Down Expand Up @@ -182,21 +212,25 @@ func (oa *OCIArtifact) ExtractObjects(ctx context.Context, obj objects.TektonObj
log.Error(err)
continue
}
objs = append(objs, dgst)
out = append(out, dgst)
}
}

// Now check TaskResults
resultImages := ExtractOCIImagesFromResults(ctx, obj)
objs = append(objs, resultImages...)
digests, err := extractOCIImagesFromResults(ctx, obj)
if err != nil {
log.Warnf("error extracting digests from results: %v", err)
return nil, err
}
out = append(out, digests...)

return objs
return out, nil
}

func ExtractOCIImagesFromResults(ctx context.Context, obj objects.TektonObject) []interface{} {
func extractOCIImagesFromResults(ctx context.Context, obj objects.TektonObject) ([]name.Digest, error) {
logger := logging.FromContext(ctx)
objs := []interface{}{}

out := []name.Digest{}
extractor := structuredSignableExtractor{
uriSuffix: "IMAGE_URL",
digestSuffix: "IMAGE_DIGEST",
Expand All @@ -209,7 +243,7 @@ func ExtractOCIImagesFromResults(ctx context.Context, obj objects.TektonObject)
continue
}

objs = append(objs, dgst)
out = append(out, dgst)
}

// look for a comma separated list of images
Expand All @@ -229,11 +263,10 @@ func ExtractOCIImagesFromResults(ctx context.Context, obj objects.TektonObject)
logger.Errorf("error getting digest for img %s: %v", trimmed, err)
continue
}
objs = append(objs, dgst)
out = append(out, dgst)
}
}

return objs
return out, nil
}

// ExtractSignableTargetFromResults extracts signable targets that aim to generate intoto provenance as materials within TaskRun results and store them as StructuredSignable.
Expand Down
11 changes: 7 additions & 4 deletions pkg/artifacts/signable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,16 +331,19 @@ func TestExtractOCIImagesFromResults(t *testing.T) {
},
}
obj := objects.NewTaskRunObject(tr)
want := []interface{}{
want := []name.Digest{
createDigest(t, fmt.Sprintf("img1@%s", digest1)),
createDigest(t, fmt.Sprintf("img2@%s", digest2)),
createDigest(t, fmt.Sprintf("img3@%s", digest1)),
}
ctx := logtesting.TestContextWithLogger(t)
got := ExtractOCIImagesFromResults(ctx, obj)
got, err := extractOCIImagesFromResults(ctx, obj)
if err != nil {
t.Fatal(err)
}
sort.Slice(got, func(i, j int) bool {
a := got[i].(name.Digest)
b := got[j].(name.Digest)
a := got[i]
b := got[j]
return a.String() < b.String()
})
if !cmp.Equal(got, want, ignore...) {
Expand Down
9 changes: 9 additions & 0 deletions pkg/chains/formats/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,21 @@ import (
)

// Payloader is an interface to generate a chains Payload from a TaskRun
// Deprecated: Use Formatter instead.
type Payloader interface {
CreatePayload(ctx context.Context, obj interface{}) (interface{}, error)
Type() config.PayloadType
Wrap() bool
}

// Formatter transforms an extracted Input artifact into an Output
// artifact suitable for signing + storage.
type Formatter[Input any, Output any] interface {
// Effectively the same as CreatePayload, but using a different name so that
// this interface can coexist with Payloader.
FormatPayload(ctx context.Context, in Input) (Output, error)
}

const (
PayloadTypeTekton config.PayloadType = "tekton"
PayloadTypeSimpleSigning config.PayloadType = "simplesigning"
Expand Down
15 changes: 15 additions & 0 deletions pkg/chains/formats/simple/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package simple

import (
"context"
"encoding/json"
"fmt"

"github.com/sigstore/sigstore/pkg/signature/payload"
Expand Down Expand Up @@ -66,6 +67,20 @@ func (i SimpleContainerImage) ImageName() string {
return fmt.Sprintf("%s@%s", i.Critical.Identity.DockerReference, i.Critical.Image.DockerManifestDigest)
}

func (i SimpleContainerImage) MarshalBinary() ([]byte, error) {
return json.Marshal(i)
}

func (i *SimpleSigning) Type() config.PayloadType {
return formats.PayloadTypeSimpleSigning
}

var (
_ formats.Formatter[name.Digest, SimpleContainerImage] = &SimpleSigningPayloader{}
)

type SimpleSigningPayloader SimpleSigning

func (SimpleSigningPayloader) FormatPayload(_ context.Context, v name.Digest) (SimpleContainerImage, error) {
return NewSimpleStruct(v), nil
}
22 changes: 11 additions & 11 deletions pkg/chains/formats/slsa/extract/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"fmt"
"strings"

"github.com/google/go-containerregistry/pkg/name"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
"github.com/tektoncd/chains/internal/backport"
Expand Down Expand Up @@ -95,16 +94,17 @@ func subjectsFromTektonObject(ctx context.Context, obj objects.TektonObject) []i
logger := logging.FromContext(ctx)
var subjects []intoto.Subject

imgs := artifacts.ExtractOCIImagesFromResults(ctx, obj)
for _, i := range imgs {
if d, ok := i.(name.Digest); ok {
subjects = artifact.AppendSubjects(subjects, intoto.Subject{
Name: d.Repository.Name(),
Digest: common.DigestSet{
"sha256": strings.TrimPrefix(d.DigestStr(), "sha256:"),
},
})
}
imgs, err := artifacts.ExtractOCI(ctx, obj)
if err != nil {
logger.Warnf("error extracting OCI artifacts: %v", err)
}
for _, d := range imgs {
subjects = artifact.AppendSubjects(subjects, intoto.Subject{
Name: d.Repository.Name(),
Digest: common.DigestSet{
"sha256": strings.TrimPrefix(d.DigestStr(), "sha256:"),
},
})
}

sts := artifacts.ExtractSignableTargetFromResults(ctx, obj)
Expand Down
80 changes: 74 additions & 6 deletions pkg/chains/formats/slsa/v1/intotoite6.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ package v1

import (
"context"
"encoding/json"
"fmt"

"github.com/in-toto/in-toto-golang/in_toto"
"github.com/tektoncd/chains/pkg/chains/formats"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/v1/pipelinerun"
Expand All @@ -34,21 +36,57 @@ const (
)

func init() {
formats.RegisterPayloader(PayloadTypeInTotoIte6, NewFormatter)
formats.RegisterPayloader(PayloadTypeSlsav1, NewFormatter)
formats.RegisterPayloader(PayloadTypeInTotoIte6, NewPayloader)
formats.RegisterPayloader(PayloadTypeSlsav1, NewPayloader)
}

type InTotoIte6 struct {
slsaConfig *slsaconfig.SlsaConfig
}

func NewFormatter(cfg config.Config) (formats.Payloader, error) {
func NewPayloader(cfg config.Config) (formats.Payloader, error) {
return NewPayloaderFromConfig(cfg), nil
}

func NewPayloaderFromConfig(cfg config.Config) *InTotoIte6 {
opts := []Option{
WithBuilderID(cfg.Builder.ID),
WithDeepInspection(cfg.Artifacts.PipelineRuns.DeepInspectionEnabled),
}
return NewFormatter(opts...)
}

type options struct {
builderID string
deepInspection bool
}

type Option func(*options)

func WithDeepInspection(enabled bool) Option {
return func(o *options) {
o.deepInspection = enabled
}
}

func WithBuilderID(id string) Option {
return func(o *options) {
o.builderID = id
}
}

func NewFormatter(opts ...Option) *InTotoIte6 {
o := &options{}
for _, f := range opts {
f(o)
}

return &InTotoIte6{
slsaConfig: &slsaconfig.SlsaConfig{
BuilderID: cfg.Builder.ID,
DeepInspectionEnabled: cfg.Artifacts.PipelineRuns.DeepInspectionEnabled,
BuilderID: o.builderID,
DeepInspectionEnabled: o.deepInspection,
},
}, nil
}
}

func (i *InTotoIte6) Wrap() bool {
Expand All @@ -66,6 +104,36 @@ func (i *InTotoIte6) CreatePayload(ctx context.Context, obj interface{}) (interf
}
}

func (i *InTotoIte6) FormatPayload(ctx context.Context, obj objects.TektonObject) (*ProvenanceStatement, error) {
var (
s *in_toto.ProvenanceStatement
err error
)

switch v := obj.(type) {
case *objects.TaskRunObject:
s, err = taskrun.GenerateAttestation(ctx, v, i.slsaConfig)
case *objects.PipelineRunObject:
s, err = pipelinerun.GenerateAttestation(ctx, v, i.slsaConfig)
default:
return nil, fmt.Errorf("intoto does not support type: %s", v)
}

if err != nil {
return nil, err
}
// Wrap output in BinaryMarshaller so we know how to format this.
out := ProvenanceStatement(*s)
return &out, nil

}

func (i *InTotoIte6) Type() config.PayloadType {
return formats.PayloadTypeSlsav1
}

type ProvenanceStatement in_toto.ProvenanceStatement

func (s ProvenanceStatement) MarshalBinary() ([]byte, error) {
return json.Marshal(s)
}
Loading

0 comments on commit bac7923

Please sign in to comment.