Add support for OCI Referrers and fallback (#50)

* Add support for OCI Referrers and fallback
This commit is contained in:
James Carnegie
2024-06-13 16:10:41 +01:00
committed by GitHub
parent e120439035
commit 8ba9656645
15 changed files with 463 additions and 67 deletions

View File

@@ -96,23 +96,8 @@ type AnnotatedStatement struct {
Annotations map[string]string
}
func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) {
idx, err := layout.ImageIndexFromPath(path)
if err != nil {
return nil, fmt.Errorf("failed to load image index: %w", err)
}
idxm, err := idx.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to get digest: %w", err)
}
idxDigest := idxm.Manifests[0].Digest
mfs, err := idx.ImageIndex(idxDigest)
if err != nil {
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
}
mfs2, err := mfs.IndexManifest()
func ExtractStatementsFromIndex(idx v1.ImageIndex, mediaType string) ([]*AnnotatedStatement, error) {
mfs2, err := idx.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
}
@@ -124,7 +109,7 @@ func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStat
continue
}
attestationImage, err := mfs.Image(mf.Digest)
attestationImage, err := idx.Image(mf.Digest)
if err != nil {
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
}
@@ -189,3 +174,22 @@ func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStat
}
return statements, nil
}
func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) {
idx, err := layout.ImageIndexFromPath(path)
if err != nil {
return nil, fmt.Errorf("failed to load image index: %w", err)
}
idxm, err := idx.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to get digest: %w", err)
}
idxDigest := idxm.Manifests[0].Digest
mfs, err := idx.ImageIndex(idxDigest)
if err != nil {
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
}
return ExtractStatementsFromIndex(mfs, mediaType)
}

View File

