Skip to content

Commit f94176b

Browse files
authored
Merge pull request #6514 from tonistiigi/attestation-resolve
containerimage: add resolve attestation support
2 parents bb7bb20 + b0ba823 commit f94176b

File tree

8 files changed

+359
-13
lines changed

8 files changed

+359
-13
lines changed

client/client_test.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import (
7373
"github.com/moby/buildkit/util/testutil/httpserver"
7474
"github.com/moby/buildkit/util/testutil/integration"
7575
"github.com/moby/buildkit/util/testutil/workers"
76+
policyimage "github.com/moby/policy-helpers/image"
7677
digest "github.com/opencontainers/go-digest"
7778
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
7879
"github.com/pkg/errors"
@@ -251,6 +252,8 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
251252
testHTTPResolveMultiBuild,
252253
testGitResolveMutatedSource,
253254
testImageResolveAttestationChainRequiresNetwork,
255+
testImageResolveAttestationChainLocal,
256+
testImageResolveProvenanceAttestation,
254257
testSourcePolicySession,
255258
testSourcePolicySessionDenyMessages,
256259
testSourceMetaPolicySession,
@@ -12404,6 +12407,216 @@ func testImageResolveAttestationChainRequiresNetwork(t *testing.T, sb integratio
1240412407
require.NoError(t, err)
1240512408
}
1240612409

12410+
func testImageResolveProvenanceAttestation(t *testing.T, sb integration.Sandbox) {
12411+
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureProvenance)
12412+
requiresLinux(t)
12413+
12414+
ctx := sb.Context()
12415+
c, err := New(ctx, sb.Address())
12416+
require.NoError(t, err)
12417+
defer c.Close()
12418+
12419+
target, platform := buildProvenanceImage(ctx, t, c, sb)
12420+
12421+
_, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
12422+
md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{
12423+
Identifier: "docker-image://" + target,
12424+
}, sourceresolver.Opt{
12425+
ImageOpt: &sourceresolver.ResolveImageOpt{
12426+
NoConfig: true,
12427+
ResolveAttestations: []string{
12428+
policyimage.SLSAProvenancePredicateType02,
12429+
policyimage.SLSAProvenancePredicateType1,
12430+
},
12431+
Platform: &platform,
12432+
ResolveMode: pb.AttrImageResolveModeForcePull,
12433+
},
12434+
})
12435+
if err != nil {
12436+
return nil, err
12437+
}
12438+
require.NotNil(t, md.Image)
12439+
require.NotNil(t, md.Image.AttestationChain)
12440+
ac := md.Image.AttestationChain
12441+
require.NotEmpty(t, ac.AttestationManifest)
12442+
att := ac.Blobs[ac.AttestationManifest]
12443+
require.NotEmpty(t, att.Data)
12444+
12445+
var manifest ocispecs.Manifest
12446+
require.NoError(t, json.Unmarshal(att.Data, &manifest))
12447+
require.NotEmpty(t, manifest.Layers)
12448+
var (
12449+
stmtBytes []byte
12450+
foundLayer ocispecs.Descriptor
12451+
)
12452+
for _, layer := range manifest.Layers {
12453+
if !isSLSAPredicateType(layer.Annotations["in-toto.io/predicate-type"]) {
12454+
continue
12455+
}
12456+
blob, ok := ac.Blobs[layer.Digest]
12457+
if !ok {
12458+
continue
12459+
}
12460+
stmtBytes = blob.Data
12461+
foundLayer = layer
12462+
break
12463+
}
12464+
require.NotEmpty(t, stmtBytes)
12465+
require.Contains(t, []string{
12466+
policyimage.SLSAProvenancePredicateType02,
12467+
policyimage.SLSAProvenancePredicateType1,
12468+
}, foundLayer.Annotations["in-toto.io/predicate-type"])
12469+
12470+
var stmt intoto.Statement
12471+
require.NoError(t, json.Unmarshal(stmtBytes, &stmt))
12472+
require.Equal(t, "https://in-toto.io/Statement/v0.1", stmt.Type)
12473+
require.Contains(t, []string{
12474+
policyimage.SLSAProvenancePredicateType02,
12475+
policyimage.SLSAProvenancePredicateType1,
12476+
}, stmt.PredicateType)
12477+
require.Equal(t, stmt.Subject[0].Digest["sha256"], ac.ImageManifest.Hex())
12478+
return nil, nil
12479+
}, nil)
12480+
require.NoError(t, err)
12481+
}
12482+
12483+
func testImageResolveAttestationChainLocal(t *testing.T, sb integration.Sandbox) {
12484+
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush, workers.FeatureProvenance)
12485+
requiresLinux(t)
12486+
12487+
ctx := sb.Context()
12488+
c, err := New(ctx, sb.Address())
12489+
require.NoError(t, err)
12490+
defer c.Close()
12491+
12492+
target, platform := buildProvenanceImage(ctx, t, c, sb)
12493+
12494+
_, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
12495+
md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{
12496+
Identifier: "docker-image://" + target,
12497+
}, sourceresolver.Opt{
12498+
ImageOpt: &sourceresolver.ResolveImageOpt{
12499+
NoConfig: true,
12500+
AttestationChain: true,
12501+
Platform: &platform,
12502+
ResolveMode: pb.AttrImageResolveModeForcePull,
12503+
},
12504+
})
12505+
if err != nil {
12506+
return nil, err
12507+
}
12508+
require.NotNil(t, md.Image)
12509+
require.NotNil(t, md.Image.AttestationChain)
12510+
ac := md.Image.AttestationChain
12511+
require.NotEmpty(t, ac.AttestationManifest)
12512+
att := ac.Blobs[ac.AttestationManifest]
12513+
require.NotEmpty(t, att.Data)
12514+
12515+
var manifest ocispecs.Manifest
12516+
require.NoError(t, json.Unmarshal(att.Data, &manifest))
12517+
require.NotEmpty(t, manifest.Layers)
12518+
found := false
12519+
for _, layer := range manifest.Layers {
12520+
if isSLSAPredicateType(layer.Annotations["in-toto.io/predicate-type"]) {
12521+
found = true
12522+
break
12523+
}
12524+
}
12525+
require.True(t, found)
12526+
return nil, nil
12527+
}, nil)
12528+
require.NoError(t, err)
12529+
}
12530+
12531+
func buildProvenanceImage(ctx context.Context, t *testing.T, c *Client, sb integration.Sandbox) (string, ocispecs.Platform) {
12532+
t.Helper()
12533+
12534+
registry, err := sb.NewRegistry()
12535+
if errors.Is(err, integration.ErrRequirements) {
12536+
t.Skip(err.Error())
12537+
}
12538+
require.NoError(t, err)
12539+
12540+
platform := platforms.Normalize(platforms.DefaultSpec())
12541+
platformKey := platforms.Format(platform)
12542+
target := registry + "/buildkit/testprovenance:latest"
12543+
12544+
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
12545+
res := gateway.NewResult()
12546+
12547+
st := llb.Scratch().File(
12548+
llb.Mkfile("/greeting", 0600, []byte("hello provenance")),
12549+
)
12550+
def, err := st.Marshal(ctx)
12551+
if err != nil {
12552+
return nil, err
12553+
}
12554+
r, err := c.Solve(ctx, gateway.SolveRequest{
12555+
Definition: def.ToPB(),
12556+
})
12557+
if err != nil {
12558+
return nil, err
12559+
}
12560+
ref, err := r.SingleRef()
12561+
if err != nil {
12562+
return nil, err
12563+
}
12564+
_, err = ref.ToState()
12565+
if err != nil {
12566+
return nil, err
12567+
}
12568+
res.AddRef(platformKey, ref)
12569+
12570+
img := ocispecs.Image{
12571+
Platform: platform,
12572+
}
12573+
config, err := json.Marshal(img)
12574+
if err != nil {
12575+
return nil, errors.Wrapf(err, "failed to marshal image config")
12576+
}
12577+
res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, platformKey), config)
12578+
12579+
expPlatforms := &exptypes.Platforms{
12580+
Platforms: []exptypes.Platform{{ID: platformKey, Platform: platform}},
12581+
}
12582+
dt, err := json.Marshal(expPlatforms)
12583+
if err != nil {
12584+
return nil, err
12585+
}
12586+
res.AddMeta(exptypes.ExporterPlatformsKey, dt)
12587+
12588+
return res, nil
12589+
}
12590+
12591+
_, err = c.Build(ctx, SolveOpt{
12592+
FrontendAttrs: map[string]string{
12593+
"attest:provenance": "mode=max",
12594+
},
12595+
Exports: []ExportEntry{
12596+
{
12597+
Type: ExporterImage,
12598+
Attrs: map[string]string{
12599+
"name": target,
12600+
"push": "true",
12601+
},
12602+
},
12603+
},
12604+
}, "", frontend, nil)
12605+
require.NoError(t, err)
12606+
12607+
return target, platform
12608+
}
12609+
12610+
// isSLSAPredicateType reports whether the predicate type represents SLSA provenance.
12611+
func isSLSAPredicateType(v string) bool {
12612+
switch v {
12613+
case policyimage.SLSAProvenancePredicateType02, policyimage.SLSAProvenancePredicateType1:
12614+
return true
12615+
default:
12616+
return false
12617+
}
12618+
}
12619+
1240712620
func testHTTPPruneAfterCacheKey(t *testing.T, sb integration.Sandbox) {
1240812621
// this test depends on hitting race condition in internal functions.
1240912622
// If debugging and expecting failure you can add small sleep in beginning of source/http.Exec() to hit reliably

client/llb/sourceresolver/types.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ type MetaResponse struct {
3939
}
4040

4141
type ResolveImageOpt struct {
42-
Platform *ocispecs.Platform
43-
ResolveMode string
44-
NoConfig bool
45-
AttestationChain bool
42+
Platform *ocispecs.Platform
43+
ResolveMode string
44+
NoConfig bool
45+
AttestationChain bool
46+
ResolveAttestations []string
4647
}
4748

4849
type ResolveImageResponse struct {

frontend/gateway/gateway.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@ func (lbf *llbBridgeForwarder) ResolveSourceMeta(ctx context.Context, req *pb.Re
591591
if req.Image != nil {
592592
resolveopt.ImageOpt.NoConfig = req.Image.NoConfig
593593
resolveopt.ImageOpt.AttestationChain = req.Image.AttestationChain
594+
resolveopt.ImageOpt.ResolveAttestations = slices.Clone(req.Image.ResolveAttestations)
594595
}
595596
resolveopt.OCILayoutOpt = &sourceresolver.ResolveOCILayoutOpt{
596597
Platform: platform,

frontend/gateway/grpcclient/client.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"maps"
99
"net"
1010
"os"
11+
"slices"
1112
"strings"
1213
"sync"
1314
"syscall"
@@ -523,9 +524,15 @@ func (c *grpcClient) ResolveSourceMetadata(ctx context.Context, op *opspb.Source
523524
SourcePolicies: opt.SourcePolicies,
524525
}
525526
if opt.ImageOpt != nil {
527+
attestationChain := opt.ImageOpt.AttestationChain
528+
if len(opt.ImageOpt.ResolveAttestations) > 0 {
529+
attestationChain = true
530+
}
531+
req.ResolveMode = opt.ImageOpt.ResolveMode
526532
req.Image = &pb.ResolveSourceImageRequest{
527-
NoConfig: opt.ImageOpt.NoConfig,
528-
AttestationChain: opt.ImageOpt.AttestationChain,
533+
NoConfig: opt.ImageOpt.NoConfig,
534+
AttestationChain: attestationChain,
535+
ResolveAttestations: slices.Clone(opt.ImageOpt.ResolveAttestations),
529536
}
530537
}
531538

frontend/gateway/pb/gateway.pb.go

Lines changed: 16 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/gateway/pb/gateway.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ message ResolveSourceMetaResponse {
155155
message ResolveSourceImageRequest {
156156
bool NoConfig = 1;
157157
bool AttestationChain = 2;
158+
repeated string ResolveAttestations = 3;
158159
}
159160

160161
message AttestationChain {

0 commit comments

Comments
 (0)