Merge pull request #8949 from steiza/steiza/multi-attestation
Add support to `attestation` command for more predicate types.
This commit is contained in:
commit
a42450e9a3
9 changed files with 112 additions and 44 deletions
2
go.mod
2
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type Options struct {
|
|||
Store MetadataStore
|
||||
OCIClient oci.Client
|
||||
Owner string
|
||||
PredicateType string
|
||||
Repo string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ type Options struct {
|
|||
NoPublicGood bool
|
||||
OIDCIssuer string
|
||||
Owner string
|
||||
PredicateType string
|
||||
Repo string
|
||||
SAN string
|
||||
SANRegex string
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue