Support referrers using digest, not just tag (#55)

* Support referrers using digest, not just tag

* ParseRef and switch on type

* Call DigestStr instead of String
This commit is contained in:
James Carnegie
2024-06-17 17:30:12 +01:00
committed by GitHub
parent 0d0d86854c
commit 130e1f640b
2 changed files with 72 additions and 31 deletions

View File

@@ -13,7 +13,9 @@ import (
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -33,6 +35,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
for _, tc := range []struct {
server *httptest.Server
skipSubject bool
useDigest bool
}{
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
@@ -44,6 +47,10 @@ func TestAttestationReferenceTypes(t *testing.T) {
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
skipSubject: true,
},
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
useDigest: true,
},
} {
s := tc.server
defer s.Close()
@@ -65,7 +72,18 @@ func TestAttestationReferenceTypes(t *testing.T) {
for _, platform := range platforms {
// can eval policy in the normal way
resolver, err := oci.NewRegistryAttestationResolver(indexName, platform)
ref := indexName
if tc.useDigest {
options := oci.WithOptions(ctx, nil)
subjectRef, err := name.ParseReference(indexName)
require.NoError(t, err)
desc, err := remote.Index(subjectRef, options...)
require.NoError(t, err)
idxDigest, err := desc.Digest()
require.NoError(t, err)
ref = fmt.Sprintf("%s/repo@%s", u.Host, idxDigest.String())
}
resolver, err := oci.NewRegistryAttestationResolver(ref, platform)
require.NoError(t, err)
policyOpts := &policy.PolicyOptions{
@@ -74,9 +92,22 @@ func TestAttestationReferenceTypes(t *testing.T) {
results, err := attest.Verify(ctx, policyOpts, resolver)
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
if !tc.skipSubject {
// can evaluate policy using referrers
referrersResolver, err := oci.NewReferrersAttestationResolver(indexName, oci.WithPlatform(platform))
if tc.useDigest {
p, err := oci.ParsePlatform(platform)
require.NoError(t, err)
options := oci.WithOptions(ctx, p)
subjectRef, err := name.ParseReference(indexName)
require.NoError(t, err)
desc, err := remote.Image(subjectRef, options...)
require.NoError(t, err)
subjectDigest, err := desc.Digest()
require.NoError(t, err)
ref = fmt.Sprintf("%s/repo@%s", u.Host, subjectDigest.String())
}
referrersResolver, err := oci.NewReferrersAttestationResolver(ref, oci.WithPlatform(platform))
require.NoError(t, err)
results, err = attest.Verify(ctx, policyOpts, referrersResolver)
@@ -137,7 +168,7 @@ func TestReferencesInDifferentRepo(t *testing.T) {
require.NoError(t, err)
for _, mf := range mfs2.Manifests {
//skip signed/unsigned attestations
if mf.Annotations[attestation.DockerReferenceType] == "attestation-manifest" {
if mf.Annotations[attestation.DockerReferenceType] == attestation.AttestationManifestType {
continue
}
// can evaluate policy using referrers in a different repo

View File

@@ -198,6 +198,7 @@ func (r *OCILayoutResolver) ImageDigest(ctx context.Context) (string, error) {
type ReferrersResolver struct {
image string
platform *v1.Platform
digest string
referrersRepo string
manifests []*AttestationManifest
}
@@ -236,27 +237,20 @@ func WithPlatform(platform string) func(*ReferrersResolver) error {
func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
if r.manifests == nil {
options := withOptions(ctx, r.platform)
subjectRef, err := name.ParseReference(r.image)
if err != nil {
return fmt.Errorf("failed to parse reference: %w", err)
}
desc, err := remote.Image(subjectRef, options...)
if err != nil {
return fmt.Errorf("failed to get image manifest: %w", err)
}
subjectDigest, err := desc.Digest()
if err != nil {
return fmt.Errorf("failed to get image digest: %w", err)
}
subjectDigest, err := r.ImageDigest(ctx)
var referrersSubjectRef name.Digest
if r.referrersRepo != "" {
referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest.String()))
referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest))
if err != nil {
return fmt.Errorf("failed to create referrers reference: %w", err)
}
} else {
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest.String())
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest)
}
referrersIndex, err := remote.Referrers(referrersSubjectRef)
if err != nil {
@@ -284,7 +278,7 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
if manifest.Annotations[att.DockerReferenceType] != AttestationManifestType {
continue
}
if manifest.Annotations[att.DockerReferenceDigest] != subjectDigest.String() {
if manifest.Annotations[att.DockerReferenceDigest] != subjectDigest {
continue
}
attest := &AttestationManifest{
@@ -292,7 +286,7 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
Image: attestationImage,
Manifest: manifest,
Descriptor: &m,
Digest: subjectDigest.String(),
Digest: subjectDigest,
Platform: r.platform,
}
aManifests = append(aManifests, attest)
@@ -331,14 +325,30 @@ func (r *ReferrersResolver) ImagePlatform() (*v1.Platform, error) {
}
func (r *ReferrersResolver) ImageDigest(ctx context.Context) (string, error) {
err := r.resolveAttestations(ctx)
if err != nil {
return "", fmt.Errorf("failed to resolve attestations: %w", err)
if r.digest == "" {
subjectRef, err := name.ParseReference(r.image)
if err != nil {
return "", fmt.Errorf("failed to parse reference: %w", err)
}
switch t := subjectRef.(type) {
case name.Digest:
r.digest = t.DigestStr()
case name.Tag:
options := WithOptions(ctx, r.platform)
desc, err := remote.Image(t, options...)
if err != nil {
return "", fmt.Errorf("failed to get image manifest: %w", err)
}
subjectDigest, err := desc.Digest()
if err != nil {
return "", fmt.Errorf("failed to get image digest: %w", err)
}
r.digest = subjectDigest.String()
default:
return "", fmt.Errorf("unsupported reference type: %T", t)
}
}
if len(r.manifests) == 0 {
return "", errors.New("no attestation manifests found")
}
return r.manifests[0].Digest, nil
return r.digest, nil
}
type RegistryResolver struct {
@@ -390,20 +400,20 @@ func (r *RegistryResolver) Attestations(ctx context.Context, predicateType strin
func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*AttestationManifest, error) {
// we want to get to the image index, so ignoring platform for now
options := withOptions(ctx, nil)
options := WithOptions(ctx, nil)
ref, err := name.ParseReference(image)
if err != nil {
return nil, fmt.Errorf("failed to parse reference: %w", err)
}
desc, err := remote.Index(ref, options...)
index, err := remote.Index(ref, options...)
if err != nil {
return nil, fmt.Errorf("failed to obtain index manifest: %w", err)
return nil, fmt.Errorf("failed to get index: %w", err)
}
ix, err := desc.IndexManifest()
indexManifest, err := index.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to obtain index manifest: %w", err)
return nil, fmt.Errorf("failed to get index manifest: %w", err)
}
digest, err := imageDigestForPlatform(ix, platform)
digest, err := imageDigestForPlatform(indexManifest, platform)
if err != nil {
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
}
@@ -412,7 +422,7 @@ func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Pl
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
}
attestationDigest, err := attestationDigestForDigest(ix, digest, "attestation-manifest")
attestationDigest, err := attestationDigestForDigest(indexManifest, digest, "attestation-manifest")
if err != nil {
return nil, fmt.Errorf("failed to obtain attestation for image: %w", err)
}
@@ -444,7 +454,7 @@ func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Pl
return attest, nil
}
func withOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
// prepare options
options := []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithTransport(HttpTransport()), remote.WithContext(ctx)}