Skip to content

Commit dff6323

Browse files
committed
gha: add signed cache support
Signed-off-by: Tonis Tiigi <[email protected]>
1 parent 06fc06b commit dff6323

File tree

1,140 files changed

+191189
-2033
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,140 files changed

+191189
-2033
lines changed

cache/remotecache/gha/gha.go

Lines changed: 186 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io"
99
"os"
10+
"os/exec"
1011
"strconv"
1112
"strings"
1213
"sync"
@@ -16,6 +17,7 @@ import (
1617
"github.com/containerd/containerd/v2/pkg/labels"
1718
cerrdefs "github.com/containerd/errdefs"
1819
"github.com/moby/buildkit/cache/remotecache"
20+
"github.com/moby/buildkit/cache/remotecache/gha/ghatypes"
1921
v1 "github.com/moby/buildkit/cache/remotecache/v1"
2022
cacheimporttypes "github.com/moby/buildkit/cache/remotecache/v1/types"
2123
"github.com/moby/buildkit/session"
@@ -26,9 +28,11 @@ import (
2628
"github.com/moby/buildkit/util/tracing"
2729
bkversion "github.com/moby/buildkit/version"
2830
"github.com/moby/buildkit/worker"
31+
policy "github.com/moby/policy-helpers"
2932
digest "github.com/opencontainers/go-digest"
3033
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
3134
"github.com/pkg/errors"
35+
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
3236
actionscache "github.com/tonistiigi/go-actions-cache"
3337
"golang.org/x/sync/errgroup"
3438
)
@@ -51,6 +55,8 @@ const (
5155
defaultTimeout = 10 * time.Minute
5256
)
5357

58+
type VerifierProvider func() (*policy.Verifier, error)
59+
5460
type Config struct {
5561
Scope string
5662
URL string
@@ -59,9 +65,12 @@ type Config struct {
5965
Repository string
6066
Version int
6167
Timeout time.Duration
68+
69+
*ghatypes.CacheConfig
70+
verifier VerifierProvider
6271
}
6372

64-
func getConfig(attrs map[string]string) (*Config, error) {
73+
func getConfig(conf *ghatypes.CacheConfig, v VerifierProvider, attrs map[string]string) (*Config, error) {
6574
scope, ok := attrs[attrScope]
6675
if !ok {
6776
scope = "buildkit"
@@ -109,21 +118,28 @@ func getConfig(attrs map[string]string) (*Config, error) {
109118
return nil, errors.Wrap(err, "failed to parse timeout for github actions cache")
110119
}
111120
}
121+
122+
if conf == nil {
123+
conf = &ghatypes.CacheConfig{}
124+
}
125+
112126
return &Config{
113-
Scope: scope,
114-
URL: url,
115-
Token: token,
116-
Timeout: timeout,
117-
GHToken: attrs[attrGHToken],
118-
Repository: attrs[attrRepository],
119-
Version: apiVersionInt,
127+
Scope: scope,
128+
URL: url,
129+
Token: token,
130+
Timeout: timeout,
131+
GHToken: attrs[attrGHToken],
132+
Repository: attrs[attrRepository],
133+
Version: apiVersionInt,
134+
CacheConfig: conf,
135+
verifier: v,
120136
}, nil
121137
}
122138

123139
// ResolveCacheExporterFunc for Github actions cache exporter.
124-
func ResolveCacheExporterFunc() remotecache.ResolveCacheExporterFunc {
140+
func ResolveCacheExporterFunc(conf *ghatypes.CacheConfig, v VerifierProvider) remotecache.ResolveCacheExporterFunc {
125141
return func(ctx context.Context, g session.Group, attrs map[string]string) (remotecache.Exporter, error) {
126-
cfg, err := getConfig(attrs)
142+
cfg, err := getConfig(conf, v, attrs)
127143
if err != nil {
128144
return nil, err
129145
}
@@ -163,12 +179,12 @@ func (ce *exporter) Config() remotecache.Config {
163179
}
164180
}
165181

166-
func (ce *exporter) blobKeyPrefix() string {
182+
func blobKeyPrefix() string {
167183
return "buildkit-blob-" + version + "-"
168184
}
169185

170-
func (ce *exporter) blobKey(dgst digest.Digest) string {
171-
return ce.blobKeyPrefix() + dgst.String()
186+
func blobKey(dgst digest.Digest) string {
187+
return blobKeyPrefix() + dgst.String()
172188
}
173189

174190
func (ce *exporter) indexKey() string {
@@ -178,8 +194,17 @@ func (ce *exporter) indexKey() string {
178194
scope = s.Scope
179195
}
180196
}
197+
return indexKey(scope, ce.config)
198+
}
199+
200+
func indexKey(scope string, config *Config) string {
181201
scope = digest.FromBytes([]byte(scope)).Hex()[:8]
182-
return "index-" + ce.config.Scope + "-" + version + "-" + scope
202+
key := "index-" + config.Scope + "-" + version + "-" + scope
203+
// just to be sure lets namespace the signed vs unsigned caches
204+
if config.Sign != nil || config.Verify.Required {
205+
key += "-sig"
206+
}
207+
return key
183208
}
184209

185210
func (ce *exporter) initActiveKeyMap(ctx context.Context) {
@@ -204,14 +229,14 @@ func (ce *exporter) initActiveKeyMapOnce(ctx context.Context) (map[string]struct
204229
if err != nil {
205230
return nil, err
206231
}
207-
keys, err := ce.cache.AllKeys(ctx, api, ce.blobKeyPrefix())
232+
keys, err := ce.cache.AllKeys(ctx, api, blobKeyPrefix())
208233
if err != nil {
209234
return nil, err
210235
}
211236
return keys, nil
212237
}
213238

214-
func (ce *exporter) Finalize(ctx context.Context) (map[string]string, error) {
239+
func (ce *exporter) Finalize(ctx context.Context) (_ map[string]string, err error) {
215240
// res := make(map[string]string)
216241
config, descs, err := ce.chains.Marshal(ctx)
217242
if err != nil {
@@ -239,7 +264,7 @@ func (ce *exporter) Finalize(ctx context.Context) (map[string]string, error) {
239264
diffID = dgst
240265
ce.initActiveKeyMap(ctx)
241266

242-
key := ce.blobKey(dgstPair.Descriptor.Digest)
267+
key := blobKey(dgstPair.Descriptor.Digest)
243268

244269
exists := false
245270
if ce.keyMap != nil {
@@ -294,13 +319,111 @@ func (ce *exporter) Finalize(ctx context.Context) (map[string]string, error) {
294319
return nil, err
295320
}
296321

322+
if ce.config.Sign == nil {
323+
return nil, nil
324+
}
325+
326+
args := ce.config.Sign.Command
327+
if len(args) == 0 {
328+
return nil, nil
329+
}
330+
331+
dgst := digest.FromBytes(dt)
332+
signDone := progress.OneOff(ctx, fmt.Sprintf("signing cache index %s", dgst))
333+
defer signDone(err)
334+
335+
cmd := exec.Command(args[0], args[1:]...)
336+
cmd.Stdin = bytes.NewReader(dt)
337+
var out bytes.Buffer
338+
cmd.Stdout = &out
339+
var stderr bytes.Buffer
340+
cmd.Stderr = &stderr
341+
if err := cmd.Run(); err != nil {
342+
return nil, errors.Wrapf(err, "signing command failed: %s", stderr.String())
343+
}
344+
345+
// validate signature before uploading
346+
if err := verifySignature(ctx, dgst, out.Bytes(), ce.config); err != nil {
347+
return nil, err
348+
}
349+
350+
key := blobKey(dgst + "-sig")
351+
if err := ce.cache.Save(ctx, key, actionscache.NewBlob(out.Bytes())); err != nil {
352+
return nil, err
353+
}
354+
297355
return nil, nil
298356
}
299357

358+
func verifySignature(ctx context.Context, dgst digest.Digest, bundle []byte, config *Config) error {
359+
v, err := config.verifier()
360+
if err != nil {
361+
return err
362+
}
363+
if v == nil {
364+
return errors.New("no verifier available for signed github actions cache")
365+
}
366+
367+
sig, err := v.VerifyArtifact(ctx, dgst, bundle, policy.WithSLSANotRequired())
368+
if err != nil {
369+
return err
370+
}
371+
if sig.Signer == nil {
372+
return errors.New("signature verification failed: no signer found")
373+
}
374+
numTimestamps := len(sig.Timestamps)
375+
numTlog := 0
376+
for _, t := range sig.Timestamps {
377+
if t.Type == "Tlog" {
378+
numTlog++
379+
}
380+
}
381+
policyRules := config.Verify.Policy
382+
if policyRules.TimestampTreshold > numTimestamps {
383+
return errors.Errorf("signature verification failed: not enough timestamp authorities: have %d, need %d", numTimestamps, policyRules.TimestampTreshold)
384+
}
385+
if policyRules.TlogThreshold > numTlog {
386+
return errors.Errorf("signature verification failed: not enough tlog authorities: have %d, need %d", numTlog, policyRules.TlogThreshold)
387+
}
388+
389+
certRules, err := certToStringMap(&config.Verify.Policy.Summary)
390+
if err != nil {
391+
return err
392+
}
393+
certFields, err := certToStringMap(sig.Signer)
394+
if err != nil {
395+
return err
396+
}
397+
bklog.G(ctx).Debugf("signature verification: %+v", sig)
398+
bklog.G(ctx).Debugf("signer: %+v", sig.Signer)
399+
for k, v := range certRules {
400+
if v == "" {
401+
continue
402+
}
403+
if !simplePatternMatch(v, certFields[k]) {
404+
return errors.Errorf("signature verification failed: certificate field %q does not match policy (%q != %q)", k, certFields[k], v)
405+
}
406+
bklog.G(ctx).Debugf("certificate field %q matches policy (%q)", k, certFields[k])
407+
}
408+
return nil
409+
}
410+
411+
func certToStringMap(cert *certificate.Summary) (map[string]string, error) {
412+
dt, err := json.Marshal(cert)
413+
if err != nil {
414+
return nil, err
415+
}
416+
m := map[string]string{}
417+
if err := json.Unmarshal(dt, &m); err != nil {
418+
return nil, err
419+
}
420+
return m, nil
421+
}
422+
300423
// ResolveCacheImporterFunc for Github actions cache importer.
301-
func ResolveCacheImporterFunc() remotecache.ResolveCacheImporterFunc {
424+
func ResolveCacheImporterFunc(conf *ghatypes.CacheConfig, v VerifierProvider) remotecache.ResolveCacheImporterFunc {
302425
return func(ctx context.Context, g session.Group, attrs map[string]string) (remotecache.Importer, ocispecs.Descriptor, error) {
303-
cfg, err := getConfig(attrs)
426+
cfg, err := getConfig(conf, v, attrs)
304427
if err != nil {
305428
return nil, ocispecs.Descriptor{}, err
306429
}
@@ -360,8 +483,7 @@ func (ci *importer) makeDescriptorProviderPair(l cacheimporttypes.CacheLayer) (*
360483
}
361484

362485
func (ci *importer) loadScope(ctx context.Context, scope string) (*v1.CacheChains, error) {
363-
scope = digest.FromBytes([]byte(scope)).Hex()[:8]
364-
key := "index-" + ci.config.Scope + "-" + version + "-" + scope
486+
key := indexKey(scope, ci.config)
365487

366488
entry, err := ci.cache.Load(ctx, key)
367489
if err != nil {
@@ -371,12 +493,38 @@ func (ci *importer) loadScope(ctx context.Context, scope string) (*v1.CacheChain
371493
return v1.NewCacheChains(), nil
372494
}
373495

374-
// TODO: this buffer can be removed
375496
buf := &bytes.Buffer{}
376497
if err := entry.WriteTo(ctx, buf); err != nil {
377498
return nil, err
378499
}
379500

501+
if ci.config.Verify.Required {
502+
dgst := digest.FromBytes(buf.Bytes())
503+
504+
verifyDone := progress.OneOff(ctx, fmt.Sprintf("verifying signature of cache index %s", dgst))
505+
sigKey := blobKey(dgst) + "-sig"
506+
sigEntry, err := ci.cache.Load(ctx, sigKey)
507+
if err != nil {
508+
verifyDone(err)
509+
return nil, err
510+
}
511+
if sigEntry == nil {
512+
err := errors.Errorf("missing signature for github actions cache")
513+
verifyDone(err)
514+
return nil, err
515+
}
516+
sigBuf := &bytes.Buffer{}
517+
if err := sigEntry.WriteTo(ctx, sigBuf); err != nil {
518+
verifyDone(err)
519+
return nil, err
520+
}
521+
if err := verifySignature(ctx, dgst, sigBuf.Bytes(), ci.config); err != nil {
522+
verifyDone(err)
523+
return nil, err
524+
}
525+
verifyDone(nil)
526+
}
527+
380528
var config cacheimporttypes.CacheConfig
381529
if err := json.Unmarshal(buf.Bytes(), &config); err != nil {
382530
return nil, errors.WithStack(err)
@@ -500,3 +648,19 @@ func (r *readerAt) ReadAt(p []byte, off int64) (int, error) {
500648
func (r *readerAt) Size() int64 {
501649
return r.desc.Size
502650
}
651+
652+
func simplePatternMatch(pat, s string) bool {
653+
if pat == "*" {
654+
return true
655+
}
656+
if strings.HasPrefix(pat, "*") && strings.HasSuffix(pat, "*") {
657+
return strings.Contains(s, pat[1:len(pat)-1])
658+
}
659+
if strings.HasPrefix(pat, "*") {
660+
return strings.HasSuffix(s, pat[1:])
661+
}
662+
if strings.HasSuffix(pat, "*") {
663+
return strings.HasPrefix(s, pat[:len(pat)-1])
664+
}
665+
return s == pat
666+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package ghatypes
2+
3+
import "github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
4+
5+
type CacheConfig struct {
6+
Sign *SignConfig `toml:"sign"`
7+
Verify VerifyConfig `toml:"verify"`
8+
}
9+
10+
type SignConfig struct {
11+
Command []string `toml:"command"`
12+
}
13+
14+
type VerifyConfig struct {
15+
Required bool `toml:"required"`
16+
Policy VerifyPolicy `toml:"policy"`
17+
}
18+
19+
type VerifyPolicy struct {
20+
TimestampTreshold int `toml:"timestampTreshold"`
21+
TlogThreshold int `toml:"tlogThreshold"`
22+
certificate.Summary
23+
}

cmd/buildkitd/config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config
22

33
import (
4+
"github.com/moby/buildkit/cache/remotecache/gha/ghatypes"
45
resolverconfig "github.com/moby/buildkit/util/resolver/config"
56
)
67

@@ -46,6 +47,12 @@ type Config struct {
4647
// ProvenanceEnvDir is the directory where extra config is loaded
4748
// that is added to the provenance of builds. Defaults to /etc/buildkit/provenance.d/ ,
4849
ProvenanceEnvDir string `toml:"provenanceEnvDir"`
50+
51+
Cache CacheConfig `toml:"cache"`
52+
}
53+
54+
type CacheConfig struct {
55+
GHA *ghatypes.CacheConfig `toml:"gha"`
4956
}
5057

5158
type SystemConfig struct {

0 commit comments

Comments
 (0)