Merge branch 'main' into dependabot/go_modules/github.com/aws/aws-sdk-go-v2/config-1.27.21
This commit is contained in:
@@ -39,19 +39,6 @@ func ExampleVerify_remote() {
|
|||||||
// create a resolver for remote attestations
|
// create a resolver for remote attestations
|
||||||
image := "registry-1.docker.io/library/notary:server"
|
image := "registry-1.docker.io/library/notary:server"
|
||||||
platform := "linux/amd64"
|
platform := "linux/amd64"
|
||||||
resolver, err := oci.NewRegistryAttestationResolver(
|
|
||||||
image, // path to image index in OCI registry containing image attestations
|
|
||||||
platform) // platform of subject image (image that attestations are being verified against)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
// example using a local resolver
|
|
||||||
// path := "/myimage"
|
|
||||||
// platform := "linux/amd64"
|
|
||||||
// resolver := &oci.OCILayoutResolver{
|
|
||||||
// Path: path, // file path to OCI layout containing image attestations
|
|
||||||
// Platform: platform, // platform of subject image (image that attestations are being verified against)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// configure policy options
|
// configure policy options
|
||||||
opts := &policy.PolicyOptions{
|
opts := &policy.PolicyOptions{
|
||||||
@@ -62,7 +49,11 @@ func ExampleVerify_remote() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// verify attestations
|
// verify attestations
|
||||||
result, err := attest.Verify(context.Background(), opts, resolver)
|
src, err := oci.ParseImageSpec(image, oci.WithPlatform(platform))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
result, err := attest.Verify(context.Background(), src, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func TestSignVerifyOCILayout(t *testing.T) {
|
|||||||
{"no provenance (replace)", NoProvenanceImage, 0, 2, true},
|
{"no provenance (replace)", NoProvenanceImage, 0, 2, true},
|
||||||
{"no provenance (no replace)", NoProvenanceImage, 2, 2, false},
|
{"no provenance (no replace)", NoProvenanceImage, 2, 2, false},
|
||||||
}
|
}
|
||||||
policyResolver := &policy.PolicyOptions{
|
policyOpts := &policy.PolicyOptions{
|
||||||
LocalPolicyDir: PassPolicyDir,
|
LocalPolicyDir: PassPolicyDir,
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -74,9 +74,9 @@ func TestSignVerifyOCILayout(t *testing.T) {
|
|||||||
})
|
})
|
||||||
_, err = layout.Write(outputLayout, idx)
|
_, err = layout.Write(outputLayout, idx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resolver, err := oci.NewOCILayoutAttestationResolver(outputLayout, "")
|
src, err := oci.ParseImageSpec("oci://" + outputLayout)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
policy, err := Verify(ctx, policyResolver, resolver)
|
policy, err := Verify(ctx, src, policyOpts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equalf(t, OutcomeSuccess, policy.Outcome, "Policy should have been found")
|
assert.Equalf(t, OutcomeSuccess, policy.Outcome, "Policy should have been found")
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,25 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/attest/pkg/attestation"
|
"github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/docker/attest/pkg/config"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
"github.com/docker/attest/pkg/policy"
|
"github.com/docker/attest/pkg/policy"
|
||||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Verify(ctx context.Context, opts *policy.PolicyOptions, resolver oci.AttestationResolver) (result *VerificationResult, err error) {
|
func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.PolicyOptions) (result *VerificationResult, err error) {
|
||||||
pctx, err := policy.ResolvePolicy(ctx, resolver, opts)
|
// so that we can resolve mapping from the image name earlier
|
||||||
|
detailsResolver, err := policy.CreateImageDetailsResolver(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create image details resolver: %w", err)
|
||||||
|
}
|
||||||
|
if opts.AttestationStyle == "" {
|
||||||
|
opts.AttestationStyle = config.AttestationStyleReferrers
|
||||||
|
}
|
||||||
|
if opts.ReferrersRepo != "" && opts.AttestationStyle != config.AttestationStyleReferrers {
|
||||||
|
return nil, fmt.Errorf("referrers repo specified but attestation source not set to referrers")
|
||||||
|
}
|
||||||
|
pctx, err := policy.ResolvePolicy(ctx, detailsResolver, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to resolve policy: %w", err)
|
return nil, fmt.Errorf("failed to resolve policy: %w", err)
|
||||||
}
|
}
|
||||||
@@ -22,7 +34,23 @@ func Verify(ctx context.Context, opts *policy.PolicyOptions, resolver oci.Attest
|
|||||||
Outcome: OutcomeNoPolicy,
|
Outcome: OutcomeNoPolicy,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
// this is overriding the mapping with a referrers config. Useful for testing if nothing else
|
||||||
|
if opts.ReferrersRepo != "" {
|
||||||
|
pctx.Mapping.Attestations = &config.ReferrersConfig{
|
||||||
|
Repo: opts.ReferrersRepo,
|
||||||
|
Style: config.AttestationStyleReferrers,
|
||||||
|
}
|
||||||
|
} else if opts.AttestationStyle == config.AttestationStyleAttached {
|
||||||
|
pctx.Mapping.Attestations = &config.ReferrersConfig{
|
||||||
|
Repo: opts.ReferrersRepo,
|
||||||
|
Style: config.AttestationStyleAttached,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// because we have a mapping now, we can select a resolver based on its contents (ie. referrers or attached)
|
||||||
|
resolver, err := policy.CreateAttestationResolver(detailsResolver, pctx.Mapping)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create attestation resolver: %w", err)
|
||||||
|
}
|
||||||
result, err = VerifyAttestations(ctx, resolver, pctx)
|
result, err = VerifyAttestations(ctx, resolver, pctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to evaluate policy: %w", err)
|
return nil, fmt.Errorf("failed to evaluate policy: %w", err)
|
||||||
@@ -90,7 +118,7 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get image name: %w", err)
|
return nil, fmt.Errorf("failed to get image name: %w", err)
|
||||||
}
|
}
|
||||||
platform, err := resolver.ImagePlatform()
|
platform, err := resolver.ImagePlatform(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ var (
|
|||||||
ExampleAttestation = filepath.Join("..", "..", "test", "testdata", "example_attestation.json")
|
ExampleAttestation = filepath.Join("..", "..", "test", "testdata", "example_attestation.json")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LinuxAMD64 = "linux/amd64"
|
||||||
|
)
|
||||||
|
|
||||||
func TestVerifyAttestations(t *testing.T) {
|
func TestVerifyAttestations(t *testing.T) {
|
||||||
ex, err := os.ReadFile(ExampleAttestation)
|
ex, err := os.ReadFile(ExampleAttestation)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -94,14 +98,13 @@ func TestVSA(t *testing.T) {
|
|||||||
_, err = layout.Write(outputLayout, idx)
|
_, err = layout.Write(outputLayout, idx)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
resolver, err := oci.NewOCILayoutAttestationResolver(outputLayout, "linux/amd64")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// mocked vsa query should pass
|
// mocked vsa query should pass
|
||||||
policyOpts := &policy.PolicyOptions{
|
policyOpts := &policy.PolicyOptions{
|
||||||
LocalPolicyDir: PassPolicyDir,
|
LocalPolicyDir: PassPolicyDir,
|
||||||
}
|
}
|
||||||
results, err := Verify(ctx, policyOpts, resolver)
|
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||||
|
require.NoError(t, err)
|
||||||
|
results, err := Verify(ctx, src, policyOpts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
||||||
assert.Empty(t, results.Violations)
|
assert.Empty(t, results.Violations)
|
||||||
@@ -151,14 +154,13 @@ func TestVerificationFailure(t *testing.T) {
|
|||||||
_, err = layout.Write(outputLayout, idx)
|
_, err = layout.Write(outputLayout, idx)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
resolver, err := oci.NewOCILayoutAttestationResolver(outputLayout, "linux/amd64")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// mocked vsa query should fail
|
// mocked vsa query should fail
|
||||||
policyOpts := &policy.PolicyOptions{
|
policyOpts := &policy.PolicyOptions{
|
||||||
LocalPolicyDir: FailPolicyDir,
|
LocalPolicyDir: FailPolicyDir,
|
||||||
}
|
}
|
||||||
results, err := Verify(ctx, policyOpts, resolver)
|
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||||
|
require.NoError(t, err)
|
||||||
|
results, err := Verify(ctx, src, policyOpts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, OutcomeFailure, results.Outcome)
|
assert.Equal(t, OutcomeFailure, results.Outcome)
|
||||||
assert.Len(t, results.Violations, 1)
|
assert.Len(t, results.Violations, 1)
|
||||||
@@ -225,13 +227,12 @@ func TestSignVerifyNoTL(t *testing.T) {
|
|||||||
_, err = layout.Write(outputLayout, idx)
|
_, err = layout.Write(outputLayout, idx)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
resolver, err := oci.NewOCILayoutAttestationResolver(outputLayout, "linux/amd64")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
policyOpts := &policy.PolicyOptions{
|
policyOpts := &policy.PolicyOptions{
|
||||||
LocalPolicyDir: tc.policyDir,
|
LocalPolicyDir: tc.policyDir,
|
||||||
}
|
}
|
||||||
results, err := Verify(ctx, policyOpts, resolver)
|
src, err := oci.ParseImageSpec("oci://"+outputLayout, oci.WithPlatform(LinuxAMD64))
|
||||||
|
require.NoError(t, err)
|
||||||
|
results, err := Verify(ctx, src, policyOpts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/docker/attest/internal/test"
|
"github.com/docker/attest/internal/test"
|
||||||
"github.com/docker/attest/pkg/attest"
|
"github.com/docker/attest/pkg/attest"
|
||||||
"github.com/docker/attest/pkg/attestation"
|
"github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/docker/attest/pkg/config"
|
||||||
"github.com/docker/attest/pkg/mirror"
|
"github.com/docker/attest/pkg/mirror"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
"github.com/docker/attest/pkg/policy"
|
"github.com/docker/attest/pkg/policy"
|
||||||
@@ -21,21 +22,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||||
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
|
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
|
||||||
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
|
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
|
||||||
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
|
LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy")
|
||||||
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
|
LocalPolicyAttached = filepath.Join("..", "..", "test", "testdata", "local-policy-attached")
|
||||||
TestTempDir = "attest-sign-test"
|
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) {
|
func TestAttestationReferenceTypes(t *testing.T) {
|
||||||
ctx, signer := test.Setup(t)
|
ctx, signer := test.Setup(t)
|
||||||
|
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
||||||
platforms := []string{"linux/amd64", "linux/arm64"}
|
platforms := []string{"linux/amd64", "linux/arm64"}
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
server *httptest.Server
|
server *httptest.Server
|
||||||
skipSubject bool
|
referrersServer *httptest.Server
|
||||||
useDigest bool
|
skipSubject bool
|
||||||
|
useDigest bool
|
||||||
|
referrersRepo string
|
||||||
|
attestationSource config.AttestationStyle
|
||||||
|
expectFailure bool
|
||||||
|
policyDir string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
@@ -44,77 +53,135 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
|||||||
server: httptest.NewServer(registry.New()),
|
server: httptest.NewServer(registry.New()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
skipSubject: true,
|
skipSubject: true,
|
||||||
|
attestationSource: config.AttestationStyleAttached,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
useDigest: true,
|
useDigest: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
expectFailure: true, //mismatched args
|
||||||
|
attestationSource: config.AttestationStyleAttached,
|
||||||
|
referrersRepo: "referrers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
expectFailure: true, // no policy
|
||||||
|
attestationSource: config.AttestationStyleReferrers,
|
||||||
|
referrersRepo: "referrers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
attestationSource: config.AttestationStyleReferrers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(false))),
|
||||||
|
attestationSource: config.AttestationStyleReferrers,
|
||||||
|
referrersServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
s := tc.server
|
t.Run(fmt.Sprint(tc), func(t *testing.T) {
|
||||||
defer s.Close()
|
s := tc.server
|
||||||
u, err := url.Parse(s.URL)
|
defer s.Close()
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
opts := &attestation.SigningOptions{
|
if tc.referrersServer != nil {
|
||||||
Replace: true,
|
defer tc.referrersServer.Close()
|
||||||
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.PushIndexToRegistry(signedIndex, indexName)
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
resolver, err := oci.NewRegistryAttestationResolver(ref, platform)
|
u, err := url.Parse(s.URL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
policyOpts := &policy.PolicyOptions{
|
opts := &attestation.SigningOptions{
|
||||||
LocalPolicyDir: PassPolicyDir,
|
Replace: true,
|
||||||
|
SkipSubject: tc.skipSubject,
|
||||||
}
|
}
|
||||||
results, err := attest.Verify(ctx, policyOpts, resolver)
|
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
|
|
||||||
|
|
||||||
if !tc.skipSubject {
|
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||||
// can evaluate policy using referrers
|
require.NoError(t, err)
|
||||||
if tc.useDigest {
|
|
||||||
p, err := oci.ParsePlatform(platform)
|
if tc.referrersServer != nil {
|
||||||
|
ru, err := url.Parse(s.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := fmt.Sprintf("%s/referrers", ru.Host)
|
||||||
|
tc.referrersRepo = repo
|
||||||
|
images, err := attest.SignedAttestationImages(ctx, attIdx.Index, signer, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
||||||
|
for _, img := range images {
|
||||||
|
err = mirror.PushImageToRegistry(img.Image, fmt.Sprintf("%s:tag-does-not-matter", repo))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
options := oci.WithOptions(ctx, p)
|
}
|
||||||
|
} else {
|
||||||
|
signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = mirror.PushIndexToRegistry(signedIndex, indexName)
|
||||||
|
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)
|
subjectRef, err := name.ParseReference(indexName)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
desc, err := remote.Image(subjectRef, options...)
|
desc, err := remote.Index(subjectRef, options...)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
subjectDigest, err := desc.Digest()
|
idxDigest, err := desc.Digest()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
ref = fmt.Sprintf("%s/repo@%s", u.Host, subjectDigest.String())
|
ref = fmt.Sprintf("%s/repo@%s", u.Host, idxDigest.String())
|
||||||
}
|
}
|
||||||
referrersResolver, err := oci.NewReferrersAttestationResolver(ref, oci.WithPlatform(platform))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
results, err = attest.Verify(ctx, policyOpts, referrersResolver)
|
policyOpts := &policy.PolicyOptions{
|
||||||
|
LocalPolicyDir: LocalPolicy,
|
||||||
|
}
|
||||||
|
if tc.policyDir != "" {
|
||||||
|
policyOpts.LocalPolicyDir = tc.policyDir
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
|
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
|
||||||
|
|
||||||
|
if !tc.skipSubject {
|
||||||
|
// can evaluate policy using referrers
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,14 +239,13 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// can evaluate policy using referrers in a different repo
|
// 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())
|
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
|
||||||
referrersResolver, err := oci.NewReferrersAttestationResolver(referencedImage, oci.WithReferrersRepo(repo))
|
|
||||||
require.NoError(t, err)
|
|
||||||
policyOpts := &policy.PolicyOptions{
|
policyOpts := &policy.PolicyOptions{
|
||||||
LocalPolicyDir: PassPolicyDir,
|
LocalPolicyDir: PassPolicyDir,
|
||||||
}
|
}
|
||||||
results, err := attest.Verify(ctx, policyOpts, referrersResolver)
|
src, err := oci.ParseImageSpec(referencedImage)
|
||||||
|
require.NoError(t, err)
|
||||||
|
results, err := attest.Verify(ctx, src, policyOpts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
|
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
|
||||||
}
|
}
|
||||||
|
|||||||
49
pkg/config/config.go
Normal file
49
pkg/config/config.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/docker/attest/pkg/tuf"
|
||||||
|
goyaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MappingFilename = "mapping.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadLocalMappings(configDir string) (*PolicyMappings, error) {
|
||||||
|
if configDir == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
mappings := &PolicyMappings{}
|
||||||
|
path := filepath.Join(configDir, MappingFilename)
|
||||||
|
mappingFile, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read local policy mapping file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
err = goyaml.Unmarshal(mappingFile, mappings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return mappings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
|
||||||
|
if tufClient == nil {
|
||||||
|
return nil, fmt.Errorf("tuf client not set")
|
||||||
|
}
|
||||||
|
filename := MappingFilename
|
||||||
|
_, fileContents, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to download policy mapping file %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
mappings := &PolicyMappings{}
|
||||||
|
|
||||||
|
err = goyaml.Unmarshal(fileContents, mappings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
return mappings, nil
|
||||||
|
}
|
||||||
48
pkg/config/types.go
Normal file
48
pkg/config/types.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type PolicyMappings struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Policies []*PolicyMapping `json:"policies"`
|
||||||
|
Mirrors []*PolicyMirror `json:"mirrors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttestationStyle string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AttestationStyleAttached AttestationStyle = "attached"
|
||||||
|
AttestationStyleReferrers AttestationStyle = "referrers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PolicyMapping struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Origin *PolicyOrigin `json:"origin"`
|
||||||
|
Files []PolicyMappingFile `json:"files"`
|
||||||
|
Attestations *ReferrersConfig `json:"attestations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReferrersConfig struct {
|
||||||
|
Style AttestationStyle `json:"style"`
|
||||||
|
Repo string `json:"repo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyMappingFile struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyMirror struct {
|
||||||
|
PolicyId string `yaml:"policy-id"`
|
||||||
|
Mirror MirrorSpec `json:"mirror"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MirrorSpec struct {
|
||||||
|
Domains []string `json:"domains"`
|
||||||
|
Prefix string `json:"prefix"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyOrigin struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Prefix string `json:"prefix"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package mirror
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/docker/attest/internal/embed"
|
"github.com/docker/attest/internal/embed"
|
||||||
@@ -30,12 +29,12 @@ func PushImageToRegistry(image v1.Image, imageName string) error {
|
|||||||
// Parse the image name
|
// Parse the image name
|
||||||
ref, err := name.ParseReference(imageName)
|
ref, err := name.ParseReference(imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to parse image name: %v", err)
|
return fmt.Errorf("Failed to parse image name '%s': %w", imageName, err)
|
||||||
}
|
}
|
||||||
// Get the authenticator from the default Docker keychain
|
// Get the authenticator from the default Docker keychain
|
||||||
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
|
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to get authenticator: %v", err)
|
return fmt.Errorf("Failed to get authenticator: %w", err)
|
||||||
}
|
}
|
||||||
// Push the image to the registry
|
// Push the image to the registry
|
||||||
return remote.Write(ref, image, remote.WithAuth(auth))
|
return remote.Write(ref, image, remote.WithAuth(auth))
|
||||||
@@ -45,12 +44,12 @@ func PushIndexToRegistry(image v1.ImageIndex, imageName string) error {
|
|||||||
// Parse the index name
|
// Parse the index name
|
||||||
ref, err := name.ParseReference(imageName)
|
ref, err := name.ParseReference(imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to parse image name: %v", err)
|
return fmt.Errorf("Failed to parse image name: %w", err)
|
||||||
}
|
}
|
||||||
// Get the authenticator from the default Docker keychain
|
// Get the authenticator from the default Docker keychain
|
||||||
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
|
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to get authenticator: %v", err)
|
return fmt.Errorf("Failed to get authenticator: %w", err)
|
||||||
}
|
}
|
||||||
// Push the index to the registry
|
// Push the index to the registry
|
||||||
return remote.WriteIndex(ref, image, remote.WithAuth(auth))
|
return remote.WriteIndex(ref, image, remote.WithAuth(auth))
|
||||||
|
|||||||
151
pkg/oci/layout.go
Normal file
151
pkg/oci/layout.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
att "github.com/docker/attest/pkg/attestation"
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// implementation of AttestationResolver that closes over attestations from an oci layout
|
||||||
|
type OCILayoutResolver struct {
|
||||||
|
*AttestationManifest
|
||||||
|
*ImageSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOCILayoutAttestationResolver(src *ImageSpec) (*OCILayoutResolver, error) {
|
||||||
|
r := &OCILayoutResolver{
|
||||||
|
ImageSpec: src,
|
||||||
|
}
|
||||||
|
_, err := r.fetchAttestationManifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OCILayoutResolver) fetchAttestationManifest() (*AttestationManifest, error) {
|
||||||
|
if r.AttestationManifest == nil {
|
||||||
|
m, err := attestationManifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.AttestationManifest = m
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.AttestationManifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||||
|
attestationImage := r.AttestationManifest.Image
|
||||||
|
layers, err := attestationImage.Layers()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
|
||||||
|
}
|
||||||
|
var envs []*att.Envelope
|
||||||
|
manifest := r.AttestationManifest.Manifest
|
||||||
|
for i, l := range manifest.Layers {
|
||||||
|
if l.Annotations[InTotoPredicateType] != predicateType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
layer := layers[i]
|
||||||
|
mt, err := layer.MediaType()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||||
|
}
|
||||||
|
mts := string(mt)
|
||||||
|
if !strings.HasSuffix(mts, "+dsse") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var env = new(att.Envelope)
|
||||||
|
// parse layer blob as json
|
||||||
|
r, err := layer.Uncompressed()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
err = json.NewDecoder(r).Decode(env)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode envelope: %w", err)
|
||||||
|
}
|
||||||
|
envs = append(envs, env)
|
||||||
|
}
|
||||||
|
return envs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OCILayoutResolver) ImageName(ctx context.Context) (string, error) {
|
||||||
|
return r.Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OCILayoutResolver) ImageDigest(ctx context.Context) (string, error) {
|
||||||
|
return r.Digest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OCILayoutResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
||||||
|
return r.ImageSpec.Platform, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*AttestationManifest, error) {
|
||||||
|
idx, err := layout.ImageIndexFromPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
idxm, err := idx.IndexManifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idxDescriptor := idxm.Manifests[0]
|
||||||
|
name := idxDescriptor.Annotations["org.opencontainers.image.ref.name"]
|
||||||
|
idxDigest := idxDescriptor.Digest
|
||||||
|
|
||||||
|
mfs, err := idx.ImageIndex(idxDigest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
||||||
|
}
|
||||||
|
mfs2, err := mfs.IndexManifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||||
|
}
|
||||||
|
var imageDigest string
|
||||||
|
for _, mf := range mfs2.Manifests {
|
||||||
|
if mf.Platform.Equals(*platform) {
|
||||||
|
imageDigest = mf.Digest.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, mf := range mfs2.Manifests {
|
||||||
|
if mf.Annotations[att.DockerReferenceType] != AttestationManifestType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if mf.Annotations[att.DockerReferenceDigest] != imageDigest {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
attestationImage, err := mfs.Image(mf.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
||||||
|
}
|
||||||
|
manifest, err := attestationImage.Manifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
||||||
|
}
|
||||||
|
attest := &AttestationManifest{
|
||||||
|
Name: name,
|
||||||
|
Image: attestationImage,
|
||||||
|
Manifest: manifest,
|
||||||
|
Descriptor: &mf,
|
||||||
|
Digest: imageDigest,
|
||||||
|
Platform: platform,
|
||||||
|
}
|
||||||
|
return attest, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("attestation manifest not found")
|
||||||
|
}
|
||||||
426
pkg/oci/oci.go
426
pkg/oci/oci.go
@@ -10,9 +10,7 @@ import (
|
|||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
att "github.com/docker/attest/pkg/attestation"
|
att "github.com/docker/attest/pkg/attestation"
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
|
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
@@ -38,422 +36,6 @@ func ParsePlatform(platformStr string) (*v1.Platform, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*AttestationManifest, error) {
|
|
||||||
idx, err := layout.ImageIndexFromPath(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load image index: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
idxm, err := idx.IndexManifest()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
idxDescriptor := idxm.Manifests[0]
|
|
||||||
name := idxDescriptor.Annotations["org.opencontainers.image.ref.name"]
|
|
||||||
idxDigest := idxDescriptor.Digest
|
|
||||||
|
|
||||||
mfs, err := idx.ImageIndex(idxDigest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
|
||||||
}
|
|
||||||
mfs2, err := mfs.IndexManifest()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
|
||||||
}
|
|
||||||
var imageDigest string
|
|
||||||
for _, mf := range mfs2.Manifests {
|
|
||||||
if mf.Platform.Equals(*platform) {
|
|
||||||
imageDigest = mf.Digest.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, mf := range mfs2.Manifests {
|
|
||||||
if mf.Annotations[att.DockerReferenceType] != AttestationManifestType {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if mf.Annotations[att.DockerReferenceDigest] != imageDigest {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
attestationImage, err := mfs.Image(mf.Digest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
|
||||||
}
|
|
||||||
manifest, err := attestationImage.Manifest()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
|
||||||
}
|
|
||||||
attest := &AttestationManifest{
|
|
||||||
Name: name,
|
|
||||||
Image: attestationImage,
|
|
||||||
Manifest: manifest,
|
|
||||||
Descriptor: &mf,
|
|
||||||
Digest: imageDigest,
|
|
||||||
Platform: platform,
|
|
||||||
}
|
|
||||||
return attest, nil
|
|
||||||
}
|
|
||||||
return nil, errors.New("attestation manifest not found")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// implementation of AttestationResolver that closes over attestations from an oci layout
|
|
||||||
type OCILayoutResolver struct {
|
|
||||||
path string
|
|
||||||
platform *v1.Platform
|
|
||||||
*AttestationManifest
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOCILayoutAttestationResolver(path string, platform string) (*OCILayoutResolver, error) {
|
|
||||||
p, err := ParsePlatform(platform)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &OCILayoutResolver{
|
|
||||||
path: path,
|
|
||||||
platform: p,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *OCILayoutResolver) ImagePlatform() (*v1.Platform, error) {
|
|
||||||
return r.platform, nil
|
|
||||||
}
|
|
||||||
func (r *OCILayoutResolver) fetchAttestationManifest() (*AttestationManifest, error) {
|
|
||||||
if r.AttestationManifest == nil {
|
|
||||||
m, err := attestationManifestFromOCILayout(r.path, r.platform)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get attestation manifest: %w", err)
|
|
||||||
}
|
|
||||||
r.AttestationManifest = m
|
|
||||||
}
|
|
||||||
return r.AttestationManifest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
|
||||||
if r.AttestationManifest == nil {
|
|
||||||
_, err := r.fetchAttestationManifest()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get attestation manifest: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
attestationImage := r.AttestationManifest.Image
|
|
||||||
layers, err := attestationImage.Layers()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
|
|
||||||
}
|
|
||||||
var envs []*att.Envelope
|
|
||||||
manifest := r.AttestationManifest.Manifest
|
|
||||||
for i, l := range manifest.Layers {
|
|
||||||
if l.Annotations[InTotoPredicateType] != predicateType {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
layer := layers[i]
|
|
||||||
mt, err := layer.MediaType()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
|
||||||
}
|
|
||||||
mts := string(mt)
|
|
||||||
if !strings.HasSuffix(mts, "+dsse") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var env = new(att.Envelope)
|
|
||||||
// parse layer blob as json
|
|
||||||
r, err := layer.Uncompressed()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
err = json.NewDecoder(r).Decode(env)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode envelope: %w", err)
|
|
||||||
}
|
|
||||||
envs = append(envs, env)
|
|
||||||
}
|
|
||||||
return envs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *OCILayoutResolver) ImageName(ctx context.Context) (string, error) {
|
|
||||||
if r.AttestationManifest == nil {
|
|
||||||
_, err := r.fetchAttestationManifest()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get attestation manifest: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.Name, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *OCILayoutResolver) ImageDigest(ctx context.Context) (string, error) {
|
|
||||||
if r.AttestationManifest == nil {
|
|
||||||
_, err := r.fetchAttestationManifest()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get attestation manifest: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r.Digest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReferrersResolver struct {
|
|
||||||
image string
|
|
||||||
platform *v1.Platform
|
|
||||||
digest string
|
|
||||||
referrersRepo string
|
|
||||||
manifests []*AttestationManifest
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewReferrersAttestationResolver(image string, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
|
|
||||||
res := &ReferrersResolver{
|
|
||||||
image: image,
|
|
||||||
}
|
|
||||||
for _, opt := range options {
|
|
||||||
err := opt(res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithReferrersRepo(repo string) func(*ReferrersResolver) error {
|
|
||||||
return func(r *ReferrersResolver) error {
|
|
||||||
r.referrersRepo = repo
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithPlatform sets the platform for the resolver (needed for platform specific tag resolution)
|
|
||||||
func WithPlatform(platform string) func(*ReferrersResolver) error {
|
|
||||||
return func(r *ReferrersResolver) error {
|
|
||||||
p, err := ParsePlatform(platform)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
r.platform = p
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
|
|
||||||
if r.manifests == nil {
|
|
||||||
subjectRef, err := name.ParseReference(r.image)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse reference: %w", err)
|
|
||||||
}
|
|
||||||
subjectDigest, err := r.ImageDigest(ctx)
|
|
||||||
|
|
||||||
var referrersSubjectRef name.Digest
|
|
||||||
if r.referrersRepo != "" {
|
|
||||||
referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create referrers reference: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest)
|
|
||||||
}
|
|
||||||
referrersIndex, err := remote.Referrers(referrersSubjectRef)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get referrers: %w", err)
|
|
||||||
}
|
|
||||||
referrersIndexManifest, err := referrersIndex.IndexManifest()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get index manifest: %w", err)
|
|
||||||
}
|
|
||||||
if len(referrersIndexManifest.Manifests) == 0 {
|
|
||||||
return errors.New("no referrers found")
|
|
||||||
}
|
|
||||||
aManifests := make([]*AttestationManifest, 0)
|
|
||||||
for _, m := range referrersIndexManifest.Manifests {
|
|
||||||
|
|
||||||
remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String())
|
|
||||||
attestationImage, err := remote.Image(remoteRef)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get referred image: %w", err)
|
|
||||||
}
|
|
||||||
manifest, err := attestationImage.Manifest()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get manifest: %w", err)
|
|
||||||
}
|
|
||||||
if manifest.Annotations[att.DockerReferenceType] != AttestationManifestType {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if manifest.Annotations[att.DockerReferenceDigest] != subjectDigest {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
attest := &AttestationManifest{
|
|
||||||
Name: r.image,
|
|
||||||
Image: attestationImage,
|
|
||||||
Manifest: manifest,
|
|
||||||
Descriptor: &m,
|
|
||||||
Digest: subjectDigest,
|
|
||||||
Platform: r.platform,
|
|
||||||
}
|
|
||||||
aManifests = append(aManifests, attest)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(aManifests) == 0 {
|
|
||||||
return errors.New("no attestation manifests found")
|
|
||||||
}
|
|
||||||
r.manifests = aManifests
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
|
||||||
err := r.resolveAttestations(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to resolve attestations: %w", err)
|
|
||||||
}
|
|
||||||
var envs []*att.Envelope
|
|
||||||
for _, attest := range r.manifests {
|
|
||||||
es, err := ExtractEnvelopes(attest, predicateType)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract envelopes: %w", err)
|
|
||||||
}
|
|
||||||
envs = append(envs, es...)
|
|
||||||
}
|
|
||||||
return envs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ReferrersResolver) ImageName(ctx context.Context) (string, error) {
|
|
||||||
return r.image, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ReferrersResolver) ImagePlatform() (*v1.Platform, error) {
|
|
||||||
return r.platform, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ReferrersResolver) ImageDigest(ctx context.Context) (string, error) {
|
|
||||||
if r.digest == "" {
|
|
||||||
subjectRef, err := name.ParseReference(r.image)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to parse reference: %w", err)
|
|
||||||
}
|
|
||||||
switch t := subjectRef.(type) {
|
|
||||||
case name.Digest:
|
|
||||||
r.digest = t.DigestStr()
|
|
||||||
case name.Tag:
|
|
||||||
options := WithOptions(ctx, r.platform)
|
|
||||||
desc, err := remote.Image(t, options...)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get image manifest: %w", err)
|
|
||||||
}
|
|
||||||
subjectDigest, err := desc.Digest()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get image digest: %w", err)
|
|
||||||
}
|
|
||||||
r.digest = subjectDigest.String()
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("unsupported reference type: %T", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r.digest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type RegistryResolver struct {
|
|
||||||
image string
|
|
||||||
platform *v1.Platform
|
|
||||||
*AttestationManifest
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRegistryAttestationResolver(image string, platform string) (*RegistryResolver, error) {
|
|
||||||
p, err := ParsePlatform(platform)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &RegistryResolver{
|
|
||||||
image: image,
|
|
||||||
platform: p,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RegistryResolver) ImageName(ctx context.Context) (string, error) {
|
|
||||||
return r.image, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RegistryResolver) ImagePlatform() (*v1.Platform, error) {
|
|
||||||
return r.platform, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RegistryResolver) ImageDigest(ctx context.Context) (string, error) {
|
|
||||||
if r.AttestationManifest == nil {
|
|
||||||
attest, err := FetchAttestationManifest(ctx, r.image, r.platform)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get attestation manifest: %w", err)
|
|
||||||
}
|
|
||||||
r.AttestationManifest = attest
|
|
||||||
}
|
|
||||||
return r.Digest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
|
||||||
if r.AttestationManifest == nil {
|
|
||||||
attest, err := FetchAttestationManifest(ctx, r.image, r.platform)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get attestation manifest: %w", err)
|
|
||||||
}
|
|
||||||
r.AttestationManifest = attest
|
|
||||||
}
|
|
||||||
return ExtractEnvelopes(r.AttestationManifest, predicateType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*AttestationManifest, error) {
|
|
||||||
// we want to get to the image index, so ignoring platform for now
|
|
||||||
options := WithOptions(ctx, nil)
|
|
||||||
ref, err := name.ParseReference(image)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse reference: %w", err)
|
|
||||||
}
|
|
||||||
index, err := remote.Index(ref, options...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get index: %w", err)
|
|
||||||
}
|
|
||||||
indexManifest, err := index.IndexManifest()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
|
||||||
}
|
|
||||||
digest, err := imageDigestForPlatform(indexManifest, platform)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
|
|
||||||
}
|
|
||||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
attestationDigest, err := attestationDigestForDigest(indexManifest, digest, "attestation-manifest")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to obtain attestation for image: %w", err)
|
|
||||||
}
|
|
||||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
|
||||||
}
|
|
||||||
remoteDescriptor, err := remote.Get(ref, options...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get attestation: %w", err)
|
|
||||||
}
|
|
||||||
manifest := new(v1.Manifest)
|
|
||||||
err = json.Unmarshal(remoteDescriptor.Manifest, manifest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal attestation: %w", err)
|
|
||||||
}
|
|
||||||
attestationImage, err := remoteDescriptor.Image()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get attestation image: %w", err)
|
|
||||||
}
|
|
||||||
attest := &AttestationManifest{
|
|
||||||
Name: image,
|
|
||||||
Image: attestationImage,
|
|
||||||
Manifest: manifest,
|
|
||||||
Descriptor: &remoteDescriptor.Descriptor,
|
|
||||||
Digest: digest,
|
|
||||||
Platform: platform,
|
|
||||||
}
|
|
||||||
return attest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
|
func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
|
||||||
// prepare options
|
// prepare options
|
||||||
options := []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithTransport(HttpTransport()), remote.WithContext(ctx)}
|
options := []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithTransport(HttpTransport()), remote.WithContext(ctx)}
|
||||||
@@ -467,11 +49,9 @@ func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
|
|||||||
|
|
||||||
func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Envelope, error) {
|
func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Envelope, error) {
|
||||||
manifest := ia.Manifest
|
manifest := ia.Manifest
|
||||||
im := ia.Image
|
image := ia.Image
|
||||||
|
|
||||||
var envs []*att.Envelope
|
var envs []*att.Envelope
|
||||||
|
layers, err := image.Layers()
|
||||||
ls, err := im.Layers()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get layers: %w", err)
|
return nil, fmt.Errorf("failed to get layers: %w", err)
|
||||||
}
|
}
|
||||||
@@ -479,7 +59,7 @@ func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Env
|
|||||||
if (strings.HasPrefix(string(l.MediaType), "application/vnd.in-toto.")) &&
|
if (strings.HasPrefix(string(l.MediaType), "application/vnd.in-toto.")) &&
|
||||||
strings.HasSuffix(string(l.MediaType), "+dsse") &&
|
strings.HasSuffix(string(l.MediaType), "+dsse") &&
|
||||||
l.Annotations[InTotoPredicateType] == predicateType {
|
l.Annotations[InTotoPredicateType] == predicateType {
|
||||||
reader, err := ls[i].Uncompressed()
|
reader, err := layers[i].Uncompressed()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,3 +85,24 @@ func TestImageDigestForPlatform(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest)
|
assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWithoutTag(t *testing.T) {
|
||||||
|
tc := []struct {
|
||||||
|
name string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{name: "image:tag", expected: "index.docker.io/library/image"},
|
||||||
|
{name: "image", expected: "index.docker.io/library/image"},
|
||||||
|
{name: "image:sha256-digest.att", expected: "index.docker.io/library/image"},
|
||||||
|
{name: "docker://image:tag", expected: "docker://index.docker.io/library/image"},
|
||||||
|
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image"},
|
||||||
|
{name: "docker://image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "docker://index.docker.io/library/image"},
|
||||||
|
{name: "docker://127.0.0.1:36555/repo:latest", expected: "docker://127.0.0.1:36555/repo"},
|
||||||
|
}
|
||||||
|
for _, c := range tc {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
notag, _ := WithoutTag(c.name)
|
||||||
|
assert.Equal(t, c.expected, notag)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
121
pkg/oci/referrers.go
Normal file
121
pkg/oci/referrers.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
att "github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReferrersResolver struct {
|
||||||
|
digest string
|
||||||
|
referrersRepo string
|
||||||
|
manifests []*AttestationManifest
|
||||||
|
*RegistryImageDetailsResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReferrersAttestationResolver(src *RegistryImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
|
||||||
|
res := &ReferrersResolver{
|
||||||
|
RegistryImageDetailsResolver: src,
|
||||||
|
}
|
||||||
|
for _, opt := range options {
|
||||||
|
err := opt(res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithReferrersRepo(repo string) func(*ReferrersResolver) error {
|
||||||
|
return func(r *ReferrersResolver) error {
|
||||||
|
r.referrersRepo = repo
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
|
||||||
|
if r.manifests == nil {
|
||||||
|
subjectRef, err := name.ParseReference(r.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse reference: %w", err)
|
||||||
|
}
|
||||||
|
subjectDigest, err := r.ImageDigest(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get digest: %w", err)
|
||||||
|
}
|
||||||
|
var referrersSubjectRef name.Digest
|
||||||
|
if r.referrersRepo != "" {
|
||||||
|
referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", r.referrersRepo, subjectDigest))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create referrers reference: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest)
|
||||||
|
}
|
||||||
|
referrersIndex, err := remote.Referrers(referrersSubjectRef)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get referrers: %w", err)
|
||||||
|
}
|
||||||
|
referrersIndexManifest, err := referrersIndex.IndexManifest()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get index manifest: %w", err)
|
||||||
|
}
|
||||||
|
if len(referrersIndexManifest.Manifests) == 0 {
|
||||||
|
return errors.New("no referrers found")
|
||||||
|
}
|
||||||
|
aManifests := make([]*AttestationManifest, 0)
|
||||||
|
for _, m := range referrersIndexManifest.Manifests {
|
||||||
|
|
||||||
|
remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String())
|
||||||
|
attestationImage, err := remote.Image(remoteRef)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get referred image: %w", err)
|
||||||
|
}
|
||||||
|
manifest, err := attestationImage.Manifest()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get manifest: %w", err)
|
||||||
|
}
|
||||||
|
if manifest.Annotations[att.DockerReferenceType] != AttestationManifestType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if manifest.Annotations[att.DockerReferenceDigest] != subjectDigest {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
attest := &AttestationManifest{
|
||||||
|
Name: r.Identifier,
|
||||||
|
Image: attestationImage,
|
||||||
|
Manifest: manifest,
|
||||||
|
Descriptor: &m,
|
||||||
|
Digest: subjectDigest,
|
||||||
|
Platform: r.Platform,
|
||||||
|
}
|
||||||
|
aManifests = append(aManifests, attest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(aManifests) == 0 {
|
||||||
|
return errors.New("no attestation manifests found")
|
||||||
|
}
|
||||||
|
r.manifests = aManifests
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||||
|
err := r.resolveAttestations(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve attestations: %w", err)
|
||||||
|
}
|
||||||
|
var envs []*att.Envelope
|
||||||
|
for _, attest := range r.manifests {
|
||||||
|
es, err := ExtractEnvelopes(attest, predicateType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract envelopes: %w", err)
|
||||||
|
}
|
||||||
|
envs = append(envs, es...)
|
||||||
|
}
|
||||||
|
return envs, nil
|
||||||
|
}
|
||||||
129
pkg/oci/registry.go
Normal file
129
pkg/oci/registry.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
att "github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegistryResolver struct {
|
||||||
|
*RegistryImageDetailsResolver
|
||||||
|
*AttestationManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegistryImageDetailsResolver struct {
|
||||||
|
*ImageSpec
|
||||||
|
digest string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistryImageDetailsResolver(src *ImageSpec) (*RegistryImageDetailsResolver, error) {
|
||||||
|
return &RegistryImageDetailsResolver{
|
||||||
|
ImageSpec: src,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistryAttestationResolver(src *RegistryImageDetailsResolver) (*RegistryResolver, error) {
|
||||||
|
return &RegistryResolver{
|
||||||
|
RegistryImageDetailsResolver: src,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegistryImageDetailsResolver) ImageName(ctx context.Context) (string, error) {
|
||||||
|
return r.Identifier, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegistryImageDetailsResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
||||||
|
return r.Platform, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegistryImageDetailsResolver) ImageDigest(ctx context.Context) (string, error) {
|
||||||
|
if r.digest == "" {
|
||||||
|
subjectRef, err := name.ParseReference(r.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse reference: %w", err)
|
||||||
|
}
|
||||||
|
options := WithOptions(ctx, r.Platform)
|
||||||
|
desc, err := remote.Image(subjectRef, options...)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get image manifest: %w", err)
|
||||||
|
}
|
||||||
|
subjectDigest, err := desc.Digest()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get image digest: %w", err)
|
||||||
|
}
|
||||||
|
r.digest = subjectDigest.String()
|
||||||
|
}
|
||||||
|
return r.digest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||||
|
if r.AttestationManifest == nil {
|
||||||
|
attest, err := FetchAttestationManifest(ctx, r.Identifier, r.ImageSpec.Platform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.AttestationManifest = attest
|
||||||
|
}
|
||||||
|
return ExtractEnvelopes(r.AttestationManifest, predicateType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*AttestationManifest, error) {
|
||||||
|
// we want to get to the image index, so ignoring platform for now
|
||||||
|
options := WithOptions(ctx, nil)
|
||||||
|
ref, err := name.ParseReference(image)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse reference: %w", err)
|
||||||
|
}
|
||||||
|
index, err := remote.Index(ref, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get index: %w", err)
|
||||||
|
}
|
||||||
|
indexManifest, err := index.IndexManifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
||||||
|
}
|
||||||
|
digest, err := imageDigestForPlatform(indexManifest, platform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
|
||||||
|
}
|
||||||
|
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
attestationDigest, err := attestationDigestForDigest(indexManifest, digest, "attestation-manifest")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to obtain attestation for image: %w", err)
|
||||||
|
}
|
||||||
|
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||||
|
}
|
||||||
|
remoteDescriptor, err := remote.Get(ref, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get attestation: %w", err)
|
||||||
|
}
|
||||||
|
manifest := new(v1.Manifest)
|
||||||
|
err = json.Unmarshal(remoteDescriptor.Manifest, manifest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal attestation: %w", err)
|
||||||
|
}
|
||||||
|
attestationImage, err := remoteDescriptor.Image()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get attestation image: %w", err)
|
||||||
|
}
|
||||||
|
attest := &AttestationManifest{
|
||||||
|
Name: image,
|
||||||
|
Image: attestationImage,
|
||||||
|
Manifest: manifest,
|
||||||
|
Descriptor: &remoteDescriptor.Descriptor,
|
||||||
|
Digest: digest,
|
||||||
|
Platform: platform,
|
||||||
|
}
|
||||||
|
return attest, nil
|
||||||
|
}
|
||||||
50
pkg/oci/registry_test.go
Normal file
50
pkg/oci/registry_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package oci_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegistry(t *testing.T) {
|
||||||
|
ctx, signer := test.Setup(t)
|
||||||
|
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(false)))
|
||||||
|
defer server.Close()
|
||||||
|
u, err := url.Parse(server.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
opts := &attestation.SigningOptions{
|
||||||
|
Replace: true,
|
||||||
|
SkipSubject: true,
|
||||||
|
}
|
||||||
|
attIdx, err := oci.SubjectIndexFromPath(oci.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.PushIndexToRegistry(signedIndex, indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
spec, err := oci.ParseImageSpec(indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resolver, err := policy.CreateImageDetailsResolver(spec)
|
||||||
|
require.NoError(t, err)
|
||||||
|
digest, err := resolver.ImageDigest(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, strings.Contains(digest, "sha256:"))
|
||||||
|
}
|
||||||
@@ -23,12 +23,16 @@ type AttestationManifest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AttestationResolver interface {
|
type AttestationResolver interface {
|
||||||
ImageName(ctx context.Context) (string, error)
|
ImageDetailsResolver
|
||||||
ImagePlatform() (*v1.Platform, error)
|
|
||||||
ImageDigest(ctx context.Context) (string, error)
|
|
||||||
Attestations(ctx context.Context, mediaType string) ([]*att.Envelope, error)
|
Attestations(ctx context.Context, mediaType string) ([]*att.Envelope, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImageDetailsResolver interface {
|
||||||
|
ImageName(ctx context.Context) (string, error)
|
||||||
|
ImagePlatform(ctx context.Context) (*v1.Platform, error)
|
||||||
|
ImageDigest(ctx context.Context) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
type MockResolver struct {
|
type MockResolver struct {
|
||||||
Envs []*att.Envelope
|
Envs []*att.Envelope
|
||||||
}
|
}
|
||||||
@@ -45,6 +49,6 @@ func (r MockResolver) ImageDigest(ctx context.Context) (string, error) {
|
|||||||
return "sha256:test-digest", nil
|
return "sha256:test-digest", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r MockResolver) ImagePlatform() (*v1.Platform, error) {
|
func (r MockResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
||||||
return ParsePlatform("linux/amd64")
|
return ParsePlatform("linux/amd64")
|
||||||
}
|
}
|
||||||
|
|||||||
133
pkg/oci/types.go
133
pkg/oci/types.go
@@ -2,7 +2,7 @@ package oci
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
@@ -12,16 +12,37 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AttestationManifestType = "attestation-manifest"
|
AttestationManifestType = "attestation-manifest"
|
||||||
InTotoPredicateType = "in-toto.io/predicate-type"
|
InTotoPredicateType = "in-toto.io/predicate-type"
|
||||||
OciReferenceTarget = "org.opencontainers.image.ref.name"
|
OciReferenceTarget = "org.opencontainers.image.ref.name"
|
||||||
|
LocalPrefix = "oci://"
|
||||||
|
RegistryPrefix = "docker://"
|
||||||
|
OCI SourceType = "OCI"
|
||||||
|
Docker SourceType = "Docker"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SourceType string
|
||||||
type SubjectIndex struct {
|
type SubjectIndex struct {
|
||||||
Index v1.ImageIndex
|
Index v1.ImageIndex
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AttestationOptions struct {
|
||||||
|
NoReferrers bool
|
||||||
|
Attach bool
|
||||||
|
ReferrersRepo string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageSpecOption func(*ImageSpec) error
|
||||||
|
|
||||||
|
type ImageSpec struct {
|
||||||
|
// OCI or Docker
|
||||||
|
Type SourceType
|
||||||
|
// without oci:// or docker:// (name or path)
|
||||||
|
Identifier string
|
||||||
|
Platform *v1.Platform
|
||||||
|
}
|
||||||
|
|
||||||
func SubjectIndexFromPath(path string) (*SubjectIndex, error) {
|
func SubjectIndexFromPath(path string) (*SubjectIndex, error) {
|
||||||
wrapperIdx, err := layout.ImageIndexFromPath(path)
|
wrapperIdx, err := layout.ImageIndexFromPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -48,12 +69,12 @@ func SubjectIndexFromPath(path string) (*SubjectIndex, error) {
|
|||||||
func SubjectIndexFromRemote(image string) (*SubjectIndex, error) {
|
func SubjectIndexFromRemote(image string) (*SubjectIndex, error) {
|
||||||
ref, err := name.ParseReference(image)
|
ref, err := name.ParseReference(image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to parse image name: %v", err)
|
return nil, fmt.Errorf("failed to parse image reference %s: %w", image, err)
|
||||||
}
|
}
|
||||||
// Get the authenticator from the default Docker keychain
|
// Get the authenticator from the default Docker keychain
|
||||||
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
|
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to get authenticator: %v", err)
|
return nil, fmt.Errorf("failed to resolve auth for image %s: %w", image, err)
|
||||||
}
|
}
|
||||||
// Pull the image from the registry
|
// Pull the image from the registry
|
||||||
idx, err := remote.Index(ref, remote.WithAuth(auth))
|
idx, err := remote.Index(ref, remote.WithAuth(auth))
|
||||||
@@ -65,3 +86,103 @@ func SubjectIndexFromRemote(image string) (*SubjectIndex, error) {
|
|||||||
Name: image,
|
Name: image,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LoadSubjectIndex(input *ImageSpec) (*SubjectIndex, error) {
|
||||||
|
if input.Type == OCI {
|
||||||
|
return SubjectIndexFromPath(input.Identifier)
|
||||||
|
} else {
|
||||||
|
return SubjectIndexFromRemote(input.Identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ImageSpec) ForPlatforms(platform string) ([]*ImageSpec, error) {
|
||||||
|
platforms := strings.Split(platform, ",")
|
||||||
|
var specs []*ImageSpec
|
||||||
|
for _, pStr := range platforms {
|
||||||
|
p, err := ParsePlatform(pStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
spec := &ImageSpec{
|
||||||
|
Type: i.Type,
|
||||||
|
Identifier: i.Identifier,
|
||||||
|
Platform: p,
|
||||||
|
}
|
||||||
|
specs = append(specs, spec)
|
||||||
|
}
|
||||||
|
return specs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseImageSpec(img string, options ...ImageSpecOption) (*ImageSpec, error) {
|
||||||
|
img = strings.TrimSpace(img)
|
||||||
|
if strings.Contains(img, ",") {
|
||||||
|
return nil, fmt.Errorf("only one image is supported")
|
||||||
|
}
|
||||||
|
withoutPrefix := strings.TrimPrefix(strings.TrimPrefix(img, LocalPrefix), RegistryPrefix)
|
||||||
|
src := &ImageSpec{
|
||||||
|
Identifier: withoutPrefix,
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(img, LocalPrefix) {
|
||||||
|
src.Type = OCI
|
||||||
|
} else {
|
||||||
|
src.Type = Docker
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
err := option(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if src.Platform == nil {
|
||||||
|
platform, err := ParsePlatform("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
src.Platform = platform
|
||||||
|
}
|
||||||
|
return src, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPlatform(platform string) ImageSpecOption {
|
||||||
|
return func(i *ImageSpec) error {
|
||||||
|
if strings.Contains(platform, ",") {
|
||||||
|
return fmt.Errorf("only one platform is supported")
|
||||||
|
}
|
||||||
|
p, err := ParsePlatform(platform)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Platform = p
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseImageSpecs(img string) ([]*ImageSpec, error) {
|
||||||
|
outputs := strings.Split(img, ",")
|
||||||
|
var sources []*ImageSpec
|
||||||
|
for _, output := range outputs {
|
||||||
|
src, err := ParseImageSpec(output)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sources = append(sources, src)
|
||||||
|
}
|
||||||
|
return sources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithoutTag(image string) (string, error) {
|
||||||
|
if strings.HasPrefix(image, LocalPrefix) {
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
|
prefix := ""
|
||||||
|
if strings.HasPrefix(image, RegistryPrefix) {
|
||||||
|
image = strings.TrimPrefix(image, RegistryPrefix)
|
||||||
|
prefix = RegistryPrefix
|
||||||
|
}
|
||||||
|
ref, err := name.ParseReference(image)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
repo := ref.Context().Name()
|
||||||
|
return prefix + repo, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,95 +10,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/attest/pkg/config"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
"github.com/docker/attest/pkg/tuf"
|
|
||||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
|
||||||
|
|
||||||
goyaml "gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
|
||||||
PolicyMappingFileName = "mapping.yaml"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Summary struct {
|
|
||||||
Subjects []intoto.Subject `json:"subjects"`
|
|
||||||
SLSALevels []string `json:"slsa_levels"`
|
|
||||||
Verifier string `json:"verifier"`
|
|
||||||
PolicyURI string `json:"policy_uri"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Violation struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Attestation *intoto.Statement `json:"attestation"`
|
|
||||||
Details map[string]any `json:"details"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Result struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Violations []Violation `json:"violations"`
|
|
||||||
Summary Summary `json:"summary"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyMappings struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
Kind string `json:"kind"`
|
|
||||||
Policies []PolicyMapping `json:"policies"`
|
|
||||||
Mirrors []PolicyMirror `json:"mirrors"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyMapping struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Origin PolicyOrigin `json:"origin"`
|
|
||||||
Files []PolicyMappingFile `json:"files"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyMappingFile struct {
|
|
||||||
Path string `json:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyMirror struct {
|
|
||||||
PolicyId string `yaml:"policy-id"`
|
|
||||||
Mirror MirrorSpec `json:"mirror"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MirrorSpec struct {
|
|
||||||
Domains []string `json:"domains"`
|
|
||||||
Prefix string `json:"prefix"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyOrigin struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Prefix string `json:"prefix"`
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyOptions struct {
|
|
||||||
TufClient tuf.TUFClient
|
|
||||||
LocalTargetsDir string
|
|
||||||
LocalPolicyDir string
|
|
||||||
PolicyId string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Policy struct {
|
|
||||||
InputFiles []*PolicyFile
|
|
||||||
Query string
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyInput struct {
|
|
||||||
Digest string `json:"digest"`
|
|
||||||
Purl string `json:"purl"`
|
|
||||||
IsCanonical bool `json:"isCanonical"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyFile struct {
|
|
||||||
Path string
|
|
||||||
Content []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveLocalPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, error) {
|
|
||||||
if opts.LocalPolicyDir == "" {
|
if opts.LocalPolicyDir == "" {
|
||||||
return nil, fmt.Errorf("local policy dir not set")
|
return nil, fmt.Errorf("local policy dir not set")
|
||||||
}
|
}
|
||||||
@@ -117,28 +33,12 @@ func resolveLocalPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, e
|
|||||||
}
|
}
|
||||||
policy := &Policy{
|
policy := &Policy{
|
||||||
InputFiles: files,
|
InputFiles: files,
|
||||||
|
Mapping: mapping,
|
||||||
}
|
}
|
||||||
return policy, nil
|
return policy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadLocalMappings(opts *PolicyOptions) (*PolicyMappings, error) {
|
func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
|
||||||
if opts.LocalPolicyDir == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
mappings := &PolicyMappings{}
|
|
||||||
path := path.Join(opts.LocalPolicyDir, PolicyMappingFileName)
|
|
||||||
mappingFile, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read policy mapping file %s: %w", path, err)
|
|
||||||
}
|
|
||||||
err = goyaml.Unmarshal(mappingFile, mappings)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", path, err)
|
|
||||||
}
|
|
||||||
return mappings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, error) {
|
|
||||||
files := make([]*PolicyFile, 0, len(mapping.Files))
|
files := make([]*PolicyFile, 0, len(mapping.Files))
|
||||||
for _, f := range mapping.Files {
|
for _, f := range mapping.Files {
|
||||||
filename := f.Path
|
filename := f.Path
|
||||||
@@ -153,34 +53,17 @@ func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, err
|
|||||||
}
|
}
|
||||||
policy := &Policy{
|
policy := &Policy{
|
||||||
InputFiles: files,
|
InputFiles: files,
|
||||||
|
Mapping: mapping,
|
||||||
}
|
}
|
||||||
return policy, nil
|
return policy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
|
func findPolicyMatch(named reference.Named, mappings *config.PolicyMappings) (*config.PolicyMapping, *config.PolicyMirror) {
|
||||||
if tufClient == nil {
|
|
||||||
return nil, fmt.Errorf("tuf client not set")
|
|
||||||
}
|
|
||||||
filename := PolicyMappingFileName
|
|
||||||
_, fileContents, err := tufClient.DownloadTarget(filename, filepath.Join(localTargetsDir, filename))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to download policy file %s: %w", filename, err)
|
|
||||||
}
|
|
||||||
mappings := &PolicyMappings{}
|
|
||||||
|
|
||||||
err = goyaml.Unmarshal(fileContents, mappings)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal policy mapping file %s: %w", filename, err)
|
|
||||||
}
|
|
||||||
return mappings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findPolicyMatch(named reference.Named, mappings *PolicyMappings) (*PolicyMapping, *PolicyMirror) {
|
|
||||||
if mappings != nil {
|
if mappings != nil {
|
||||||
for _, mapping := range mappings.Policies {
|
for _, mapping := range mappings.Policies {
|
||||||
if mapping.Origin.Domain == reference.Domain(named) &&
|
if mapping.Origin.Domain == reference.Domain(named) &&
|
||||||
strings.HasPrefix(reference.Path(named), mapping.Origin.Prefix) {
|
strings.HasPrefix(reference.Path(named), mapping.Origin.Prefix) {
|
||||||
return &mapping, nil
|
return mapping, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// now search mirrors
|
// now search mirrors
|
||||||
@@ -190,10 +73,10 @@ func findPolicyMatch(named reference.Named, mappings *PolicyMappings) (*PolicyMa
|
|||||||
strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) {
|
strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) {
|
||||||
for _, mapping := range mappings.Policies {
|
for _, mapping := range mappings.Policies {
|
||||||
if mapping.Id == mirror.PolicyId {
|
if mapping.Id == mirror.PolicyId {
|
||||||
return &mapping, nil
|
return mapping, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, &mirror
|
return nil, mirror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,26 +85,26 @@ func findPolicyMatch(named reference.Named, mappings *PolicyMappings) (*PolicyMa
|
|||||||
|
|
||||||
func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
|
func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
|
||||||
if opts.PolicyId != "" {
|
if opts.PolicyId != "" {
|
||||||
localMappings, err := LoadLocalMappings(opts)
|
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
||||||
}
|
}
|
||||||
if localMappings != nil {
|
if localMappings != nil {
|
||||||
for _, mapping := range localMappings.Policies {
|
for _, mapping := range localMappings.Policies {
|
||||||
if mapping.Id == opts.PolicyId {
|
if mapping.Id == opts.PolicyId {
|
||||||
return resolveLocalPolicy(opts, &mapping)
|
return resolveLocalPolicy(opts, mapping)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// must check tuf
|
// must check tuf
|
||||||
tufMappings, err := loadTufMappings(opts.TufClient, opts.LocalTargetsDir)
|
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load tuf policy mappings: %w", err)
|
return nil, fmt.Errorf("failed to load tuf policy mappings by id: %w", err)
|
||||||
}
|
}
|
||||||
for _, mapping := range tufMappings.Policies {
|
for _, mapping := range tufMappings.Policies {
|
||||||
if mapping.Id == opts.PolicyId {
|
if mapping.Id == opts.PolicyId {
|
||||||
return resolveTufPolicy(opts, &mapping)
|
return resolveTufPolicy(opts, mapping)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("policy with id %s not found", opts.PolicyId)
|
return nil, fmt.Errorf("policy with id %s not found", opts.PolicyId)
|
||||||
@@ -229,7 +112,7 @@ func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *PolicyOptions) (*Policy, error) {
|
func ResolvePolicy(ctx context.Context, detailsResolver oci.ImageDetailsResolver, opts *PolicyOptions) (*Policy, error) {
|
||||||
p, err := resolvePolicyById(opts)
|
p, err := resolvePolicyById(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
|
return nil, fmt.Errorf("failed to resolve policy by id: %w", err)
|
||||||
@@ -237,8 +120,7 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
|
|||||||
if p != nil {
|
if p != nil {
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
imageName, err := detailsResolver.ImageName(ctx)
|
||||||
imageName, err := resolver.ImageName(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get image name: %w", err)
|
return nil, fmt.Errorf("failed to get image name: %w", err)
|
||||||
}
|
}
|
||||||
@@ -246,7 +128,7 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
||||||
}
|
}
|
||||||
localMappings, err := LoadLocalMappings(opts)
|
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
|
||||||
}
|
}
|
||||||
@@ -255,16 +137,16 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
|
|||||||
return resolveLocalPolicy(opts, mapping)
|
return resolveLocalPolicy(opts, mapping)
|
||||||
}
|
}
|
||||||
// must check tuf
|
// must check tuf
|
||||||
tufMappings, err := loadTufMappings(opts.TufClient, opts.LocalTargetsDir)
|
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load tuf policy mappings: %w", err)
|
return nil, fmt.Errorf("failed to load tuf policy mappings as fallback: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// it's a mirror of a tuf policy
|
// it's a mirror of a tuf policy
|
||||||
if mirror != nil {
|
if mirror != nil {
|
||||||
for _, mapping := range tufMappings.Policies {
|
for _, mapping := range tufMappings.Policies {
|
||||||
if mapping.Id == mirror.PolicyId {
|
if mapping.Id == mirror.PolicyId {
|
||||||
return resolveTufPolicy(opts, &mapping)
|
return resolveTufPolicy(opts, mapping)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,3 +158,32 @@ func ResolvePolicy(ctx context.Context, resolver oci.AttestationResolver, opts *
|
|||||||
}
|
}
|
||||||
return resolveTufPolicy(opts, mapping)
|
return resolveTufPolicy(opts, mapping)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {
|
||||||
|
switch imageSource.Type {
|
||||||
|
case oci.OCI:
|
||||||
|
return oci.NewOCILayoutAttestationResolver(imageSource)
|
||||||
|
case oci.Docker:
|
||||||
|
return oci.NewRegistryImageDetailsResolver(imageSource)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unsupported image source type: %s", imageSource.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (oci.AttestationResolver, error) {
|
||||||
|
switch resolver := resolver.(type) {
|
||||||
|
case *oci.RegistryImageDetailsResolver:
|
||||||
|
if mapping.Attestations != nil && mapping.Attestations.Style == config.AttestationStyleAttached {
|
||||||
|
return oci.NewRegistryAttestationResolver(resolver)
|
||||||
|
} else {
|
||||||
|
if mapping.Attestations != nil && mapping.Attestations.Repo != "" {
|
||||||
|
return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo))
|
||||||
|
} else {
|
||||||
|
return oci.NewReferrersAttestationResolver(resolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *oci.OCILayoutResolver:
|
||||||
|
return resolver, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/attest/internal/test"
|
"github.com/docker/attest/internal/test"
|
||||||
"github.com/docker/attest/pkg/attestation"
|
"github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/docker/attest/pkg/config"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
"github.com/docker/attest/pkg/policy"
|
"github.com/docker/attest/pkg/policy"
|
||||||
"github.com/docker/attest/pkg/tuf"
|
"github.com/docker/attest/pkg/tuf"
|
||||||
@@ -76,8 +77,14 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
PolicyId: tc.policyId,
|
PolicyId: tc.policyId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
imageName, err := tc.resolver.ImageName(ctx)
|
||||||
policy, err := policy.ResolvePolicy(ctx, tc.resolver, tc.policy)
|
require.NoError(t, err)
|
||||||
|
platform, err := tc.resolver.ImagePlatform(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
src, err := oci.ParseImageSpec(imageName, oci.WithPlatform(platform.String()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
resolver, err := policy.CreateImageDetailsResolver(src)
|
||||||
|
policy, err := policy.ResolvePolicy(ctx, resolver, tc.policy)
|
||||||
if tc.errorStr != "" {
|
if tc.errorStr != "" {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), tc.errorStr)
|
assert.Contains(t, err.Error(), tc.errorStr)
|
||||||
@@ -98,10 +105,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadingMappings(t *testing.T) {
|
func TestLoadingMappings(t *testing.T) {
|
||||||
opts := &policy.PolicyOptions{
|
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mock-tuf-allow"))
|
||||||
LocalPolicyDir: filepath.Join("testdata", "mock-tuf-allow"),
|
|
||||||
}
|
|
||||||
policyMappings, err := policy.LoadLocalMappings(opts)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, len(policyMappings.Mirrors), 1)
|
assert.Equal(t, len(policyMappings.Mirrors), 1)
|
||||||
for _, mirror := range policyMappings.Mirrors {
|
for _, mirror := range policyMappings.Mirrors {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ policies:
|
|||||||
prefix: library/
|
prefix: library/
|
||||||
id: docker-official-images
|
id: docker-official-images
|
||||||
description: Docker Official Images
|
description: Docker Official Images
|
||||||
|
attestations:
|
||||||
|
repo: "localhost:5001/library-refs"
|
||||||
files:
|
files:
|
||||||
- path: doi/policy.rego
|
- path: doi/policy.rego
|
||||||
mirrors:
|
mirrors:
|
||||||
|
|||||||
53
pkg/policy/types.go
Normal file
53
pkg/policy/types.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/attest/pkg/config"
|
||||||
|
"github.com/docker/attest/pkg/tuf"
|
||||||
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Summary struct {
|
||||||
|
Subjects []intoto.Subject `json:"subjects"`
|
||||||
|
SLSALevels []string `json:"slsa_levels"`
|
||||||
|
Verifier string `json:"verifier"`
|
||||||
|
PolicyURI string `json:"policy_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Violation struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Attestation *intoto.Statement `json:"attestation"`
|
||||||
|
Details map[string]any `json:"details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Violations []Violation `json:"violations"`
|
||||||
|
Summary Summary `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyOptions struct {
|
||||||
|
TufClient tuf.TUFClient
|
||||||
|
LocalTargetsDir string
|
||||||
|
LocalPolicyDir string
|
||||||
|
PolicyId string
|
||||||
|
ReferrersRepo string
|
||||||
|
AttestationStyle config.AttestationStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
type Policy struct {
|
||||||
|
InputFiles []*PolicyFile
|
||||||
|
Query string
|
||||||
|
Mapping *config.PolicyMapping
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyInput struct {
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
Purl string `json:"purl"`
|
||||||
|
IsCanonical bool `json:"isCanonical"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyFile struct {
|
||||||
|
Path string
|
||||||
|
Content []byte
|
||||||
|
}
|
||||||
49
test/testdata/local-policy/doi/policy.rego
vendored
Normal file
49
test/testdata/local-policy/doi/policy.rego
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package attest
|
||||||
|
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
keys := [{
|
||||||
|
"id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4",
|
||||||
|
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----",
|
||||||
|
"from": "2023-12-15T14:00:00Z",
|
||||||
|
"to": null,
|
||||||
|
"status": "active",
|
||||||
|
"signing-format": "dssev1",
|
||||||
|
}]
|
||||||
|
|
||||||
|
provs(pred) := p if {
|
||||||
|
res := attest.fetch(pred)
|
||||||
|
not res.error
|
||||||
|
p := res.value
|
||||||
|
}
|
||||||
|
|
||||||
|
atts := union({
|
||||||
|
provs("https://slsa.dev/provenance/v0.2"),
|
||||||
|
provs("https://spdx.dev/Document"),
|
||||||
|
})
|
||||||
|
|
||||||
|
opts := {"keys": keys}
|
||||||
|
|
||||||
|
statements contains s if {
|
||||||
|
some att in atts
|
||||||
|
res := attest.verify(att, opts)
|
||||||
|
not res.error
|
||||||
|
s := res.value
|
||||||
|
}
|
||||||
|
|
||||||
|
subjects contains subject if {
|
||||||
|
some statement in statements
|
||||||
|
some subject in statement.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
result := {
|
||||||
|
"success": count(atts) > 0,
|
||||||
|
"violations": set(),
|
||||||
|
"attestations": statements,
|
||||||
|
"summary": {
|
||||||
|
"subjects": subjects,
|
||||||
|
"slsa_level": "SLSA_BUILD_LEVEL_3",
|
||||||
|
"verifier": "docker-official-images",
|
||||||
|
"policy_uri": "https://docker.com/official/policy/v0.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
29
test/testdata/local-policy/mapping.yaml
vendored
Normal file
29
test/testdata/local-policy/mapping.yaml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# map repos to policies
|
||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- origin:
|
||||||
|
domain: docker.io
|
||||||
|
prefix: library/
|
||||||
|
id: test-images
|
||||||
|
description: Local test images
|
||||||
|
files:
|
||||||
|
- path: "doi/policy.rego"
|
||||||
|
|
||||||
|
mirrors:
|
||||||
|
- policy-id: test-images
|
||||||
|
mirror:
|
||||||
|
domains: ["*"]
|
||||||
|
prefix: "repo"
|
||||||
|
- policy-id: test-images
|
||||||
|
mirror:
|
||||||
|
domains: ["*"]
|
||||||
|
prefix: "library/"
|
||||||
|
- policy-id: test-images
|
||||||
|
mirror:
|
||||||
|
domains: ["*"]
|
||||||
|
prefix: "test-image"
|
||||||
|
- policy-id: test-images
|
||||||
|
mirror:
|
||||||
|
domains: ["*"]
|
||||||
|
prefix: "image-signer-verifier-test"
|
||||||
Reference in New Issue
Block a user