Merge pull request #8949 from steiza/steiza/multi-attestation

Add support to `attestation` command for more predicate types.
This commit is contained in:
Andy Feller 2024-04-12 11:12:59 -04:00 committed by GitHub
commit a42450e9a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 112 additions and 44 deletions

2
go.mod
View file

@ -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

View file

@ -20,7 +20,7 @@ func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Comman
opts := &Options{}
downloadCmd := &cobra.Command{
Use: "download [<file-path> | oci://<image-uri>] [--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 <owner>/<repo>")
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)

View file

@ -22,6 +22,7 @@ type Options struct {
Store MetadataStore
OCIClient oci.Client
Owner string
PredicateType string
Repo string
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -23,6 +23,7 @@ type Options struct {
NoPublicGood bool
OIDCIssuer string
Owner string
PredicateType string
Repo string
SAN string
SANRegex string

View file

@ -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"
)

View file

@ -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 [<file-path> | oci://<image-uri>] [--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 <owner>/<repo>")
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
}

View file

@ -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)
}