diff --git a/go.mod b/go.mod index e779ae75f..ab4d519a2 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,6 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.3.0 github.com/henvic/httpretty v0.1.3 - github.com/in-toto/in-toto-golang v0.9.0 github.com/joho/godotenv v1.5.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-colorable v0.1.13 @@ -97,6 +96,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/in-toto/in-toto-golang v0.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/gojq v0.12.15 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect diff --git a/pkg/cmd/attestation/download/download.go b/pkg/cmd/attestation/download/download.go index 2f40970fc..7154a6edd 100644 --- a/pkg/cmd/attestation/download/download.go +++ b/pkg/cmd/attestation/download/download.go @@ -20,7 +20,7 @@ func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Comman opts := &Options{} downloadCmd := &cobra.Command{ Use: "download [ | oci://] [--owner | --repo]", - Args: cobra.ExactArgs(1), + Args: cmdutil.MinimumArgs(1, "must specify file path or container image URI, as well as one of --owner or --repo"), Short: "Download an artifact's Sigstore bundle(s) for offline use", Long: heredoc.Docf(` Download an artifact's attestations, aka Sigstore bundle(s), for offline use. @@ -102,6 +102,7 @@ func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Comman downloadCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format /") downloadCmd.MarkFlagsMutuallyExclusive("owner", "repo") downloadCmd.MarkFlagsOneRequired("owner", "repo") + downloadCmd.Flags().StringVarP(&opts.PredicateType, "predicate-type", "", "", "Filter attestations by provided predicate type") cmdutil.StringEnumFlag(downloadCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact") downloadCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch") @@ -132,6 +133,17 @@ func runDownload(opts *Options) error { return fmt.Errorf("failed to fetch attestations: %v", err) } + // Apply predicate type filter to returned attestations + if opts.PredicateType != "" { + filteredAttestations := verification.FilterAttestations(opts.PredicateType, attestations) + + if len(filteredAttestations) == 0 { + return fmt.Errorf("no attestations found with predicate type: %s", opts.PredicateType) + } + + attestations = filteredAttestations + } + metadataFilePath, err := opts.Store.createMetadataFile(artifact.DigestWithAlg(), attestations) if err != nil { return fmt.Errorf("failed to write attestation: %v", err) diff --git a/pkg/cmd/attestation/download/options.go b/pkg/cmd/attestation/download/options.go index 6a64ecc30..cc1c4d3b5 100644 --- a/pkg/cmd/attestation/download/options.go +++ b/pkg/cmd/attestation/download/options.go @@ -22,6 +22,7 @@ type Options struct { Store MetadataStore OCIClient oci.Client Owner string + PredicateType string Repo string } diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 7fb4a1615..b131b7acd 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -2,6 +2,7 @@ package verification import ( "bufio" + "encoding/json" "errors" "fmt" "os" @@ -113,3 +114,31 @@ func GetRemoteAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error } return nil, fmt.Errorf("owner or repo must be provided") } + +type IntotoStatement struct { + PredicateType string `json:"predicateType"` +} + +func FilterAttestations(predicateType string, attestations []*api.Attestation) []*api.Attestation { + filteredAttestations := []*api.Attestation{} + + for _, each := range attestations { + dsseEnvelope := each.Bundle.GetDsseEnvelope() + if dsseEnvelope != nil { + if dsseEnvelope.PayloadType != "application/vnd.in-toto+json" { + // Don't fail just because an entry isn't intoto + continue + } + var intotoStatement IntotoStatement + if err := json.Unmarshal([]byte(dsseEnvelope.Payload), &intotoStatement); err != nil { + // Don't fail just because a single entry can't be unmarshalled + continue + } + if intotoStatement.PredicateType == predicateType { + filteredAttestations = append(filteredAttestations, each) + } + } + } + + return filteredAttestations +} diff --git a/pkg/cmd/attestation/verification/attestation_test.go b/pkg/cmd/attestation/verification/attestation_test.go index b178a0682..ef7c7d879 100644 --- a/pkg/cmd/attestation/verification/attestation_test.go +++ b/pkg/cmd/attestation/verification/attestation_test.go @@ -3,7 +3,12 @@ package verification import ( "testing" + protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + dsse "github.com/sigstore/protobuf-specs/gen/pb-go/dsse" + "github.com/sigstore/sigstore-go/pkg/bundle" "github.com/stretchr/testify/require" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" ) func TestLoadBundlesFromJSONLinesFile(t *testing.T) { @@ -47,3 +52,51 @@ func TestGetLocalAttestations(t *testing.T) { require.Nil(t, attestations) }) } + +func TestFilterAttestations(t *testing.T) { + attestations := []*api.Attestation{ + { + Bundle: &bundle.ProtobufBundle{ + Bundle: &protobundle.Bundle{ + Content: &protobundle.Bundle_DsseEnvelope{ + DsseEnvelope: &dsse.Envelope{ + PayloadType: "application/vnd.in-toto+json", + Payload: []byte("{\"predicateType\": \"https://slsa.dev/provenance/v1\"}"), + }, + }, + }, + }, + }, + { + Bundle: &bundle.ProtobufBundle{ + Bundle: &protobundle.Bundle{ + Content: &protobundle.Bundle_DsseEnvelope{ + DsseEnvelope: &dsse.Envelope{ + PayloadType: "application/vnd.something-other-than-in-toto+json", + Payload: []byte("{\"predicateType\": \"https://slsa.dev/provenance/v1\"}"), + }, + }, + }, + }, + }, + { + Bundle: &bundle.ProtobufBundle{ + Bundle: &protobundle.Bundle{ + Content: &protobundle.Bundle_DsseEnvelope{ + DsseEnvelope: &dsse.Envelope{ + PayloadType: "application/vnd.in-toto+json", + Payload: []byte("{\"predicateType\": \"https://spdx.dev/Document/v2.3\"}"), + }, + }, + }, + }, + }, + } + + filtered := FilterAttestations("https://slsa.dev/provenance/v1", attestations) + + require.Len(t, filtered, 1) + + filtered = FilterAttestations("NonExistantPredicate", attestations) + require.Len(t, filtered, 0) +} diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index 62735df54..bfa03f16e 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -23,6 +23,7 @@ type Options struct { NoPublicGood bool OIDCIssuer string Owner string + PredicateType string Repo string SAN string SANRegex string diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index 83d1f62a7..f96ca4e8a 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -11,8 +11,7 @@ import ( ) const ( - GitHubOIDCIssuer = "https://token.actions.githubusercontent.com" - SLSAPredicateType = "https://slsa.dev/provenance/v1" + GitHubOIDCIssuer = "https://token.actions.githubusercontent.com" // represents the GitHub hosted runner in the certificate RunnerEnvironment extension GitHubRunner = "github-hosted" ) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index e1ad77b1e..df604b9de 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -16,13 +16,11 @@ import ( "github.com/spf13/cobra" ) -var ErrNoMatchingSLSAPredicate = fmt.Errorf("the attestation does not have the expected SLSA predicate type: %s", SLSAPredicateType) - func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command { opts := &Options{} verifyCmd := &cobra.Command{ Use: "verify [ | oci://] [--owner | --repo]", - Args: cobra.ExactArgs(1), + Args: cmdutil.MinimumArgs(1, "must specify file path or container image URI, as well as one of --owner or --repo"), Short: "Verify an artifact's integrity using attestations", Long: heredoc.Docf(` Verify the integrity and provenance of an artifact using its associated @@ -132,6 +130,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format /") verifyCmd.MarkFlagsMutuallyExclusive("owner", "repo") verifyCmd.MarkFlagsOneRequired("owner", "repo") + verifyCmd.Flags().StringVarP(&opts.PredicateType, "predicate-type", "", "", "Filter attestations by provided predicate type") verifyCmd.Flags().BoolVarP(&opts.NoPublicGood, "no-public-good", "", false, "Only verify attestations signed with GitHub's Sigstore instance") verifyCmd.Flags().StringVarP(&opts.CustomTrustedRoot, "custom-trusted-root", "", "", "Path to a custom trustedroot.json file to use for verification") verifyCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch") @@ -170,6 +169,17 @@ func runVerify(opts *Options) error { return fmt.Errorf("failed to fetch attestations for subject: %s", artifact.DigestWithAlg()) } + // Apply predicate type filter to returned attestations + if opts.PredicateType != "" { + filteredAttestations := verification.FilterAttestations(opts.PredicateType, attestations) + + if len(filteredAttestations) == 0 { + return fmt.Errorf("no attestations found with predicate type: %s", opts.PredicateType) + } + + attestations = filteredAttestations + } + policy, err := buildVerifyPolicy(opts, *artifact) if err != nil { return fmt.Errorf("failed to build policy: %v", err) @@ -184,11 +194,6 @@ func runVerify(opts *Options) error { "Successfully verified all attestations against Sigstore!\n", )) - // Try verifying the attestation's predicate type against the expect SLSA predicate type - if err = verifySLSAPredicateType(opts.Logger, sigstoreRes.VerifyResults); err != nil { - return fmt.Errorf("at least one attestation failed to verify predicate type verification: %v", err) - } - opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green("Successfully verified the SLSA predicate type of all attestations!\n")) opts.Logger.Println(opts.Logger.ColorScheme.Green("All attestations have been successfully verified!")) @@ -203,15 +208,3 @@ func runVerify(opts *Options) error { // All attestations passed verification and policy evaluation return nil } - -func verifySLSAPredicateType(logger *io.Handler, apr []*verification.AttestationProcessingResult) error { - logger.VerbosePrint("Evaluating attestations have valid SLSA predicate type") - - for _, result := range apr { - if result.VerificationResult.Statement.PredicateType != SLSAPredicateType { - return ErrNoMatchingSLSAPredicate - } - } - - return nil -} diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go index 1959ed40c..f5d034541 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -19,9 +19,6 @@ import ( "github.com/cli/cli/v2/pkg/iostreams" "github.com/stretchr/testify/assert" - "github.com/in-toto/in-toto-golang/in_toto" - "github.com/sigstore/sigstore-go/pkg/verify" - "github.com/stretchr/testify/require" ) @@ -418,20 +415,3 @@ func TestRunVerify(t *testing.T) { require.Error(t, runVerify(&customOpts)) }) } - -func TestVerifySLSAPredicateType_InvalidPredicate(t *testing.T) { - statement := &in_toto.Statement{} - statement.PredicateType = "some-other-predicate-type" - - apr := []*verification.AttestationProcessingResult{ - { - VerificationResult: &verify.VerificationResult{ - Statement: statement, - }, - }, - } - - err := verifySLSAPredicateType(io.NewTestHandler(), apr) - require.Error(t, err) - require.ErrorIs(t, err, ErrNoMatchingSLSAPredicate) -}