From 6d2e4952b5de83cb09d4d9b785e81a8a2cc4fcba Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Mon, 9 Feb 2026 16:52:15 -0800 Subject: [PATCH] policy: add mock signature unit test Signed-off-by: Tonis Tiigi --- go.mod | 2 +- policy/signatures.go | 11 ++- policy/validate_test.go | 168 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 176 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 10295ca4d..933b32aaf 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b + github.com/sigstore/sigstore-go v1.1.4-0.20251124094504-b5fe07a5a7d7 github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -196,7 +197,6 @@ require ( github.com/sigstore/rekor v1.4.3 // indirect github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect github.com/sigstore/sigstore v1.10.0 // indirect - github.com/sigstore/sigstore-go v1.1.4-0.20251124094504-b5fe07a5a7d7 // indirect github.com/sigstore/timestamp-authority/v2 v2.0.2 // indirect github.com/tchap/go-patricia/v2 v2.3.3 // indirect github.com/theupdateframework/go-tuf/v2 v2.3.0 // indirect diff --git a/policy/signatures.go b/policy/signatures.go index daa0178d2..c18b3ac65 100644 --- a/policy/signatures.go +++ b/policy/signatures.go @@ -14,12 +14,17 @@ import ( gwpb "github.com/moby/buildkit/frontend/gateway/pb" policyverifier "github.com/moby/policy-helpers" policyimage "github.com/moby/policy-helpers/image" + policytypes "github.com/moby/policy-helpers/types" "github.com/opencontainers/go-digest" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) -type PolicyVerifierProvider func() (*policyverifier.Verifier, error) +type PolicyVerifier interface { + VerifyImage(context.Context, policyimage.ReferrersProvider, ocispecs.Descriptor, *ocispecs.Platform) (*policytypes.SignatureInfo, error) +} + +type PolicyVerifierProvider func() (PolicyVerifier, error) func SignatureVerifier(cfg *confutil.Config) PolicyVerifierProvider { if cfg == nil { @@ -27,9 +32,9 @@ func SignatureVerifier(cfg *confutil.Config) PolicyVerifierProvider { } var ( mu sync.Mutex - v *policyverifier.Verifier + v PolicyVerifier ) - return func() (*policyverifier.Verifier, error) { + return func() (PolicyVerifier, error) { mu.Lock() defer mu.Unlock() diff --git a/policy/validate_test.go b/policy/validate_test.go index 8b102cc0b..4a79fa1ee 100644 --- a/policy/validate_test.go +++ b/policy/validate_test.go @@ -1,6 +1,7 @@ package policy import ( + "context" "crypto/sha1" //nolint:gosec // used for git object checksums in tests "encoding/hex" "encoding/json" @@ -10,7 +11,11 @@ import ( gwpb "github.com/moby/buildkit/frontend/gateway/pb" "github.com/moby/buildkit/solver/pb" + policyimage "github.com/moby/policy-helpers/image" + policytypes "github.com/moby/policy-helpers/types" + "github.com/opencontainers/go-digest" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" "github.com/stretchr/testify/require" ) @@ -21,6 +26,7 @@ func TestSourceToInputWithLogger(t *testing.T) { name string src *gwpb.ResolveSourceMetaResponse platform *ocispecs.Platform + verifier PolicyVerifierProvider expInput Input expUnk []string expErrMsg string @@ -255,6 +261,88 @@ func TestSourceToInputWithLogger(t *testing.T) { "input.image.env", }, }, + { + name: "image-attestation-chain-with-mock-verifier-sets-signature-properties", + src: &gwpb.ResolveSourceMetaResponse{ + Source: &pb.SourceOp{ + Identifier: "docker-image://alpine:latest", + }, + Image: &gwpb.ResolveSourceImageResponse{ + Digest: "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", + AttestationChain: newTestAttestationChain(t), + }, + }, + platform: &ocispecs.Platform{OS: "linux", Architecture: "amd64"}, + verifier: func() (PolicyVerifier, error) { + return &mockPolicyVerifier{ + verifyImage: func(context.Context, policyimage.ReferrersProvider, ocispecs.Descriptor, *ocispecs.Platform) (*policytypes.SignatureInfo, error) { + ts := time.Date(2024, 2, 3, 4, 5, 6, 0, time.UTC) + return &policytypes.SignatureInfo{ + Kind: policytypes.KindDockerGithubBuilder, + SignatureType: policytypes.SignatureSimpleSigningV1, + DockerReference: "docker.io/library/alpine:latest", + IsDHI: true, + Timestamps: []policytypes.TimestampVerificationResult{ + {Type: "rekor", URI: "https://rekor.sigstore.dev", Timestamp: ts}, + }, + Signer: &certificate.Summary{ + CertificateIssuer: "https://token.actions.githubusercontent.com", + SubjectAlternativeName: "https://github.com/docker/buildx/.github/workflows/ci.yml@refs/heads/main", + Extensions: certificate.Extensions{ + BuildSignerURI: "https://github.com/docker/buildx/.github/workflows/ci.yml", + BuildSignerDigest: "sha256:1234", + RunnerEnvironment: "github-hosted", + SourceRepositoryURI: "https://github.com/docker/buildx", + SourceRepositoryDigest: "abcdef", + SourceRepositoryRef: "refs/heads/main", + SourceRepositoryOwnerURI: "https://github.com/docker", + BuildConfigURI: "https://github.com/docker/buildx/.github/workflows/ci.yml", + BuildConfigDigest: "sha256:5678", + RunInvocationURI: "https://github.com/docker/buildx/actions/runs/1", + SourceRepositoryIdentifier: "docker/buildx", + }, + }, + }, nil + }, + }, nil + }, + assert: func(t *testing.T, inp Input, unknowns []string, err error) { + t.Helper() + require.NoError(t, err) + require.Equal(t, []string{ + "input.image.labels", + "input.image.user", + "input.image.volumes", + "input.image.workingDir", + "input.image.env", + }, unknowns) + require.NotNil(t, inp.Image) + require.True(t, inp.Image.HasProvenance) + require.Len(t, inp.Image.Signatures, 1) + sig := inp.Image.Signatures[0] + require.Equal(t, SignatureKindDockerGithubBuilder, sig.SignatureKind) + require.Equal(t, SignatureTypeSimpleSigningV1, sig.SignatureType) + require.Equal(t, "docker.io/library/alpine:latest", sig.DockerReference) + require.True(t, sig.IsDHI) + require.Len(t, sig.Timestamps, 1) + require.Equal(t, "rekor", sig.Timestamps[0].Type) + require.Equal(t, "https://rekor.sigstore.dev", sig.Timestamps[0].URI) + require.NotNil(t, sig.Signer) + require.Equal(t, "https://token.actions.githubusercontent.com", sig.Signer.CertificateIssuer) + require.Equal(t, "https://github.com/docker/buildx/.github/workflows/ci.yml@refs/heads/main", sig.Signer.SubjectAlternativeName) + require.Equal(t, "https://github.com/docker/buildx/.github/workflows/ci.yml", sig.Signer.BuildSignerURI) + require.Equal(t, "sha256:1234", sig.Signer.BuildSignerDigest) + require.Equal(t, "github-hosted", sig.Signer.RunnerEnvironment) + require.Equal(t, "https://github.com/docker/buildx", sig.Signer.SourceRepositoryURI) + require.Equal(t, "abcdef", sig.Signer.SourceRepositoryDigest) + require.Equal(t, "refs/heads/main", sig.Signer.SourceRepositoryRef) + require.Equal(t, "https://github.com/docker", sig.Signer.SourceRepositoryOwnerURI) + require.Equal(t, "https://github.com/docker/buildx/.github/workflows/ci.yml", sig.Signer.BuildConfigURI) + require.Equal(t, "sha256:5678", sig.Signer.BuildConfigDigest) + require.Equal(t, "https://github.com/docker/buildx/actions/runs/1", sig.Signer.RunInvocationURI) + require.Equal(t, "docker/buildx", sig.Signer.SourceRepositoryIdentifier) + }, + }, { name: "image-attestation-chain-without-manifest-keeps-has-provenance-false", src: &gwpb.ResolveSourceMetaResponse{ @@ -635,7 +723,7 @@ func TestSourceToInputWithLogger(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - inp, unknowns, err := SourceToInputWithLogger(t.Context(), nil, tc.src, tc.platform, nil) + inp, unknowns, err := SourceToInputWithLogger(t.Context(), tc.verifier, tc.src, tc.platform, nil) if tc.assert != nil { tc.assert(t, inp, unknowns, err) return @@ -665,3 +753,81 @@ func gitObjectSHA1(objType string, raw []byte) string { sum := sha1.Sum(append(prefix, raw...)) return hex.EncodeToString(sum[:]) } + +type mockPolicyVerifier struct { + verifyImage func(context.Context, policyimage.ReferrersProvider, ocispecs.Descriptor, *ocispecs.Platform) (*policytypes.SignatureInfo, error) +} + +func (m *mockPolicyVerifier) VerifyImage(ctx context.Context, provider policyimage.ReferrersProvider, desc ocispecs.Descriptor, platform *ocispecs.Platform) (*policytypes.SignatureInfo, error) { + return m.verifyImage(ctx, provider, desc, platform) +} + +func newTestAttestationChain(t *testing.T) *gwpb.AttestationChain { + t.Helper() + + imgDigest := digest.FromString("image-manifest") + attDigest := digest.FromString("attestation-manifest") + + indexBytes := mustMarshalJSON(t, map[string]any{ + "mediaType": ocispecs.MediaTypeImageIndex, + "manifests": []map[string]any{ + { + "mediaType": ocispecs.MediaTypeImageManifest, + "digest": imgDigest.String(), + "size": int64(10), + "platform": map[string]any{ + "os": "linux", + "architecture": "amd64", + }, + }, + { + "mediaType": ocispecs.MediaTypeImageManifest, + "digest": attDigest.String(), + "size": int64(10), + "annotations": map[string]string{ + policyimage.AnnotationDockerReferenceType: policyimage.AttestationManifestType, + policyimage.AnnotationDockerReferenceDigest: imgDigest.String(), + }, + }, + }, + }) + indexDigest := digest.FromBytes(indexBytes) + + sigManifestBytes := mustMarshalJSON(t, map[string]any{ + "schemaVersion": 2, + "mediaType": ocispecs.MediaTypeImageManifest, + "artifactType": policyimage.ArtifactTypeSigstoreBundle, + }) + sigDigest := digest.FromBytes(sigManifestBytes) + + return &gwpb.AttestationChain{ + Root: indexDigest.String(), + AttestationManifest: attDigest.String(), + SignatureManifests: []string{sigDigest.String()}, + Blobs: map[string]*gwpb.Blob{ + indexDigest.String(): { + Descriptor_: &gwpb.Descriptor{ + MediaType: ocispecs.MediaTypeImageIndex, + Digest: indexDigest.String(), + Size: int64(len(indexBytes)), + }, + Data: indexBytes, + }, + sigDigest.String(): { + Descriptor_: &gwpb.Descriptor{ + MediaType: ocispecs.MediaTypeImageManifest, + Digest: sigDigest.String(), + Size: int64(len(sigManifestBytes)), + }, + Data: sigManifestBytes, + }, + }, + } +} + +func mustMarshalJSON(t *testing.T, v any) []byte { + t.Helper() + dt, err := json.Marshal(v) + require.NoError(t, err) + return dt +}