@@ -32,7 +32,7 @@ func ExampleSign_remote() {
// load image index with unsigned attestation-manifests
ref := "docker/image-signer-verifier:latest"
att, err := oci.AttestationIndexFromRemote(ref)
att, err := oci.SubjectIndexFromRemote(ref)
if err != nil {
panic(err)
}

View File

@@ -19,20 +19,42 @@ import (
)
func Sign(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) (v1.ImageIndex, error) {
images, err := SignedAttestationImages(ctx, idx, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to sign attestation images: %w", err)
}
for _, image := range images {
idx, err = addImageToIndex(idx, image.Image, image.Descriptor, image.AttestationManifest)
if err != nil {
return nil, fmt.Errorf("failed to add signed layers to index: %w", err)
}
}
return idx, nil
}
func SignedAttestationImages(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]*attestation.SignedAttestationImage, error) {
// extract attestation manifests from index
attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx)
if err != nil {
return nil, fmt.Errorf("failed to get attestation manifests: %w", err)
}
if len(attestationManifests) == 0 {
return nil, fmt.Errorf("no attestation manifests found")
}
images := []*attestation.SignedAttestationImage{}
// sign every attestation layer in each manifest
for _, manifest := range attestationManifests {
idx, err = signLayersAndAddToIndex(ctx, idx, manifest.Attestation.Layers, manifest, signer, opts)
newImg, newDescriptor, err := SignLayersAndAddToImage(ctx, manifest.Attestation.Layers, manifest, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to add signed layers: %w", err)
return nil, fmt.Errorf("failed to add signed layers to image: %w", err)
}
images = append(images, &attestation.SignedAttestationImage{
Image: newImg,
Descriptor: newDescriptor,
AttestationManifest: manifest,
})
}
return idx, nil
return images, nil
}
func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.Statement, signer dsse.SignerVerifier) (v1.ImageIndex, error) {
@@ -52,7 +74,7 @@ func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.St
}
updatedIndex := false
for _, manifest := range attestationManifests {
if subjectDigests[manifest.Annotations[oci.DockerReferenceDigest]] {
if subjectDigests[manifest.Annotations[attestation.DockerReferenceDigest]] {
attestationLayers := []attestation.AttestationLayer{
{
Statement: statement,
@@ -63,9 +85,13 @@ func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.St
},
}
// hard-coding replace to false here, because if it's true we will remove any unsigned statements, even unrelated ones
idx, err = signLayersAndAddToIndex(ctx, idx, attestationLayers, manifest, signer, &attestation.SigningOptions{Replace: false})
newImg, newDec, err := SignLayersAndAddToImage(ctx, attestationLayers, manifest, signer, &attestation.SigningOptions{Replace: false})
if err != nil {
return nil, fmt.Errorf("failed to add signed layers: %w", err)
return nil, fmt.Errorf("failed to add signed layers to image: %w", err)
}
idx, err = addImageToIndex(idx, newImg, newDec, manifest)
if err != nil {
return nil, fmt.Errorf("failed to add attestation image to index: %w", err)
}
updatedIndex = true
}
@@ -74,33 +100,34 @@ func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.St
return nil, fmt.Errorf("no attestation manifest found for statement")
}
return idx, nil
}
func signLayersAndAddToIndex(
func SignLayersAndAddToImage(
ctx context.Context,
idx v1.ImageIndex,
attestationLayers []attestation.AttestationLayer,
manifest attestation.AttestationManifest,
signer dsse.SignerVerifier,
opts *attestation.SigningOptions) (v1.ImageIndex, error) {
opts *attestation.SigningOptions) (v1.Image, *v1.Descriptor, error) {
signedLayers, err := signLayers(ctx, attestationLayers, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to sign attestations: %w", err)
return nil, nil, fmt.Errorf("failed to sign attestations: %w", err)
}
newImg, err := addSignedLayers(signedLayers, manifest, opts)
if err != nil {
return nil, fmt.Errorf("failed to add signed layers: %w", err)
return nil, nil, fmt.Errorf("failed to add signed layers: %w", err)
}
if !opts.SkipSubject {
newImg = mutate.Subject(newImg, *manifest.SubjectDescriptor).(v1.Image)
}
newDesc, err := partial.Descriptor(newImg)
if err != nil {
return nil, fmt.Errorf("failed to get descriptor: %w", err)
return nil, nil, fmt.Errorf("failed to get descriptor: %w", err)
}
cf, err := manifest.Attestation.Image.ConfigFile()
if err != nil {
return nil, fmt.Errorf("failed to get config file: %w", err)
return nil, nil, fmt.Errorf("failed to get config file: %w", err)
}
newDesc.Platform = cf.Platform()
if newDesc.Platform == nil {
@@ -111,10 +138,21 @@ func signLayersAndAddToIndex(
}
newDesc.MediaType = manifest.MediaType
newDesc.Annotations = manifest.Annotations
return newImg, newDesc, nil
}
func addImageToIndex(
idx v1.ImageIndex,
img v1.Image,
desc *v1.Descriptor,
manifest attestation.AttestationManifest,
) (v1.ImageIndex, error) {
idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest))
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
Add: newImg,
Descriptor: *newDesc,
Add: img,
Descriptor: *desc,
})
return idx, nil
}
@@ -168,6 +206,13 @@ func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signe
// addSignedLayers adds signed layers to a new or existing attestation image
func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.AttestationManifest, opts *attestation.SigningOptions) (v1.Image, error) {
withAnnotations := func(img v1.Image) v1.Image {
// this is handy when dealing with referrers
return mutate.Annotations(img, map[string]string{
attestation.DockerReferenceType: attestation.AttestationManifestType,
attestation.DockerReferenceDigest: manifest.SubjectDescriptor.Digest.String(),
}).(v1.Image)
}
var err error
if opts.Replace {
// create a new attestation image with only signed layers
@@ -189,7 +234,7 @@ func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.Attest
}
}
}
return newImg, nil
return withAnnotations(newImg), nil
}
// Add signed layers to the existing image
for _, layer := range signedLayers {
@@ -198,5 +243,5 @@ func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.Attest
return nil, fmt.Errorf("failed to append layer: %w", err)
}
}
return manifest.Attestation.Image, nil
return withAnnotations(manifest.Attestation.Image), nil
}

