diff --git a/cache/remotecache/gha/gha.go b/cache/remotecache/gha/gha.go index a340d378e..1798e6d66 100644 --- a/cache/remotecache/gha/gha.go +++ b/cache/remotecache/gha/gha.go @@ -364,7 +364,12 @@ func verifySignature(ctx context.Context, dgst digest.Digest, bundle []byte, con return errors.New("no verifier available for signed github actions cache") } - sig, err := v.VerifyArtifact(ctx, dgst, bundle, policy.WithSLSANotRequired()) + policyRules := config.Verify.Policy + opts := []policy.ArtifactVerifyOpt{policy.WithSLSANotRequired()} + if policyRules.TlogThreshold != nil { + opts = append(opts, policy.WithTlogThreshold(*policyRules.TlogThreshold)) + } + sig, err := v.VerifyArtifact(ctx, dgst, bundle, opts...) if err != nil { return err } @@ -372,19 +377,9 @@ func verifySignature(ctx context.Context, dgst digest.Digest, bundle []byte, con return errors.New("signature verification failed: no signer found") } numTimestamps := len(sig.Timestamps) - numTlog := 0 - for _, t := range sig.Timestamps { - if t.Type == "Tlog" { - numTlog++ - } - } - policyRules := config.Verify.Policy if policyRules.TimestampThreshold > numTimestamps { return errors.Errorf("signature verification failed: not enough timestamp authorities: have %d, need %d", numTimestamps, policyRules.TimestampThreshold) } - if policyRules.TlogThreshold > numTlog { - return errors.Errorf("signature verification failed: not enough tlog authorities: have %d, need %d", numTlog, policyRules.TlogThreshold) - } certRules, err := certToStringMap(&config.Verify.Policy.Summary) if err != nil { diff --git a/cache/remotecache/gha/gha_test.go b/cache/remotecache/gha/gha_test.go index 5ed02f479..d63e2295b 100644 --- a/cache/remotecache/gha/gha_test.go +++ b/cache/remotecache/gha/gha_test.go @@ -9,13 +9,33 @@ import ( "testing" "time" + "github.com/moby/buildkit/cache/remotecache/gha/ghatypes" "github.com/moby/buildkit/client" "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/util/testutil/integration" "github.com/moby/buildkit/util/testutil/workers" + policy "github.com/moby/policy-helpers" + digest "github.com/opencontainers/go-digest" "github.com/stretchr/testify/require" ) +func TestVerifySignatureNegativeTlogThreshold(t *testing.T) { + config := &Config{ + CacheConfig: &ghatypes.CacheConfig{ + Verify: ghatypes.VerifyConfig{ + Policy: ghatypes.VerifyPolicy{ + TlogThreshold: new(-1), + }, + }, + }, + verifier: func() (*policy.Verifier, error) { + return &policy.Verifier{}, nil + }, + } + err := verifySignature(t.Context(), digest.FromString("cache index"), nil, config) + require.EqualError(t, err, "transparency log threshold must not be negative") +} + func init() { if workers.IsTestDockerd() { workers.InitDockerdWorker() diff --git a/cache/remotecache/gha/ghatypes/config.go b/cache/remotecache/gha/ghatypes/config.go index 770ef624e..a138d9662 100644 --- a/cache/remotecache/gha/ghatypes/config.go +++ b/cache/remotecache/gha/ghatypes/config.go @@ -17,7 +17,7 @@ type VerifyConfig struct { } type VerifyPolicy struct { - TimestampThreshold int `toml:"timestampThreshold"` - TlogThreshold int `toml:"tlogThreshold"` + TimestampThreshold int `toml:"timestampThreshold"` + TlogThreshold *int `toml:"tlogThreshold"` certificate.Summary } diff --git a/docs/buildkitd.toml.md b/docs/buildkitd.toml.md index bba3961d2..6a42a80e3 100644 --- a/docs/buildkitd.toml.md +++ b/docs/buildkitd.toml.md @@ -267,6 +267,8 @@ provenanceEnvDir = "/etc/buildkit/provenance.d" # # [cache.gha.verify.policy] # timestampThreshold = 1 +# # If omitted, require all distinct transparency logs present in the bundle. +# # Zero skips log verification and requires a verified TSA timestamp instead. # tlogThreshold = 1 # # certificate properties that need to match. Simple wildcards (*) are supported. # certificateIssuer = "" diff --git a/go.mod b/go.mod index 9ee5e5afe..3a115b12f 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/moby/go-archive v0.2.0 github.com/moby/locker v1.0.1 github.com/moby/patternmatcher v0.6.1 - github.com/moby/policy-helpers v0.0.0-20261006174519-bd98f4747414 + github.com/moby/policy-helpers v0.0.0-20261022152518-485a1bec29f6 github.com/moby/profiles/seccomp v0.2.4 github.com/moby/sys/mountinfo v0.7.2 github.com/moby/sys/reexec v0.1.0 diff --git a/go.sum b/go.sum index cade79965..36c7cf733 100644 --- a/go.sum +++ b/go.sum @@ -374,8 +374,8 @@ github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/policy-helpers v0.0.0-20261006174519-bd98f4747414 h1:gOgxy0eXQlhvC5hB7CE+tJLLzrC9+gjTWlU+Bv5T8Ck= -github.com/moby/policy-helpers v0.0.0-20261006174519-bd98f4747414/go.mod h1:hNMdiq4WjSmuyvy8lWcflH6erzSg/MgZlFyM7vC7Z2c= +github.com/moby/policy-helpers v0.0.0-20261022152518-485a1bec29f6 h1:EROY4N1iyUx4awd7c2JtGnca7F9PhBU8+lJR/YejbYM= +github.com/moby/policy-helpers v0.0.0-20261022152518-485a1bec29f6/go.mod h1:hNMdiq4WjSmuyvy8lWcflH6erzSg/MgZlFyM7vC7Z2c= github.com/moby/profiles/seccomp v0.2.4 h1:CNKIR4jNYCGIdstSZ3plN/yF3WCqlupmNLwO1ckLwz4= github.com/moby/profiles/seccomp v0.2.4/go.mod h1:H3JkVP4MWq+Q87sSWRUEdiZvK52WErUbcxr+mJHKVoM= github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00= diff --git a/vendor/github.com/moby/policy-helpers/docker-bake.hcl b/vendor/github.com/moby/policy-helpers/docker-bake.hcl index 4e67dd339..df207da4e 100644 --- a/vendor/github.com/moby/policy-helpers/docker-bake.hcl +++ b/vendor/github.com/moby/policy-helpers/docker-bake.hcl @@ -87,11 +87,11 @@ target "vendor" { output = ["."] } -target "mod-outdated" { +target "gomod-updates" { inherits = ["_common"] dockerfile = "./hack/dockerfiles/vendor.Dockerfile" - target = "outdated" - no-cache-filter = ["outdated"] + target = "gomod-updates" + no-cache-filter = ["gomod-updates"] output = ["type=cacheonly"] } diff --git a/vendor/github.com/moby/policy-helpers/verifier.go b/vendor/github.com/moby/policy-helpers/verifier.go index 99d37ba36..b64d4ca26 100644 --- a/vendor/github.com/moby/policy-helpers/verifier.go +++ b/vendor/github.com/moby/policy-helpers/verifier.go @@ -52,6 +52,9 @@ func (v *Verifier) VerifyArtifact(ctx context.Context, dgst digest.Digest, bundl for _, o := range opt { o(opts) } + if opts.TlogThreshold != nil && *opts.TlogThreshold < 0 { + return nil, errors.New("transparency log threshold must not be negative") + } anyCert, err := anyCerificateIdentity() if err != nil { @@ -78,7 +81,23 @@ func (v *Verifier) VerifyArtifact(ctx context.Context, dgst digest.Digest, bundl return nil, errors.Wrap(err, "getting trusted root") } - gv, err := verify.NewVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) + verifierOpts := []verify.VerifierOption{ + verify.WithSignedCertificateTimestamps(1), + verify.WithObserverTimestamps(1), + } + // Sigstore thresholds count distinct logs, not individual entries. + logIDs := make(map[string]struct{}) + for _, entry := range b.GetVerificationMaterial().GetTlogEntries() { + logIDs[string(entry.GetLogId().GetKeyId())] = struct{}{} + } + tlogThreshold := len(logIDs) + if opts.TlogThreshold != nil { + tlogThreshold = *opts.TlogThreshold + } + if tlogThreshold > 0 { + verifierOpts = append(verifierOpts, verify.WithTransparencyLog(tlogThreshold)) + } + gv, err := verify.NewVerifier(trustedRoot, verifierOpts...) if err != nil { return nil, errors.Wrap(err, "creating verifier") } @@ -368,6 +387,7 @@ func anyCerificateIdentity() (verify.PolicyOption, error) { type ArtifactVerifyOpts struct { SLSANotRequired bool + TlogThreshold *int } type ArtifactVerifyOpt func(*ArtifactVerifyOpts) @@ -378,6 +398,15 @@ func WithSLSANotRequired() ArtifactVerifyOpt { } } +// WithTlogThreshold sets the minimum number of verified transparency logs. +// By default, all distinct logs in the bundle are required. Explicit zero skips +// log verification and returns no log timestamps, so a TSA timestamp is required. +func WithTlogThreshold(threshold int) ArtifactVerifyOpt { + return func(o *ArtifactVerifyOpts) { + o.TlogThreshold = &threshold + } +} + func loadBundle(dt []byte) (*bundle.Bundle, error) { var bundle bundle.Bundle bundle.Bundle = new(protobundle.Bundle) diff --git a/vendor/modules.txt b/vendor/modules.txt index d8b9a3aaa..2ba2d1385 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -815,7 +815,7 @@ github.com/moby/locker ## explicit; go 1.19 github.com/moby/patternmatcher github.com/moby/patternmatcher/ignorefile -# github.com/moby/policy-helpers v0.0.0-20261006174519-bd98f4747414 +# github.com/moby/policy-helpers v0.0.0-20261022152518-485a1bec29f6 ## explicit; go 1.25.8 github.com/moby/policy-helpers github.com/moby/policy-helpers/image