Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23849c1c2e | ||
|
|
bada1df262 | ||
|
|
4778d3de6a | ||
|
|
a4ac09e7da | ||
|
|
9250552c5b | ||
|
|
2acc30693f | ||
|
|
5db1b5c4c1 |
14
go.mod
14
go.mod
@@ -24,7 +24,7 @@ require (
|
|||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/testcontainers/testcontainers-go/modules/registry v0.33.0
|
github.com/testcontainers/testcontainers-go/modules/registry v0.33.0
|
||||||
github.com/theupdateframework/go-tuf/v2 v2.0.0
|
github.com/theupdateframework/go-tuf/v2 v2.0.0
|
||||||
google.golang.org/api v0.194.0
|
google.golang.org/api v0.195.0
|
||||||
sigs.k8s.io/yaml v1.4.0
|
sigs.k8s.io/yaml v1.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,9 +36,9 @@ require (
|
|||||||
cloud.google.com/go/auth v0.9.1 // indirect
|
cloud.google.com/go/auth v0.9.1 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||||
cloud.google.com/go/iam v1.1.12 // indirect
|
cloud.google.com/go/iam v1.1.13 // indirect
|
||||||
cloud.google.com/go/kms v1.18.4 // indirect
|
cloud.google.com/go/kms v1.18.5 // indirect
|
||||||
cloud.google.com/go/longrunning v0.5.11 // indirect
|
cloud.google.com/go/longrunning v0.5.12 // indirect
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
@@ -191,9 +191,9 @@ require (
|
|||||||
golang.org/x/term v0.23.0 // indirect
|
golang.org/x/term v0.23.0 // indirect
|
||||||
golang.org/x/text v0.17.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
golang.org/x/time v0.6.0 // indirect
|
golang.org/x/time v0.6.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142 // indirect
|
google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c // indirect
|
||||||
google.golang.org/grpc v1.65.0 // indirect
|
google.golang.org/grpc v1.65.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
|||||||
28
go.sum
28
go.sum
@@ -7,12 +7,12 @@ cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy
|
|||||||
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
|
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
|
||||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||||
cloud.google.com/go/iam v1.1.12 h1:JixGLimRrNGcxvJEQ8+clfLxPlbeZA6MuRJ+qJNQ5Xw=
|
cloud.google.com/go/iam v1.1.13 h1:7zWBXG9ERbMLrzQBRhFliAV+kjcRToDTgQT3CTwYyv4=
|
||||||
cloud.google.com/go/iam v1.1.12/go.mod h1:9LDX8J7dN5YRyzVHxwQzrQs9opFFqn0Mxs9nAeB+Hhg=
|
cloud.google.com/go/iam v1.1.13/go.mod h1:K8mY0uSXwEXS30KrnVb+j54LB/ntfZu1dr+4zFMNbus=
|
||||||
cloud.google.com/go/kms v1.18.4 h1:dYN3OCsQ6wJLLtOnI8DGUwQ5shMusXsWCCC+s09ATsk=
|
cloud.google.com/go/kms v1.18.5 h1:75LSlVs60hyHK3ubs2OHd4sE63OAMcM2BdSJc2bkuM4=
|
||||||
cloud.google.com/go/kms v1.18.4/go.mod h1:SG1bgQ3UWW6/KdPo9uuJnzELXY5YTTMJtDYvajiQ22g=
|
cloud.google.com/go/kms v1.18.5/go.mod h1:yXunGUGzabH8rjUPImp2ndHiGolHeWJJ0LODLedicIY=
|
||||||
cloud.google.com/go/longrunning v0.5.11 h1:Havn1kGjz3whCfoD8dxMLP73Ph5w+ODyZB9RUsDxtGk=
|
cloud.google.com/go/longrunning v0.5.12 h1:5LqSIdERr71CqfUsFlJdBpOkBH8FBCFD7P1nTWy3TYE=
|
||||||
cloud.google.com/go/longrunning v0.5.11/go.mod h1:rDn7//lmlfWV1Dx6IB4RatCPenTwwmqXuiP0/RgoEO4=
|
cloud.google.com/go/longrunning v0.5.12/go.mod h1:S5hMV8CDJ6r50t2ubVJSKQVv5u0rmik5//KgLO3k4lU=
|
||||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2 h1:BnG6pr9TTr6CYlrJznYUDj6V7xldD1W+1iXPum0wT/w=
|
cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2 h1:BnG6pr9TTr6CYlrJznYUDj6V7xldD1W+1iXPum0wT/w=
|
||||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2/go.mod h1:pK23AUVXuNzzTpfMCA06sxZGeVQ/75FdVtW249de9Uo=
|
cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2/go.mod h1:pK23AUVXuNzzTpfMCA06sxZGeVQ/75FdVtW249de9Uo=
|
||||||
cuelang.org/go v0.9.2 h1:pfNiry2PdRBr02G/aKm5k2vhzmqbAOoaB4WurmEbWvs=
|
cuelang.org/go v0.9.2 h1:pfNiry2PdRBr02G/aKm5k2vhzmqbAOoaB4WurmEbWvs=
|
||||||
@@ -807,19 +807,19 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||||
google.golang.org/api v0.194.0 h1:dztZKG9HgtIpbI35FhfuSNR/zmaMVdxNlntHj1sIS4s=
|
google.golang.org/api v0.195.0 h1:Ude4N8FvTKnnQJHU48RFI40jOBgIrL8Zqr3/QeST6yU=
|
||||||
google.golang.org/api v0.194.0/go.mod h1:AgvUFdojGANh3vI+P7EVnxj3AISHllxGCJSFmggmnd0=
|
google.golang.org/api v0.195.0/go.mod h1:DOGRWuv3P8TU8Lnz7uQc4hyNqrBpMtD9ppW3wBJurgc=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142 h1:oLiyxGgE+rt22duwci1+TG7bg2/L1LQsXwfjPlmuJA0=
|
google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c h1:TYOEhrQMrNDTAd2rX9m+WgGr8Ku6YNuj1D7OX6rWSok=
|
||||||
google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142/go.mod h1:G11eXq53iI5Q+kyNOmCvnzBaxEA2Q/Ik5Tj7nqBE8j4=
|
google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c/go.mod h1:2rC5OendXvZ8wGEo/cSLheztrZDZaSoHanUcd1xtZnw=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf h1:GillM0Ef0pkZPIB+5iO6SDK+4T9pf6TpaYR6ICD5rVE=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:OFMYQFHJ4TM3JRlWDZhJbZfra2uqc3WLBZiaaqP4DtU=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c h1:Kqjm4WpoWvwhMPcrAczoTyMySQmYa9Wy2iL6Con4zn8=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
|||||||
@@ -6,16 +6,14 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/attest/pkg/policy"
|
|
||||||
"github.com/docker/attest/pkg/signerverifier"
|
"github.com/docker/attest/pkg/signerverifier"
|
||||||
"github.com/docker/attest/pkg/tlog"
|
"github.com/docker/attest/pkg/tlog"
|
||||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UseMockTL = true
|
UseMockTL = true
|
||||||
UseMockKMS = true
|
UseMockKMS = true
|
||||||
UseMockPolicy = true
|
|
||||||
|
|
||||||
AWSRegion = "us-east-1"
|
AWSRegion = "us-east-1"
|
||||||
AWSKMSKeyARN = "arn:aws:kms:us-east-1:175142243308:alias/doi-signing" // sandbox
|
AWSKMSKeyARN = "arn:aws:kms:us-east-1:175142243308:alias/doi-signing" // sandbox
|
||||||
@@ -57,15 +55,6 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
|
|||||||
|
|
||||||
ctx := tlog.WithTL(context.Background(), tl)
|
ctx := tlog.WithTL(context.Background(), tl)
|
||||||
|
|
||||||
var policyEvaluator policy.Evaluator
|
|
||||||
if UseMockPolicy {
|
|
||||||
policyEvaluator = policy.GetMockPolicy()
|
|
||||||
} else {
|
|
||||||
policyEvaluator = policy.NewRegoEvaluator(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = policy.WithPolicyEvaluator(ctx, policyEvaluator)
|
|
||||||
|
|
||||||
var signer dsse.SignerVerifier
|
var signer dsse.SignerVerifier
|
||||||
var err error
|
var err error
|
||||||
if UseMockKMS {
|
if UseMockKMS {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func NewVerifier(opts *policy.Options) (Verifier, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *tufVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (result *VerificationResult, err error) {
|
func (verifier *tufVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (result *VerificationResult, err error) {
|
||||||
// so that we can resolve mapping from the image name earlier
|
// so that we can resolve mapping from the image name earlier
|
||||||
detailsResolver, err := policy.CreateImageDetailsResolver(src)
|
detailsResolver, err := policy.CreateImageDetailsResolver(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -54,35 +54,36 @@ func (v *tufVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (result *V
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to resolve image name: %w", err)
|
return nil, fmt.Errorf("failed to resolve image name: %w", err)
|
||||||
}
|
}
|
||||||
policyResolver := policy.NewResolver(v.tufClient, v.opts)
|
policyResolver := policy.NewResolver(verifier.tufClient, verifier.opts)
|
||||||
pctx, err := policyResolver.ResolvePolicy(ctx, imageName)
|
resolvedPolicy, err := policyResolver.ResolvePolicy(ctx, imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to resolve policy: %w", err)
|
return nil, fmt.Errorf("failed to resolve policy: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pctx == nil {
|
if resolvedPolicy == nil {
|
||||||
return &VerificationResult{
|
return &VerificationResult{
|
||||||
Outcome: OutcomeNoPolicy,
|
Outcome: OutcomeNoPolicy,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
// this is overriding the mapping with a referrers config. Useful for testing if nothing else
|
// this is overriding the mapping with a referrers config. Useful for testing if nothing else
|
||||||
if v.opts.ReferrersRepo != "" {
|
if verifier.opts.ReferrersRepo != "" {
|
||||||
pctx.Mapping.Attestations = &config.AttestationConfig{
|
resolvedPolicy.Mapping.Attestations = &config.AttestationConfig{
|
||||||
Repo: v.opts.ReferrersRepo,
|
Repo: verifier.opts.ReferrersRepo,
|
||||||
Style: config.AttestationStyleReferrers,
|
Style: config.AttestationStyleReferrers,
|
||||||
}
|
}
|
||||||
} else if v.opts.AttestationStyle == config.AttestationStyleAttached {
|
} else if verifier.opts.AttestationStyle == config.AttestationStyleAttached {
|
||||||
pctx.Mapping.Attestations = &config.AttestationConfig{
|
resolvedPolicy.Mapping.Attestations = &config.AttestationConfig{
|
||||||
Repo: v.opts.ReferrersRepo,
|
Repo: verifier.opts.ReferrersRepo,
|
||||||
Style: config.AttestationStyleAttached,
|
Style: config.AttestationStyleAttached,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// because we have a mapping now, we can select a resolver based on its contents (ie. referrers or attached)
|
// 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)
|
resolver, err := policy.CreateAttestationResolver(detailsResolver, resolvedPolicy.Mapping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create attestation resolver: %w", err)
|
return nil, fmt.Errorf("failed to create attestation resolver: %w", err)
|
||||||
}
|
}
|
||||||
result, err = VerifyAttestations(ctx, resolver, pctx)
|
evaluator := policy.NewRegoEvaluator(verifier.opts.Debug)
|
||||||
|
result, err = VerifyAttestations(ctx, resolver, evaluator, resolvedPolicy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to evaluate policy: %w", err)
|
return nil, fmt.Errorf("failed to evaluate policy: %w", err)
|
||||||
}
|
}
|
||||||
@@ -183,7 +184,7 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, pctx *policy.Policy) (*VerificationResult, error) {
|
func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, evaluator policy.Evaluator, resolvedPolicy *policy.Policy) (*VerificationResult, error) {
|
||||||
desc, err := resolver.ImageDescriptor(ctx)
|
desc, err := resolver.ImageDescriptor(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get image descriptor: %w", err)
|
return nil, fmt.Errorf("failed to get image descriptor: %w", err)
|
||||||
@@ -198,7 +199,7 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, pctx
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if pctx.ResolvedName != "" {
|
if resolvedPolicy.ResolvedName != "" {
|
||||||
// this means the name we have is not the one we want to use for policy evaluation
|
// this means the name we have is not the one we want to use for policy evaluation
|
||||||
// so we need to replace it with the one we resolved during policy resolution.
|
// so we need to replace it with the one we resolved during policy resolution.
|
||||||
// this can happen if the name is an alias for another image, e.g. if it is a mirror
|
// this can happen if the name is an alias for another image, e.g. if it is a mirror
|
||||||
@@ -207,7 +208,7 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, pctx
|
|||||||
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
return nil, fmt.Errorf("failed to parse image name: %w", err)
|
||||||
}
|
}
|
||||||
oldName := ref.Name()
|
oldName := ref.Name()
|
||||||
name = strings.Replace(name, oldName, pctx.ResolvedName, 1)
|
name = strings.Replace(name, oldName, resolvedPolicy.ResolvedName, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
ref, err := reference.ParseNormalizedNamed(name)
|
ref, err := reference.ParseNormalizedNamed(name)
|
||||||
@@ -239,16 +240,11 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, pctx
|
|||||||
if tag != "" {
|
if tag != "" {
|
||||||
input.Tag = tag
|
input.Tag = tag
|
||||||
}
|
}
|
||||||
|
result, err := evaluator.Evaluate(ctx, resolver, resolvedPolicy, input)
|
||||||
evaluator, err := policy.GetPolicyEvaluator(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result, err := evaluator.Evaluate(ctx, resolver, pctx, input)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("policy evaluation failed: %w", err)
|
return nil, fmt.Errorf("policy evaluation failed: %w", err)
|
||||||
}
|
}
|
||||||
verificationResult, err := toVerificationResult(pctx, input, result)
|
verificationResult, err := toVerificationResult(resolvedPolicy, input, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to convert to policy result: %w", err)
|
return nil, fmt.Errorf("failed to convert to policy result: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func TestVerifyAttestations(t *testing.T) {
|
|||||||
{"policy ok", nil, nil},
|
{"policy ok", nil, nil},
|
||||||
{"policy error", fmt.Errorf("policy error"), fmt.Errorf("policy evaluation failed: policy error")},
|
{"policy error", fmt.Errorf("policy error"), fmt.Errorf("policy evaluation failed: policy error")},
|
||||||
}
|
}
|
||||||
|
ctx := context.Background()
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
mockPE := policy.MockPolicyEvaluator{
|
mockPE := policy.MockPolicyEvaluator{
|
||||||
@@ -54,8 +54,7 @@ func TestVerifyAttestations(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := policy.WithPolicyEvaluator(context.Background(), &mockPE)
|
_, err := VerifyAttestations(ctx, resolver, &mockPE, &policy.Policy{ResolvedName: ""})
|
||||||
_, err := VerifyAttestations(ctx, resolver, &policy.Policy{ResolvedName: ""})
|
|
||||||
if tc.expectedError != nil {
|
if tc.expectedError != nil {
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
assert.Equal(t, tc.expectedError.Error(), err.Error())
|
assert.Equal(t, tc.expectedError.Error(), err.Error())
|
||||||
@@ -69,7 +68,6 @@ func TestVerifyAttestations(t *testing.T) {
|
|||||||
|
|
||||||
func TestVSA(t *testing.T) {
|
func TestVSA(t *testing.T) {
|
||||||
ctx, signer := test.Setup(t)
|
ctx, signer := test.Setup(t)
|
||||||
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
|
||||||
// setup an image with signed attestations
|
// setup an image with signed attestations
|
||||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
|
|
||||||
@@ -122,7 +120,6 @@ func TestVSA(t *testing.T) {
|
|||||||
|
|
||||||
func TestVerificationFailure(t *testing.T) {
|
func TestVerificationFailure(t *testing.T) {
|
||||||
ctx, signer := test.Setup(t)
|
ctx, signer := test.Setup(t)
|
||||||
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
|
||||||
// setup an image with signed attestations
|
// setup an image with signed attestations
|
||||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
|
|
||||||
@@ -175,7 +172,6 @@ func TestVerificationFailure(t *testing.T) {
|
|||||||
|
|
||||||
func TestSignVerify(t *testing.T) {
|
func TestSignVerify(t *testing.T) {
|
||||||
ctx, signer := test.Setup(t)
|
ctx, signer := test.Setup(t)
|
||||||
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
|
||||||
// setup an image with signed attestations
|
// setup an image with signed attestations
|
||||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ var (
|
|||||||
|
|
||||||
func TestAttestationReferenceTypes(t *testing.T) {
|
func TestAttestationReferenceTypes(t *testing.T) {
|
||||||
ctx, signer := test.Setup(t)
|
ctx, signer := test.Setup(t)
|
||||||
ctx = policy.WithPolicyEvaluator(ctx, policy.NewRegoEvaluator(true))
|
|
||||||
platforms := []string{"linux/amd64", "linux/arm64"}
|
platforms := []string{"linux/amd64", "linux/arm64"}
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -2,29 +2,10 @@ package policy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/docker/attest/pkg/attestation"
|
"github.com/docker/attest/pkg/attestation"
|
||||||
)
|
)
|
||||||
|
|
||||||
type policyEvaluatorCtxKeyType struct{}
|
|
||||||
|
|
||||||
var PolicyEvaluatorCtxKey policyEvaluatorCtxKeyType
|
|
||||||
|
|
||||||
// sets PolicyEvaluator in context.
|
|
||||||
func WithPolicyEvaluator(ctx context.Context, pe Evaluator) context.Context {
|
|
||||||
return context.WithValue(ctx, PolicyEvaluatorCtxKey, pe)
|
|
||||||
}
|
|
||||||
|
|
||||||
// gets PolicyEvaluator from context, defaults to Rego PolicyEvaluator if not set.
|
|
||||||
func GetPolicyEvaluator(ctx context.Context) (Evaluator, error) {
|
|
||||||
t, ok := ctx.Value(PolicyEvaluatorCtxKey).(Evaluator)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("no policy evaluator client set on context (set one with policy.WithPolicyEvaluator)")
|
|
||||||
}
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Evaluator interface {
|
type Evaluator interface {
|
||||||
Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
|
Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
repo string
|
policyPath string
|
||||||
expectSuccess bool
|
expectSuccess bool
|
||||||
isCanonical bool
|
isCanonical bool
|
||||||
resolver attestation.Resolver
|
resolver attestation.Resolver
|
||||||
@@ -50,19 +50,19 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
policyID string
|
policyID string
|
||||||
resolveErrorStr string
|
resolveErrorStr string
|
||||||
}{
|
}{
|
||||||
{repo: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver},
|
{policyPath: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver},
|
||||||
{repo: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"},
|
{policyPath: "testdata/policies/allow", expectSuccess: true, resolver: defaultResolver, policyID: "docker-official-images"},
|
||||||
{repo: "testdata/policies/allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr},
|
{policyPath: "testdata/policies/allow", resolver: defaultResolver, policyID: "non-existent-policy-id", resolveErrorStr: resolveErrorStr},
|
||||||
{repo: "testdata/policies/deny", resolver: defaultResolver},
|
{policyPath: "testdata/policies/deny", resolver: defaultResolver},
|
||||||
{repo: "testdata/policies/verify-sig", expectSuccess: true, resolver: defaultResolver},
|
{policyPath: "testdata/policies/verify-sig", expectSuccess: true, resolver: defaultResolver},
|
||||||
{repo: "testdata/policies/wrong-key", resolver: defaultResolver},
|
{policyPath: "testdata/policies/wrong-key", resolver: defaultResolver},
|
||||||
{repo: "testdata/policies/allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver},
|
{policyPath: "testdata/policies/allow-canonical", expectSuccess: true, isCanonical: true, resolver: defaultResolver},
|
||||||
{repo: "testdata/policies/allow-canonical", resolver: defaultResolver},
|
{policyPath: "testdata/policies/allow-canonical", resolver: defaultResolver},
|
||||||
{repo: "testdata/policies/no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"},
|
{policyPath: "testdata/policies/no-rego", resolver: defaultResolver, resolveErrorStr: "no policy file found in policy mapping"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.repo, func(t *testing.T) {
|
t.Run(tc.policyPath, func(t *testing.T) {
|
||||||
input := &policy.Input{
|
input := &policy.Input{
|
||||||
Digest: "sha256:test-digest",
|
Digest: "sha256:test-digest",
|
||||||
PURL: "test-purl",
|
PURL: "test-purl",
|
||||||
@@ -75,7 +75,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
tc.opts = &policy.Options{
|
tc.opts = &policy.Options{
|
||||||
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
|
LocalTargetsDir: test.CreateTempDir(t, "", "tuf-targets"),
|
||||||
PolicyID: tc.policyID,
|
PolicyID: tc.policyID,
|
||||||
LocalPolicyDir: tc.repo,
|
LocalPolicyDir: tc.policyPath,
|
||||||
DisableTUF: true,
|
DisableTUF: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
pkg/policy/resolver_test.go
Normal file
65
pkg/policy/resolver_test.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package policy_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/attest/internal/test"
|
||||||
|
"github.com/docker/attest/pkg/policy"
|
||||||
|
"github.com/docker/attest/pkg/tuf"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolvePolicy(t *testing.T) {
|
||||||
|
localPolicyPath := "testdata/policies/allow"
|
||||||
|
tufPolicyPath := "testdata/policies/allow-canonical"
|
||||||
|
noLocalPolicyPath := "testdata/policies/no-policy"
|
||||||
|
testPolicyID := "docker-official-images"
|
||||||
|
testImageName := "localhost:5001/test/repo:tag"
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
policyPath string
|
||||||
|
policyID string
|
||||||
|
localOverridesTUF bool // if a policy is provided locally, it should override TUF
|
||||||
|
DisableTUF bool
|
||||||
|
}{
|
||||||
|
{name: "resolve by id (TUF only)", policyID: testPolicyID, DisableTUF: false},
|
||||||
|
{name: "resolve by id (local mapping, TUF policy)", policyPath: noLocalPolicyPath, policyID: testPolicyID, DisableTUF: false},
|
||||||
|
{name: "resolve by id (local mapping, local policy, no TUF)", policyPath: localPolicyPath, policyID: testPolicyID, DisableTUF: true},
|
||||||
|
{name: "resolve by id (local mapping, local policy)", policyPath: localPolicyPath, policyID: testPolicyID, DisableTUF: false, localOverridesTUF: true},
|
||||||
|
{name: "resolve by match (TUF only)", DisableTUF: false},
|
||||||
|
{name: "resolve by match (local mapping, TUF policy)", policyPath: noLocalPolicyPath, DisableTUF: false},
|
||||||
|
{name: "resolve by match (local mapping, local policy, no TUF)", policyPath: localPolicyPath, DisableTUF: true},
|
||||||
|
{name: "resolve by match (local mapping, local policy)", policyPath: localPolicyPath, DisableTUF: false, localOverridesTUF: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
var tufClient tuf.Downloader
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
opts := &policy.Options{}
|
||||||
|
tempDir := test.CreateTempDir(t, "", "tuf-dest")
|
||||||
|
if !tc.DisableTUF {
|
||||||
|
tufClient = tuf.NewMockTufClient(tufPolicyPath)
|
||||||
|
}
|
||||||
|
if tc.policyID != "" {
|
||||||
|
opts.PolicyID = tc.policyID
|
||||||
|
}
|
||||||
|
if tc.policyPath != "" {
|
||||||
|
opts.LocalPolicyDir = tc.policyPath
|
||||||
|
}
|
||||||
|
opts.DisableTUF = tc.DisableTUF
|
||||||
|
opts.LocalTargetsDir = tempDir
|
||||||
|
resolver := policy.NewResolver(tufClient, opts)
|
||||||
|
policy, err := resolver.ResolvePolicy(context.Background(), testImageName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, policy)
|
||||||
|
if tc.DisableTUF || tc.localOverridesTUF {
|
||||||
|
assert.Contains(t, policy.URI, localPolicyPath)
|
||||||
|
} else {
|
||||||
|
assert.Contains(t, policy.URI, tufPolicyPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
10
pkg/policy/testdata/policies/no-policy/mapping.yaml
vendored
Normal file
10
pkg/policy/testdata/policies/no-policy/mapping.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# map repos to policies
|
||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/(.*)$"
|
||||||
|
policy-id: docker-official-images
|
||||||
|
- pattern: ^localhost:5001/(.*)$
|
||||||
|
rewrite: docker.io/library/$1
|
||||||
|
- pattern: ^registry[.]local:5000/(.*)$
|
||||||
|
rewrite: docker.io/library/$1
|
||||||
@@ -34,6 +34,7 @@ type Options struct {
|
|||||||
PolicyID string
|
PolicyID string
|
||||||
ReferrersRepo string
|
ReferrersRepo string
|
||||||
AttestationStyle config.AttestationStyle
|
AttestationStyle config.AttestationStyle
|
||||||
|
Debug bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Policy struct {
|
type Policy struct {
|
||||||
|
|||||||
@@ -1,5 +1,42 @@
|
|||||||
package tuf
|
package tuf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/docker/attest/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockTufClient struct {
|
||||||
|
srcPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockTufClient(srcPath string) *MockTufClient {
|
||||||
|
if srcPath == "" {
|
||||||
|
panic("srcPath must be set")
|
||||||
|
}
|
||||||
|
return &MockTufClient{
|
||||||
|
srcPath: srcPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *MockTufClient) DownloadTarget(target string, _ string) (file *TargetFile, err error) {
|
||||||
|
targetPath := filepath.Join(dc.srcPath, target)
|
||||||
|
src, err := os.Open(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
b, err := io.ReadAll(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TargetFile{TargetURI: targetPath, Data: b, Digest: util.SHA256Hex(b)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type MockVersionChecker struct {
|
type MockVersionChecker struct {
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
"github.com/google/go-containerregistry/pkg/crane"
|
"github.com/google/go-containerregistry/pkg/crane"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||||
|
"github.com/theupdateframework/go-tuf/v2/metadata/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -34,6 +36,7 @@ type RegistryFetcher struct {
|
|||||||
targetsRepo string
|
targetsRepo string
|
||||||
cache *ImageCache
|
cache *ImageCache
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
|
cfg *config.UpdaterConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageCache struct {
|
type ImageCache struct {
|
||||||
@@ -67,13 +70,31 @@ type Layers struct {
|
|||||||
MediaType string `json:"mediaType"`
|
MediaType string `json:"mediaType"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRegistryFetcher(metadataRepo, metadataTag, targetsRepo string) *RegistryFetcher {
|
func NewRegistryFetcher(cfg *config.UpdaterConfig) (*RegistryFetcher, error) {
|
||||||
|
ref, err := reference.ParseNormalizedNamed(cfg.RemoteMetadataURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse metadata repo: %w", err)
|
||||||
|
}
|
||||||
|
// add latest tag
|
||||||
|
metadataTag := LatestTag
|
||||||
|
if tag, ok := ref.(reference.Tagged); ok {
|
||||||
|
metadataTag = tag.Tag()
|
||||||
|
}
|
||||||
|
metadataRepo := ref.Name()
|
||||||
|
|
||||||
|
targetsRef, err := reference.ParseNormalizedNamed(cfg.RemoteTargetsURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse targets repo: %w", err)
|
||||||
|
}
|
||||||
|
targetsRepo := targetsRef.Name()
|
||||||
return &RegistryFetcher{
|
return &RegistryFetcher{
|
||||||
|
// we need to keep these reference so that we can unmangle the URL paths when downloading files
|
||||||
|
cfg: cfg,
|
||||||
metadataRepo: metadataRepo,
|
metadataRepo: metadataRepo,
|
||||||
metadataTag: metadataTag,
|
metadataTag: metadataTag,
|
||||||
targetsRepo: targetsRepo,
|
targetsRepo: targetsRepo,
|
||||||
cache: NewImageCache(),
|
cache: NewImageCache(),
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadFile downloads a file from an OCI registry, errors out if it failed,
|
// DownloadFile downloads a file from an OCI registry, errors out if it failed,
|
||||||
@@ -188,17 +209,17 @@ func getDataFromLayer(fileLayer v1.Layer, maxLength int64) ([]byte, error) {
|
|||||||
// parseImgRef maintains the Fetcher interface by parsing a URL path to an image reference and file name.
|
// parseImgRef maintains the Fetcher interface by parsing a URL path to an image reference and file name.
|
||||||
func (d *RegistryFetcher) parseImgRef(urlPath string) (imgRef, fileName string, err error) {
|
func (d *RegistryFetcher) parseImgRef(urlPath string) (imgRef, fileName string, err error) {
|
||||||
// Check if repo is target or metadata
|
// Check if repo is target or metadata
|
||||||
if strings.Contains(urlPath, d.targetsRepo) {
|
if strings.HasPrefix(urlPath, d.cfg.RemoteTargetsURL) {
|
||||||
// determine if the target path contains subdirectories and set image name accordingly
|
// determine if the target path contains subdirectories and set image name accordingly
|
||||||
// <repo>/<filename> -> image = <repo>:<filename>, layer = <filename>
|
// <repo>/<filename> -> image = <repo>:<filename>, layer = <filename>
|
||||||
// <repo>/<subdir>/<filename> -> index = <repo>:<subdir> , image = <filename> -> layer = <filename>
|
// <repo>/<subdir>/<filename> -> index = <repo>:<subdir> , image = <filename> -> layer = <filename>
|
||||||
target := strings.TrimPrefix(urlPath, d.targetsRepo+"/")
|
target := strings.TrimPrefix(urlPath, d.cfg.RemoteTargetsURL+"/")
|
||||||
subdir, name, found := strings.Cut(target, "/")
|
subdir, name, found := strings.Cut(target, "/")
|
||||||
if found {
|
if found {
|
||||||
return fmt.Sprintf("%s:%s", d.targetsRepo, subdir), fmt.Sprintf("%s/%s", subdir, name), nil
|
return fmt.Sprintf("%s:%s", d.targetsRepo, subdir), fmt.Sprintf("%s/%s", subdir, name), nil
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s:%s", d.targetsRepo, target), target, nil
|
return fmt.Sprintf("%s:%s", d.targetsRepo, target), target, nil
|
||||||
} else if strings.Contains(urlPath, d.metadataRepo) {
|
} else if strings.HasPrefix(urlPath, d.cfg.RemoteMetadataURL) {
|
||||||
// build the metadata image name
|
// build the metadata image name
|
||||||
// determine if role is a delegated role and set the tag accordingly
|
// determine if role is a delegated role and set the tag accordingly
|
||||||
fileName = path.Base(urlPath)
|
fileName = path.Base(urlPath)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/google/go-containerregistry/pkg/v1/static"
|
"github.com/google/go-containerregistry/pkg/v1/static"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/testcontainers/testcontainers-go/modules/registry"
|
"github.com/testcontainers/testcontainers-go/modules/registry"
|
||||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||||
"github.com/theupdateframework/go-tuf/v2/metadata/config"
|
"github.com/theupdateframework/go-tuf/v2/metadata/config"
|
||||||
@@ -47,7 +48,6 @@ func TestRegistryFetcher(t *testing.T) {
|
|||||||
LoadRegistryTestData(t, regAddr, OCITUFTestDataPath)
|
LoadRegistryTestData(t, regAddr, OCITUFTestDataPath)
|
||||||
|
|
||||||
metadataRepo := regAddr.Host + metadataPath
|
metadataRepo := regAddr.Host + metadataPath
|
||||||
metadataImgTag := LatestTag
|
|
||||||
targetsRepo := regAddr.Host + targetsPath
|
targetsRepo := regAddr.Host + targetsPath
|
||||||
targetFile := "test.txt"
|
targetFile := "test.txt"
|
||||||
delegatedRole := testRole
|
delegatedRole := testRole
|
||||||
@@ -55,33 +55,35 @@ func TestRegistryFetcher(t *testing.T) {
|
|||||||
delegatedDir := CreateTempDir(t, dir, delegatedRole)
|
delegatedDir := CreateTempDir(t, dir, delegatedRole)
|
||||||
delegatedTargetFile := fmt.Sprintf("%s/%s", delegatedRole, targetFile)
|
delegatedTargetFile := fmt.Sprintf("%s/%s", delegatedRole, targetFile)
|
||||||
|
|
||||||
cfg, err := config.New(metadataRepo, DockerTUFRootDev.Data)
|
// note - url is ignored here - needed to make http url parsing happy even when using oci
|
||||||
assert.NoError(t, err)
|
cfg, err := config.New("", DockerTUFRootDev.Data)
|
||||||
|
require.NoError(t, err)
|
||||||
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataImgTag, targetsRepo)
|
|
||||||
cfg.LocalMetadataDir = dir
|
cfg.LocalMetadataDir = dir
|
||||||
cfg.LocalTargetsDir = dir
|
cfg.LocalTargetsDir = dir
|
||||||
cfg.RemoteTargetsURL = targetsRepo
|
cfg.RemoteTargetsURL = targetsRepo
|
||||||
|
cfg.RemoteMetadataURL = metadataRepo
|
||||||
|
cfg.Fetcher, err = NewRegistryFetcher(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
// create a new Updater instance
|
// create a new Updater instance
|
||||||
up, err := updater.New(cfg)
|
up, err := updater.New(cfg)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// refresh the metadata
|
// refresh the metadata
|
||||||
err = up.Refresh()
|
err = up.Refresh()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// download top-level target
|
// download top-level target
|
||||||
targetInfo, err := up.GetTargetInfo(targetFile)
|
targetInfo, err := up.GetTargetInfo(targetFile)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, _, err = up.DownloadTarget(targetInfo, filepath.Join(dir, targetInfo.Path), "")
|
_, _, err = up.DownloadTarget(targetInfo, filepath.Join(dir, targetInfo.Path), "")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// download delegated target
|
// download delegated target
|
||||||
targetInfo, err = up.GetTargetInfo(delegatedTargetFile)
|
targetInfo, err = up.GetTargetInfo(delegatedTargetFile)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, _, err = up.DownloadTarget(targetInfo, filepath.Join(delegatedDir, targetFile), "")
|
_, _, err = up.DownloadTarget(targetInfo, filepath.Join(delegatedDir, targetFile), "")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoleFromConsistentName(t *testing.T) {
|
func TestRoleFromConsistentName(t *testing.T) {
|
||||||
@@ -187,28 +189,59 @@ func TestParseImgRef(t *testing.T) {
|
|||||||
metadataRepo := "test" + metadataPath
|
metadataRepo := "test" + metadataPath
|
||||||
metadataTag := LatestTag
|
metadataTag := LatestTag
|
||||||
delegatedRole := testRole
|
delegatedRole := testRole
|
||||||
|
validRef := fmt.Sprintf("%s/2.root.json", metadataRepo)
|
||||||
|
expectedRef := fmt.Sprintf("docker.io/%s:%s", metadataRepo, metadataTag)
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
ref string
|
ref string
|
||||||
expectedRef string
|
expectedRef string
|
||||||
expectedFile string
|
expectedFile string
|
||||||
|
metadataRepo string
|
||||||
|
metadataTag string
|
||||||
|
expectedRefError string
|
||||||
|
expectedConstructorError string
|
||||||
|
targetsRepo string
|
||||||
}{
|
}{
|
||||||
{"top-level metadata", fmt.Sprintf("%s/2.root.json", metadataRepo), fmt.Sprintf("%s:%s", metadataRepo, metadataTag), "2.root.json"},
|
{name: "top-level metadata", ref: validRef, expectedRef: expectedRef, expectedFile: "2.root.json"},
|
||||||
{"delegated metadata", fmt.Sprintf("%s/%s/5.test-role.json", metadataRepo, delegatedRole), fmt.Sprintf("%s:%s", metadataRepo, delegatedRole), "5.test-role.json"},
|
{name: "short metdata repo", ref: validRef, metadataRepo: "test" + metadataPath, expectedRef: expectedRef, expectedFile: "2.root.json"},
|
||||||
{"top-level target", fmt.Sprintf("%s/policy.yaml", targetsRepo), fmt.Sprintf("%s:policy.yaml", targetsRepo), "policy.yaml"},
|
{name: "library path", ref: fmt.Sprintf("test%s/2.root.json", metadataPath), metadataRepo: "test" + metadataPath, expectedRef: "docker.io/test/tuf-metadata:latest", expectedFile: "2.root.json"},
|
||||||
{"delegated target", fmt.Sprintf("%s/%s/policy.yaml", targetsRepo, delegatedRole), fmt.Sprintf("%s:%s", targetsRepo, delegatedRole), fmt.Sprintf("%s/policy.yaml", delegatedRole)},
|
{name: "short targets repo", ref: validRef, targetsRepo: "test" + targetsPath, expectedRef: expectedRef, expectedFile: "2.root.json"},
|
||||||
|
{name: "delegated metadata", ref: fmt.Sprintf("%s/%s/5.test-role.json", metadataRepo, delegatedRole), expectedRef: fmt.Sprintf("docker.io/%s:%s", metadataRepo, delegatedRole), expectedFile: "5.test-role.json"},
|
||||||
|
{name: "top-level target", ref: fmt.Sprintf("%s/policy.yaml", targetsRepo), expectedRef: fmt.Sprintf("docker.io/%s:policy.yaml", targetsRepo), expectedFile: "policy.yaml"},
|
||||||
|
{name: "delegated target", ref: fmt.Sprintf("%s/%s/policy.yaml", targetsRepo, delegatedRole), expectedRef: fmt.Sprintf("docker.io/%s:%s", targetsRepo, delegatedRole), expectedFile: fmt.Sprintf("%s/policy.yaml", delegatedRole)},
|
||||||
|
{name: "docker/targets", ref: fmt.Sprintf("%s/2.root.json", "docker.io/docker/targets"), expectedRef: "docker.io/docker/targets:latest", expectedFile: "2.root.json", metadataRepo: "docker.io/docker/targets"},
|
||||||
|
{name: "malformed ref", ref: fmt.Sprintf("%s/2.root.json", "@broken"), expectedRefError: "urlPath: @broken/2.root.json must be in metadata or targets repo"},
|
||||||
|
{name: "malformed metadataRepo", ref: validRef, metadataRepo: "@broken", expectedConstructorError: "failed to parse metadata repo: invalid reference format"},
|
||||||
|
{name: "malformed targetsRepo", ref: validRef, targetsRepo: "@broken", expectedConstructorError: "failed to parse targets repo: invalid reference format"},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
d := &RegistryFetcher{
|
repo := metadataRepo
|
||||||
metadataRepo: metadataRepo,
|
if tc.metadataRepo != "" {
|
||||||
metadataTag: LatestTag,
|
repo = tc.metadataRepo
|
||||||
targetsRepo: targetsRepo,
|
}
|
||||||
|
targets := targetsRepo
|
||||||
|
if tc.targetsRepo != "" {
|
||||||
|
targets = tc.targetsRepo
|
||||||
|
}
|
||||||
|
cfg := &config.UpdaterConfig{
|
||||||
|
RemoteMetadataURL: repo,
|
||||||
|
RemoteTargetsURL: targets,
|
||||||
|
}
|
||||||
|
d, err := NewRegistryFetcher(cfg)
|
||||||
|
if tc.expectedConstructorError != "" {
|
||||||
|
assert.ErrorContains(t, err, tc.expectedConstructorError)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
imgRef, file, err := d.parseImgRef(tc.ref)
|
||||||
|
if tc.expectedRefError != "" {
|
||||||
|
assert.ErrorContains(t, err, tc.expectedRefError)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.expectedRef, imgRef, "ref mismatch")
|
||||||
|
assert.Equal(t, tc.expectedFile, file, "file mismatch")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
imgRef, file, err := d.parseImgRef(tc.ref)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, tc.expectedRef, imgRef)
|
|
||||||
assert.Equal(t, tc.expectedFile, file)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,9 +388,6 @@ func RunTestRegistry(t *testing.T) (*registry.RegistryContainer, *url.URL) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to parse container address: %s", err)
|
t.Fatalf("failed to parse container address: %s", err)
|
||||||
}
|
}
|
||||||
if addr.Hostname() == "127.0.0.1" {
|
|
||||||
addr.Host = "localhost:" + addr.Port()
|
|
||||||
}
|
|
||||||
return registryContainer, addr
|
return registryContainer, addr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,21 +107,22 @@ func NewClient(opts *ClientOptions) (*Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create updater configuration
|
// create updater configuration
|
||||||
cfg, err := config.New(opts.MetadataSource, rootBytes) // default config
|
// this is parsed as an HTTP url (which doesn't work for OCI). We're setting this to make TUF happy
|
||||||
|
// and overwriding the configuration below
|
||||||
|
cfg, err := config.New("", rootBytes) // default config
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create TUF updater configuration: %w", err)
|
return nil, fmt.Errorf("failed to create TUF updater configuration: %w", err)
|
||||||
}
|
}
|
||||||
cfg.LocalMetadataDir = metadataPath
|
cfg.LocalMetadataDir = metadataPath
|
||||||
cfg.LocalTargetsDir = filepath.Join(metadataPath, "download")
|
cfg.LocalTargetsDir = filepath.Join(metadataPath, "download")
|
||||||
|
cfg.RemoteMetadataURL = opts.MetadataSource
|
||||||
cfg.RemoteTargetsURL = opts.TargetsSource
|
cfg.RemoteTargetsURL = opts.TargetsSource
|
||||||
|
|
||||||
if tufSource == OCISource {
|
if tufSource == OCISource {
|
||||||
metadataRepo, metadataTag, found := strings.Cut(opts.MetadataSource, ":")
|
cfg.Fetcher, err = NewRegistryFetcher(cfg)
|
||||||
if !found {
|
if err != nil {
|
||||||
fmt.Printf("metadata tag not found in URL, using latest\n")
|
return nil, fmt.Errorf("failed to create registry fetcher: %w", err)
|
||||||
metadataTag = LatestTag
|
|
||||||
}
|
}
|
||||||
cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, opts.TargetsSource)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a new Updater instance
|
// create a new Updater instance
|
||||||
|
|||||||
@@ -112,27 +112,29 @@ func TestDownloadTarget(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tufClient, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
require.NoErrorf(t, err, "Failed to create TUF client: %v", err)
|
tufClient, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
|
||||||
require.NotNil(t, tufClient.updater, "Failed to create updater")
|
require.NoErrorf(t, err, "Failed to create TUF client: %v", err)
|
||||||
|
require.NotNil(t, tufClient.updater, "Failed to create updater")
|
||||||
|
|
||||||
// get trusted tuf metadata
|
// get trusted tuf metadata
|
||||||
trustedMetadata := tufClient.updater.GetTrustedMetadataSet()
|
trustedMetadata := tufClient.updater.GetTrustedMetadataSet()
|
||||||
assert.NotNil(t, trustedMetadata, "Failed to get trusted metadata")
|
assert.NotNil(t, trustedMetadata, "Failed to get trusted metadata")
|
||||||
|
|
||||||
// download top-level target files
|
// download top-level target files
|
||||||
targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets
|
targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
// download target files
|
// download target files
|
||||||
_, err := tufClient.DownloadTarget(target.Path, filepath.Join(tufPath, "download"))
|
_, err := tufClient.DownloadTarget(target.Path, filepath.Join(tufPath, "download"))
|
||||||
assert.NoErrorf(t, err, "Failed to download target: %v", err)
|
assert.NoErrorf(t, err, "Failed to download target: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// download delegated target
|
// download delegated target
|
||||||
targetInfo, err := tufClient.updater.GetTargetInfo(delegatedTargetFile)
|
targetInfo, err := tufClient.updater.GetTargetInfo(delegatedTargetFile)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = tufClient.DownloadTarget(targetInfo.Path, filepath.Join(tufPath, targetInfo.Path))
|
_, err = tufClient.DownloadTarget(targetInfo.Path, filepath.Join(tufPath, targetInfo.Path))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user