Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds initial Attestor implementation. #998

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, but this should probably be called something like SimpleSigningFormatter.


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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be marked as deprecated (also NewPayloaderFromConfig)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually... I don't think I fully understand Payloader vs Formatter 🤔

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
Loading