Add support for OCI Referrers and fallback (#50)
* Add support for OCI Referrers and fallback
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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},
|
||||
|
||||
156
pkg/attestation/referrers_test.go
Normal file
156
pkg/attestation/referrers_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
150
pkg/oci/oci.go
150
pkg/oci/oci.go
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
5
test/testdata/local-policy-pass/mapping.yaml
vendored
5
test/testdata/local-policy-pass/mapping.yaml
vendored
@@ -9,3 +9,8 @@ policies:
|
||||
description: Local test images
|
||||
files:
|
||||
- path: doi/policy.rego
|
||||
mirrors:
|
||||
- policy-id: test-images
|
||||
mirror:
|
||||
domains: ["*"]
|
||||
prefix: ""
|
||||
|
||||
15
test/testdata/unsigned-test-image/index.json
vendored
15
test/testdata/unsigned-test-image/index.json
vendored
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user