Merge branch 'main' into feat-cloud-provider-authn

This commit is contained in:
Joel Kamp
2024-06-21 16:38:45 -05:00
committed by mrjoelkamp
24 changed files with 1112 additions and 707 deletions

20
go.mod
View File

@@ -4,7 +4,7 @@ go 1.22.1
require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/aws/aws-sdk-go-v2/config v1.27.19
github.com/aws/aws-sdk-go-v2/config v1.27.21
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589
github.com/containerd/containerd v1.7.18
@@ -50,20 +50,20 @@ require (
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.28.0 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.19 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.10 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.0 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.21 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.18.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.12 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.32.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.28.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.21.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.29.1 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect

40
go.sum
View File

@@ -105,20 +105,20 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W
github.com/aws/aws-sdk-go v1.53.10 h1:3enP5l5WtezT9Ql+XZqs56JBf5YUd/FEzTCg///OIGY=
github.com/aws/aws-sdk-go v1.53.10/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM=
github.com/aws/aws-sdk-go-v2 v1.28.0 h1:ne6ftNhY0lUvlazMUQF15FF6NH80wKmPRFG7g2q6TCw=
github.com/aws/aws-sdk-go-v2 v1.28.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
github.com/aws/aws-sdk-go-v2/config v1.27.19 h1:+DBS8gJP6VsxYkZ6UEV0/VsRM2rYpbQCYsosW9RRmeQ=
github.com/aws/aws-sdk-go-v2/config v1.27.19/go.mod h1:KzZcioJWzy9oV+oS5CobYXlDtU9+eW7bPG1g7gizTW4=
github.com/aws/aws-sdk-go-v2/credentials v1.17.19 h1:R18G7nBBGLby51CFEqUBFF2IVl7LUdCtYj6iosUwh/0=
github.com/aws/aws-sdk-go-v2/credentials v1.17.19/go.mod h1:xr9kUMnaLTB866HItT6pg58JgiBP77fSQLBwIa//zk8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6 h1:vVOuhRyslJ6T/HteG71ZWCTas1q2w6f0NKsNbkXHs/A=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.6/go.mod h1:jimWaqLiT0sJGLh51dKCLLtExRYPtMU7MpxuCgtbkxg=
github.com/aws/aws-sdk-go-v2 v1.30.0 h1:6qAwtzlfcTtcL8NHtbDQAqgM5s6NDipQTkPxyH/6kAA=
github.com/aws/aws-sdk-go-v2 v1.30.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
github.com/aws/aws-sdk-go-v2/config v1.27.21 h1:yPX3pjGCe2hJsetlmGNB4Mngu7UPmvWPzzWCv1+boeM=
github.com/aws/aws-sdk-go-v2/config v1.27.21/go.mod h1:4XtlEU6DzNai8RMbjSF5MgGZtYvrhBP/aKZcRtZAVdM=
github.com/aws/aws-sdk-go-v2/credentials v1.17.21 h1:pjAqgzfgFhTv5grc7xPHtXCAaMapzmwA7aU+c/SZQGw=
github.com/aws/aws-sdk-go-v2/credentials v1.17.21/go.mod h1:nhK6PtBlfHTUDVmBLr1dg+WHCOCK+1Fu/WQyVHPsgNQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8 h1:FR+oWPFb/8qMVYMWN98bUZAGqPvLHiyqg1wqQGfUAXY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8/go.mod h1:EgSKcHiuuakEIxJcKGzVNWh5srVAQ3jKaSrBGRYvM48=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.10 h1:LZIUb8sQG2cb89QaVFtMSnER10gyKkqU1k3hP3g9das=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.10/go.mod h1:BRIqay//vnIOCZjoXWSLffL2uzbtxEmnSlfbvVh7Z/4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 h1:SJ04WXGTwnHlWIODtC5kJzKbeuHt+OUNOgKg7nfnUGw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12/go.mod h1:FkpvXhA92gb3GE9LD6Og0pHHycTxW7xGpnEh5E7Opwo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.10 h1:HY7CXLA0GiQUo3WYxOP7WYkLcwvRX4cLPf5joUcrQGk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.10/go.mod h1:kfRBSxRa+I+VyON7el3wLZdrO91oxUxEwdAaWgFqN90=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 h1:hb5KgeYfObi5MHkSSZMEudnIvX30iB+E21evI4r6BnQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12/go.mod h1:CroKe/eWJdyfy9Vx4rljP5wTUjNJfb+fPz1uMYUhEGM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2 h1:y6LX9GUoEA3mO0qpFl1ZQHj1rFyPWVphlzebiSt2tKE=
@@ -127,16 +127,16 @@ github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.18.2 h1:PpbXaecV3sLAS6rjQiaKw4
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.18.2/go.mod h1:fUHpGXr4DrXkEDpGAjClPsviWf+Bszeb0daKE0blxv8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.12 h1:kO2J7WMroF/OTHN9WTcUtMjPhJ7ZoNxx0dwv6UCXQgY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.12/go.mod h1:mrNxrjYvXaSjZe5fkKaWgDnOQ6BExLn/7Ru9OpRsMPY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14 h1:zSDPny/pVnkqABXYRicYuPf9z2bTqfH13HT3v6UheIk=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14/go.mod h1:3TTcI5JSzda1nw/pkVC9dhgLre0SNBFj2lYS4GctXKI=
github.com/aws/aws-sdk-go-v2/service/kms v1.32.1 h1:FARrQLRQXpCFYylIUVF1dRij6YbPCmtwudq9NBk4kFc=
github.com/aws/aws-sdk-go-v2/service/kms v1.32.1/go.mod h1:8lETO9lelSG2B6KMXFh2OwPPqGV6WQM3RqLAEjP1xaU=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.12 h1:FsYii6U+2k8ynYBo+pywlCBY9HNAFRh+iICRHbn+Qyw=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.12/go.mod h1:j9Rps+Lcs2A0tYypWsNBeJOjgsIYUf1Styppo9Es0Wo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.6 h1:lEE+xEcq3lh9bk362tgErP1+n689q5ERdmTwmF1XT3M=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.6/go.mod h1:2tR0x1DCL5IgnVZ1NQNFDNg5/XL/kiQgWI5l7I/N5Js=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.13 h1:TSzmuUeruVJ4XWYp3bYzKCXue70ECpJWmbP3UfEvhYY=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.13/go.mod h1:FppRtFjBA9mSWTj2cIAWCP66+bbBPMuPpBfWRXC5Yi0=
github.com/aws/aws-sdk-go-v2/service/sso v1.21.1 h1:sd0BsnAvLH8gsp2e3cbaIr+9D7T1xugueQ7V/zUAsS4=
github.com/aws/aws-sdk-go-v2/service/sso v1.21.1/go.mod h1:lcQG/MmxydijbeTOp04hIuJwXGWPZGI3bwdFDGRTv14=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1 h1:1uEFNNskK/I1KoZ9Q8wJxMz5V9jyBlsiaNrM7vA3YUQ=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1/go.mod h1:z0P8K+cBIsFXUr5rzo/psUeJ20XjPN0+Nn8067Nd+E4=
github.com/aws/aws-sdk-go-v2/service/sts v1.29.1 h1:myX5CxqXE0QMZNja6FA1/FSE3Vu1rVmeUmpJMMzeZg0=
github.com/aws/aws-sdk-go-v2/service/sts v1.29.1/go.mod h1:N2mQiucsO0VwK9CYuS4/c2n6Smeh1v47Rz3dWCPFLdE=
github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=

View File

@@ -39,19 +39,6 @@ func ExampleVerify_remote() {
// create a resolver for remote attestations
image := "registry-1.docker.io/library/notary:server"
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
opts := &policy.PolicyOptions{
@@ -62,7 +49,11 @@ func ExampleVerify_remote() {
}
// 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 {
panic(err)
}

View File

@@ -48,7 +48,7 @@ func TestSignVerifyOCILayout(t *testing.T) {
{"no provenance (replace)", NoProvenanceImage, 0, 2, true},
{"no provenance (no replace)", NoProvenanceImage, 2, 2, false},
}
policyResolver := &policy.PolicyOptions{
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: PassPolicyDir,
}
for _, tc := range testCases {
@@ -74,9 +74,9 @@ func TestSignVerifyOCILayout(t *testing.T) {
})
_, err = layout.Write(outputLayout, idx)
require.NoError(t, err)
resolver, err := oci.NewOCILayoutAttestationResolver(outputLayout, "")
src, err := oci.ParseImageSpec("oci://" + outputLayout)
require.NoError(t, err)
policy, err := Verify(ctx, policyResolver, resolver)
policy, err := Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equalf(t, OutcomeSuccess, policy.Outcome, "Policy should have been found")

View File

@@ -6,13 +6,25 @@ import (
"time"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
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) {
pctx, err := policy.ResolvePolicy(ctx, resolver, opts)
func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.PolicyOptions) (result *VerificationResult, err error) {
// 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 {
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,
}, 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)
if err != nil {
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 {
return nil, fmt.Errorf("failed to get image name: %w", err)
}
platform, err := resolver.ImagePlatform()
platform, err := resolver.ImagePlatform(ctx)
if err != nil {
return nil, err
}

View File

@@ -25,6 +25,10 @@ var (
ExampleAttestation = filepath.Join("..", "..", "test", "testdata", "example_attestation.json")
)
const (
LinuxAMD64 = "linux/amd64"
)
func TestVerifyAttestations(t *testing.T) {
ex, err := os.ReadFile(ExampleAttestation)
assert.NoError(t, err)
@@ -94,14 +98,13 @@ func TestVSA(t *testing.T) {
_, err = layout.Write(outputLayout, idx)
assert.NoError(t, err)
resolver, err := oci.NewOCILayoutAttestationResolver(outputLayout, "linux/amd64")
require.NoError(t, err)
// mocked vsa query should pass
policyOpts := &policy.PolicyOptions{
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)
assert.Equal(t, OutcomeSuccess, results.Outcome)
assert.Empty(t, results.Violations)
@@ -151,14 +154,13 @@ func TestVerificationFailure(t *testing.T) {
_, err = layout.Write(outputLayout, idx)
assert.NoError(t, err)
resolver, err := oci.NewOCILayoutAttestationResolver(outputLayout, "linux/amd64")
require.NoError(t, err)
// mocked vsa query should fail
policyOpts := &policy.PolicyOptions{
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)
assert.Equal(t, OutcomeFailure, results.Outcome)
assert.Len(t, results.Violations, 1)
@@ -225,13 +227,12 @@ func TestSignVerifyNoTL(t *testing.T) {
_, err = layout.Write(outputLayout, idx)
assert.NoError(t, err)
resolver, err := oci.NewOCILayoutAttestationResolver(outputLayout, "linux/amd64")
require.NoError(t, err)
policyOpts := &policy.PolicyOptions{
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)
assert.Equal(t, OutcomeSuccess, results.Outcome)
})

View File

@@ -10,6 +10,7 @@ import (
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attest"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/mirror"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
@@ -21,21 +22,29 @@ import (
)
var (
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
TestTempDir = "attest-sign-test"
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy")
LocalPolicyAttached = filepath.Join("..", "..", "test", "testdata", "local-policy-attached")
PassNoTLPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-no-tl")
FailPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-fail")
TestTempDir = "attest-sign-test"
)
func TestAttestationReferenceTypes(t *testing.T) {
ctx, signer := test.Setup(t)
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
platforms := []string{"linux/amd64", "linux/arm64"}
for _, tc := range []struct {
server *httptest.Server
skipSubject bool
useDigest bool
server *httptest.Server
referrersServer *httptest.Server
skipSubject bool
useDigest bool
referrersRepo string
attestationSource config.AttestationStyle
expectFailure bool
policyDir string
}{
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
@@ -44,78 +53,135 @@ func TestAttestationReferenceTypes(t *testing.T) {
server: httptest.NewServer(registry.New()),
},
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
skipSubject: true,
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
skipSubject: true,
attestationSource: config.AttestationStyleAttached,
},
{
server: httptest.NewServer(registry.New(registry.WithReferrersSupport(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
defer s.Close()
u, err := url.Parse(s.URL)
require.NoError(t, err)
t.Run(fmt.Sprint(tc), func(t *testing.T) {
s := tc.server
defer s.Close()
opts := &attestation.SigningOptions{
Replace: true,
SkipSubject: tc.skipSubject,
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
require.NoError(t, err)
signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/repo:root", u.Host)
require.NoError(t, err)
err = mirror.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)
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())
if tc.referrersServer != nil {
defer tc.referrersServer.Close()
}
resolver, err := oci.NewRegistryAttestationResolver(ref, platform)
u, err := url.Parse(s.URL)
require.NoError(t, err)
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: PassPolicyDir,
opts := &attestation.SigningOptions{
Replace: true,
SkipSubject: tc.skipSubject,
}
results, err := attest.Verify(ctx, policyOpts, resolver)
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
if !tc.skipSubject {
// can evaluate policy using referrers
if tc.useDigest {
p, err := oci.ParsePlatform(platform)
indexName := fmt.Sprintf("%s/repo:root", u.Host)
require.NoError(t, err)
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)
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)
require.NoError(t, err)
desc, err := remote.Image(subjectRef, options...)
desc, err := remote.Index(subjectRef, options...)
require.NoError(t, err)
subjectDigest, err := desc.Digest()
idxDigest, err := desc.Digest()
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)
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)
}
}
}
})
}
}
@@ -173,14 +239,13 @@ func TestReferencesInDifferentRepo(t *testing.T) {
continue
}
// can evaluate policy using referrers in a different repo
repo := fmt.Sprintf("%s/%s", refServerUrl.Host, repoName)
referencedImage := fmt.Sprintf("%s@%s", indexName, mf.Digest.String())
referrersResolver, err := oci.NewReferrersAttestationResolver(referencedImage, oci.WithReferrersRepo(repo))
require.NoError(t, err)
policyOpts := &policy.PolicyOptions{
LocalPolicyDir: PassPolicyDir,
}
results, err := attest.Verify(ctx, policyOpts, referrersResolver)
src, err := oci.ParseImageSpec(referencedImage)
require.NoError(t, err)
results, err := attest.Verify(ctx, src, policyOpts)
require.NoError(t, err)
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
}

49
pkg/config/config.go Normal file
View 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
View 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"`
}

