Merge pull request #9421 from cli/eugene/attestation/fetch-oci-bundle
Fetch bundle from OCI registry for verify
This commit is contained in:
commit
ef9069a1b0
10 changed files with 329 additions and 56 deletions
|
|
@ -7,6 +7,8 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
|
||||
)
|
||||
|
||||
|
|
@ -22,6 +24,7 @@ type DigestedArtifact struct {
|
|||
URL string
|
||||
digest string
|
||||
digestAlg string
|
||||
nameRef name.Reference
|
||||
}
|
||||
|
||||
func normalizeReference(reference string, pathSeparator rune) (normalized string, artifactType artifactType, err error) {
|
||||
|
|
@ -77,3 +80,7 @@ func (a *DigestedArtifact) Algorithm() string {
|
|||
func (a *DigestedArtifact) DigestWithAlg() string {
|
||||
return fmt.Sprintf("%s:%s", a.digestAlg, a.digest)
|
||||
}
|
||||
|
||||
func (a *DigestedArtifact) NameRef() name.Reference {
|
||||
return a.nameRef
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtif
|
|||
return nil, fmt.Errorf("artifact %s is not a valid registry reference: %v", url, err)
|
||||
}
|
||||
|
||||
digest, err := client.GetImageDigest(named.String())
|
||||
digest, nameRef, err := client.GetImageDigest(named.String())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -24,5 +25,6 @@ func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtif
|
|||
URL: fmt.Sprintf("oci://%s", named.String()),
|
||||
digest: digest.Hex,
|
||||
digestAlg: digest.Algorithm,
|
||||
nameRef: nameRef,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,16 @@ package oci
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/sigstore/sigstore-go/pkg/bundle"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
||||
)
|
||||
|
|
@ -15,7 +21,8 @@ var ErrDenied = errors.New("the provided token was denied access to the requeste
|
|||
var ErrRegistryAuthz = errors.New("remote registry authorization failed, please authenticate with the registry and try again")
|
||||
|
||||
type Client interface {
|
||||
GetImageDigest(imgName string) (*v1.Hash, error)
|
||||
GetImageDigest(imgName string) (*v1.Hash, name.Reference, error)
|
||||
GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error)
|
||||
}
|
||||
|
||||
func checkForUnauthorizedOrDeniedErr(err transport.Error) error {
|
||||
|
|
@ -35,13 +42,16 @@ type LiveClient struct {
|
|||
get func(name.Reference, ...remote.Option) (*remote.Descriptor, error)
|
||||
}
|
||||
|
||||
func (c LiveClient) ParseReference(ref string) (name.Reference, error) {
|
||||
return c.parseReference(ref)
|
||||
}
|
||||
|
||||
// where name is formed like ghcr.io/github/my-image-repo
|
||||
func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, error) {
|
||||
func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) {
|
||||
name, err := c.parseReference(imgName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create image tag: %v", err)
|
||||
return nil, nil, fmt.Errorf("failed to create image tag: %v", err)
|
||||
}
|
||||
|
||||
// The user must already be authenticated with the container registry
|
||||
// The authn.DefaultKeychain argument indicates that Get should checks the
|
||||
// user's configuration for the registry credentials
|
||||
|
|
@ -50,13 +60,92 @@ func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, error) {
|
|||
var transportErr *transport.Error
|
||||
if errors.As(err, &transportErr) {
|
||||
if accessErr := checkForUnauthorizedOrDeniedErr(*transportErr); accessErr != nil {
|
||||
return nil, accessErr
|
||||
return nil, nil, accessErr
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("failed to fetch remote image: %v", err)
|
||||
return nil, nil, fmt.Errorf("failed to fetch remote image: %v", err)
|
||||
}
|
||||
|
||||
return &desc.Digest, nil
|
||||
return &desc.Digest, name, nil
|
||||
}
|
||||
|
||||
type noncompliantRegistryTransport struct{}
|
||||
|
||||
// RoundTrip will check if a request and associated response fulfill the following:
|
||||
// 1. The response returns a 406 status code
|
||||
// 2. The request path contains /referrers/
|
||||
// If both conditions are met, the response's status code will be overwritten to 404
|
||||
// This is a temporary solution to handle non compliant registries that return
|
||||
// an unexpected status code 406 when the go-containerregistry library used
|
||||
// by this code attempts to make a request to the referrers API.
|
||||
// The go-containerregistry library can handle 404 response but not a 406 response.
|
||||
// See the related go-containerregistry issue: https://github.com/google/go-containerregistry/issues/1962
|
||||
func (a *noncompliantRegistryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
resp, err := http.DefaultTransport.RoundTrip(req)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
if resp.StatusCode == http.StatusNotAcceptable && strings.Contains(req.URL.Path, "/referrers/") {
|
||||
resp.StatusCode = http.StatusNotFound
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c LiveClient) GetAttestations(ref name.Reference, digest string) ([]*api.Attestation, error) {
|
||||
attestations := make([]*api.Attestation, 0)
|
||||
|
||||
transportOpts := []remote.Option{remote.WithTransport(&noncompliantRegistryTransport{}), remote.WithAuthFromKeychain(authn.DefaultKeychain)}
|
||||
referrers, err := remote.Referrers(ref.Context().Digest(digest), transportOpts...)
|
||||
if err != nil {
|
||||
return attestations, fmt.Errorf("error getting referrers: %w", err)
|
||||
}
|
||||
refManifest, err := referrers.IndexManifest()
|
||||
if err != nil {
|
||||
return attestations, fmt.Errorf("error getting referrers manifest: %w", err)
|
||||
}
|
||||
|
||||
for _, refDesc := range refManifest.Manifests {
|
||||
if !strings.HasPrefix(refDesc.ArtifactType, "application/vnd.dev.sigstore.bundle") {
|
||||
continue
|
||||
}
|
||||
|
||||
refImg, err := remote.Image(ref.Context().Digest(refDesc.Digest.String()), remote.WithAuthFromKeychain(authn.DefaultKeychain))
|
||||
if err != nil {
|
||||
return attestations, fmt.Errorf("error getting referrer image: %w", err)
|
||||
}
|
||||
layers, err := refImg.Layers()
|
||||
if err != nil {
|
||||
return attestations, fmt.Errorf("error getting referrer image: %w", err)
|
||||
}
|
||||
|
||||
if len(layers) > 0 {
|
||||
layer0, err := layers[0].Uncompressed()
|
||||
if err != nil {
|
||||
return attestations, fmt.Errorf("error getting referrer image: %w", err)
|
||||
}
|
||||
defer layer0.Close()
|
||||
|
||||
bundleBytes, err := io.ReadAll(layer0)
|
||||
|
||||
if err != nil {
|
||||
return attestations, fmt.Errorf("error getting referrer image: %w", err)
|
||||
}
|
||||
|
||||
b := &bundle.ProtobufBundle{}
|
||||
err = b.UnmarshalJSON(bundleBytes)
|
||||
|
||||
if err != nil {
|
||||
return attestations, fmt.Errorf("error unmarshalling bundle: %w", err)
|
||||
}
|
||||
|
||||
a := api.Attestation{Bundle: b}
|
||||
attestations = append(attestations, &a)
|
||||
} else {
|
||||
return attestations, fmt.Errorf("error getting referrer image: no layers found")
|
||||
}
|
||||
}
|
||||
return attestations, nil
|
||||
}
|
||||
|
||||
// Unlike other parts of this command set, we cannot pass a custom HTTP client
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
||||
|
||||
|
|
@ -30,9 +30,10 @@ func TestGetImageDigest_Success(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
digest, err := c.GetImageDigest("test")
|
||||
digest, nameRef, err := c.GetImageDigest("test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &expectedDigest, digest)
|
||||
require.Equal(t, name.Tag{}, nameRef)
|
||||
}
|
||||
|
||||
func TestGetImageDigest_ReferenceFail(t *testing.T) {
|
||||
|
|
@ -45,9 +46,10 @@ func TestGetImageDigest_ReferenceFail(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
digest, err := c.GetImageDigest("test")
|
||||
digest, nameRef, err := c.GetImageDigest("test")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, digest)
|
||||
require.Nil(t, nameRef)
|
||||
}
|
||||
|
||||
func TestGetImageDigest_AuthFail(t *testing.T) {
|
||||
|
|
@ -60,10 +62,11 @@ func TestGetImageDigest_AuthFail(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
digest, err := c.GetImageDigest("test")
|
||||
digest, nameRef, err := c.GetImageDigest("test")
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, ErrRegistryAuthz)
|
||||
require.Nil(t, digest)
|
||||
require.Nil(t, nameRef)
|
||||
}
|
||||
|
||||
func TestGetImageDigest_Denied(t *testing.T) {
|
||||
|
|
@ -76,8 +79,9 @@ func TestGetImageDigest_Denied(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
digest, err := c.GetImageDigest("test")
|
||||
digest, nameRef, err := c.GetImageDigest("test")
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, ErrDenied)
|
||||
require.Nil(t, digest)
|
||||
require.Nil(t, nameRef)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,32 +3,83 @@ package oci
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/test/data"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
func makeTestAttestation() api.Attestation {
|
||||
return api.Attestation{Bundle: data.SigstoreBundle(nil)}
|
||||
}
|
||||
|
||||
type MockClient struct{}
|
||||
|
||||
func (c MockClient) GetImageDigest(imgName string) (*v1.Hash, error) {
|
||||
func (c MockClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) {
|
||||
return &v1.Hash{
|
||||
Hex: "1234567890abcdef",
|
||||
Algorithm: "sha256",
|
||||
}, nil
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func (c MockClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) {
|
||||
att1 := makeTestAttestation()
|
||||
att2 := makeTestAttestation()
|
||||
return []*api.Attestation{&att1, &att2}, nil
|
||||
}
|
||||
|
||||
type ReferenceFailClient struct{}
|
||||
|
||||
func (c ReferenceFailClient) GetImageDigest(imgName string) (*v1.Hash, error) {
|
||||
return nil, fmt.Errorf("failed to parse reference")
|
||||
func (c ReferenceFailClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) {
|
||||
return nil, nil, fmt.Errorf("failed to parse reference")
|
||||
}
|
||||
|
||||
func (c ReferenceFailClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type AuthFailClient struct{}
|
||||
|
||||
func (c AuthFailClient) GetImageDigest(imgName string) (*v1.Hash, error) {
|
||||
return nil, ErrRegistryAuthz
|
||||
func (c AuthFailClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) {
|
||||
return nil, nil, ErrRegistryAuthz
|
||||
}
|
||||
|
||||
func (c AuthFailClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type DeniedClient struct{}
|
||||
|
||||
func (c DeniedClient) GetImageDigest(imgName string) (*v1.Hash, error) {
|
||||
return nil, ErrDenied
|
||||
func (c DeniedClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) {
|
||||
return nil, nil, ErrDenied
|
||||
}
|
||||
|
||||
func (c DeniedClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type NoAttestationsClient struct{}
|
||||
|
||||
func (c NoAttestationsClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) {
|
||||
return &v1.Hash{
|
||||
Hex: "1234567890abcdef",
|
||||
Algorithm: "sha256",
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func (c NoAttestationsClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type FailedToFetchAttestationsClient struct{}
|
||||
|
||||
func (c FailedToFetchAttestationsClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) {
|
||||
return &v1.Hash{
|
||||
Hex: "1234567890abcdef",
|
||||
Algorithm: "sha256",
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func (c FailedToFetchAttestationsClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) {
|
||||
return nil, fmt.Errorf("failed to fetch attestations")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
|
||||
"github.com/sigstore/sigstore-go/pkg/bundle"
|
||||
)
|
||||
|
|
@ -16,12 +18,15 @@ import (
|
|||
var ErrUnrecognisedBundleExtension = errors.New("bundle file extension not supported, must be json or jsonl")
|
||||
|
||||
type FetchAttestationsConfig struct {
|
||||
APIClient api.Client
|
||||
BundlePath string
|
||||
Digest string
|
||||
Limit int
|
||||
Owner string
|
||||
Repo string
|
||||
APIClient api.Client
|
||||
BundlePath string
|
||||
Digest string
|
||||
Limit int
|
||||
Owner string
|
||||
Repo string
|
||||
OCIClient oci.Client
|
||||
UseBundleFromRegistry bool
|
||||
NameRef name.Reference
|
||||
}
|
||||
|
||||
func (c *FetchAttestationsConfig) IsBundleProvided() bool {
|
||||
|
|
@ -32,6 +37,11 @@ func GetAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) {
|
|||
if c.IsBundleProvided() {
|
||||
return GetLocalAttestations(c.BundlePath)
|
||||
}
|
||||
|
||||
if c.UseBundleFromRegistry {
|
||||
return GetOCIAttestations(c)
|
||||
}
|
||||
|
||||
return GetRemoteAttestations(c)
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +125,17 @@ func GetRemoteAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error
|
|||
return nil, fmt.Errorf("owner or repo must be provided")
|
||||
}
|
||||
|
||||
func GetOCIAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) {
|
||||
attestations, err := c.OCIClient.GetAttestations(c.NameRef, c.Digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch OCI attestations: %w", err)
|
||||
}
|
||||
if len(attestations) == 0 {
|
||||
return nil, fmt.Errorf("no attestations found in the OCI registry. Retry the command without the --bundle-from-oci flag to check GitHub for the attestation")
|
||||
}
|
||||
return attestations, nil
|
||||
}
|
||||
|
||||
type IntotoStatement struct {
|
||||
PredicateType string `json:"predicateType"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,27 +15,28 @@ import (
|
|||
|
||||
// Options captures the options for the verify command
|
||||
type Options struct {
|
||||
ArtifactPath string
|
||||
BundlePath string
|
||||
Config func() (gh.Config, error)
|
||||
TrustedRoot string
|
||||
DenySelfHostedRunner bool
|
||||
DigestAlgorithm string
|
||||
Limit int
|
||||
NoPublicGood bool
|
||||
OIDCIssuer string
|
||||
Owner string
|
||||
PredicateType string
|
||||
Repo string
|
||||
SAN string
|
||||
SANRegex string
|
||||
SignerRepo string
|
||||
SignerWorkflow string
|
||||
APIClient api.Client
|
||||
Logger *io.Handler
|
||||
OCIClient oci.Client
|
||||
SigstoreVerifier verification.SigstoreVerifier
|
||||
exporter cmdutil.Exporter
|
||||
ArtifactPath string
|
||||
BundlePath string
|
||||
UseBundleFromRegistry bool
|
||||
Config func() (gh.Config, error)
|
||||
TrustedRoot string
|
||||
DenySelfHostedRunner bool
|
||||
DigestAlgorithm string
|
||||
Limit int
|
||||
NoPublicGood bool
|
||||
OIDCIssuer string
|
||||
Owner string
|
||||
PredicateType string
|
||||
Repo string
|
||||
SAN string
|
||||
SANRegex string
|
||||
SignerRepo string
|
||||
SignerWorkflow string
|
||||
APIClient api.Client
|
||||
Logger *io.Handler
|
||||
OCIClient oci.Client
|
||||
SigstoreVerifier verification.SigstoreVerifier
|
||||
exporter cmdutil.Exporter
|
||||
}
|
||||
|
||||
// Clean cleans the file path option values
|
||||
|
|
@ -83,6 +84,16 @@ func (opts *Options) AreFlagsValid() error {
|
|||
return fmt.Errorf("limit %d not allowed, must be between 1 and 1000", opts.Limit)
|
||||
}
|
||||
|
||||
// Check that the bundle-from-oci flag is only used with OCI artifact paths
|
||||
if opts.UseBundleFromRegistry && !strings.HasPrefix(opts.ArtifactPath, "oci://") {
|
||||
return fmt.Errorf("bundle-from-oci flag can only be used with OCI artifact paths")
|
||||
}
|
||||
|
||||
// Check that both the bundle-from-oci and bundle-path flags are not used together
|
||||
if opts.UseBundleFromRegistry && opts.BundlePath != "" {
|
||||
return fmt.Errorf("bundle-from-oci flag cannot be used with bundle-path flag")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,4 +116,47 @@ func TestSetPolicyFlags(t *testing.T) {
|
|||
require.Equal(t, "sigstore", opts.Owner)
|
||||
require.Equal(t, "^https://github/foo", opts.SANRegex)
|
||||
})
|
||||
|
||||
t.Run("returns error when UseBundleFromRegistry is true and ArtifactPath is not an OCI path", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: publicGoodArtifactPath,
|
||||
DigestAlgorithm: "sha512",
|
||||
Owner: "sigstore",
|
||||
UseBundleFromRegistry: true,
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "bundle-from-oci flag can only be used with OCI artifact paths")
|
||||
})
|
||||
|
||||
t.Run("does not return error when UseBundleFromRegistry is true and ArtifactPath is an OCI path", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: "oci://sigstore/sigstore-js:2.1.0",
|
||||
DigestAlgorithm: "sha512",
|
||||
OIDCIssuer: "some issuer",
|
||||
Owner: "sigstore",
|
||||
UseBundleFromRegistry: true,
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("returns error when UseBundleFromRegistry is true and BundlePath is provided", func(t *testing.T) {
|
||||
opts := Options{
|
||||
ArtifactPath: "oci://sigstore/sigstore-js:2.1.0",
|
||||
BundlePath: publicGoodBundlePath,
|
||||
DigestAlgorithm: "sha512",
|
||||
Owner: "sigstore",
|
||||
UseBundleFromRegistry: true,
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err := opts.AreFlagsValid()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "bundle-from-oci flag cannot be used with bundle-path flag")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
|
|||
// general flags
|
||||
verifyCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles")
|
||||
cmdutil.DisableAuthCheckFlag(verifyCmd.Flags().Lookup("bundle"))
|
||||
verifyCmd.Flags().BoolVarP(&opts.UseBundleFromRegistry, "bundle-from-oci", "", false, "When verifying an OCI image, fetch the attestation bundle from the OCI registry instead of from GitHub")
|
||||
cmdutil.StringEnumFlag(verifyCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact")
|
||||
verifyCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "GitHub organization to scope attestation lookup by")
|
||||
verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format <owner>/<repo>")
|
||||
|
|
@ -182,12 +183,15 @@ func runVerify(opts *Options) error {
|
|||
opts.Logger.Printf("Loaded digest %s for %s\n", artifact.DigestWithAlg(), artifact.URL)
|
||||
|
||||
c := verification.FetchAttestationsConfig{
|
||||
APIClient: opts.APIClient,
|
||||
BundlePath: opts.BundlePath,
|
||||
Digest: artifact.DigestWithAlg(),
|
||||
Limit: opts.Limit,
|
||||
Owner: opts.Owner,
|
||||
Repo: opts.Repo,
|
||||
APIClient: opts.APIClient,
|
||||
BundlePath: opts.BundlePath,
|
||||
Digest: artifact.DigestWithAlg(),
|
||||
Limit: opts.Limit,
|
||||
Owner: opts.Owner,
|
||||
Repo: opts.Repo,
|
||||
OCIClient: opts.OCIClient,
|
||||
UseBundleFromRegistry: opts.UseBundleFromRegistry,
|
||||
NameRef: artifact.NameRef(),
|
||||
}
|
||||
attestations, err := verification.GetAttestations(c)
|
||||
if err != nil {
|
||||
|
|
@ -198,6 +202,8 @@ func runVerify(opts *Options) error {
|
|||
|
||||
if c.IsBundleProvided() {
|
||||
opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ Loading attestations from %s failed\n"), artifact.URL)
|
||||
} else if c.UseBundleFromRegistry {
|
||||
opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Loading attestations from OCI registry failed"))
|
||||
} else {
|
||||
opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Loading attestations from GitHub API failed"))
|
||||
}
|
||||
|
|
@ -207,6 +213,8 @@ func runVerify(opts *Options) error {
|
|||
pluralAttestation := text.Pluralize(len(attestations), "attestation")
|
||||
if c.IsBundleProvided() {
|
||||
opts.Logger.Printf("Loaded %s from %s\n", pluralAttestation, opts.BundlePath)
|
||||
} else if c.UseBundleFromRegistry {
|
||||
opts.Logger.Printf("Loaded %s from %s\n", pluralAttestation, opts.ArtifactPath)
|
||||
} else {
|
||||
opts.Logger.Printf("Loaded %s from GitHub API\n", pluralAttestation)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -463,4 +463,41 @@ func TestRunVerify(t *testing.T) {
|
|||
customOpts.BundlePath = ""
|
||||
require.Error(t, runVerify(&customOpts))
|
||||
})
|
||||
|
||||
t.Run("with valid OCI artifact", func(t *testing.T) {
|
||||
customOpts := publicGoodOpts
|
||||
customOpts.ArtifactPath = "oci://ghcr.io/github/test"
|
||||
customOpts.BundlePath = ""
|
||||
|
||||
require.Nil(t, runVerify(&customOpts))
|
||||
})
|
||||
|
||||
t.Run("with valid OCI artifact with UseBundleFromRegistry flag", func(t *testing.T) {
|
||||
customOpts := publicGoodOpts
|
||||
customOpts.ArtifactPath = "oci://ghcr.io/github/test"
|
||||
customOpts.BundlePath = ""
|
||||
customOpts.UseBundleFromRegistry = true
|
||||
|
||||
require.Nil(t, runVerify(&customOpts))
|
||||
})
|
||||
|
||||
t.Run("with valid OCI artifact with UseBundleFromRegistry flag but no bundle return from registry", func(t *testing.T) {
|
||||
customOpts := publicGoodOpts
|
||||
customOpts.ArtifactPath = "oci://ghcr.io/github/test"
|
||||
customOpts.BundlePath = ""
|
||||
customOpts.UseBundleFromRegistry = true
|
||||
customOpts.OCIClient = oci.NoAttestationsClient{}
|
||||
|
||||
require.ErrorContains(t, runVerify(&customOpts), "no attestations found in the OCI registry. Retry the command without the --bundle-from-oci flag to check GitHub for the attestation")
|
||||
})
|
||||
|
||||
t.Run("with valid OCI artifact with UseBundleFromRegistry flag but fail on fetching bundle from registry", func(t *testing.T) {
|
||||
customOpts := publicGoodOpts
|
||||
customOpts.ArtifactPath = "oci://ghcr.io/github/test"
|
||||
customOpts.BundlePath = ""
|
||||
customOpts.UseBundleFromRegistry = true
|
||||
customOpts.OCIClient = oci.NoAttestationsClient{}
|
||||
|
||||
require.ErrorContains(t, runVerify(&customOpts), "no attestations found in the OCI registry. Retry the command without the --bundle-from-oci flag to check GitHub for the attestation")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue