fix: verify mapped image name against subjects (#156)

* fix: verify mapped image name against subjects
This commit is contained in:
James Carnegie
2024-09-05 14:08:55 +01:00
committed by GitHub
parent a363be7f3a
commit ed0ae8ecf6
10 changed files with 195 additions and 8 deletions

View File

@@ -2,11 +2,17 @@ package test
import (
"context"
"crypto"
"crypto/x509"
_ "embed"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/docker/attest/attestation"
"github.com/docker/attest/signerverifier"
"github.com/docker/attest/tlog"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
@@ -74,3 +80,38 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
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
}

View File

@@ -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
}

View File

@@ -69,14 +69,18 @@ func VerifySubject(ctx context.Context, subject []intoto.Subject, resolver attes
if err != nil {
continue
}
if purl.Type != "docker" {
if purl.Type != packageurl.TypeDocker {
continue
}
if purl.Qualifiers.Map()["platform"] != platform.String() {
continue
}
// 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 {
continue
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/docker/attest/policy"
v1 "github.com/google/go-containerregistry/pkg/v1"
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/require"
)
@@ -180,6 +181,8 @@ func TestCreateAttestationResolver(t *testing.T) {
func TestVerifySubject(t *testing.T) {
ctx, _ := test.Setup(t)
defaultResolver := attestation.MockResolver{}
hostWithPort := packageurl.QualifiersFromMap(map[string]string{"platform": "linux/amd64"})
withHost := packageurl.NewPackageURL(packageurl.TypeDocker, "localhost:1234", "alpine", "", hostWithPort, "")
testCases := []struct {
name string
subject []intoto.Subject
@@ -205,6 +208,24 @@ func TestVerifySubject(t *testing.T) {
},
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",
subject: []intoto.Subject{
@@ -337,6 +358,10 @@ func TestVerifySubject(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
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
tc.subject[0].Digest = map[string]string{"sha256": "da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620"}
if tc.digest != "" {

View File

@@ -0,0 +1 @@
config.yaml

View 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

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

View File

@@ -1,8 +1,10 @@
package attest
import (
"context"
"fmt"
"github.com/docker/attest/attestation"
"github.com/docker/attest/policy"
v1 "github.com/google/go-containerregistry/pkg/v1"
intoto "github.com/in-toto/in-toto-golang/in_toto"
@@ -35,3 +37,12 @@ type VerificationResult struct {
Violations []policy.Violation
SubjectDescriptor *v1.Descriptor
}
type wrappedResolver struct {
imageName string
attestation.Resolver
}
func (w *wrappedResolver) ImageName(_ context.Context) (string, error) {
return w.imageName, nil
}

View File

@@ -209,6 +209,7 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, eval
}
oldName := ref.Name()
name = strings.Replace(name, oldName, resolvedPolicy.ResolvedName, 1)
resolver = WithImageName(resolver, resolvedPolicy.ResolvedName)
}
ref, err := reference.ParseNormalizedNamed(name)
@@ -251,3 +252,11 @@ func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, eval
verificationResult.SubjectDescriptor = desc
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,
}
}

View File

@@ -18,9 +18,13 @@ import (
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/stretchr/testify/assert"
"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 (
LinuxAMD64 = "linux/amd64"
@@ -53,7 +57,6 @@ func TestVerifyAttestations(t *testing.T) {
return policy.AllowedResult(), tc.policyEvaluationError
},
}
_, err := VerifyAttestations(ctx, resolver, &mockPE, &policy.Policy{ResolvedName: ""})
if tc.expectedError != nil {
if assert.Error(t, err) {
@@ -175,6 +178,20 @@ func TestSignVerify(t *testing.T) {
// setup an image with signed attestations
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 {
name string
signTL bool
@@ -185,9 +202,10 @@ func TestSignVerify(t *testing.T) {
{name: "happy path", signTL: true, policyDir: PassNoTLPolicyDir},
{name: "sign tl, verify no tl", signTL: true, policyDir: PassPolicyDir},
{name: "no tl", signTL: false, policyDir: PassPolicyDir},
{name: "mirror", signTL: true, 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", signTL: false, policyDir: PassMirrorPolicyDir, imageName: "mirror.org/library/test-image:test"},
{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: "mirror with verification", signTL: false, policyDir: LocalKeysPolicy, imageName: "mirror.org/library/test-image:test"},
}
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage())
@@ -196,7 +214,7 @@ func TestSignVerify(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opts := &attestation.SigningOptions{
SkipTL: tc.signTL,
SkipTL: !tc.signTL,
}
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
@@ -218,6 +236,7 @@ func TestSignVerify(t *testing.T) {
policyOpts := &policy.Options{
LocalPolicyDir: tc.policyDir,
DisableTUF: true,
Debug: true,
}
results, err := Verify(ctx, spec, policyOpts)
require.NoError(t, err)
@@ -225,6 +244,9 @@ func TestSignVerify(t *testing.T) {
assert.Equal(t, tc.expectedNonSuccess, results.Outcome)
return
}
if results.Outcome == OutcomeFailure {
t.Logf("Violations: %v", results.Violations)
}
assert.Equal(t, OutcomeSuccess, results.Outcome)
platform, err := oci.ParsePlatform(LinuxAMD64)
require.NoError(t, err)