From 87975b4168e02adbb199a5fd54c5b56e3f3c3159 Mon Sep 17 00:00:00 2001 From: John Kjell Date: Thu, 9 May 2024 10:26:14 -0500 Subject: [PATCH] Link & SLSA attestor (#149) * Initial link attestor Signed-off-by: John Kjell * refactor: move gitoid code to cyrptoutil, use digestvalue everywhere (#139) When the functionality to calculate gitoids was added, there was a bit of tech debt incurred since they didn't implement hash.Hash. This remedies this with an admitedly hacky implementation of hash.Hash that wraps the gitoid code. This also standardizes our cryptoutil fucntions around the DigestValue struct that was added around this time to differentiate between gitoids and regular hash functions. Signed-off-by: Mikhail Swift Signed-off-by: John Kjell * chore: bump actions/upload-artifact from 4.2.0 to 4.3.0 (#142) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/694cdabd8bdb0f10b2cea11669e1bf5453eed0a6...26f96dfa697d77e81fd5907df203aa23a56210a8) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: John Kjell * chore: bump github/codeql-action from 3.23.1 to 3.23.2 (#143) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.23.1 to 3.23.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/0b21cf2492b6b02c465a3e5d7c473717ad7721ba...b7bf0a3ed3ecfa44160715d7c442788f65f0f923) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tom Meadows Signed-off-by: John Kjell * Adding job to auto cut releases (#141) adding job to auto cut releases Signed-off-by: chaosinthecrd Signed-off-by: John Kjell * fixing error in github actions workflow (#147) fixing error in workflow Signed-off-by: chaosinthecrd Signed-off-by: John Kjell * RunAttestors refactor (#131) * improving run attestors Signed-off-by: chaosinthecrd * finalising changes. Signed-off-by: chaosinthecrd * improving run attestors Signed-off-by: chaosinthecrd * finalising changes. Signed-off-by: chaosinthecrd * addressing review, restoring run type order Signed-off-by: chaosinthecrd * updating error handling logic Signed-off-by: chaosinthecrd * updating to go 1.21 for errors.Join Signed-off-by: chaosinthecrd --------- Signed-off-by: chaosinthecrd Signed-off-by: Tom Meadows Signed-off-by: John Kjell * Adding workaround due to failing workflows (#145) adding workaround due to failing workflows Signed-off-by: chaosinthecrd Signed-off-by: John Kjell * Checking policy signature against cert constraints (#144) * adding logic so policy signature can be checked against constraints * threaded options into policy validation functionary --------- Signed-off-by: chaosinthecrd Signed-off-by: John Kjell Co-authored-by: John Kjell Signed-off-by: John Kjell * [StepSecurity] ci: Harden GitHub Actions (#148) Signed-off-by: StepSecurity Bot Signed-off-by: John Kjell * Add import for init and export variables Signed-off-by: John Kjell * Add mulitple results to run to allow exporting attestors to indivudal files Signed-off-by: John Kjell * Add collection to result array Signed-off-by: John Kjell * Replace export parameters in run with attestor option Signed-off-by: John Kjell * Fix golang lint isues Signed-off-by: John Kjell * Update link attestor testing Signed-off-by: John Kjell * Add SLSA attestor Signed-off-by: John Kjell * Add interface for product attestor Signed-off-by: John Kjell * Add more attestor interfaces Signed-off-by: John Kjell * Address some review feedback, licenses, and golanglint Signed-off-by: John Kjell * More golangcilint errors Signed-off-by: John Kjell * WIP - Improve testing interfaces for exposing data fields Signed-off-by: John Kjell * added changes * adding changes to merge into main PR * Link attestor proposed changes (#204) * unmarshal the time in the attestation collection correctly (#203) * add StepName to AttestorContext * use CollectionAttestion to properly set start/end times --------- Signed-off-by: John Kjell Co-authored-by: Cole Kennedy Co-authored-by: Cole Co-authored-by: John Kjell * Passing SLSA Attest tests for GitHub and GitLab Signed-off-by: John Kjell * Clean up Signed-off-by: John Kjell * Add attestation test for link attestor Signed-off-by: John Kjell * Add data function for git interface and remove unused code Signed-off-by: John Kjell * adding warning mesage for slsa attestor Signed-off-by: chaosinthecrd * Try to gracefully handle gitlab jwt Signed-off-by: John Kjell * ran go mod tidy Signed-off-by: chaosinthecrd * ensuring link and slsa attestation exporting is optional Signed-off-by: chaosinthecrd --------- Signed-off-by: John Kjell Signed-off-by: Mikhail Swift Signed-off-by: dependabot[bot] Signed-off-by: chaosinthecrd Signed-off-by: Tom Meadows Signed-off-by: StepSecurity Bot Co-authored-by: Mikhail Swift Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tom Meadows Co-authored-by: StepSecurity Bot Co-authored-by: Cole Kennedy Co-authored-by: Cole --- attestation/aws-iid/aws-iid_test.go | 5 +- attestation/commandrun/commandrun.go | 14 + attestation/context.go | 8 +- attestation/environment/environment.go | 14 + attestation/environment/environment_test.go | 2 +- attestation/factory.go | 5 + attestation/git/git.go | 30 ++ attestation/git/git_test.go | 4 +- attestation/github/github.go | 30 +- attestation/gitlab/gitlab.go | 26 +- attestation/link/link.go | 154 ++++++++++ attestation/link/link_test.go | 221 ++++++++++++++ attestation/material/material.go | 12 + attestation/maven/maven_test.go | 2 +- attestation/oci/oci.go | 12 + attestation/oci/oci_test.go | 2 +- attestation/product/product.go | 28 +- attestation/product/product_test.go | 12 +- attestation/slsa/slsa.go | 264 ++++++++++++++++ attestation/slsa/slsa_test.go | 318 ++++++++++++++++++++ go.mod | 6 +- go.sum | 12 +- imports.go | 2 + internal/attestors/commandrun.go | 58 ++++ internal/attestors/environment.go | 53 ++++ internal/attestors/git.go | 62 ++++ internal/attestors/github.go | 64 ++++ internal/attestors/gitlab.go | 64 ++++ internal/attestors/material.go | 60 ++++ internal/attestors/oci.go | 62 ++++ internal/attestors/product.go | 58 ++++ run.go | 46 ++- 32 files changed, 1667 insertions(+), 43 deletions(-) create mode 100644 attestation/link/link.go create mode 100644 attestation/link/link_test.go create mode 100644 attestation/slsa/slsa.go create mode 100644 attestation/slsa/slsa_test.go create mode 100644 internal/attestors/commandrun.go create mode 100644 internal/attestors/environment.go create mode 100644 internal/attestors/git.go create mode 100644 internal/attestors/github.go create mode 100644 internal/attestors/gitlab.go create mode 100644 internal/attestors/material.go create mode 100644 internal/attestors/oci.go create mode 100644 internal/attestors/product.go 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 }