Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed0ae8ecf6 |
@@ -2,11 +2,17 @@ package test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/x509"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/attest/attestation"
|
||||||
"github.com/docker/attest/signerverifier"
|
"github.com/docker/attest/signerverifier"
|
||||||
"github.com/docker/attest/tlog"
|
"github.com/docker/attest/tlog"
|
||||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||||
@@ -74,3 +80,38 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
|
|||||||
|
|
||||||
return ctx, signer
|
return ctx, signer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func publicKeyToPEM(pubKey crypto.PublicKey) (string, error) {
|
||||||
|
derBytes, err := x509.MarshalPKIXPublicKey(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
pemBlock := &pem.Block{
|
||||||
|
Type: "PUBLIC KEY",
|
||||||
|
Bytes: derBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(pem.EncodeToMemory(pemBlock)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadKeyMetadata loads the key metadata for the given signer verifier.
|
||||||
|
func GenKeyMetadata(sv dsse.SignerVerifier) (*attestation.KeyMetadata, error) {
|
||||||
|
pub := sv.Public()
|
||||||
|
pem, err := publicKeyToPEM(pub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert public key to PEM: %w", err)
|
||||||
|
}
|
||||||
|
id, err := sv.KeyID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &attestation.KeyMetadata{
|
||||||
|
ID: id,
|
||||||
|
Status: "active",
|
||||||
|
SigningFormat: "dssev1",
|
||||||
|
From: time.Now(),
|
||||||
|
PEM: pem,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func RefToPURL(named reference.Named, platform *v1.Platform) (string, bool, erro
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
p := packageurl.NewPackageURL("docker", ns, name, version, qualifiers, "")
|
p := packageurl.NewPackageURL(packageurl.TypeDocker, ns, name, version, qualifiers, "")
|
||||||
return p.ToString(), isCanonical, nil
|
return p.ToString(), isCanonical, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,14 +69,18 @@ func VerifySubject(ctx context.Context, subject []intoto.Subject, resolver attes
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if purl.Type != "docker" {
|
if purl.Type != packageurl.TypeDocker {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if purl.Qualifiers.Map()["platform"] != platform.String() {
|
if purl.Qualifiers.Map()["platform"] != platform.String() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// ensure reference is normalized before comparing
|
// ensure reference is normalized before comparing
|
||||||
subjectName, err := reference.ParseNormalizedNamed(purl.Name)
|
withNamespace := purl.Name
|
||||||
|
if purl.Namespace != "" {
|
||||||
|
withNamespace = purl.Namespace + "/" + purl.Name
|
||||||
|
}
|
||||||
|
subjectName, err := reference.ParseNormalizedNamed(withNamespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/docker/attest/policy"
|
"github.com/docker/attest/policy"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
|
"github.com/package-url/packageurl-go"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -180,6 +181,8 @@ func TestCreateAttestationResolver(t *testing.T) {
|
|||||||
func TestVerifySubject(t *testing.T) {
|
func TestVerifySubject(t *testing.T) {
|
||||||
ctx, _ := test.Setup(t)
|
ctx, _ := test.Setup(t)
|
||||||
defaultResolver := attestation.MockResolver{}
|
defaultResolver := attestation.MockResolver{}
|
||||||
|
hostWithPort := packageurl.QualifiersFromMap(map[string]string{"platform": "linux/amd64"})
|
||||||
|
withHost := packageurl.NewPackageURL(packageurl.TypeDocker, "localhost:1234", "alpine", "", hostWithPort, "")
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
subject []intoto.Subject
|
subject []intoto.Subject
|
||||||
@@ -205,6 +208,24 @@ func TestVerifySubject(t *testing.T) {
|
|||||||
},
|
},
|
||||||
img: "alpine",
|
img: "alpine",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "with host and port",
|
||||||
|
subject: []intoto.Subject{
|
||||||
|
{
|
||||||
|
Name: withHost.ToString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
img: "localhost:1234/alpine",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with host and port (from image-signer-verifier tests)",
|
||||||
|
subject: []intoto.Subject{
|
||||||
|
{
|
||||||
|
Name: "pkg:docker/registry.local%3A5000/image-signer-verifier-test@10710107227?platform=linux%2Famd64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
img: "registry.local:5000/image-signer-verifier-test",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "with library",
|
name: "with library",
|
||||||
subject: []intoto.Subject{
|
subject: []intoto.Subject{
|
||||||
@@ -337,6 +358,10 @@ func TestVerifySubject(t *testing.T) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
defaultResolver.Image = tc.img
|
defaultResolver.Image = tc.img
|
||||||
|
// make sure we're using a fixed platform vs a detected one
|
||||||
|
defaultResolver.PlatformFn = func() (*v1.Platform, error) {
|
||||||
|
return &v1.Platform{Architecture: "amd64", OS: "linux"}, nil
|
||||||
|
}
|
||||||
// digest from mock resolver
|
// digest from mock resolver
|
||||||
tc.subject[0].Digest = map[string]string{"sha256": "da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620"}
|
tc.subject[0].Digest = map[string]string{"sha256": "da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620"}
|
||||||
if tc.digest != "" {
|
if tc.digest != "" {
|
||||||
|
|||||||
1
test/testdata/local-policy-real/.gitignore
vendored
Normal file
1
test/testdata/local-policy-real/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
config.yaml
|
||||||
15
test/testdata/local-policy-real/mapping.yaml
vendored
Normal file
15
test/testdata/local-policy-real/mapping.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
version: v1
|
||||||
|
kind: policy-mapping
|
||||||
|
policies:
|
||||||
|
- id: test-images
|
||||||
|
description: Local test images
|
||||||
|
files:
|
||||||
|
- path: policy.rego
|
||||||
|
- path: config.yaml #auto generated
|
||||||
|
attestations:
|
||||||
|
style: attached
|
||||||
|
rules:
|
||||||
|
- pattern: "^docker[.]io/library/test-image$"
|
||||||
|
policy-id: test-images
|
||||||
|
- pattern: "^mirror[.]org/library/(.*)$"
|
||||||
|
rewrite: docker.io/library/$1
|
||||||
59
test/testdata/local-policy-real/policy.rego
vendored
Normal file
59
test/testdata/local-policy-real/policy.rego
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package attest
|
||||||
|
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
import data.keys
|
||||||
|
|
||||||
|
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, "skip_tl": true}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe_statement_from_attestation(att) := statement if {
|
||||||
|
payload := att.payload
|
||||||
|
statement := json.unmarshal(base64.decode(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
violations contains violation if {
|
||||||
|
some att in atts
|
||||||
|
statement := unsafe_statement_from_attestation(att)
|
||||||
|
res := attest.verify(att, opts)
|
||||||
|
err := res.error
|
||||||
|
violation := {
|
||||||
|
"type": "unsigned_statement",
|
||||||
|
"description": sprintf("Statement is not correctly signed: %v", [err]),
|
||||||
|
"attestation": statement,
|
||||||
|
"details": {"error": err},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := {
|
||||||
|
"success": count(statements) > 0,
|
||||||
|
"violations": violations,
|
||||||
|
"summary": {
|
||||||
|
"subjects": subjects,
|
||||||
|
"slsa_level": "SLSA_BUILD_LEVEL_3",
|
||||||
|
"verifier": "docker-official-images",
|
||||||
|
"policy_uri": "https://docker.com/official/policy/v0.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
11
types.go
11
types.go
@@ -1,8 +1,10 @@
|
|||||||
package attest
|
package attest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/attest/attestation"
|
||||||
"github.com/docker/attest/policy"
|
"github.com/docker/attest/policy"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
@@ -35,3 +37,12 @@ type VerificationResult struct {
|
|||||||
Violations []policy.Violation
|
Violations []policy.Violation
|
||||||
SubjectDescriptor *v1.Descriptor
|
SubjectDescriptor *v1.Descriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type wrappedResolver struct {
|
||||||
|
imageName string
|
||||||
|
attestation.Resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedResolver) ImageName(_ context.Context) (string, error) {
|
||||||
|
return w.imageName, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, eval
|
|||||||
}
|
}
|
||||||
oldName := ref.Name()
|
oldName := ref.Name()
|
||||||
name = strings.Replace(name, oldName, resolvedPolicy.ResolvedName, 1)
|
name = strings.Replace(name, oldName, resolvedPolicy.ResolvedName, 1)
|
||||||
|
resolver = WithImageName(resolver, resolvedPolicy.ResolvedName)
|
||||||
}
|
}
|
||||||
|
|
||||||
ref, err := reference.ParseNormalizedNamed(name)
|
ref, err := reference.ParseNormalizedNamed(name)
|
||||||
@@ -251,3 +252,11 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, eval
|
|||||||
verificationResult.SubjectDescriptor = desc
|
verificationResult.SubjectDescriptor = desc
|
||||||
return verificationResult, nil
|
return verificationResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithImageName returns a new resolver that will return the given image name as this can be updated by the mapping file.
|
||||||
|
func WithImageName(resolver attestation.Resolver, s string) attestation.Resolver {
|
||||||
|
return &wrappedResolver{
|
||||||
|
Resolver: resolver,
|
||||||
|
imageName: s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,9 +18,13 @@ import (
|
|||||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ExampleAttestation = filepath.Join("test", "testdata", "example_attestation.json")
|
var (
|
||||||
|
ExampleAttestation = filepath.Join("test", "testdata", "example_attestation.json")
|
||||||
|
LocalKeysPolicy = filepath.Join("test", "testdata", "local-policy-real")
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LinuxAMD64 = "linux/amd64"
|
LinuxAMD64 = "linux/amd64"
|
||||||
@@ -53,7 +57,6 @@ func TestVerifyAttestations(t *testing.T) {
|
|||||||
return policy.AllowedResult(), tc.policyEvaluationError
|
return policy.AllowedResult(), tc.policyEvaluationError
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := VerifyAttestations(ctx, resolver, &mockPE, &policy.Policy{ResolvedName: ""})
|
_, err := VerifyAttestations(ctx, resolver, &mockPE, &policy.Policy{ResolvedName: ""})
|
||||||
if tc.expectedError != nil {
|
if tc.expectedError != nil {
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
@@ -175,6 +178,20 @@ func TestSignVerify(t *testing.T) {
|
|||||||
// setup an image with signed attestations
|
// setup an image with signed attestations
|
||||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
|
|
||||||
|
keys, err := test.GenKeyMetadata(signer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
config := struct {
|
||||||
|
Keys []*attestation.KeyMetadata `json:"keys"`
|
||||||
|
}{
|
||||||
|
Keys: []*attestation.KeyMetadata{keys},
|
||||||
|
}
|
||||||
|
keysYaml, err := yaml.Marshal(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// write keysYaml to config.yaml in LocalKeysPolicy.
|
||||||
|
err = os.WriteFile(filepath.Join(LocalKeysPolicy, "config.yaml"), keysYaml, 0o600)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
signTL bool
|
signTL bool
|
||||||
@@ -185,9 +202,10 @@ func TestSignVerify(t *testing.T) {
|
|||||||
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
|
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
|
||||||
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir},
|
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir},
|
||||||
{name: "no tl", signTL: false, policyDir: PassPolicyDir},
|
{name: "no tl", signTL: false, policyDir: PassPolicyDir},
|
||||||
{name: "mirror", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"},
|
{name: "mirror", signTL: false, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"},
|
||||||
{name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectedNonSuccess: OutcomeNoPolicy},
|
{name: "mirror no match", signTL: false, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectedNonSuccess: OutcomeNoPolicy},
|
||||||
{name: "verify inputs", signTL: false, policyDir: InputsPolicyDir},
|
{name: "verify inputs", signTL: false, policyDir: InputsPolicyDir},
|
||||||
|
{name: "mirror with verification", signTL: false, policyDir: LocalKeysPolicy, imageName: "mirror.org/library/test-image:test"},
|
||||||
}
|
}
|
||||||
|
|
||||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage())
|
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage())
|
||||||
@@ -196,7 +214,7 @@ func TestSignVerify(t *testing.T) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
opts := &attestation.SigningOptions{
|
opts := &attestation.SigningOptions{
|
||||||
SkipTL: tc.signTL,
|
SkipTL: !tc.signTL,
|
||||||
}
|
}
|
||||||
|
|
||||||
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
||||||
@@ -218,6 +236,7 @@ func TestSignVerify(t *testing.T) {
|
|||||||
policyOpts := &policy.Options{
|
policyOpts := &policy.Options{
|
||||||
LocalPolicyDir: tc.policyDir,
|
LocalPolicyDir: tc.policyDir,
|
||||||
DisableTUF: true,
|
DisableTUF: true,
|
||||||
|
Debug: true,
|
||||||
}
|
}
|
||||||
results, err := Verify(ctx, spec, policyOpts)
|
results, err := Verify(ctx, spec, policyOpts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -225,6 +244,9 @@ func TestSignVerify(t *testing.T) {
|
|||||||
assert.Equal(t, tc.expectedNonSuccess, results.Outcome)
|
assert.Equal(t, tc.expectedNonSuccess, results.Outcome)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if results.Outcome == OutcomeFailure {
|
||||||
|
t.Logf("Violations: %v", results.Violations)
|
||||||
|
}
|
||||||
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
assert.Equal(t, OutcomeSuccess, results.Outcome)
|
||||||
platform, err := oci.ParsePlatform(LinuxAMD64)
|
platform, err := oci.ParsePlatform(LinuxAMD64)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user