View File

@@ -2,7 +2,6 @@ package mirror
import (
"fmt"
"log"
"os"
"github.com/docker/attest/internal/embed"
@@ -29,7 +28,7 @@ func NewTufMirror(root []byte, tufPath, metadataURL, targetsURL string, versionC
func PushImageToRegistry(image v1.Image, imageName string) error {
ref, err := name.ParseReference(imageName)
if err != nil {
log.Fatalf("Failed to parse image name: %v", err)
return fmt.Errorf("Failed to parse image name '%s': %w", imageName, err)
}
// Push the image to the registry
@@ -40,8 +39,9 @@ func PushIndexToRegistry(image v1.ImageIndex, imageName string) error {
// Parse the index name
ref, err := name.ParseReference(imageName)
if err != nil {
log.Fatalf("Failed to parse image name: %v", err)
return fmt.Errorf("Failed to parse image name: %w", err)
}
// Push the index to the registry
return remote.WriteIndex(ref, image, oci.MultiKeychainOption())
}

151
pkg/oci/layout.go Normal file
View 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")
}

View File

@@ -9,9 +9,7 @@ import (
"github.com/containerd/containerd/platforms"
"github.com/distribution/reference"
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/layout"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@@ -37,425 +35,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)
if err != nil {
return fmt.Errorf("failed to get image 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.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 {
// prepare options
options := []remote.Option{MultiKeychainOption(), remote.WithTransport(HttpTransport()), remote.WithContext(ctx)}
@@ -469,11 +48,9 @@ func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Envelope, error) {
manifest := ia.Manifest
im := ia.Image
image := ia.Image
var envs []*att.Envelope
ls, err := im.Layers()
layers, err := image.Layers()
if err != nil {
return nil, fmt.Errorf("failed to get layers: %w", err)
}
@@ -481,7 +58,7 @@ func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Env
if (strings.HasPrefix(string(l.MediaType), "application/vnd.in-toto.")) &&
strings.HasSuffix(string(l.MediaType), "+dsse") &&
l.Annotations[InTotoPredicateType] == predicateType {
reader, err := ls[i].Uncompressed()
reader, err := layers[i].Uncompressed()
if err != nil {
return nil, fmt.Errorf("failed to get layer contents: %w", err)
}

View File

@@ -85,3 +85,24 @@ func TestImageDigestForPlatform(t *testing.T) {
assert.NoError(t, err)
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
View 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
View 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
View 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:"))
}

View File

@@ -23,12 +23,16 @@ type AttestationManifest struct {
}
type AttestationResolver interface {
ImageName(ctx context.Context) (string, error)
ImagePlatform() (*v1.Platform, error)
ImageDigest(ctx context.Context) (string, error)
ImageDetailsResolver
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 {
Envs []*att.Envelope
}
@@ -45,6 +49,6 @@ func (r MockResolver) ImageDigest(ctx context.Context) (string, error) {
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")
}

View File

@@ -2,7 +2,7 @@ package oci
import (
"fmt"
"log"
"strings"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -11,16 +11,37 @@ import (
)
const (
AttestationManifestType = "attestation-manifest"
InTotoPredicateType = "in-toto.io/predicate-type"
OciReferenceTarget = "org.opencontainers.image.ref.name"
AttestationManifestType = "attestation-manifest"
InTotoPredicateType = "in-toto.io/predicate-type"
OciReferenceTarget = "org.opencontainers.image.ref.name"
LocalPrefix = "oci://"
RegistryPrefix = "docker://"
OCI SourceType = "OCI"
Docker SourceType = "Docker"
)
type SourceType string
type SubjectIndex struct {
Index v1.ImageIndex
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) {
wrapperIdx, err := layout.ImageIndexFromPath(path)
if err != nil {
@@ -47,8 +68,9 @@ func SubjectIndexFromPath(path string) (*SubjectIndex, error) {
func SubjectIndexFromRemote(image string) (*SubjectIndex, error) {
ref, err := name.ParseReference(image)
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)
}
// Pull the image from the registry
idx, err := remote.Index(ref, MultiKeychainOption())
if err != nil {
@@ -59,3 +81,103 @@ func SubjectIndexFromRemote(image string) (*SubjectIndex, error) {
Name: image,
}, 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
}

View File

@@ -10,95 +10,11 @@ import (
"strings"
"github.com/distribution/reference"
"github.com/docker/attest/pkg/config"
"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 (
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) {
func resolveLocalPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
if opts.LocalPolicyDir == "" {
return nil, fmt.Errorf("local policy dir not set")
}
@@ -117,28 +33,12 @@ func resolveLocalPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, e
}
policy := &Policy{
InputFiles: files,
Mapping: mapping,
}
return policy, nil
}
func LoadLocalMappings(opts *PolicyOptions) (*PolicyMappings, 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) {
func resolveTufPolicy(opts *PolicyOptions, mapping *config.PolicyMapping) (*Policy, error) {
files := make([]*PolicyFile, 0, len(mapping.Files))
for _, f := range mapping.Files {
filename := f.Path
@@ -153,34 +53,17 @@ func resolveTufPolicy(opts *PolicyOptions, mapping *PolicyMapping) (*Policy, err
}
policy := &Policy{
InputFiles: files,
Mapping: mapping,
}
return policy, nil
}
func loadTufMappings(tufClient tuf.TUFClient, localTargetsDir string) (*PolicyMappings, error) {
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) {
func findPolicyMatch(named reference.Named, mappings *config.PolicyMappings) (*config.PolicyMapping, *config.PolicyMirror) {
if mappings != nil {
for _, mapping := range mappings.Policies {
if mapping.Origin.Domain == reference.Domain(named) &&
strings.HasPrefix(reference.Path(named), mapping.Origin.Prefix) {
return &mapping, nil
return mapping, nil
}
}
// now search mirrors
@@ -190,10 +73,10 @@ func findPolicyMatch(named reference.Named, mappings *PolicyMappings) (*PolicyMa
strings.HasPrefix(reference.Path(named), mirror.Mirror.Prefix) {
for _, mapping := range mappings.Policies {
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) {
if opts.PolicyId != "" {
localMappings, err := LoadLocalMappings(opts)
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
if err != nil {
return nil, fmt.Errorf("failed to load local policy mappings: %w", err)
}
if localMappings != nil {
for _, mapping := range localMappings.Policies {
if mapping.Id == opts.PolicyId {
return resolveLocalPolicy(opts, &mapping)
return resolveLocalPolicy(opts, mapping)
}
}
}
// must check tuf
tufMappings, err := loadTufMappings(opts.TufClient, opts.LocalTargetsDir)
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
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 {
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)
@@ -229,7 +112,7 @@ func resolvePolicyById(opts *PolicyOptions) (*Policy, error) {
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)
if err != nil {
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 {
return p, nil
}
imageName, err := resolver.ImageName(ctx)
imageName, err := detailsResolver.ImageName(ctx)
if err != nil {
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 {
return nil, fmt.Errorf("failed to parse image name: %w", err)
}
localMappings, err := LoadLocalMappings(opts)
localMappings, err := config.LoadLocalMappings(opts.LocalPolicyDir)
if err != nil {
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)
}
// must check tuf
tufMappings, err := loadTufMappings(opts.TufClient, opts.LocalTargetsDir)
tufMappings, err := config.LoadTufMappings(opts.TufClient, opts.LocalTargetsDir)
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
if mirror != nil {
for _, mapping := range tufMappings.Policies {
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)
}
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)
}
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/docker/attest/internal/test"
"github.com/docker/attest/pkg/attestation"
"github.com/docker/attest/pkg/config"
"github.com/docker/attest/pkg/oci"
"github.com/docker/attest/pkg/policy"
"github.com/docker/attest/pkg/tuf"
@@ -76,8 +77,14 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
PolicyId: tc.policyId,
}
}
policy, err := policy.ResolvePolicy(ctx, tc.resolver, tc.policy)
imageName, err := tc.resolver.ImageName(ctx)
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 != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorStr)
@@ -98,10 +105,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
}
func TestLoadingMappings(t *testing.T) {
opts := &policy.PolicyOptions{
LocalPolicyDir: filepath.Join("testdata", "mock-tuf-allow"),
}
policyMappings, err := policy.LoadLocalMappings(opts)
policyMappings, err := config.LoadLocalMappings(filepath.Join("testdata", "mock-tuf-allow"))
require.NoError(t, err)
assert.Equal(t, len(policyMappings.Mirrors), 1)
for _, mirror := range policyMappings.Mirrors {

View File

@@ -7,6 +7,8 @@ policies:
prefix: library/
id: docker-official-images
description: Docker Official Images
attestations:
repo: "localhost:5001/library-refs"
files:
- path: doi/policy.rego
mirrors:

53
pkg/policy/types.go Normal file
View 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
}

View 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
View 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"