View File

@@ -42,7 +42,7 @@ func TestSignVerifyOCILayout(t *testing.T) {
replace bool
}{
{"signed replaced (does nothing)", UnsignedTestImage, 0, 4, true},
{"signed replaced", UnsignedTestImage, 0, 4, true},
{"without replace", UnsignedTestImage, 4, 4, false},
// image without provenance doesn't fail
{"no provenance (replace)", NoProvenanceImage, 0, 2, true},
@@ -57,7 +57,7 @@ func TestSignVerifyOCILayout(t *testing.T) {
opts := &attestation.SigningOptions{
Replace: tc.replace,
}
attIdx, err := oci.AttestationIndexFromPath(tc.TestImage)
attIdx, err := oci.SubjectIndexFromPath(tc.TestImage)
require.NoError(t, err)
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
@@ -107,7 +107,7 @@ func TestAddAttestation(t *testing.T) {
expectedStatements := 4
outputLayout := test.CreateTempDir(t, "", TestTempDir)
attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage)
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
require.NoError(t, err)
statementToAdd := &intoto.Statement{
@@ -200,6 +200,7 @@ func TestAddSignedLayerAnnotations(t *testing.T) {
},
},
},
SubjectDescriptor: &v1.Descriptor{},
}
newImg, err := addSignedLayers(signedLayers, manifest, opts)
require.NoError(t, err)

View File

