diff --git a/attestation/aws-iid/aws-iid_test.go b/attestation/aws-iid/aws-iid_test.go index bcda075b..dd4bf7b1 100644 --- a/attestation/aws-iid/aws-iid_test.go +++ b/attestation/aws-iid/aws-iid_test.go @@ -112,11 +112,10 @@ func TestAttestor_Attest(t *testing.T) { conf: conf, } - ctx, err := attestation.NewContext([]attestation.Attestor{a}) + ctx, err := attestation.NewContext("test", []attestation.Attestor{a}) require.NoError(t, err) err = a.Attest(ctx) require.NoError(t, err) - } func TestAttestor_getIID(t *testing.T) { @@ -154,7 +153,7 @@ func TestAttestor_Subjects(t *testing.T) { conf: conf, } - ctx, err := attestation.NewContext([]attestation.Attestor{a}) + ctx, err := attestation.NewContext("test", []attestation.Attestor{a}) require.NoError(t, err) err = a.Attest(ctx) require.NoError(t, err) diff --git a/attestation/commandrun/commandrun.go b/attestation/commandrun/commandrun.go index 1aea1784..c148544e 100644 --- a/attestation/commandrun/commandrun.go +++ b/attestation/commandrun/commandrun.go @@ -35,8 +35,18 @@ const ( // doesn't implement the expected interfaces. var ( _ attestation.Attestor = &CommandRun{} + _ CommandRunAttestor = &CommandRun{} ) +type CommandRunAttestor interface { + // Attestor + Name() string + Type() string + RunType() attestation.RunType + Attest(ctx *attestation.AttestationContext) error + Data() *CommandRun +} + func init() { attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { return New() @@ -129,6 +139,10 @@ func (rc *CommandRun) Attest(ctx *attestation.AttestationContext) error { return nil } +func (rc *CommandRun) Data() *CommandRun { + return rc +} + func (rc *CommandRun) Name() string { return Name } diff --git a/attestation/context.go b/attestation/context.go index 7980b223..0e916731 100644 --- a/attestation/context.go +++ b/attestation/context.go @@ -97,6 +97,7 @@ type AttestationContext struct { completedAttestors []CompletedAttestor products map[string]Product materials map[string]cryptoutil.DigestSet + stepName string } type Product struct { @@ -104,7 +105,7 @@ type Product struct { Digest cryptoutil.DigestSet `json:"digest"` } -func NewContext(attestors []Attestor, opts ...AttestationContextOption) (*AttestationContext, error) { +func NewContext(stepName string, attestors []Attestor, opts ...AttestationContextOption) (*AttestationContext, error) { wd, err := os.Getwd() if err != nil { return nil, err @@ -117,6 +118,7 @@ func NewContext(attestors []Attestor, opts ...AttestationContextOption) (*Attest hashes: []cryptoutil.DigestValue{{Hash: crypto.SHA256}, {Hash: crypto.SHA256, GitOID: true}, {Hash: crypto.SHA1, GitOID: true}}, materials: make(map[string]cryptoutil.DigestSet), products: make(map[string]Product), + stepName: stepName, } for _, opt := range opts { @@ -219,6 +221,10 @@ func (ctx *AttestationContext) Products() map[string]Product { return out } +func (ctx *AttestationContext) StepName() string { + return ctx.stepName +} + func (ctx *AttestationContext) addMaterials(materialer Materialer) { newMats := materialer.Materials() for k, v := range newMats { diff --git a/attestation/environment/environment.go b/attestation/environment/environment.go index bb3e3d96..8def421c 100644 --- a/attestation/environment/environment.go +++ b/attestation/environment/environment.go @@ -33,8 +33,18 @@ const ( // doesn't implement the expected interfaces. var ( _ attestation.Attestor = &Attestor{} + _ EnvironmentAttestor = &Attestor{} ) +type EnvironmentAttestor interface { + // Attestor + Name() string + Type() string + RunType() attestation.RunType + Attest(ctx *attestation.AttestationContext) error + Data() *Attestor +} + func init() { attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { return New() @@ -101,6 +111,10 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { return nil } +func (a *Attestor) Data() *Attestor { + return a +} + // splitVariable splits a string representing an environment variable in the format of // "KEY=VAL" and returns the key and val separately. func splitVariable(v string) (key, val string) { diff --git a/attestation/environment/environment_test.go b/attestation/environment/environment_test.go index b958cd32..8a4eddc8 100644 --- a/attestation/environment/environment_test.go +++ b/attestation/environment/environment_test.go @@ -24,7 +24,7 @@ import ( func TestEnvironment(t *testing.T) { attestor := New() - ctx, err := attestation.NewContext([]attestation.Attestor{attestor}) + ctx, err := attestation.NewContext("test", []attestation.Attestor{attestor}) require.NoError(t, err) t.Setenv("AWS_ACCESS_KEY_ID", "super secret") diff --git a/attestation/factory.go b/attestation/factory.go index a78bfabb..9b237380 100644 --- a/attestation/factory.go +++ b/attestation/factory.go @@ -56,6 +56,11 @@ type Producer interface { Products() map[string]Product } +// Exporter allows attestors to export their attestations for separation from the collection. +type Exporter interface { + Export() bool +} + // BackReffer allows attestors to indicate which of their subjects are good candidates // to find related attestations. For example the git attestor's commit hash subject // is a good candidate to find all attestation collections that also refer to a specific diff --git a/attestation/git/git.go b/attestation/git/git.go index cbcc189c..3b39df29 100644 --- a/attestation/git/git.go +++ b/attestation/git/git.go @@ -39,8 +39,24 @@ var ( _ attestation.Attestor = &Attestor{} _ attestation.Subjecter = &Attestor{} _ attestation.BackReffer = &Attestor{} + _ GitAttestor = &Attestor{} ) +type GitAttestor interface { + // Attestor + Name() string + Type() string + RunType() attestation.RunType + Attest(ctx *attestation.AttestationContext) error + Data() *Attestor + + // Subjecter + Subjects() map[string]cryptoutil.DigestSet + + // Backreffer + BackRefs() map[string]cryptoutil.DigestSet +} + func init() { attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { return New() @@ -75,6 +91,7 @@ type Attestor struct { ParentHashes []string `json:"parenthashes,omitempty"` TreeHash string `json:"treehash,omitempty"` Refs []string `json:"refs,omitempty"` + Remotes []string `json:"remotes,omitempty"` Tags []Tag `json:"tags,omitempty"` } @@ -125,6 +142,15 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { }: commit.Hash.String(), } + remotes, err := repo.Remotes() + if err != nil { + return err + } + + for _, remote := range remotes { + a.Remotes = append(a.Remotes, remote.Config().URLs...) + } + //get all the refs for the repo refs, err := repo.References() if err != nil { @@ -218,6 +244,10 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { return nil } +func (a *Attestor) Data() *Attestor { + return a +} + func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { subjects := make(map[string]cryptoutil.DigestSet) hashes := []cryptoutil.DigestValue{{Hash: crypto.SHA256}} diff --git a/attestation/git/git_test.go b/attestation/git/git_test.go index 30c06944..0c1ce744 100644 --- a/attestation/git/git_test.go +++ b/attestation/git/git_test.go @@ -49,7 +49,7 @@ func TestRunWorksWithCommits(t *testing.T) { _, dir, cleanup := createTestRepo(t, true) defer cleanup() - ctx, err := attestation.NewContext([]attestation.Attestor{attestor}, attestation.WithWorkingDir(dir)) + ctx, err := attestation.NewContext("test", []attestation.Attestor{attestor}, attestation.WithWorkingDir(dir)) require.NoError(t, err, "Expected no error from NewContext") err = ctx.RunAttestors() @@ -146,7 +146,7 @@ func TestRunWorksWithoutCommits(t *testing.T) { _, dir, cleanup := createTestRepo(t, false) defer cleanup() - ctx, err := attestation.NewContext([]attestation.Attestor{attestor}, attestation.WithWorkingDir(dir)) + ctx, err := attestation.NewContext("test", []attestation.Attestor{attestor}, attestation.WithWorkingDir(dir)) require.NoError(t, err, "Expected no error from NewContext") err = ctx.RunAttestors() diff --git a/attestation/github/github.go b/attestation/github/github.go index 436c98a5..02a0aea2 100644 --- a/attestation/github/github.go +++ b/attestation/github/github.go @@ -48,8 +48,24 @@ var ( _ attestation.Attestor = &Attestor{} _ attestation.Subjecter = &Attestor{} _ attestation.BackReffer = &Attestor{} + _ GitHubAttestor = &Attestor{} ) +type GitHubAttestor interface { + // Attestor + Name() string + Type() string + RunType() attestation.RunType + Attest(ctx *attestation.AttestationContext) error + Data() *Attestor + + // Subjecter + Subjects() map[string]cryptoutil.DigestSet + + // Backreffer + BackRefs() map[string]cryptoutil.DigestSet +} + // init registers the github attestor. func init() { attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { @@ -57,11 +73,11 @@ func init() { }) } -// ErrNotGitlab is an error type that indicates the environment is not a github ci job. -type ErrNotGitlab struct{} +// ErrNotGitHub is an error type that indicates the environment is not a github ci job. +type ErrNotGitHub struct{} -// Error returns the error message for ErrNotGitlab. -func (e ErrNotGitlab) Error() string { +// Error returns the error message for ErrNotGitHub. +func (e ErrNotGitHub) Error() string { return "not in a github ci job" } @@ -111,7 +127,7 @@ func (a *Attestor) RunType() attestation.RunType { // Attest performs the attestation for the github environment. func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { if os.Getenv("GITHUB_ACTIONS") != "true" { - return ErrNotGitlab{} + return ErrNotGitHub{} } jwtString, err := fetchToken(a.tokenURL, os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN"), "witness") @@ -142,6 +158,10 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { return nil } +func (a *Attestor) Data() *Attestor { + return a +} + // Subjects returns a map of subjects and their corresponding digest sets. func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { subjects := make(map[string]cryptoutil.DigestSet) diff --git a/attestation/gitlab/gitlab.go b/attestation/gitlab/gitlab.go index eea831d7..f4be947b 100644 --- a/attestation/gitlab/gitlab.go +++ b/attestation/gitlab/gitlab.go @@ -38,8 +38,24 @@ var ( _ attestation.Attestor = &Attestor{} _ attestation.Subjecter = &Attestor{} _ attestation.BackReffer = &Attestor{} + _ GitLabAttestor = &Attestor{} ) +type GitLabAttestor interface { + // Attestor + Name() string + Type() string + RunType() attestation.RunType + Attest(ctx *attestation.AttestationContext) error + Data() *Attestor + + // Subjecter + Subjects() map[string]cryptoutil.DigestSet + + // Backreffer + BackRefs() map[string]cryptoutil.DigestSet +} + func init() { attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { return New() @@ -91,13 +107,15 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { } a.CIServerUrl = os.Getenv("CI_SERVER_URL") - jwksUrl := fmt.Sprintf("%s/-/jwks", a.CIServerUrl) - jwtString := os.Getenv("CI_JOB_JWT") + jwksUrl := fmt.Sprintf("%s/oauth/discovery/keys", a.CIServerUrl) + jwtString := os.Getenv("ID_TOKEN") if jwtString != "" { a.JWT = jwt.New(jwt.WithToken(jwtString), jwt.WithJWKSUrl(jwksUrl)) if err := a.JWT.Attest(ctx); err != nil { return err } + } else { + log.Warn("(attestation/gitlab) no jwt token found in environment") } a.CIConfigPath = os.Getenv("CI_CONFIG_PATH") @@ -116,6 +134,10 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { return nil } +func (a *Attestor) Data() *Attestor { + return a +} + func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { subjects := make(map[string]cryptoutil.DigestSet) hashes := []cryptoutil.DigestValue{{Hash: crypto.SHA256}} diff --git a/attestation/link/link.go b/attestation/link/link.go new file mode 100644 index 00000000..8b9852d4 --- /dev/null +++ b/attestation/link/link.go @@ -0,0 +1,154 @@ +// Copyright 2024 The Witness Contributors +// +// 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 link + +import ( + "encoding/json" + "fmt" + + v0 "github.com/in-toto/attestation/go/predicates/link/v0" + v1 "github.com/in-toto/attestation/go/v1" + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/attestation/commandrun" + "github.com/in-toto/go-witness/attestation/environment" + "github.com/in-toto/go-witness/attestation/material" + "github.com/in-toto/go-witness/attestation/product" + "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/registry" + "google.golang.org/protobuf/types/known/structpb" +) + +const ( + Name = "link" + Type = "https://in-toto.io/attestation/link/v0.3" + RunType = attestation.PostProductRunType + + defaultExport = false +) + +// This is a hacky way to create a compile time error in case the attestor +// doesn't implement the expected interfaces. +var ( + _ attestation.Attestor = &Link{} + _ attestation.Subjecter = &Link{} +) + +func init() { + attestation.RegisterAttestation(Name, Type, RunType, + func() attestation.Attestor { return New() }, + registry.BoolConfigOption( + "export", + "Export the Link predicate in its own attestation", + defaultExport, + func(a attestation.Attestor, export bool) (attestation.Attestor, error) { + linkAttestor, ok := a.(*Link) + if !ok { + return a, fmt.Errorf("unexpected attestor type: %T is not a Link provenance attestor", a) + } + WithExport(export)(linkAttestor) + return linkAttestor, nil + }, + ), + ) +} + +type Option func(*Link) + +func WithExport(export bool) Option { + return func(l *Link) { + l.export = export + } +} + +type Link struct { + PbLink v0.Link + products map[string]attestation.Product + export bool +} + +func New() *Link { + return &Link{} +} + +func (l *Link) Name() string { + return Name +} + +func (l *Link) Type() string { + return Type +} + +func (l *Link) RunType() attestation.RunType { + return RunType +} + +func (l *Link) Export() bool { + return l.export +} + +func (l *Link) Attest(ctx *attestation.AttestationContext) error { + l.PbLink.Name = ctx.StepName() + for _, attestor := range ctx.CompletedAttestors() { + switch name := attestor.Attestor.Name(); name { + case commandrun.Name: + l.PbLink.Command = attestor.Attestor.(commandrun.CommandRunAttestor).Data().Cmd + case material.Name: + mats := attestor.Attestor.(material.MaterialAttestor).Materials() + for name, digestSet := range mats { + digests, _ := digestSet.ToNameMap() + l.PbLink.Materials = append(l.PbLink.Materials, &v1.ResourceDescriptor{ + Name: name, + Digest: digests, + }) + } + case environment.Name: + envs := attestor.Attestor.(environment.EnvironmentAttestor).Data().Variables + pbEnvs := make(map[string]interface{}, len(envs)) + for name, value := range envs { + pbEnvs[name] = value + } + + var err error + l.PbLink.Environment, err = structpb.NewStruct(pbEnvs) + if err != nil { + return err + } + case product.ProductName: + l.products = attestor.Attestor.(product.ProductAttestor).Products() + } + } + return nil +} + +func (l *Link) MarshalJSON() ([]byte, error) { + return json.Marshal(&l.PbLink) +} + +func (l *Link) UnmarshalJSON(data []byte) error { + if err := json.Unmarshal(data, &l.PbLink); err != nil { + return err + } + + return nil +} + +func (l *Link) Subjects() map[string]cryptoutil.DigestSet { + subjects := make(map[string]cryptoutil.DigestSet) + for productName, product := range l.products { + subjects[fmt.Sprintf("file:%v", productName)] = product.Digest + } + + return subjects +} diff --git a/attestation/link/link_test.go b/attestation/link/link_test.go new file mode 100644 index 00000000..8c8a27e4 --- /dev/null +++ b/attestation/link/link_test.go @@ -0,0 +1,221 @@ +// Copyright 2022 The Witness Contributors +// +// 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 link + +import ( + "bytes" + "crypto" + "encoding/json" + "testing" + + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/internal/attestors" +) + +func TestName(t *testing.T) { + link := New() + if link.Name() != Name { + t.Errorf("expected %s, got %s", Name, link.Name()) + } +} + +func TestType(t *testing.T) { + link := New() + if link.Type() != Type { + t.Errorf("expected %s, got %s", Type, link.Type()) + } +} + +func TestRunType(t *testing.T) { + link := New() + if link.RunType() != RunType { + t.Errorf("expected %s, got %s", RunType, link.RunType()) + } +} + +func TestExport(t *testing.T) { + link := New() + if link.export != defaultExport { + t.Errorf("expected %t, got %t", defaultExport, link.export) + } + + WithExport(true)(link) + if !link.export { + t.Errorf("expected %t, got %t", true, link.export) + } + + if link.Export() != true { + t.Errorf("expected %t, got %t", true, link.Export()) + } +} + +func TestUnmarshalJSON(t *testing.T) { + link := New() + if err := link.UnmarshalJSON([]byte(testLinkJSON)); err != nil { + t.Errorf("unexpected error: %s", err) + } +} + +func TestUnmarshalBadJSON(t *testing.T) { + link := New() + if err := link.UnmarshalJSON([]byte("}")); err == nil { + t.Error("Expected error") + } +} + +func TestMarshalJSON(t *testing.T) { + link := New() + if err := link.UnmarshalJSON([]byte(testLinkJSON)); err != nil { + t.Errorf("unexpected error: %s", err) + } + + _, err := link.MarshalJSON() + if err != nil { + t.Errorf("unexpected error: %s", err) + } +} + +func TestAttest(t *testing.T) { + // Setup Env + e := attestors.NewTestEnvironmentAttestor() + e.Data().Variables = map[string]string{ + "COLORFGBG": "7;0", + "COLORTERM": "truecolor", + } + + // Setup Materials + m := attestors.NewTestMaterialAttestor() + materials := make(map[string]cryptoutil.DigestSet) + materials["test2"] = cryptoutil.DigestSet{{Hash: crypto.SHA256, GitOID: false}: "a53d0741798b287c6dd7afa64aee473f305e65d3f49463bb9d7408ec3b12bf5f"} + materials["test1"] = cryptoutil.DigestSet{{Hash: crypto.SHA256, GitOID: false}: "a53d0741798b287c6dd7afa64aee473f305e65d3f49463bb9d7408ec3b12bf5f"} + m.SetMaterials(materials) + + // Setup CommandRun + c := attestors.NewTestCommandRunAttestor() + c.Data().Cmd = []string{"touch", "test.txt"} + + // Setup Products + p := attestors.NewTestProductAttestor() + + l := New() + + ctx, err := attestation.NewContext("test", []attestation.Attestor{e, m, c, p, l}) + if err != nil { + t.Errorf("error creating attestation context: %s", err) + } + + err = ctx.RunAttestors() + if err != nil { + t.Errorf("error attesting: %s", err.Error()) + } + + var linkJson []byte + if linkJson, err = json.MarshalIndent(l, "", " "); err != nil { + t.Errorf("unexpected error: %s", err) + } + + testJson := []byte(testLinkJSON) + if !bytes.Equal(linkJson, testJson) { + t.Errorf("expected \n%s\n, got \n%s\n", testJson, linkJson) + } +} + +func TestSubjects(t *testing.T) { + link := setupLink(t) + + subjects := link.Subjects() + + if len(subjects) != 1 { + t.Errorf("expected 1 subjects, got %d", len(subjects)) + } + + digests := subjects["file:test.txt"] + nameMap, err := digests.ToNameMap() + if err != nil { + t.Errorf("unexpected error: %s", err) + } + + if len(nameMap) != 1 { + t.Errorf("expected 1 digest found, got %d", len(nameMap)) + } + + if nameMap["sha256"] != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" { + t.Errorf("expected e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855, got %s", nameMap["sha256"]) + } +} + +func setupLink(t *testing.T) *Link { + link := New() + if err := link.UnmarshalJSON([]byte(testLinkJSON)); err != nil { + t.Errorf("unexpected error: %s", err) + } + + link.products = make(map[string]attestation.Product) + digestsByName := make(map[string]string) + digestsByName["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + digestSet, err := cryptoutil.NewDigestSet(digestsByName) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + link.products["test.txt"] = attestation.Product{ + MimeType: "text/plain", + Digest: digestSet, + } + + return link +} +func TestRegistration(t *testing.T) { + registrations := attestation.RegistrationEntries() + + var found bool + for _, registration := range registrations { + if registration.Name == Name { + found = true + } + } + + if !found { + t.Errorf("expected %s to be registered", Name) + } + +} + +const testLinkJSON = `{ + "name": "test", + "command": [ + "touch", + "test.txt" + ], + "materials": [ + { + "name": "test2", + "digest": { + "sha256": "a53d0741798b287c6dd7afa64aee473f305e65d3f49463bb9d7408ec3b12bf5f" + } + }, + { + "name": "test1", + "digest": { + "sha256": "a53d0741798b287c6dd7afa64aee473f305e65d3f49463bb9d7408ec3b12bf5f" + } + } + ], + "environment": { + "COLORFGBG": "7;0", + "COLORTERM": "truecolor" + } +}` diff --git a/attestation/material/material.go b/attestation/material/material.go index 458515a1..10125918 100644 --- a/attestation/material/material.go +++ b/attestation/material/material.go @@ -33,8 +33,20 @@ const ( var ( _ attestation.Attestor = &Attestor{} _ attestation.Materialer = &Attestor{} + _ MaterialAttestor = &Attestor{} ) +type MaterialAttestor interface { + // Attestor + Name() string + Type() string + RunType() attestation.RunType + Attest(ctx *attestation.AttestationContext) error + + // Materialer + Materials() map[string]cryptoutil.DigestSet +} + func init() { attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { return New() diff --git a/attestation/maven/maven_test.go b/attestation/maven/maven_test.go index 8e67ccd8..84815c79 100644 --- a/attestation/maven/maven_test.go +++ b/attestation/maven/maven_test.go @@ -67,7 +67,7 @@ func TestMaven(t *testing.T) { } t.Run(test.name, func(t *testing.T) { - ctx, err := attestation.NewContext([]attestation.Attestor{}, attestation.WithWorkingDir(workingDir)) + ctx, err := attestation.NewContext("test", []attestation.Attestor{}, attestation.WithWorkingDir(workingDir)) require.NoError(t, err) a := New(WithPom(p)) require.NoError(t, a.Attest(ctx)) diff --git a/attestation/oci/oci.go b/attestation/oci/oci.go index 853b0580..34b895b9 100644 --- a/attestation/oci/oci.go +++ b/attestation/oci/oci.go @@ -44,8 +44,20 @@ const ( var ( _ attestation.Attestor = &Attestor{} _ attestation.Subjecter = &Attestor{} + _ OCIAttestor = &Attestor{} ) +type OCIAttestor interface { + // Attestor + Name() string + Type() string + RunType() attestation.RunType + Attest(ctx *attestation.AttestationContext) error + + // Subjector + Subjects() map[string]cryptoutil.DigestSet +} + func init() { attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { return New() diff --git a/attestation/oci/oci_test.go b/attestation/oci/oci_test.go index fd64e74f..4068e2d9 100644 --- a/attestation/oci/oci_test.go +++ b/attestation/oci/oci_test.go @@ -108,7 +108,7 @@ func TestAttestor_Attest(t *testing.T) { Digest: tarDigest, } - ctx, err := attestation.NewContext([]attestation.Attestor{testProducter{testProductSet}, a}) + ctx, err := attestation.NewContext("test", []attestation.Attestor{testProducter{testProductSet}, a}) if err != nil { t.Fatal(err) } diff --git a/attestation/product/product.go b/attestation/product/product.go index 1754d841..61b9f511 100644 --- a/attestation/product/product.go +++ b/attestation/product/product.go @@ -31,9 +31,9 @@ import ( ) const ( - Name = "product" - Type = "https://witness.dev/attestations/product/v0.1" - RunType = attestation.ProductRunType + ProductName = "product" + ProductType = "https://witness.dev/attestations/product/v0.1" + ProductRunType = attestation.ProductRunType defaultIncludeGlob = "*" defaultExcludeGlob = "" @@ -47,8 +47,22 @@ var ( _ attestation.Producer = &Attestor{} ) +type ProductAttestor interface { + // Attestor + Name() string + Type() string + RunType() attestation.RunType + Attest(ctx *attestation.AttestationContext) error + + // Subjector + Subjects() map[string]cryptoutil.DigestSet + + // Producter + Products() map[string]attestation.Product +} + func init() { - attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { return New() }, + attestation.RegisterAttestation(ProductName, ProductType, ProductRunType, func() attestation.Attestor { return New() }, registry.StringConfigOption( "include-glob", "Pattern to use when recording products. Files that match this pattern will be included as subjects on the attestation.", @@ -126,15 +140,15 @@ func fromDigestMap(digestMap map[string]cryptoutil.DigestSet) map[string]attesta } func (a *Attestor) Name() string { - return Name + return ProductName } func (a *Attestor) Type() string { - return Type + return ProductType } func (a *Attestor) RunType() attestation.RunType { - return RunType + return ProductRunType } func New(opts ...Option) *Attestor { diff --git a/attestation/product/product_test.go b/attestation/product/product_test.go index 6495f52f..4923081f 100644 --- a/attestation/product/product_test.go +++ b/attestation/product/product_test.go @@ -42,17 +42,17 @@ func TestFromDigestMap(t *testing.T) { func TestAttestorName(t *testing.T) { a := New() - assert.Equal(t, a.Name(), Name) + assert.Equal(t, a.Name(), ProductName) } func TestAttestorType(t *testing.T) { a := New() - assert.Equal(t, a.Type(), Type) + assert.Equal(t, a.Type(), ProductType) } func TestAttestorRunType(t *testing.T) { a := New() - assert.Equal(t, a.RunType(), RunType) + assert.Equal(t, a.RunType(), ProductRunType) } func TestAttestorAttest(t *testing.T) { @@ -65,7 +65,7 @@ func TestAttestorAttest(t *testing.T) { testDigestSet := make(map[string]cryptoutil.DigestSet) testDigestSet["test"] = testDigest a.baseArtifacts = testDigestSet - ctx, err := attestation.NewContext([]attestation.Attestor{a}) + ctx, err := attestation.NewContext("test", []attestation.Attestor{a}) require.NoError(t, err) require.NoError(t, a.Attest(ctx)) } @@ -174,7 +174,7 @@ func TestIncludeExcludeGlobs(t *testing.T) { } t.Run("default include all", func(t *testing.T) { - ctx, err := attestation.NewContext([]attestation.Attestor{}, attestation.WithWorkingDir(workingDir)) + ctx, err := attestation.NewContext("test", []attestation.Attestor{}, attestation.WithWorkingDir(workingDir)) require.NoError(t, err) a := New() require.NoError(t, a.Attest(ctx)) @@ -183,7 +183,7 @@ func TestIncludeExcludeGlobs(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ctx, err := attestation.NewContext([]attestation.Attestor{}, attestation.WithWorkingDir(workingDir)) + ctx, err := attestation.NewContext("test", []attestation.Attestor{}, attestation.WithWorkingDir(workingDir)) require.NoError(t, err) a := New() WithIncludeGlob(test.includeGlob)(a) diff --git a/attestation/slsa/slsa.go b/attestation/slsa/slsa.go new file mode 100644 index 00000000..bbf2b9e0 --- /dev/null +++ b/attestation/slsa/slsa.go @@ -0,0 +1,264 @@ +// Copyright 2024 The Witness Contributors +// +// 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 slsa + +import ( + "encoding/json" + "fmt" + "strings" + + prov "github.com/in-toto/attestation/go/predicates/provenance/v1" + v1 "github.com/in-toto/attestation/go/v1" + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/attestation/commandrun" + "github.com/in-toto/go-witness/attestation/environment" + "github.com/in-toto/go-witness/attestation/git" + "github.com/in-toto/go-witness/attestation/github" + "github.com/in-toto/go-witness/attestation/gitlab" + "github.com/in-toto/go-witness/attestation/material" + "github.com/in-toto/go-witness/attestation/oci" + "github.com/in-toto/go-witness/attestation/product" + "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/log" + "github.com/in-toto/go-witness/registry" + "golang.org/x/exp/maps" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + Name = "slsa" + Type = "https://slsa.dev/provenance/v1.0" + RunType = attestation.PostProductRunType + defaultExport = false + BuildType = "https://witness.dev/slsa-build@v0.1" + DefaultBuilderId = "https://witness.dev/witness-default-builder@v0.1" + GHABuilderId = "https://witness.dev/witness-github-action-builder@v0.1" + GLCBuilderId = "https://witness.dev/witness-gitlab-component-builder@v0.1" +) + +// This is a hacky way to create a compile time error in case the attestor +// doesn't implement the expected interfaces. +var ( + _ attestation.Attestor = &Provenance{} + _ attestation.Subjecter = &Provenance{} +) + +func init() { + attestation.RegisterAttestation(Name, Type, RunType, + func() attestation.Attestor { return New() }, + registry.BoolConfigOption( + "export", + "Export the SLSA provenance predicate in its own attestation", + defaultExport, + func(a attestation.Attestor, export bool) (attestation.Attestor, error) { + slsaAttestor, ok := a.(*Provenance) + if !ok { + return a, fmt.Errorf("unexpected attestor type: %T is not a SLSA provenance attestor", a) + } + WithExport(export)(slsaAttestor) + return slsaAttestor, nil + }, + ), + ) +} + +type Option func(*Provenance) + +func WithExport(export bool) Option { + return func(p *Provenance) { + p.export = export + } +} + +type Provenance struct { + PbProvenance prov.Provenance + products map[string]attestation.Product + subjects map[string]cryptoutil.DigestSet + export bool +} + +func New() *Provenance { + return &Provenance{} +} + +func (p *Provenance) Name() string { + return Name +} + +func (p *Provenance) Type() string { + return Type +} + +func (p *Provenance) RunType() attestation.RunType { + return RunType +} + +func (p *Provenance) Export() bool { + return p.export +} + +func (p *Provenance) Attest(ctx *attestation.AttestationContext) error { + builder := prov.Builder{} + metadata := prov.BuildMetadata{} + p.PbProvenance.BuildDefinition = &prov.BuildDefinition{} + p.PbProvenance.RunDetails = &prov.RunDetails{Builder: &builder, Metadata: &metadata} + + p.PbProvenance.BuildDefinition.BuildType = BuildType + p.PbProvenance.RunDetails.Builder.Id = DefaultBuilderId + + internalParameters := make(map[string]interface{}) + + for _, attestor := range ctx.CompletedAttestors() { + switch name := attestor.Attestor.Name(); name { + // Pre-material Attestors + case environment.Name: + envs := attestor.Attestor.(environment.EnvironmentAttestor).Data().Variables + pbEnvs := make(map[string]interface{}, len(envs)) + for name, value := range envs { + pbEnvs[name] = value + } + + internalParameters["env"] = pbEnvs + + case git.Name: + digestSet := attestor.Attestor.(git.GitAttestor).Data().CommitDigest + remotes := attestor.Attestor.(git.GitAttestor).Data().Remotes + digests, _ := digestSet.ToNameMap() + + for _, remote := range remotes { + p.PbProvenance.BuildDefinition.ResolvedDependencies = append( + p.PbProvenance.BuildDefinition.ResolvedDependencies, + &v1.ResourceDescriptor{ + Name: remote, + Digest: digests, + }) + } + + case github.Name: + gh := attestor.Attestor.(github.GitHubAttestor) + p.PbProvenance.RunDetails.Builder.Id = GHABuilderId + p.PbProvenance.RunDetails.Metadata.InvocationId = gh.Data().PipelineUrl + digest := make(map[string]string) + + if gh.Data().JWT == nil { + log.Warn("No JWT found in GitHub attestor") + continue + } + + digest["sha1"] = gh.Data().JWT.Claims["sha"].(string) + + case gitlab.Name: + gl := attestor.Attestor.(gitlab.GitLabAttestor) + p.PbProvenance.RunDetails.Builder.Id = GLCBuilderId + p.PbProvenance.RunDetails.Metadata.InvocationId = gl.Data().PipelineUrl + digest := make(map[string]string) + + if gl.Data().JWT == nil { + log.Warn("No JWT found in GitLab attestor") + continue + } + + sha, found := gl.Data().JWT.Claims["sha"] + if found { + digest["sha1"] = sha.(string) + } else { + log.Warn("No SHA found in GitLab JWT") + } + + // Material Attestors + case material.Name: + mats := attestor.Attestor.(material.MaterialAttestor).Materials() + for name, digestSet := range mats { + digests, _ := digestSet.ToNameMap() + p.PbProvenance.BuildDefinition.ResolvedDependencies = append( + p.PbProvenance.BuildDefinition.ResolvedDependencies, + &v1.ResourceDescriptor{ + Name: name, + Digest: digests, + }) + } + + // CommandRun Attestors + case commandrun.Name: + var err error + ep := make(map[string]interface{}) + ep["command"] = strings.Join(attestor.Attestor.(commandrun.CommandRunAttestor).Data().Cmd, " ") + p.PbProvenance.BuildDefinition.ExternalParameters, err = structpb.NewStruct(ep) + if err != nil { + return err + } + + p.PbProvenance.RunDetails.Metadata.StartedOn = timestamppb.New(attestor.StartTime) + p.PbProvenance.RunDetails.Metadata.FinishedOn = timestamppb.New(attestor.EndTime) + + // Product Attestors + case product.ProductName: + if p.products == nil { + p.products = ctx.Products() + } else { + maps.Copy(p.products, ctx.Products()) + } + + if p.subjects == nil { + p.subjects = attestor.Attestor.(attestation.Subjecter).Subjects() + } else { + maps.Copy(p.subjects, attestor.Attestor.(attestation.Subjecter).Subjects()) + } + + // Post Attestors + case oci.Name: + if p.subjects == nil { + p.subjects = attestor.Attestor.(attestation.Subjecter).Subjects() + } else { + maps.Copy(p.subjects, attestor.Attestor.(attestation.Subjecter).Subjects()) + } + } + } + + // NOTE: We want to warn users that they can use the github and gitlab attestors to enrich their provenance + if p.PbProvenance.RunDetails.Builder.Id == DefaultBuilderId { + log.Warn("No build system attestor invoked. Consider using github or gitlab attestors (if appropriate) to enrich your SLSA provenance") + } + + var err error + p.PbProvenance.BuildDefinition.InternalParameters, err = structpb.NewStruct(internalParameters) + if err != nil { + return err + } + + return nil +} + +func (p *Provenance) MarshalJSON() ([]byte, error) { + return json.Marshal(&p.PbProvenance) +} + +func (p *Provenance) UnmarshalJSON(data []byte) error { + if err := json.Unmarshal(data, &p.PbProvenance); err != nil { + return err + } + + return nil +} + +func (p *Provenance) Subjects() map[string]cryptoutil.DigestSet { + subjects := make(map[string]cryptoutil.DigestSet) + for productName, product := range p.products { + subjects[fmt.Sprintf("file:%v", productName)] = product.Digest + } + + return subjects +} diff --git a/attestation/slsa/slsa_test.go b/attestation/slsa/slsa_test.go new file mode 100644 index 00000000..d043648b --- /dev/null +++ b/attestation/slsa/slsa_test.go @@ -0,0 +1,318 @@ +// Copyright 2022 The Witness Contributors +// +// 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 slsa + +import ( + "bytes" + "crypto" + "encoding/json" + "testing" + + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/internal/attestors" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestName(t *testing.T) { + provenance := New() + if provenance.Name() != Name { + t.Errorf("expected %s, got %s", Name, provenance.Name()) + } +} + +func TestType(t *testing.T) { + provenance := New() + if provenance.Type() != Type { + t.Errorf("expected %s, got %s", Type, provenance.Type()) + } +} + +func TestRunType(t *testing.T) { + provenance := New() + if provenance.RunType() != RunType { + t.Errorf("expected %s, got %s", RunType, provenance.RunType()) + } +} + +func TestExport(t *testing.T) { + provenance := New() + if provenance.export != defaultExport { + t.Errorf("expected %t, got %t", defaultExport, provenance.export) + } + + WithExport(true)(provenance) + if !provenance.export { + t.Errorf("expected %t, got %t", true, provenance.export) + } + + if provenance.Export() != true { + t.Errorf("expected %t, got %t", true, provenance.Export()) + } +} + +func TestUnmarshalJSON(t *testing.T) { + provenance := New() + if err := provenance.UnmarshalJSON([]byte(testGHProvJSON)); err != nil { + t.Errorf("unexpected error: %s", err) + } +} + +func TestUnmarshalBadJSON(t *testing.T) { + provenance := New() + if err := provenance.UnmarshalJSON([]byte("}")); err == nil { + t.Error("Expected error") + } +} + +func TestMarshalJSON(t *testing.T) { + provenance := New() + if err := provenance.UnmarshalJSON([]byte(testGHProvJSON)); err != nil { + t.Errorf("unexpected error: %s", err) + } + + _, err := provenance.MarshalJSON() + if err != nil { + t.Errorf("unexpected error: %s", err) + } +} + +func TestAttest(t *testing.T) { + // Setup Env + e := attestors.NewTestEnvironmentAttestor() + e.Data().Variables = map[string]string{ + "SHELL": "/bin/zsh", + "TERM": "xterm-256color", + "TERM_PROGRAM": "iTerm.app", + } + + // Setup Git + g := attestors.NewTestGitAttestor() + g.Data().CommitDigest = cryptoutil.DigestSet{ + {Hash: crypto.SHA1, GitOID: false}: "abc123", + } + g.Data().Remotes = []string{"git@github.com:in-toto/witness.git"} + + // Setup GitHub + gh := attestors.NewTestGitHubAttestor() + gh.Data().JWT.Claims["sha"] = "abc123" + gh.Data().PipelineUrl = "https://github.com/testifysec/swf/actions/runs/7879307166" + + // Setup GitLab + gl := attestors.NewTestGitLabAttestor() + gl.Data().JWT.Claims["sha"] = "abc123" + gl.Data().PipelineUrl = "https://github.com/testifysec/swf/actions/runs/7879307166" + + // Setup Materials + m := attestors.NewTestMaterialAttestor() + + // Setup CommandRun + c := attestors.NewTestCommandRunAttestor() + c.Data().Cmd = []string{"touch", "test.txt"} + + // Setup Products + p := attestors.NewTestProductAttestor() + + // Setup OCI + o := attestors.NewTestOCIAttestor() + + var tests = []struct { + name string + attestors []attestation.Attestor + expectedJson string + }{ + {"github", []attestation.Attestor{e, g, gh, m, c, p, o}, testGHProvJSON}, + {"gitlab", []attestation.Attestor{e, g, gl, m, c, p, o}, testGLProvJSON}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Logf("Running test %s", test.name) + s := New() + + ctx, err := attestation.NewContext("test", append(test.attestors, s)) + if err != nil { + t.Errorf("error creating attestation context: %s", err) + } + + err = ctx.RunAttestors() + if err != nil { + t.Errorf("error attesting: %s", err.Error()) + } + + // TODO: We don't have a way to mock out times on attestor runs + // Set attestor times manually to match testProvenanceJSON + s.PbProvenance.RunDetails.Metadata.StartedOn = ×tamppb.Timestamp{ + Seconds: 1711199861, + Nanos: 560152000, + } + s.PbProvenance.RunDetails.Metadata.FinishedOn = ×tamppb.Timestamp{ + Seconds: 1711199861, + Nanos: 560152000, + } + + var prov []byte + if prov, err = json.MarshalIndent(s, "", " "); err != nil { + t.Errorf("unexpected error: %s", err) + } + + testJson := []byte(test.expectedJson) + if !bytes.Equal(prov, testJson) { + t.Errorf("expected \n%s\n, got \n%s\n", testJson, prov) + } + }) + } +} + +func TestSubjects(t *testing.T) { + provenance := setupProvenance(t) + + subjects := provenance.Subjects() + + if len(subjects) != 1 { + t.Errorf("expected 1 subjects, got %d", len(subjects)) + } + + digests := subjects["file:test.txt"] + nameMap, err := digests.ToNameMap() + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + + if len(nameMap) != 1 { + t.Errorf("expected 1 digest found, got %d", len(nameMap)) + } + + if nameMap["sha256"] != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" { + t.Errorf("expected e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855, got %s", nameMap["sha256"]) + } +} + +func setupProvenance(t *testing.T) *Provenance { + provenance := New() + if err := provenance.UnmarshalJSON([]byte(testGHProvJSON)); err != nil { + t.Errorf("unexpected error: %s", err) + } + + provenance.products = make(map[string]attestation.Product) + digestsByName := make(map[string]string) + digestsByName["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + digestSet, err := cryptoutil.NewDigestSet(digestsByName) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + provenance.products["test.txt"] = attestation.Product{ + MimeType: "text/plain", + Digest: digestSet, + } + + return provenance +} +func TestRegistration(t *testing.T) { + registrations := attestation.RegistrationEntries() + + var found bool + for _, registration := range registrations { + if registration.Name == Name { + found = true + } + } + + if !found { + t.Errorf("expected %s to be registered", Name) + } + +} + +const testGHProvJSON = `{ + "build_definition": { + "build_type": "https://witness.dev/slsa-build@v0.1", + "external_parameters": { + "command": "touch test.txt" + }, + "internal_parameters": { + "env": { + "SHELL": "/bin/zsh", + "TERM": "xterm-256color", + "TERM_PROGRAM": "iTerm.app" + } + }, + "resolved_dependencies": [ + { + "name": "git@github.com:in-toto/witness.git", + "digest": { + "sha1": "abc123" + } + } + ] + }, + "run_details": { + "builder": { + "id": "https://witness.dev/witness-github-action-builder@v0.1" + }, + "metadata": { + "invocation_id": "https://github.com/testifysec/swf/actions/runs/7879307166", + "started_on": { + "seconds": 1711199861, + "nanos": 560152000 + }, + "finished_on": { + "seconds": 1711199861, + "nanos": 560152000 + } + } + } +}` + +const testGLProvJSON = `{ + "build_definition": { + "build_type": "https://witness.dev/slsa-build@v0.1", + "external_parameters": { + "command": "touch test.txt" + }, + "internal_parameters": { + "env": { + "SHELL": "/bin/zsh", + "TERM": "xterm-256color", + "TERM_PROGRAM": "iTerm.app" + } + }, + "resolved_dependencies": [ + { + "name": "git@github.com:in-toto/witness.git", + "digest": { + "sha1": "abc123" + } + } + ] + }, + "run_details": { + "builder": { + "id": "https://witness.dev/witness-gitlab-component-builder@v0.1" + }, + "metadata": { + "invocation_id": "https://github.com/testifysec/swf/actions/runs/7879307166", + "started_on": { + "seconds": 1711199861, + "nanos": 560152000 + }, + "finished_on": { + "seconds": 1711199861, + "nanos": 560152000 + } + } + } +}` diff --git a/go.mod b/go.mod index 29c08e9b..fc82b410 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/go-git/go-git/v5 v5.11.0 github.com/go-jose/go-jose/v3 v3.0.3 github.com/in-toto/archivista v0.4.0 + github.com/in-toto/attestation v1.0.1 github.com/jellydator/ttlcache/v3 v3.2.0 github.com/mattn/go-isatty v0.0.20 github.com/mitchellh/go-homedir v1.1.0 @@ -99,11 +100,11 @@ require ( go.opentelemetry.io/otel/sdk v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect go.uber.org/goleak v1.3.0 // indirect - golang.org/x/mod v0.15.0 // indirect + golang.org/x/mod v0.16.0 // indirect golang.org/x/oauth2 v0.19.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.18.0 // indirect + golang.org/x/tools v0.19.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect @@ -137,6 +138,7 @@ require ( github.com/yashtewari/glob-intersection v0.2.0 // indirect github.com/zeebo/errs v1.3.0 // indirect golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 golang.org/x/net v0.24.0 // indirect golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index d052cda3..607c6c1d 100644 --- a/go.sum +++ b/go.sum @@ -200,6 +200,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0Q github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= github.com/in-toto/archivista v0.4.0 h1:5g79iqmyXblnnwuD+768lrEbeoE0V5H7URYJFnr0p4I= github.com/in-toto/archivista v0.4.0/go.mod h1:HgqAu7az0Ql0Jf844Paf0Ji5PdUMKxO5JIBh4hOjMs8= +github.com/in-toto/attestation v1.0.1 h1:DgX1XuBkryTpj1Piq8AiMK3CMfEcec3Qv6+Ku+uI3WY= +github.com/in-toto/attestation v1.0.1/go.mod h1:hCR5COCuENh5+VfojEkJnt7caOymbEgvyZdKifD6pOw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE= @@ -368,6 +370,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -375,8 +379,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -462,8 +466,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/imports.go b/imports.go index f6580d39..ef7a4e71 100644 --- a/imports.go +++ b/imports.go @@ -25,12 +25,14 @@ import ( _ "github.com/in-toto/go-witness/attestation/github" _ "github.com/in-toto/go-witness/attestation/gitlab" _ "github.com/in-toto/go-witness/attestation/jwt" + _ "github.com/in-toto/go-witness/attestation/link" _ "github.com/in-toto/go-witness/attestation/material" _ "github.com/in-toto/go-witness/attestation/maven" _ "github.com/in-toto/go-witness/attestation/oci" _ "github.com/in-toto/go-witness/attestation/policyverify" _ "github.com/in-toto/go-witness/attestation/product" _ "github.com/in-toto/go-witness/attestation/sarif" + _ "github.com/in-toto/go-witness/attestation/slsa" // signer providers _ "github.com/in-toto/go-witness/signer/file" diff --git a/internal/attestors/commandrun.go b/internal/attestors/commandrun.go new file mode 100644 index 00000000..d7d255af --- /dev/null +++ b/internal/attestors/commandrun.go @@ -0,0 +1,58 @@ +// Copyright 2024 The Witness Contributors +// +// 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 attestors + +import ( + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/attestation/commandrun" + "github.com/in-toto/go-witness/cryptoutil" +) + +var ( + _ commandrun.CommandRunAttestor = &TestCommandRunAttestor{} +) + +type TestCommandRunAttestor struct { + comAtt commandrun.CommandRun +} + +func NewTestCommandRunAttestor() *TestCommandRunAttestor { + att := commandrun.New() + return &TestCommandRunAttestor{comAtt: *att} +} + +func (t *TestCommandRunAttestor) Name() string { + return t.comAtt.Name() +} + +func (t *TestCommandRunAttestor) Type() string { + return t.comAtt.Type() +} + +func (t *TestCommandRunAttestor) RunType() attestation.RunType { + return t.comAtt.RunType() +} + +func (t *TestCommandRunAttestor) Attest(ctx *attestation.AttestationContext) error { + return nil +} + +func (t *TestCommandRunAttestor) Data() *commandrun.CommandRun { + return &t.comAtt +} + +func (t *TestCommandRunAttestor) CommandRuns() map[string]cryptoutil.DigestSet { + return nil +} diff --git a/internal/attestors/environment.go b/internal/attestors/environment.go new file mode 100644 index 00000000..9ea5298a --- /dev/null +++ b/internal/attestors/environment.go @@ -0,0 +1,53 @@ +// Copyright 2024 The Witness Contributors +// +// 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 attestors + +import ( + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/attestation/environment" +) + +var ( + _ environment.EnvironmentAttestor = &TestEnvironmentAttestor{} +) + +type TestEnvironmentAttestor struct { + environmentAtt environment.Attestor +} + +func NewTestEnvironmentAttestor() *TestEnvironmentAttestor { + att := environment.New() + return &TestEnvironmentAttestor{environmentAtt: *att} +} + +func (t *TestEnvironmentAttestor) Name() string { + return t.environmentAtt.Name() +} + +func (t *TestEnvironmentAttestor) Type() string { + return t.environmentAtt.Type() +} + +func (t *TestEnvironmentAttestor) RunType() attestation.RunType { + return t.environmentAtt.RunType() +} + +func (t *TestEnvironmentAttestor) Attest(ctx *attestation.AttestationContext) error { + return nil +} + +func (t *TestEnvironmentAttestor) Data() *environment.Attestor { + return &t.environmentAtt +} diff --git a/internal/attestors/git.go b/internal/attestors/git.go new file mode 100644 index 00000000..bb47e37c --- /dev/null +++ b/internal/attestors/git.go @@ -0,0 +1,62 @@ +// Copyright 2024 The Witness Contributors +// +// 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 attestors + +import ( + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/attestation/git" + "github.com/in-toto/go-witness/cryptoutil" +) + +var ( + _ git.GitAttestor = &TestGitAttestor{} +) + +type TestGitAttestor struct { + gitAtt git.Attestor +} + +func NewTestGitAttestor() *TestGitAttestor { + att := git.New() + return &TestGitAttestor{gitAtt: *att} +} + +func (t *TestGitAttestor) Name() string { + return t.gitAtt.Name() +} + +func (t *TestGitAttestor) Type() string { + return t.gitAtt.Type() +} + +func (t *TestGitAttestor) RunType() attestation.RunType { + return t.gitAtt.RunType() +} + +func (t *TestGitAttestor) Attest(ctx *attestation.AttestationContext) error { + return nil +} + +func (t *TestGitAttestor) Data() *git.Attestor { + return &t.gitAtt +} + +func (t *TestGitAttestor) Subjects() map[string]cryptoutil.DigestSet { + return nil +} + +func (t *TestGitAttestor) BackRefs() map[string]cryptoutil.DigestSet { + return nil +} diff --git a/internal/attestors/github.go b/internal/attestors/github.go new file mode 100644 index 00000000..38286083 --- /dev/null +++ b/internal/attestors/github.go @@ -0,0 +1,64 @@ +// Copyright 2024 The Witness Contributors +// +// 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 attestors + +import ( + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/attestation/github" + "github.com/in-toto/go-witness/attestation/jwt" + "github.com/in-toto/go-witness/cryptoutil" +) + +var ( + _ github.GitHubAttestor = &TestGitHubAttestor{} +) + +type TestGitHubAttestor struct { + githubAtt github.Attestor +} + +func NewTestGitHubAttestor() *TestGitHubAttestor { + att := github.New() + att.JWT = jwt.New() + return &TestGitHubAttestor{githubAtt: *att} +} + +func (t *TestGitHubAttestor) Name() string { + return t.githubAtt.Name() +} + +func (t *TestGitHubAttestor) Type() string { + return t.githubAtt.Type() +} + +func (t *TestGitHubAttestor) RunType() attestation.RunType { + return t.githubAtt.RunType() +} + +func (t *TestGitHubAttestor) Attest(ctx *attestation.AttestationContext) error { + return nil +} + +func (t *TestGitHubAttestor) Data() *github.Attestor { + return &t.githubAtt +} + +func (t *TestGitHubAttestor) Subjects() map[string]cryptoutil.DigestSet { + return nil +} + +func (t *TestGitHubAttestor) BackRefs() map[string]cryptoutil.DigestSet { + return nil +} diff --git a/internal/attestors/gitlab.go b/internal/attestors/gitlab.go new file mode 100644 index 00000000..0127a1f6 --- /dev/null +++ b/internal/attestors/gitlab.go @@ -0,0 +1,64 @@ +// Copyright 2024 The Witness Contributors +// +// 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 attestors + +import ( + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/attestation/gitlab" + "github.com/in-toto/go-witness/attestation/jwt" + "github.com/in-toto/go-witness/cryptoutil" +) + +var ( + _ gitlab.GitLabAttestor = &TestGitLabAttestor{} +) + +type TestGitLabAttestor struct { + gitlabAtt gitlab.Attestor +} + +func NewTestGitLabAttestor() *TestGitLabAttestor { + att := gitlab.Attestor{} + att.JWT = jwt.New() + return &TestGitLabAttestor{gitlabAtt: att} +} + +func (t *TestGitLabAttestor) Name() string { + return t.gitlabAtt.Name() +} + +func (t *TestGitLabAttestor) Type() string { + return t.gitlabAtt.Type() +} + +func (t *TestGitLabAttestor) RunType() attestation.RunType { + return t.gitlabAtt.RunType() +} + +func (t *TestGitLabAttestor) Attest(ctx *attestation.AttestationContext) error { + return nil +} + +func (t *TestGitLabAttestor) Data() *gitlab.Attestor { + return &t.gitlabAtt +} + +func (t *TestGitLabAttestor) Subjects() map[string]cryptoutil.DigestSet { + return nil +} + +func (t *TestGitLabAttestor) BackRefs() map[string]cryptoutil.DigestSet { + return nil +} diff --git a/internal/attestors/material.go b/internal/attestors/material.go new file mode 100644 index 00000000..57fc66b0 --- /dev/null +++ b/internal/attestors/material.go @@ -0,0 +1,60 @@ +// Copyright 2024 The Witness Contributors +// +// 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 attestors + +import ( + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/attestation/material" + "github.com/in-toto/go-witness/cryptoutil" +) + +var ( + _ material.MaterialAttestor = &TestMaterialAttestor{} +) + +type TestMaterialAttestor struct { + matAtt *material.Attestor + materials map[string]cryptoutil.DigestSet +} + +func NewTestMaterialAttestor() *TestMaterialAttestor { + att := material.New() + mat := make(map[string]cryptoutil.DigestSet) + return &TestMaterialAttestor{matAtt: att, materials: mat} +} + +func (t *TestMaterialAttestor) Name() string { + return t.matAtt.Name() +} + +func (t *TestMaterialAttestor) Type() string { + return t.matAtt.Type() +} + +func (t *TestMaterialAttestor) RunType() attestation.RunType { + return t.matAtt.RunType() +} + +func (t *TestMaterialAttestor) Attest(ctx *attestation.AttestationContext) error { + return nil +} + +func (t *TestMaterialAttestor) Materials() map[string]cryptoutil.DigestSet { + return t.materials +} + +func (t *TestMaterialAttestor) SetMaterials(mats map[string]cryptoutil.DigestSet) { + t.materials = mats +} diff --git a/internal/attestors/oci.go b/internal/attestors/oci.go new file mode 100644 index 00000000..6f92841e --- /dev/null +++ b/internal/attestors/oci.go @@ -0,0 +1,62 @@ +// Copyright 2024 The Witness Contributors +// +// 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 attestors + +import ( + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/attestation/oci" + "github.com/in-toto/go-witness/cryptoutil" +) + +var ( + _ oci.OCIAttestor = &TestOCIAttestor{} +) + +type TestOCIAttestor struct { + ociAtt oci.Attestor +} + +func NewTestOCIAttestor() *TestOCIAttestor { + att := oci.New() + return &TestOCIAttestor{ociAtt: *att} +} + +func (t *TestOCIAttestor) Name() string { + return t.ociAtt.Name() +} + +func (t *TestOCIAttestor) Type() string { + return t.ociAtt.Type() +} + +func (t *TestOCIAttestor) RunType() attestation.RunType { + return t.ociAtt.RunType() +} + +func (t *TestOCIAttestor) Attest(ctx *attestation.AttestationContext) error { + return nil +} + +func (t *TestOCIAttestor) Data() *oci.Attestor { + return &t.ociAtt +} + +func (t *TestOCIAttestor) Subjects() map[string]cryptoutil.DigestSet { + return nil +} + +func (t *TestOCIAttestor) BackRefs() map[string]cryptoutil.DigestSet { + return nil +} diff --git a/internal/attestors/product.go b/internal/attestors/product.go new file mode 100644 index 00000000..93299bd7 --- /dev/null +++ b/internal/attestors/product.go @@ -0,0 +1,58 @@ +// Copyright 2024 The Witness Contributors +// +// 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 attestors + +import ( + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/attestation/product" + "github.com/in-toto/go-witness/cryptoutil" +) + +var ( + _ product.ProductAttestor = &TestProductAttestor{} +) + +type TestProductAttestor struct { + prodAtt product.ProductAttestor +} + +func NewTestProductAttestor() *TestProductAttestor { + att := product.New() + return &TestProductAttestor{prodAtt: att} +} + +func (t *TestProductAttestor) Name() string { + return t.prodAtt.Name() +} + +func (t *TestProductAttestor) Type() string { + return t.prodAtt.Type() +} + +func (t *TestProductAttestor) RunType() attestation.RunType { + return t.prodAtt.RunType() +} + +func (t *TestProductAttestor) Attest(ctx *attestation.AttestationContext) error { + return nil +} + +func (t *TestProductAttestor) Subjects() map[string]cryptoutil.DigestSet { + return nil +} + +func (t *TestProductAttestor) Products() map[string]attestation.Product { + return nil +} diff --git a/run.go b/run.go index 63cfdfba..b904999e 100644 --- a/run.go +++ b/run.go @@ -24,6 +24,7 @@ import ( "github.com/in-toto/go-witness/cryptoutil" "github.com/in-toto/go-witness/dsse" "github.com/in-toto/go-witness/intoto" + "github.com/in-toto/go-witness/log" "github.com/in-toto/go-witness/timestamp" ) @@ -82,9 +83,23 @@ func RunWithSigners(signers ...cryptoutil.Signer) RunOption { type RunResult struct { Collection attestation.Collection SignedEnvelope dsse.Envelope + AttestorName string } +// Deprecated: Use RunWithExports instead func Run(stepName string, opts ...RunOption) (RunResult, error) { + results, err := run(stepName, opts) + if len(results) > 1 { + return RunResult{}, errors.New("expected a single result, got multiple") + } + return results[0], err +} + +func RunWithExports(stepName string, opts ...RunOption) ([]RunResult, error) { + return run(stepName, opts) +} + +func run(stepName string, opts []RunOption) ([]RunResult, error) { ro := runOptions{ stepName: stepName, insecure: false, @@ -94,12 +109,12 @@ func Run(stepName string, opts ...RunOption) (RunResult, error) { opt(&ro) } - result := RunResult{} + result := []RunResult{} if err := validateRunOpts(ro); err != nil { return result, err } - runCtx, err := attestation.NewContext(ro.attestors, ro.attestationOpts...) + runCtx, err := attestation.NewContext(stepName, ro.attestors, ro.attestationOpts...) if err != nil { return result, fmt.Errorf("failed to create attestation context: %w", err) } @@ -112,6 +127,20 @@ func Run(stepName string, opts ...RunOption) (RunResult, error) { for _, r := range runCtx.CompletedAttestors() { if r.Error != nil { errs = append(errs, r.Error) + } else { + if exporter, ok := r.Attestor.(attestation.Exporter); ok { + if !exporter.Export() { + log.Debugf("%s attestor not configured to be exported as its own attestation", r.Attestor.Name()) + continue + } + if subjecter, ok := r.Attestor.(attestation.Subjecter); ok { + envelope, err := createAndSignEnvelope(r.Attestor, r.Attestor.Type(), subjecter.Subjects(), dsse.SignWithSigners(ro.signers...), dsse.SignWithTimestampers(ro.timestampers...)) + if err != nil { + return result, fmt.Errorf("failed to sign envelope: %w", err) + } + result = append(result, RunResult{SignedEnvelope: envelope, AttestorName: r.Attestor.Name()}) + } + } } } @@ -120,14 +149,15 @@ func Run(stepName string, opts ...RunOption) (RunResult, error) { return result, errors.Join(errs...) } - result.Collection = attestation.NewCollection(ro.stepName, runCtx.CompletedAttestors()) - + var collectionResult RunResult + collectionResult.Collection = attestation.NewCollection(ro.stepName, runCtx.CompletedAttestors()) if !ro.insecure { - result.SignedEnvelope, err = signCollection(result.Collection, dsse.SignWithSigners(ro.signers...), dsse.SignWithTimestampers(ro.timestampers...)) + collectionResult.SignedEnvelope, err = createAndSignEnvelope(collectionResult.Collection, attestation.CollectionType, collectionResult.Collection.Subjects(), dsse.SignWithSigners(ro.signers...), dsse.SignWithTimestampers(ro.timestampers...)) if err != nil { return result, fmt.Errorf("failed to sign collection: %w", err) } } + result = append(result, collectionResult) return result, nil } @@ -144,13 +174,13 @@ func validateRunOpts(ro runOptions) error { return nil } -func signCollection(collection attestation.Collection, opts ...dsse.SignOption) (dsse.Envelope, error) { - data, err := json.Marshal(&collection) +func createAndSignEnvelope(predicate interface{}, predType string, subjects map[string]cryptoutil.DigestSet, opts ...dsse.SignOption) (dsse.Envelope, error) { + data, err := json.Marshal(&predicate) if err != nil { return dsse.Envelope{}, err } - stmt, err := intoto.NewStatement(attestation.CollectionType, data, collection.Subjects()) + stmt, err := intoto.NewStatement(predType, data, subjects) if err != nil { return dsse.Envelope{}, err }