policy: add GitHub attestation verification

Add github_attestation and github_release_attestation policy support.
Fetch GitHub attestation bundles (including bundle_url .json.sn decode)
and verify against input.http.checksum.

Wire source metadata resolver progress through resolver options and add
ResolveState support for policy HTTP attestation fetches.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2026-02-20 08:56:40 -08:00
parent ac9e4c512c
commit 5c3551beeb
28 changed files with 4114 additions and 32 deletions
+71 -9
View File
@@ -6,7 +6,9 @@ import (
"sync"
"sync/atomic"
"github.com/docker/buildx/util/progress"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/client/llb/sourceresolver"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/solver/pb"
@@ -14,37 +16,72 @@ import (
var _ sourceresolver.MetaResolver = &Resolver{}
type gatewayResolver interface {
sourceresolver.MetaResolver
Solve(context.Context, gwclient.SolveRequest) (*gwclient.Result, error)
}
type Resolver struct {
startOnce sync.Once
closeOnce sync.Once
started atomic.Bool
mu sync.Mutex
ready chan sourceresolver.MetaResolver
ready chan gatewayResolver
done chan struct{}
openErr error
doneErr error
cancel context.CancelCauseFunc
metaResolver sourceresolver.MetaResolver
run func(context.Context, chan<- sourceresolver.MetaResolver) error
metaResolver gatewayResolver
run func(context.Context, chan<- gatewayResolver) error
closeErr error
}
func NewResolver(c *client.Client) *Resolver {
return newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
type Option func(*newResolverOpts)
type newResolverOpts struct {
progressWriter progress.Writer
}
func WithProgressWriter(pw progress.Writer) Option {
return func(o *newResolverOpts) {
o.progressWriter = pw
}
}
func NewResolver(c *client.Client, opts ...Option) *Resolver {
var cfg newResolverOpts
for _, opt := range opts {
if opt != nil {
opt(&cfg)
}
}
return newWithRun(func(ctx context.Context, ready chan<- gatewayResolver) error {
var (
statusChan chan *client.SolveStatus
done chan struct{}
)
if cfg.progressWriter != nil {
statusChan, done = progress.NewChannel(progress.WithPrefix(cfg.progressWriter, "policy", false))
defer func() {
<-done
}()
}
_, err := c.Build(ctx, client.SolveOpt{Internal: true}, "buildx", func(ctx context.Context, gw gwclient.Client) (*gwclient.Result, error) {
ready <- gw
<-ctx.Done()
return nil, context.Cause(ctx)
}, nil)
}, statusChan)
return err
})
}
func newWithRun(run func(context.Context, chan<- sourceresolver.MetaResolver) error) *Resolver {
func newWithRun(run func(context.Context, chan<- gatewayResolver) error) *Resolver {
return &Resolver{
ready: make(chan sourceresolver.MetaResolver, 1),
ready: make(chan gatewayResolver, 1),
done: make(chan struct{}),
run: run,
}
@@ -58,6 +95,31 @@ func (r *Resolver) ResolveSourceMetadata(ctx context.Context, op *pb.SourceOp, o
return mr.ResolveSourceMetadata(ctx, op, opt)
}
func (r *Resolver) ResolveState(ctx context.Context, st llb.State) (gwclient.Reference, error) {
mr, err := r.open(ctx)
if err != nil {
return nil, err
}
def, err := st.Marshal(ctx)
if err != nil {
return nil, err
}
res, err := mr.Solve(ctx, gwclient.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return nil, err
}
ref, err := res.SingleRef()
if err != nil {
return nil, err
}
return ref, nil
}
func (r *Resolver) Close() error {
r.closeOnce.Do(func() {
if !r.started.Load() {
@@ -76,7 +138,7 @@ func (r *Resolver) Close() error {
return r.closeErr
}
func (r *Resolver) open(ctx context.Context) (sourceresolver.MetaResolver, error) {
func (r *Resolver) open(ctx context.Context) (gatewayResolver, error) {
r.startOnce.Do(func() {
r.started.Store(true)
buildCtx, cancel := context.WithCancelCause(context.Background())
+53 -20
View File
@@ -8,27 +8,40 @@ import (
"testing"
"time"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/client/llb/sourceresolver"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/solver/pb"
"github.com/stretchr/testify/require"
)
type fakeMetaResolver struct {
calls atomic.Int32
resp *sourceresolver.MetaResponse
err error
type fakeGatewayClient struct {
metaCalls atomic.Int32
solveCalls atomic.Int32
metaResp *sourceresolver.MetaResponse
metaErr error
solveRes *gwclient.Result
solveErr error
}
func (f *fakeMetaResolver) ResolveSourceMetadata(ctx context.Context, op *pb.SourceOp, opt sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
f.calls.Add(1)
return f.resp, f.err
func (f *fakeGatewayClient) ResolveSourceMetadata(context.Context, *pb.SourceOp, sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
f.metaCalls.Add(1)
return f.metaResp, f.metaErr
}
func (f *fakeGatewayClient) Solve(context.Context, gwclient.SolveRequest) (*gwclient.Result, error) {
f.solveCalls.Add(1)
if f.solveRes == nil && f.solveErr == nil {
return nil, errors.New("unexpected solve call")
}
return f.solveRes, f.solveErr
}
func TestResolverCloseNoopBeforeResolve(t *testing.T) {
t.Parallel()
var called atomic.Int32
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
r := newWithRun(func(ctx context.Context, ready chan<- gatewayResolver) error {
called.Add(1)
return nil
})
@@ -41,8 +54,8 @@ func TestResolverResolveOpensOnce(t *testing.T) {
t.Parallel()
var runs atomic.Int32
mr := &fakeMetaResolver{resp: &sourceresolver.MetaResponse{}}
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
mr := &fakeGatewayClient{metaResp: &sourceresolver.MetaResponse{}}
r := newWithRun(func(ctx context.Context, ready chan<- gatewayResolver) error {
runs.Add(1)
ready <- mr
<-ctx.Done()
@@ -56,7 +69,7 @@ func TestResolverResolveOpensOnce(t *testing.T) {
require.NoError(t, err)
require.EqualValues(t, 1, runs.Load())
require.EqualValues(t, 2, mr.calls.Load())
require.EqualValues(t, 2, mr.metaCalls.Load())
require.NoError(t, r.Close())
}
@@ -64,8 +77,8 @@ func TestResolverCloseAfterOpenCancelsBuild(t *testing.T) {
t.Parallel()
var canceled atomic.Bool
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
ready <- &fakeMetaResolver{resp: &sourceresolver.MetaResponse{}}
r := newWithRun(func(ctx context.Context, ready chan<- gatewayResolver) error {
ready <- &fakeGatewayClient{metaResp: &sourceresolver.MetaResponse{}}
<-ctx.Done()
canceled.Store(true)
return context.Cause(ctx)
@@ -82,7 +95,7 @@ func TestResolverOpenFailureIsSticky(t *testing.T) {
expected := errors.New("boom")
var runs atomic.Int32
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
r := newWithRun(func(ctx context.Context, ready chan<- gatewayResolver) error {
runs.Add(1)
return expected
})
@@ -108,7 +121,7 @@ func TestResolverCloseIgnoresTerminalContextErrors(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
r := newWithRun(func(ctx context.Context, ready chan<- gatewayResolver) error {
return tc.err
})
_, err := r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
@@ -122,8 +135,8 @@ func TestResolverConcurrentResolveUsesSingleOpen(t *testing.T) {
t.Parallel()
var runs atomic.Int32
mr := &fakeMetaResolver{resp: &sourceresolver.MetaResponse{}}
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
mr := &fakeGatewayClient{metaResp: &sourceresolver.MetaResponse{}}
r := newWithRun(func(ctx context.Context, ready chan<- gatewayResolver) error {
runs.Add(1)
ready <- mr
<-ctx.Done()
@@ -148,7 +161,7 @@ func TestResolverConcurrentResolveUsesSingleOpen(t *testing.T) {
require.NoError(t, err)
}
require.EqualValues(t, 1, runs.Load())
require.EqualValues(t, n, mr.calls.Load())
require.EqualValues(t, n, mr.metaCalls.Load())
done := make(chan struct{})
closeErr := make(chan error, 1)
@@ -167,11 +180,11 @@ func TestResolverConcurrentResolveUsesSingleOpen(t *testing.T) {
func TestResolverFirstCanceledContextDoesNotPoisonFutureCalls(t *testing.T) {
t.Parallel()
mr := &fakeMetaResolver{resp: &sourceresolver.MetaResponse{}}
mr := &fakeGatewayClient{metaResp: &sourceresolver.MetaResponse{}}
started := make(chan struct{})
release := make(chan struct{})
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
r := newWithRun(func(ctx context.Context, ready chan<- gatewayResolver) error {
close(started)
<-release
ready <- mr
@@ -191,3 +204,23 @@ func TestResolverFirstCanceledContextDoesNotPoisonFutureCalls(t *testing.T) {
require.NoError(t, err)
require.NoError(t, r.Close())
}
func TestResolverResolveStateUsesSolve(t *testing.T) {
t.Parallel()
mr := &fakeGatewayClient{solveErr: errors.New("solve boom")}
r := newWithRun(func(ctx context.Context, ready chan<- gatewayResolver) error {
ready <- mr
<-ctx.Done()
return context.Cause(ctx)
})
st := llb.Scratch()
_, err := r.ResolveState(t.Context(), st)
require.EqualError(t, err, "solve boom")
require.EqualValues(t, 1, mr.solveCalls.Load())
require.NoError(t, r.Close())
}
var _ gatewayResolver = (*fakeGatewayClient)(nil)
var _ sourceresolver.MetaResolver = (*fakeGatewayClient)(nil)