310 lines
11 KiB
Go
310 lines
11 KiB
Go
package attest
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/distribution/reference"
|
|
"github.com/docker/attest/attestation"
|
|
"github.com/docker/attest/config"
|
|
"github.com/docker/attest/internal/test"
|
|
"github.com/docker/attest/oci"
|
|
"github.com/docker/attest/policy"
|
|
"github.com/docker/attest/tuf"
|
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var ExampleAttestation = filepath.Join("test", "testdata", "example_attestation.json")
|
|
|
|
const (
|
|
LinuxAMD64 = "linux/amd64"
|
|
)
|
|
|
|
func TestVerifyAttestations(t *testing.T) {
|
|
ex, err := os.ReadFile(ExampleAttestation)
|
|
assert.NoError(t, err)
|
|
|
|
env := new(attestation.Envelope)
|
|
err = json.Unmarshal(ex, env)
|
|
assert.NoError(t, err)
|
|
resolver := &attestation.MockResolver{
|
|
Envs: []*attestation.Envelope{env},
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
policyEvaluationError error
|
|
expectedError error
|
|
}{
|
|
{"policy ok", nil, nil},
|
|
{"policy error", fmt.Errorf("policy error"), fmt.Errorf("policy evaluation failed: policy error")},
|
|
}
|
|
ctx := context.Background()
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
mockPE := policy.MockPolicyEvaluator{
|
|
EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *policy.Policy, _ *policy.Input) (*policy.Result, error) {
|
|
return policy.AllowedResult(), tc.policyEvaluationError
|
|
},
|
|
}
|
|
|
|
_, err := VerifyAttestations(ctx, resolver, &mockPE, &policy.Policy{ResolvedName: ""})
|
|
if tc.expectedError != nil {
|
|
if assert.Error(t, err) {
|
|
assert.Equal(t, tc.expectedError.Error(), err.Error())
|
|
}
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVSA(t *testing.T) {
|
|
ctx, signer := test.Setup(t)
|
|
// setup an image with signed attestations
|
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
|
|
|
opts := &attestation.SigningOptions{}
|
|
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage())
|
|
assert.NoError(t, err)
|
|
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
|
require.NoError(t, err)
|
|
signedIndex := attIdx.Index
|
|
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests)
|
|
require.NoError(t, err)
|
|
|
|
// output signed attestations
|
|
spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64))
|
|
require.NoError(t, err)
|
|
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name)
|
|
assert.NoError(t, err)
|
|
|
|
// mocked vsa query should pass
|
|
policyOpts := &policy.Options{
|
|
LocalPolicyDir: PassPolicyDir,
|
|
AttestationStyle: config.AttestationStyleAttached,
|
|
DisableTUF: true,
|
|
}
|
|
results, err := Verify(ctx, spec, policyOpts)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
|
assert.Empty(t, results.Violations)
|
|
|
|
if assert.NotNil(t, results.Input) {
|
|
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", results.Input.Digest)
|
|
assert.NotNil(t, results.Input.Tag)
|
|
}
|
|
|
|
assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type)
|
|
assert.Equal(t, attestation.VSAPredicateType, results.VSA.PredicateType)
|
|
assert.Len(t, results.VSA.Subject, 1)
|
|
|
|
require.IsType(t, attestation.VSAPredicate{}, results.VSA.Predicate)
|
|
attestationPredicate, ok := results.VSA.Predicate.(attestation.VSAPredicate)
|
|
require.True(t, ok)
|
|
|
|
assert.Equal(t, "PASSED", attestationPredicate.VerificationResult)
|
|
assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID)
|
|
assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
|
|
assert.Equal(t, PassPolicyDir+"/policy.rego", attestationPredicate.Policy.DownloadLocation)
|
|
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
|
|
assert.Equal(t, map[string]string{"sha256": "d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0"}, attestationPredicate.Policy.Digest)
|
|
}
|
|
|
|
func TestVerificationFailure(t *testing.T) {
|
|
ctx, signer := test.Setup(t)
|
|
// setup an image with signed attestations
|
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
|
|
|
opts := &attestation.SigningOptions{}
|
|
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage())
|
|
assert.NoError(t, err)
|
|
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
|
require.NoError(t, err)
|
|
signedIndex := attIdx.Index
|
|
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests, attestation.WithReplacedLayers(true))
|
|
require.NoError(t, err)
|
|
|
|
// output signed attestations
|
|
spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64))
|
|
require.NoError(t, err)
|
|
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name)
|
|
assert.NoError(t, err)
|
|
|
|
// mocked vsa query should fail
|
|
policyOpts := &policy.Options{
|
|
LocalPolicyDir: FailPolicyDir,
|
|
AttestationStyle: config.AttestationStyleAttached,
|
|
DisableTUF: true,
|
|
}
|
|
results, err := Verify(ctx, spec, policyOpts)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, OutcomeFailure, results.Outcome)
|
|
assert.Len(t, results.Violations, 1)
|
|
|
|
violation := results.Violations[0]
|
|
assert.Equal(t, "missing_attestation", violation.Type)
|
|
assert.Equal(t, "Attestation missing for subject", violation.Description)
|
|
assert.Nil(t, violation.Attestation)
|
|
|
|
assert.Equal(t, intoto.StatementInTotoV01, results.VSA.Type)
|
|
assert.Equal(t, attestation.VSAPredicateType, results.VSA.PredicateType)
|
|
assert.Len(t, results.VSA.Subject, 1)
|
|
|
|
require.IsType(t, attestation.VSAPredicate{}, results.VSA.Predicate)
|
|
attestationPredicate, ok := results.VSA.Predicate.(attestation.VSAPredicate)
|
|
require.True(t, ok)
|
|
|
|
assert.Equal(t, "FAILED", attestationPredicate.VerificationResult)
|
|
assert.Equal(t, "docker-official-images", attestationPredicate.Verifier.ID)
|
|
assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
|
|
assert.Equal(t, FailPolicyDir+"/policy.rego", attestationPredicate.Policy.DownloadLocation)
|
|
assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI)
|
|
assert.Equal(t, map[string]string{"sha256": "ad045e1bd7cd602d90196acf68f2c57d7b51565d59e6e30e30d94ae86aa16201"}, attestationPredicate.Policy.Digest)
|
|
}
|
|
|
|
func TestSignVerify(t *testing.T) {
|
|
ctx, signer := test.Setup(t)
|
|
// setup an image with signed attestations
|
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
signTL bool
|
|
policyDir string
|
|
imageName string
|
|
expectedNonSuccess Outcome
|
|
}{
|
|
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
|
|
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir},
|
|
{name: "no tl", signTL: false, policyDir: PassPolicyDir},
|
|
{name: "mirror", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"},
|
|
{name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectedNonSuccess: OutcomeNoPolicy},
|
|
{name: "verify inputs", signTL: false, policyDir: InputsPolicyDir},
|
|
}
|
|
|
|
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage())
|
|
assert.NoError(t, err)
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
opts := &attestation.SigningOptions{
|
|
SkipTL: tc.signTL,
|
|
}
|
|
|
|
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
|
require.NoError(t, err)
|
|
signedIndex := attIdx.Index
|
|
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests, attestation.WithReplacedLayers(true))
|
|
require.NoError(t, err)
|
|
|
|
imageName := tc.imageName
|
|
if imageName == "" {
|
|
imageName = attIdx.Name
|
|
}
|
|
// output signed attestations
|
|
spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64))
|
|
require.NoError(t, err)
|
|
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, imageName)
|
|
require.NoError(t, err)
|
|
|
|
policyOpts := &policy.Options{
|
|
LocalPolicyDir: tc.policyDir,
|
|
DisableTUF: true,
|
|
}
|
|
results, err := Verify(ctx, spec, policyOpts)
|
|
require.NoError(t, err)
|
|
if tc.expectedNonSuccess != "" {
|
|
assert.Equal(t, tc.expectedNonSuccess, results.Outcome)
|
|
return
|
|
}
|
|
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
|
platform, err := oci.ParsePlatform(LinuxAMD64)
|
|
require.NoError(t, err)
|
|
|
|
ref, err := reference.ParseNormalizedNamed(attIdx.Name)
|
|
require.NoError(t, err)
|
|
expectedPURL, _, err := oci.RefToPURL(ref, platform)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedPURL, results.Input.PURL)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDefaultOptions(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
tufOpts *tuf.ClientOptions
|
|
localTargetsDir string
|
|
attestationStyle config.AttestationStyle
|
|
referrersRepo string
|
|
expectedError string
|
|
disableTuf bool
|
|
localPolicyDir string
|
|
}{
|
|
{name: "empty"},
|
|
{name: "tufClient provided", tufOpts: &tuf.ClientOptions{MetadataSource: "a", TargetsSource: "b"}},
|
|
{name: "localTargetsDir provided", localTargetsDir: test.CreateTempDir(t, "", TestTempDir)},
|
|
{name: "attestationStyle provided", attestationStyle: config.AttestationStyleAttached},
|
|
{name: "referrersRepo provided", referrersRepo: "referrers"},
|
|
{name: "referrersRepo provided with attached", referrersRepo: "referrers", attestationStyle: config.AttestationStyleAttached, expectedError: "referrers repo specified but attestation source not set to referrers"},
|
|
{name: "tuf disabled and no local-policy-dir", disableTuf: true, expectedError: "local policy dir must be set if not using TUF"},
|
|
{name: "tuf disabled but options set", disableTuf: true, tufOpts: &tuf.ClientOptions{MetadataSource: "a", TargetsSource: "b"}, localPolicyDir: "foo", expectedError: "TUF client options set but TUF disabled"},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
defaultTargets, err := defaultLocalTargetsDir()
|
|
require.NoError(t, err)
|
|
|
|
opts := &policy.Options{
|
|
TUFClientOptions: tc.tufOpts,
|
|
LocalTargetsDir: tc.localTargetsDir,
|
|
AttestationStyle: tc.attestationStyle,
|
|
ReferrersRepo: tc.referrersRepo,
|
|
DisableTUF: tc.disableTuf,
|
|
LocalPolicyDir: tc.localPolicyDir,
|
|
}
|
|
|
|
err = populateDefaultOptions(opts)
|
|
if tc.expectedError != "" {
|
|
require.Error(t, err)
|
|
assert.Equal(t, tc.expectedError, err.Error())
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
if tc.localTargetsDir != "" {
|
|
assert.Equal(t, tc.localTargetsDir, opts.LocalTargetsDir)
|
|
} else {
|
|
assert.Equal(t, defaultTargets, opts.LocalTargetsDir)
|
|
}
|
|
|
|
if tc.attestationStyle != "" {
|
|
assert.Equal(t, tc.attestationStyle, opts.AttestationStyle)
|
|
} else {
|
|
assert.Equal(t, config.AttestationStyleReferrers, opts.AttestationStyle)
|
|
}
|
|
|
|
if tc.tufOpts != nil {
|
|
assert.Equal(t, tc.tufOpts, opts.TUFClientOptions)
|
|
} else {
|
|
assert.NotNil(t, opts.TUFClientOptions)
|
|
}
|
|
|
|
if tc.referrersRepo != "" {
|
|
assert.Equal(t, tc.referrersRepo, opts.ReferrersRepo)
|
|
} else {
|
|
assert.Empty(t, opts.ReferrersRepo)
|
|
}
|
|
})
|
|
}
|
|
}
|