@@ -76,7 +76,7 @@ func TestVSA(t *testing.T) {
opts := &attestation.SigningOptions{
Replace: true,
}
attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage)
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
assert.NoError(t, err)
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
assert.NoError(t, err)
@@ -128,7 +128,7 @@ func TestVerificationFailure(t *testing.T) {
opts := &attestation.SigningOptions{
Replace: true,
}
attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage)
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
assert.NoError(t, err)
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
assert.NoError(t, err)
@@ -194,7 +194,7 @@ func TestSignVerifyNoTL(t *testing.T) {
{name: "no tl", signTL: false, policyDir: PassPolicyDir, success: false},
}
attIdx, err := oci.AttestationIndexFromPath(UnsignedTestImage)
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
assert.NoError(t, err)
for _, tc := range testCases {

View File

@@ -17,9 +17,19 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]AttestationManifes
if err != nil {
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
}
subjects := make(map[string]*v1.Descriptor)
for _, subject := range idx.Manifests {
subjects[subject.Digest.String()] = &subject
}
var attestationManifests []AttestationManifest
for _, manifest := range idx.Manifests {
if manifest.Annotations[DockerReferenceType] == AttestationManifestType {
subject := subjects[manifest.Annotations[DockerReferenceDigest]]
if subject == nil {
return nil, fmt.Errorf("failed to find subject for attestation manifest: %w", err)
}
attestationImage, err := index.Image(manifest.Digest)
if err != nil {
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", manifest.Digest.String(), err)
@@ -30,7 +40,8 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]AttestationManifes
}
attestationManifests = append(attestationManifests,
AttestationManifest{
Manifest: manifest,
Descriptor: manifest,
SubjectDescriptor: subject,
Attestation: AttestationImage{
Layers: attestationLayers,
Image: attestationImage},

View File

@@ -0,0 +1,156 @@
package attestation_test
import (
"fmt"
"net/http/httptest"
"net/url"
"path/filepath"
"testing"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attest"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/google/go-containerregistry/pkg/registry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
TestTempDir = "attest-sign-test"
)
func TestAttestationReferenceTypes(t *testing.T) {
ctx, signer := test.Setup(t)
platforms := []string{"linux/amd64", "linux/arm64"}
for _, tc := range []struct {
server *httptest.Server
skipSubject bool
}{
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
},
{
server: httptest.NewServer(registry.New()),
},
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
skipSubject: true,
},
} {
s := tc.server
defer s.Close()
u, err := url.Parse(s.URL)
require.NoError(t, err)
opts := &attestation.SigningOptions{
Replace: true,
SkipSubject: tc.skipSubject,
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
require.NoError(t, err)
signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/repo:root", u.Host)
require.NoError(t, err)
err = mirror.PushToRegistry(signedIndex, indexName)
for _, platform := range platforms {
// can eval policy in the normal way
resolver, err := oci.NewRegistryAttestationResolver(indexName, platform)
require.NoError(t, err)
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: PassPolicyDir,
}
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))
require.NoError(t, err)
results, err = attest.Verify(ctx, policyOpts, referrersResolver)
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
}
}
}
}
func TestReferencesInDifferentRepo(t *testing.T) {
ctx, signer := test.Setup(t)
repoName := "repo"
for _, tc := range []struct {
server *httptest.Server
refServer *httptest.Server
}{
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
},
{
server: httptest.NewServer(registry.New()),
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
},
} {
server := tc.server
defer server.Close()
serverUrl, err := url.Parse(server.URL)
require.NoError(t, err)
refServer := tc.refServer
defer refServer.Close()
refServerUrl, err := url.Parse(refServer.URL)
require.NoError(t, err)
opts := &attestation.SigningOptions{
Replace: true,
SkipTL: true,
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
err = mirror.PushToRegistry(attIdx.Index, indexName)
require.NoError(t, err)
signedImages, err := attest.SignedAttestationImages(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
// push signed attestation image to the ref server
for _, img := range signedImages {
// push references using subject-digest.att convention
err = mirror.PushToRegistry(&img.Image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
require.NoError(t, err)
}
mfs2, err := attIdx.Index.IndexManifest()
require.NoError(t, err)
for _, mf := range mfs2.Manifests {
//skip signed/unsigned attestations
if mf.Annotations[attestation.DockerReferenceType] == "attestation-manifest" {
continue
}
// can evaluate policy using referrers in a different repo
repo := fmt.Sprintf("%s/%s", refServerUrl.Host, repoName)
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
referrersResolver, err := oci.NewReferrersAttestationResolver(referencedImage, oci.WithReferrersRepo(repo))
require.NoError(t, err)
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: PassPolicyDir,
}
results, err := attest.Verify(ctx, policyOpts, referrersResolver)
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
}
}
}

View File

@@ -12,10 +12,7 @@ import (
// SignDSSE signs a payload with a given signer and uploads the signature to the transparency log
func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
payloadType := opts.PayloadType
if payloadType == "" {
payloadType = intoto.PayloadType
}
payloadType := intoto.PayloadType
env := new(Envelope)
env.Payload = base64Encoding.EncodeToString(payload)
env.PayloadType = payloadType

View File

@@ -14,6 +14,7 @@ import (
const (
DockerReferenceType = "vnd.docker.reference.type"
AttestationManifestType = "attestation-manifest"
DockerReferenceDigest = "vnd.docker.reference.digest"
DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json"
RekorTlExtKind = "Rekor"
OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse"
@@ -33,12 +34,19 @@ type AttestationImage struct {
Image v1.Image
}
type SignedAttestationImage struct {
Image v1.Image
Descriptor *v1.Descriptor
AttestationManifest AttestationManifest
}
type AttestationManifest struct {
Manifest v1.Descriptor
Attestation AttestationImage
MediaType types.MediaType
Annotations map[string]string
Digest v1.Hash
Descriptor v1.Descriptor
Attestation AttestationImage
MediaType types.MediaType
Annotations map[string]string
Digest v1.Hash
SubjectDescriptor *v1.Descriptor
}
// the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged
@@ -72,9 +80,12 @@ type VerifyOptions struct {
}
type SigningOptions struct {
Replace bool
SkipTL bool
PayloadType string
// replace unsigned statements with signed attestations
Replace bool
// don't log to the configured transparency log
SkipTL bool
// don't add OCI subject field to attestation image
SkipSubject bool
}
func DSSEMediaType(predicateType string) (string, error) {

View File

@@ -72,7 +72,7 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*Atte
continue
}
if mf.Annotations[DockerReferenceDigest] != imageDigest {
if mf.Annotations[att.DockerReferenceDigest] != imageDigest {
continue
}
@@ -195,6 +195,152 @@ func (r *OCILayoutResolver) ImageDigest(ctx context.Context) (string, error) {
return r.Digest, nil
}
type ReferrersResolver struct {
image string
platform *v1.Platform
referrersRepo string
manifests []*AttestationManifest
}
func NewReferrersAttestationResolver(image string, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
res := &ReferrersResolver{
image: image,
}
for _, opt := range options {
err := opt(res)
if err != nil {
return nil, err
}
}
return res, nil
}
func WithReferrersRepo(repo string) func(*ReferrersResolver) error {
return func(r *ReferrersResolver) error {
r.referrersRepo = repo
return nil
}
}
// WithPlatform sets the platform for the resolver (needed for platform specific tag resolution)
func WithPlatform(platform string) func(*ReferrersResolver) error {
return func(r *ReferrersResolver) error {
p, err := ParsePlatform(platform)
if err != nil {
return err
}
r.platform = p
return nil
}
}
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)
}
var referrersSubjectRef name.Digest
if r.referrersRepo != "" {
referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest.String()))
if err != nil {
return fmt.Errorf("failed to create referrers reference: %w", err)
}
} else {
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest.String())
}
referrersIndex, err := remote.Referrers(referrersSubjectRef)
if err != nil {
return fmt.Errorf("failed to get referrers: %w", err)
}
referrersIndexManifest, err := referrersIndex.IndexManifest()
if err != nil {
return fmt.Errorf("failed to get index manifest: %w", err)
}
if len(referrersIndexManifest.Manifests) == 0 {
return errors.New("no referrers found")
}
aManifests := make([]*AttestationManifest, 0)
for _, m := range referrersIndexManifest.Manifests {
remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String())
attestationImage, err := remote.Image(remoteRef)
if err != nil {
return fmt.Errorf("failed to get referred image: %w", err)
}
manifest, err := attestationImage.Manifest()
if err != nil {
return fmt.Errorf("failed to get manifest: %w", err)
}
if manifest.Annotations[att.DockerReferenceType] != AttestationManifestType {
continue
}
if manifest.Annotations[att.DockerReferenceDigest] != subjectDigest.String() {
continue
}
attest := &AttestationManifest{
Name: r.image,
Image: attestationImage,
Manifest: manifest,
Descriptor: &m,
Digest: subjectDigest.String(),
Platform: r.platform,
}
aManifests = append(aManifests, attest)
}
if len(aManifests) == 0 {
return errors.New("no attestation manifests found")
}
r.manifests = aManifests
}
return nil
}
func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
err := r.resolveAttestations(ctx)
if err != nil {
return nil, fmt.Errorf("failed to resolve attestations: %w", err)
}
var envs []*att.Envelope
for _, attest := range r.manifests {
es, err := ExtractEnvelopes(attest, predicateType)
if err != nil {
return nil, fmt.Errorf("failed to extract envelopes: %w", err)
}
envs = append(envs, es...)
}
return envs, nil
}
func (r *ReferrersResolver) ImageName(ctx context.Context) (string, error) {
return r.image, nil
}
func (r *ReferrersResolver) ImagePlatform() (*v1.Platform, error) {
return r.platform, nil
}
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 len(r.manifests) == 0 {
return "", errors.New("no attestation manifests found")
}
return r.manifests[0].Digest, nil
}
type RegistryResolver struct {
image string
platform *v1.Platform
@@ -352,7 +498,7 @@ func imageDigestForPlatform(ix *v1.IndexManifest, platform *v1.Platform) (string
func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
for _, m := range ix.Manifests {
if v, ok := m.Annotations[att.DockerReferenceType]; ok && v == attestType {
if d, ok := m.Annotations[DockerReferenceDigest]; ok && d == imageDigest {
if d, ok := m.Annotations[att.DockerReferenceDigest]; ok && d == imageDigest {
return m.Digest.String(), nil
}
}

View File

@@ -7,6 +7,10 @@ import (
v1 "github.com/google/go-containerregistry/pkg/v1"
)
type AttestationManifests struct {
Manifests []*AttestationManifest
}
type AttestationManifest struct {
// attestation image details
Image v1.Image

View File

@@ -12,18 +12,17 @@ import (
)
const (
DockerReferenceDigest = "vnd.docker.reference.digest"
AttestationManifestType = "attestation-manifest"
InTotoPredicateType = "in-toto.io/predicate-type"
OciReferenceTarget = "org.opencontainers.image.ref.name"
)
type AttestationIndex struct {
type SubjectIndex struct {
Index v1.ImageIndex
Name string
}
func AttestationIndexFromPath(path string) (*AttestationIndex, error) {
func SubjectIndexFromPath(path string) (*SubjectIndex, error) {
wrapperIdx, err := layout.ImageIndexFromPath(path)
if err != nil {
return nil, fmt.Errorf("failed to load image index: %w", err)
@@ -40,13 +39,13 @@ func AttestationIndexFromPath(path string) (*AttestationIndex, error) {
if err != nil {
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
}
return &AttestationIndex{
return &SubjectIndex{
Index: idx,
Name: imageName,
}, nil
}
func AttestationIndexFromRemote(image string) (*AttestationIndex, error) {
func SubjectIndexFromRemote(image string) (*SubjectIndex, error) {
ref, err := name.ParseReference(image)
if err != nil {
log.Fatalf("Failed to parse image name: %v", err)
@@ -61,7 +60,7 @@ func AttestationIndexFromRemote(image string) (*AttestationIndex, error) {
if err != nil {
return nil, fmt.Errorf("failed to pull image %s: %w", image, err)
}
return &AttestationIndex{
return &SubjectIndex{
Index: idx,
Name: image,
}, nil

View File

@@ -158,6 +158,9 @@ func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, err
}
func loadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
if tufClient == nil {
return nil, fmt.Errorf("tuf client not set")
}
filename := PolicyMappingFileName
_, fileContents, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
if err != nil {
@@ -182,7 +185,8 @@ func findPolicyMatch(named reference.Named, mappings *PolicyMappings) (*PolicyMa
}
// now search mirrors
for _, mirror := range mappings.Mirrors {
if slices.Contains(mirror.Mirror.Domains, reference.Domain(named)) &&
if (slices.Contains(mirror.Mirror.Domains, reference.Domain(named)) ||
slices.Contains(mirror.Mirror.Domains, "*")) &&
strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) {
for _, mapping := range mappings.Policies {
if mapping.Id == mirror.PolicyId {

View File

@@ -9,3 +9,8 @@ policies:
description: Local test images
files:
- path: doi/policy.rego
mirrors:
- policy-id: test-images
mirror:
domains: ["*"]
prefix: ""

View File

@@ -1 +1,14 @@
{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:db8f2a6e112ea6396f57d073269ecfac61e8dcdad3a4a643dcb577522492f898","size":1607,"annotations":{"org.opencontainers.image.created":"2024-04-29T10:23:46Z","org.opencontainers.image.ref.name":"docker.io/library/test-image:test"}}]}
{
"schemaVersion": 2,
"manifests": [
{
"mediaType": "application/vnd.oci.image.index.v1+json",
"digest": "sha256:db8f2a6e112ea6396f57d073269ecfac61e8dcdad3a4a643dcb577522492f898",
"size": 1607,
"annotations": {
"org.opencontainers.image.created": "2024-04-29T10:23:46Z",
"org.opencontainers.image.ref.name": "docker.io/library/test-image:test"
}
}
]
}