338 lines
11 KiB
Go
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")
|
|
}
|
|
}
|
|
}
|
|
}
|