Add support for OCI Referrers and fallback (#50)
* Add support for OCI Referrers and fallback
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user