Files
attest/attestation/referrers_test.go
2024-10-18 09:25:31 -05:00

338 lines
11 KiB
Go

/*
Copyright Docker attest authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package attestation_test
import (
"fmt"
"net/http/httptest"
"net/url"
"path/filepath"
"testing"
"github.com/docker/attest"
"github.com/docker/attest/attestation"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/mapping"
"github.com/docker/attest/oci"
"github.com/docker/attest/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"
)
var (
NoProvenanceImage = filepath.Join("..", "test", "testdata", "no-provenance-image")
PassPolicyDir = filepath.Join("..", "test", "testdata", "local-policy-pass")
LocalPolicy = filepath.Join("..", "test", "testdata", "local-policy")
LocalPolicyAttached = filepath.Join("..", "test", "testdata", "local-policy-attached")
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 {
name string
server *httptest.Server
referrersServer *httptest.Server
useDigest bool
referrersRepo string
attestationSource mapping.AttestationStyle
expectFailure bool
}{
{
name: "referrers support, defaults",
server: test.NewLocalRegistry(ctx, registry.WithReferrersSupport(true)),
},
{
name: "use digest",
server: test.NewLocalRegistry(ctx, registry.WithReferrersSupport(true)),
useDigest: true,
},
{
name: "attached attestations, referrers repo (mismatched args)",
server: test.NewLocalRegistry(ctx, registry.WithReferrersSupport(true)),
expectFailure: true, // mismatched args
attestationSource: mapping.AttestationStyleAttached,
referrersRepo: "referrers",
},
{
name: "referrers attestations, referrers repo (no policy)",
server: test.NewLocalRegistry(ctx, registry.WithReferrersSupport(true)),
expectFailure: true, // no policy
attestationSource: mapping.AttestationStyleReferrers,
referrersRepo: "referrers",
},
{
name: "referrers attestations",
server: test.NewLocalRegistry(ctx, registry.WithReferrersSupport(true)),
attestationSource: mapping.AttestationStyleReferrers,
},
{
name: "referrers attestations, no referrers support on server",
server: test.NewLocalRegistry(ctx, registry.WithReferrersSupport(false)),
attestationSource: mapping.AttestationStyleReferrers,
referrersServer: test.NewLocalRegistry(ctx, registry.WithReferrersSupport(true)),
},
} {
t.Run(tc.name, func(t *testing.T) {
s := tc.server
defer s.Close()
if tc.referrersServer != nil {
defer tc.referrersServer.Close()
}
u, err := url.Parse(s.URL)
require.NoError(t, err)
opts := &attestation.SigningOptions{}
attIdx, err := oci.IndexFromPath(test.UnsignedTestIndex(".."))
require.NoError(t, err)
indexName := fmt.Sprintf("%s/repo:root", u.Host)
require.NoError(t, err)
outputRepo := indexName
if tc.referrersServer != nil {
ru, err := url.Parse(s.URL)
require.NoError(t, err)
tc.referrersRepo = fmt.Sprintf("%s/referrers", ru.Host)
outputRepo = tc.referrersRepo
}
// sign all the statements in the index
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
// push subject image so that it can be resolved
require.NoError(t, err)
err = oci.PushIndexToRegistry(ctx, attIdx.Index, indexName)
require.NoError(t, err)
// upload referrers
output, err := oci.ParseImageSpec(outputRepo)
require.NoError(t, err)
for _, attIdx := range signedManifests {
images, err := attIdx.BuildReferringArtifacts()
require.NoError(t, err)
err = oci.SaveImagesNoTag(ctx, images, []*oci.ImageSpec{output})
require.NoError(t, err)
}
for _, platform := range platforms {
// can eval policy in the normal way
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())
}
policyOpts := &policy.Options{
LocalPolicyDir: LocalPolicy,
DisableTUF: true,
}
if tc.referrersRepo != "" {
policyOpts.ReferrersRepo = tc.referrersRepo
}
if tc.attestationSource != "" {
policyOpts.AttestationStyle = tc.attestationSource
}
src, err := oci.ParseImageSpec(ref, oci.WithPlatform(platform))
require.NoError(t, err)
results, err := attest.Verify(ctx, src, policyOpts)
if tc.expectFailure {
require.Error(t, err)
continue
}
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
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())
}
src, err = oci.ParseImageSpec(ref, oci.WithPlatform(platform))
require.NoError(t, err)
results, err = attest.Verify(ctx, src, policyOpts)
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 {
name string
server *httptest.Server
refServer *httptest.Server
}{
{
name: "referrers support",
server: test.NewLocalRegistry(ctx, registry.WithReferrersSupport(true)),
refServer: test.NewLocalRegistry(ctx, registry.WithReferrersSupport(true)),
},
{
name: "no referrers support",
server: test.NewLocalRegistry(ctx, registry.WithReferrersSupport(false)),
refServer: test.NewLocalRegistry(ctx, 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{}
attIdx, err := oci.IndexFromPath(test.UnsignedTestIndex(".."))
require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
err = oci.PushIndexToRegistry(ctx, attIdx.Index, indexName)
require.NoError(t, err)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
// push signed attestation image to the ref server
for _, signedManifest := range signedManifests {
// push references using subject-digest.att convention
image, err := signedManifest.BuildImage()
require.NoError(t, err)
err = oci.PushImageToRegistry(ctx, image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerURL.Host, repoName))
require.NoError(t, err)
refServer := tc.refServer
defer refServer.Close()
refServerURL, err := url.Parse(refServer.URL)
require.NoError(t, err)
opts := &attestation.SigningOptions{}
attIdx, err := oci.IndexFromPath(test.UnsignedTestIndex(".."))
require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
err = oci.PushIndexToRegistry(ctx, attIdx.Index, indexName)
require.NoError(t, err)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
// push signed attestation image to the ref server
for _, mf := range signedManifests {
// push references using subject-digest.att convention
imgs, err := mf.BuildReferringArtifacts()
require.NoError(t, err)
for _, img := range imgs {
err = oci.PushImageToRegistry(ctx, img, 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.AttestationManifestType {
continue
}
// can evaluate policy using referrers in a different repo
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
policyOpts := &policy.Options{
LocalPolicyDir: PassPolicyDir,
DisableTUF: true,
}
src, err := oci.ParseImageSpec(referencedImage)
require.NoError(t, err)
results, err := attest.Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
}
}
}
}
func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
ctx, signer := test.Setup(t)
regServer := test.NewLocalRegistry(ctx, registry.WithReferrersSupport(false))
defer regServer.Close()
serverURL, err := url.Parse(regServer.URL)
require.NoError(t, err)
repoName := "repo"
opts := &attestation.SigningOptions{}
attIdx, err := oci.IndexFromPath(test.UnsignedTestIndex(".."))
require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
err = oci.PushIndexToRegistry(ctx, attIdx.Index, indexName)
require.NoError(t, err)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
// this should create and maintain an index of referrers
for _, mf := range signedManifests {
imgs, err := mf.BuildReferringArtifacts()
require.NoError(t, err)
for _, img := range imgs {
err = oci.PushImageToRegistry(ctx, img, fmt.Sprintf("%s/%s:tag-does-not-matter", serverURL.Host, repoName))
require.NoError(t, err)
mf, err := img.Manifest()
require.NoError(t, err)
subject := mf.Subject
subjectRef, err := name.ParseReference(fmt.Sprintf("%s/%s:sha256-%s", serverURL.Host, repoName, subject.Digest.Hex))
require.NoError(t, err)
idx, err := remote.Index(subjectRef, oci.WithOptions(ctx, nil)...)
require.NoError(t, err)
imf, err := idx.IndexManifest()
require.NoError(t, err)
for _, m := range imf.Manifests {
assert.Contains(t, m.ArtifactType, "application/vnd.in-toto")
assert.Contains(t, m.ArtifactType, "+dsse")
}
}
}
}