Merge branch 'main' into feat-cloud-provider-authn
This commit is contained in:
20
go.mod
20
go.mod
@@ -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
40
go.sum
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
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 (
|
||||
"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
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")
|
||||
}
|
||||
429
pkg/oci/oci.go
429
pkg/oci/oci.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
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 {
|
||||
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")
|
||||
}
|
||||
|
||||
132
pkg/oci/types.go
132
pkg/oci/types.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
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