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

@@ -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) {