policy: add image.provenance input type

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2026-02-12 11:59:06 -08:00
parent 53882aec96
commit 8d605dcd87
9 changed files with 708 additions and 15 deletions
+8 -10
View File
@@ -158,7 +158,7 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
if err != nil {
return err
}
srcReq = buildSourceMetaResponse(resp, req)
srcReq = buildSourceMetaResponse(resp)
continue
}
break
@@ -249,7 +249,7 @@ func runEval(ctx context.Context, dockerCli command.Cli, source string, opts eva
if err != nil {
return err
}
srcReq = buildSourceMetaResponse(resp, next)
srcReq = buildSourceMetaResponse(resp)
}
}
@@ -293,10 +293,11 @@ func sourceResolverOpt(req *gwpb.ResolveSourceMetaRequest, platform *ocispecs.Pl
}
if req.Image != nil {
opt.ImageOpt = &sourceresolver.ResolveImageOpt{
NoConfig: req.Image.NoConfig,
AttestationChain: req.Image.AttestationChain,
Platform: platform,
ResolveMode: req.ResolveMode,
NoConfig: req.Image.NoConfig,
AttestationChain: req.Image.AttestationChain,
ResolveAttestations: slices.Clone(req.Image.ResolveAttestations),
Platform: platform,
ResolveMode: req.ResolveMode,
}
}
if req.Git != nil {
@@ -307,15 +308,12 @@ func sourceResolverOpt(req *gwpb.ResolveSourceMetaRequest, platform *ocispecs.Pl
return opt
}
func buildSourceMetaResponse(resp *sourceresolver.MetaResponse, req *gwpb.ResolveSourceMetaRequest) *gwpb.ResolveSourceMetaResponse {
func buildSourceMetaResponse(resp *sourceresolver.MetaResponse) *gwpb.ResolveSourceMetaResponse {
out := &gwpb.ResolveSourceMetaResponse{
Source: resp.Op,
}
if resp.Image != nil {
chain := toGatewayAttestationChain(resp.Image.AttestationChain)
if chain == nil && req != nil && req.Image != nil && req.Image.AttestationChain {
chain = &gwpb.AttestationChain{}
}
out.Image = &gwpb.ResolveSourceImageResponse{
Digest: resp.Image.Digest.String(),
Config: resp.Image.Config,
+27
View File
@@ -0,0 +1,27 @@
package policy
import (
"testing"
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/require"
)
func TestSourceResolverOptIncludesResolveAttestations(t *testing.T) {
req := &gwpb.ResolveSourceMetaRequest{
ResolveMode: "default",
Image: &gwpb.ResolveSourceImageRequest{
NoConfig: true,
ResolveAttestations: []string{"https://slsa.dev/provenance/v0.2"},
},
}
platform := &ocispecs.Platform{OS: "linux", Architecture: "amd64"}
opt := sourceResolverOpt(req, platform)
require.NotNil(t, opt.ImageOpt)
require.True(t, opt.ImageOpt.NoConfig)
require.Equal(t, []string{"https://slsa.dev/provenance/v0.2"}, opt.ImageOpt.ResolveAttestations)
require.Equal(t, "default", opt.ImageOpt.ResolveMode)
require.Equal(t, platform, opt.ImageOpt.Platform)
}
+1 -1
View File
@@ -167,7 +167,7 @@ func (r *policyTestResolver) Resolve(ctx context.Context, source *pb.SourceOp, r
if err != nil {
return nil, err
}
return buildSourceMetaResponse(resp, req), nil
return buildSourceMetaResponse(resp), nil
}
func (r *policyTestResolver) init(ctx context.Context) error {
+41
View File
@@ -46,6 +46,47 @@ func TestAddUnknowns(t *testing.T) {
},
},
},
{
name: "image-provenance-enables-resolve-attestations",
unknowns: []string{"image.provenance"},
initial: &gwpb.ResolveSourceMetaRequest{},
expected: &gwpb.ResolveSourceMetaRequest{
Image: &gwpb.ResolveSourceImageRequest{
NoConfig: true,
AttestationChain: true,
ResolveAttestations: resolveProvenanceAttestations,
},
},
},
{
name: "nested-image-provenance-enables-resolve-attestations",
unknowns: []string{"image.provenance.buildType"},
initial: &gwpb.ResolveSourceMetaRequest{},
expected: &gwpb.ResolveSourceMetaRequest{
Image: &gwpb.ResolveSourceImageRequest{
NoConfig: true,
AttestationChain: true,
ResolveAttestations: resolveProvenanceAttestations,
},
},
},
{
name: "image-provenance-on-existing-image-request-preserves-fields",
unknowns: []string{"image.provenance.builderID"},
initial: &gwpb.ResolveSourceMetaRequest{
Image: &gwpb.ResolveSourceImageRequest{
NoConfig: false,
AttestationChain: true,
},
},
expected: &gwpb.ResolveSourceMetaRequest{
Image: &gwpb.ResolveSourceImageRequest{
NoConfig: false,
AttestationChain: true,
ResolveAttestations: resolveProvenanceAttestations,
},
},
},
{
name: "image-attestation-on-existing-image-request",
unknowns: []string{"image.hasProvenance"},
+175
View File
@@ -0,0 +1,175 @@
package policy
import (
"encoding/json"
"slices"
"strings"
"time"
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
)
const predicateTypeAnnotation = "in-toto.io/predicate-type"
var resolveProvenanceAttestations = []string{
slsa02.PredicateSLSAProvenance,
slsa1.PredicateSLSAProvenance,
}
type inTotoStatement struct {
PredicateType string `json:"predicateType"`
Predicate json.RawMessage `json:"predicate"`
}
func parseProvenance(ac *gwpb.AttestationChain) (*ImageProvenance, error) {
if ac == nil || len(ac.Blobs) == 0 {
return nil, nil
}
// Prefer blobs that explicitly declare a provenance predicate type.
for _, b := range ac.Blobs {
if b == nil || b.Descriptor_ == nil || len(b.Data) == 0 {
continue
}
pt := b.Descriptor_.Annotations[predicateTypeAnnotation]
if pt == "" {
continue
}
if !slices.Contains(resolveProvenanceAttestations, pt) {
continue
}
prv, err := parseProvenanceBlob(b.Data, pt)
if err != nil {
return nil, err
}
if prv != nil {
return prv, nil
}
}
return nil, nil
}
func parseProvenanceBlob(dt []byte, pt string) (*ImageProvenance, error) {
var stmt inTotoStatement
if err := json.Unmarshal(dt, &stmt); err != nil || len(stmt.Predicate) == 0 {
return nil, nil
}
if stmt.PredicateType != "" && stmt.PredicateType != pt {
return nil, nil
}
switch pt {
case slsa1.PredicateSLSAProvenance:
return parseSLSA1Provenance(stmt.Predicate)
case slsa02.PredicateSLSAProvenance:
return parseSLSA02Provenance(stmt.Predicate)
}
return nil, nil
}
func parseSLSA1Provenance(dt []byte) (*ImageProvenance, error) {
var pred provenancetypes.ProvenancePredicateSLSA1
if err := json.Unmarshal(dt, &pred); err != nil {
return nil, nil
}
if pred.BuildDefinition.BuildType == "" && pred.RunDetails.Builder.ID == "" {
return nil, nil
}
prv := &ImageProvenance{
PredicateType: slsa1.PredicateSLSAProvenance,
BuildType: pred.BuildDefinition.BuildType,
BuilderID: pred.RunDetails.Builder.ID,
ConfigSource: &ImageProvenanceConfigSource{
URI: pred.BuildDefinition.ExternalParameters.ConfigSource.URI,
Digest: pred.BuildDefinition.ExternalParameters.ConfigSource.Digest,
Path: pred.BuildDefinition.ExternalParameters.ConfigSource.Path,
},
Frontend: pred.BuildDefinition.ExternalParameters.Request.Frontend,
BuildArgs: extractBuildArgs(pred.BuildDefinition.ExternalParameters.Request.Args),
RawArgs: pred.BuildDefinition.ExternalParameters.Request.Args,
}
if md := pred.RunDetails.Metadata; md != nil {
prv.InvocationID = md.InvocationID
prv.StartedOn = formatProvenanceTime(md.StartedOn)
prv.FinishedOn = formatProvenanceTime(md.FinishedOn)
prv.Reproducible = boolPtr(md.Reproducible)
prv.Hermetic = boolPtr(md.Hermetic)
prv.Completeness = &ImageProvenanceCompleteness{
Parameters: boolPtr(md.Completeness.Request),
Materials: boolPtr(md.Completeness.ResolvedDependencies),
}
}
return prv, nil
}
func parseSLSA02Provenance(dt []byte) (*ImageProvenance, error) {
var pred provenancetypes.ProvenancePredicateSLSA02
if err := json.Unmarshal(dt, &pred); err != nil {
return nil, nil
}
if pred.BuildType == "" && pred.Builder.ID == "" {
return nil, nil
}
prv := &ImageProvenance{
PredicateType: slsa02.PredicateSLSAProvenance,
BuildType: pred.BuildType,
BuilderID: pred.Builder.ID,
ConfigSource: &ImageProvenanceConfigSource{
URI: pred.Invocation.ConfigSource.URI,
Digest: pred.Invocation.ConfigSource.Digest,
Path: pred.Invocation.ConfigSource.EntryPoint,
},
Frontend: pred.Invocation.Parameters.Frontend,
BuildArgs: extractBuildArgs(pred.Invocation.Parameters.Args),
RawArgs: pred.Invocation.Parameters.Args,
}
if md := pred.Metadata; md != nil {
prv.InvocationID = md.BuildInvocationID
prv.StartedOn = formatProvenanceTime(md.BuildStartedOn)
prv.FinishedOn = formatProvenanceTime(md.BuildFinishedOn)
prv.Reproducible = boolPtr(md.Reproducible)
prv.Hermetic = boolPtr(md.Hermetic)
prv.Completeness = &ImageProvenanceCompleteness{
Parameters: boolPtr(md.Completeness.Parameters),
Environment: boolPtr(md.Completeness.Environment),
Materials: boolPtr(md.Completeness.Materials),
}
}
return prv, nil
}
func boolPtr(v bool) *bool {
return &v
}
func formatProvenanceTime(t *time.Time) string {
if t == nil {
return ""
}
return t.UTC().Format(time.RFC3339)
}
func extractBuildArgs(args map[string]string) map[string]string {
if len(args) == 0 {
return nil
}
const prefix = "build-arg:"
out := make(map[string]string)
for k, v := range args {
if name, ok := strings.CutPrefix(k, prefix); ok && name != "" {
out[name] = v
}
}
if len(out) == 0 {
return nil
}
return out
}
+32
View File
@@ -128,9 +128,41 @@ type Image struct {
WorkingDir string `json:"workingDir,omitempty"`
HasProvenance bool `json:"hasProvenance,omitempty"`
Provenance *ImageProvenance `json:"provenance,omitempty"`
Signatures []AttestationSignature `json:"signatures,omitempty"`
}
type ImageProvenance struct {
PredicateType string `json:"predicateType,omitempty"`
BuildType string `json:"buildType,omitempty"`
BuilderID string `json:"builderID,omitempty"`
InvocationID string `json:"invocationID,omitempty"`
StartedOn string `json:"startedOn,omitempty"`
FinishedOn string `json:"finishedOn,omitempty"`
ConfigSource *ImageProvenanceConfigSource `json:"configSource,omitempty"`
Frontend string `json:"frontend,omitempty"`
BuildArgs map[string]string `json:"buildArgs,omitempty"`
RawArgs map[string]string `json:"rawArgs,omitempty"`
Reproducible *bool `json:"reproducible,omitempty"`
Hermetic *bool `json:"hermetic,omitempty"`
Completeness *ImageProvenanceCompleteness `json:"completeness,omitempty"`
}
type ImageProvenanceConfigSource struct {
URI string `json:"uri,omitempty"`
Digest map[string]string `json:"digest,omitempty"`
Path string `json:"path,omitempty"`
}
type ImageProvenanceCompleteness struct {
Parameters *bool `json:"parameters,omitempty"`
Environment *bool `json:"environment,omitempty"`
Materials *bool `json:"materials,omitempty"`
}
type AttestationSignature struct {
SignatureKind SignatureKind `json:"kind,omitempty"`
SignatureType SignatureType `json:"type,omitempty"`
+33 -3
View File
@@ -555,7 +555,7 @@ func SourceToInputWithLogger(ctx context.Context, getVerifier PolicyVerifierProv
unknowns = append(unknowns, "input.image.checksum")
}
unknowns = append(unknowns, withPrefix(configFields, "input.image.")...)
unknowns = append(unknowns, "input.image.hasProvenance", "input.image.signatures")
unknowns = append(unknowns, "input.image.hasProvenance", "input.image.provenance", "input.image.signatures")
} else {
inp.Image.Checksum = src.Image.Digest
if cfg := src.Image.Config; cfg != nil {
@@ -577,7 +577,14 @@ func SourceToInputWithLogger(ctx context.Context, getVerifier PolicyVerifierProv
}
if ac := src.Image.AttestationChain; ac != nil {
inp.Image.HasProvenance = ac.AttestationManifest != ""
if prv, err := parseProvenance(ac); err != nil {
if logf != nil {
logf(logrus.DebugLevel, fmt.Sprintf("failed to parse image provenance: %v", err))
}
} else {
inp.Image.Provenance = prv
}
inp.Image.HasProvenance = ac.AttestationManifest != "" || inp.Image.Provenance != nil
if getVerifier != nil {
signatures, err := parseSignatures(ctx, getVerifier, ac, platform)
if err != nil {
@@ -589,7 +596,7 @@ func SourceToInputWithLogger(ctx context.Context, getVerifier PolicyVerifierProv
}
}
} else {
unknowns = append(unknowns, "input.image.hasProvenance", "input.image.signatures")
unknowns = append(unknowns, "input.image.hasProvenance", "input.image.provenance", "input.image.signatures")
}
}
case "local":
@@ -635,6 +642,17 @@ func AddUnknownsWithLogger(logf func(logrus.Level, string), req *gwpb.ResolveSou
logf(logrus.DebugLevel, fmt.Sprintf("collected unknowns: %+v", unk2))
}
for _, u := range unk2 {
if u == "image.provenance" || strings.HasPrefix(u, "image.provenance.") {
if req.Image == nil {
req.Image = &gwpb.ResolveSourceImageRequest{
NoConfig: true,
}
}
req.Image.AttestationChain = true
req.Image.ResolveAttestations = appendUnique(req.Image.ResolveAttestations, resolveProvenanceAttestations...)
continue
}
switch u {
case "image.checksum", "image.labels", "image.user", "image.volumes", "image.workingDir", "image.env":
if req.Image == nil {
@@ -729,6 +747,9 @@ func summarizeUnknownsForLog(unk []string) []string {
if strings.HasPrefix(u, "image.signatures") {
u = "image.signatures"
}
if strings.HasPrefix(u, "image.provenance") {
u = "image.provenance"
}
if u == "image" {
continue
}
@@ -741,6 +762,15 @@ func summarizeUnknownsForLog(unk []string) []string {
return out
}
func appendUnique(dst []string, values ...string) []string {
for _, v := range values {
if !slices.Contains(dst, v) {
dst = append(dst, v)
}
}
return dst
}
func hasHTTPUnknowns(unk []string) bool {
for _, u := range unk {
if strings.HasPrefix(u, "http.") {
+239 -1
View File
@@ -9,6 +9,8 @@ import (
"testing"
"time"
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/solver/pb"
policyimage "github.com/moby/policy-helpers/image"
@@ -177,6 +179,7 @@ func TestSourceToInputWithLogger(t *testing.T) {
"input.image.workingDir",
"input.image.env",
"input.image.hasProvenance",
"input.image.provenance",
"input.image.signatures",
},
},
@@ -208,6 +211,7 @@ func TestSourceToInputWithLogger(t *testing.T) {
"input.image.workingDir",
"input.image.env",
"input.image.hasProvenance",
"input.image.provenance",
"input.image.signatures",
},
},
@@ -343,6 +347,113 @@ func TestSourceToInputWithLogger(t *testing.T) {
require.Equal(t, "docker/buildx", sig.Signer.SourceRepositoryIdentifier)
},
},
{
name: "image-attestation-chain-loads-provenance-fields-v0.2",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{
Identifier: "docker-image://alpine:latest",
},
Image: &gwpb.ResolveSourceImageResponse{
Digest: "sha256:efefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef",
AttestationChain: newTestAttestationChainWithProvenance(t),
},
},
platform: &ocispecs.Platform{OS: "linux", Architecture: "amd64"},
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.NotNil(t, inp.Image.Provenance)
require.Equal(t, slsa02.PredicateSLSAProvenance, inp.Image.Provenance.PredicateType)
require.Equal(t, "https://example.com/build-type", inp.Image.Provenance.BuildType)
require.Equal(t, "https://example.com/builder-id", inp.Image.Provenance.BuilderID)
require.Equal(t, "inv-v02", inp.Image.Provenance.InvocationID)
require.Equal(t, "2024-01-02T03:04:05Z", inp.Image.Provenance.StartedOn)
require.Equal(t, "2024-01-02T03:05:05Z", inp.Image.Provenance.FinishedOn)
require.Equal(t, "gateway.v0", inp.Image.Provenance.Frontend)
require.Equal(t, map[string]string{"BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1"}, inp.Image.Provenance.BuildArgs)
require.Equal(t, map[string]string{
"build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1",
"cmdline": "docker/dockerfile-upstream:master",
}, inp.Image.Provenance.RawArgs)
require.NotNil(t, inp.Image.Provenance.ConfigSource)
require.Equal(t, "https://github.com/moby/buildkit.git#refs/tags/v0.21.0", inp.Image.Provenance.ConfigSource.URI)
require.Equal(t, "Dockerfile", inp.Image.Provenance.ConfigSource.Path)
require.Equal(t, map[string]string{"sha1": "52b004d2afe20c5c80967cc1784e718b52d69dae"}, inp.Image.Provenance.ConfigSource.Digest)
require.NotNil(t, inp.Image.Provenance.Completeness)
require.NotNil(t, inp.Image.Provenance.Completeness.Parameters)
require.True(t, *inp.Image.Provenance.Completeness.Parameters)
require.NotNil(t, inp.Image.Provenance.Completeness.Environment)
require.True(t, *inp.Image.Provenance.Completeness.Environment)
require.NotNil(t, inp.Image.Provenance.Completeness.Materials)
require.False(t, *inp.Image.Provenance.Completeness.Materials)
require.NotNil(t, inp.Image.Provenance.Reproducible)
require.True(t, *inp.Image.Provenance.Reproducible)
require.NotNil(t, inp.Image.Provenance.Hermetic)
require.True(t, *inp.Image.Provenance.Hermetic)
},
},
{
name: "image-attestation-chain-loads-provenance-fields-v1",
src: &gwpb.ResolveSourceMetaResponse{
Source: &pb.SourceOp{
Identifier: "docker-image://alpine:latest",
},
Image: &gwpb.ResolveSourceImageResponse{
Digest: "sha256:f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0",
AttestationChain: newTestAttestationChainWithProvenanceV1(t),
},
},
platform: &ocispecs.Platform{OS: "linux", Architecture: "amd64"},
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.NotNil(t, inp.Image.Provenance)
require.Equal(t, slsa1.PredicateSLSAProvenance, inp.Image.Provenance.PredicateType)
require.Equal(t, "https://example.com/build-type-v1", inp.Image.Provenance.BuildType)
require.Equal(t, "https://example.com/builder-id-v1", inp.Image.Provenance.BuilderID)
require.Equal(t, "inv-v1", inp.Image.Provenance.InvocationID)
require.Equal(t, "2024-02-03T04:05:06Z", inp.Image.Provenance.StartedOn)
require.Equal(t, "2024-02-03T04:06:06Z", inp.Image.Provenance.FinishedOn)
require.Equal(t, "gateway.v0", inp.Image.Provenance.Frontend)
require.Equal(t, map[string]string{"BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1"}, inp.Image.Provenance.BuildArgs)
require.Equal(t, map[string]string{
"build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1",
"source": "docker/dockerfile-upstream:master",
}, inp.Image.Provenance.RawArgs)
require.NotNil(t, inp.Image.Provenance.ConfigSource)
require.Equal(t, "https://github.com/moby/buildkit.git#refs/heads/master", inp.Image.Provenance.ConfigSource.URI)
require.Equal(t, "Dockerfile", inp.Image.Provenance.ConfigSource.Path)
require.Equal(t, map[string]string{"sha1": "9836771d0c5b21cbc7f0c38b81be39c42fc46b7b"}, inp.Image.Provenance.ConfigSource.Digest)
require.NotNil(t, inp.Image.Provenance.Completeness)
require.NotNil(t, inp.Image.Provenance.Completeness.Parameters)
require.True(t, *inp.Image.Provenance.Completeness.Parameters)
require.Nil(t, inp.Image.Provenance.Completeness.Environment)
require.NotNil(t, inp.Image.Provenance.Completeness.Materials)
require.False(t, *inp.Image.Provenance.Completeness.Materials)
require.NotNil(t, inp.Image.Provenance.Reproducible)
require.True(t, *inp.Image.Provenance.Reproducible)
require.NotNil(t, inp.Image.Provenance.Hermetic)
require.True(t, *inp.Image.Provenance.Hermetic)
},
},
{
name: "image-attestation-chain-without-manifest-keeps-has-provenance-false",
src: &gwpb.ResolveSourceMetaResponse{
@@ -428,7 +539,7 @@ func TestSourceToInputWithLogger(t *testing.T) {
WorkingDir: "/work",
},
},
expUnk: []string{"input.image.hasProvenance", "input.image.signatures"},
expUnk: []string{"input.image.hasProvenance", "input.image.provenance", "input.image.signatures"},
},
{
name: "git-source-missing-full-remote-url-attr",
@@ -855,6 +966,133 @@ func newTestAttestationChain(t *testing.T) *gwpb.AttestationChain {
}
}
func newTestAttestationChainWithProvenance(t *testing.T) *gwpb.AttestationChain {
t.Helper()
ac := newTestAttestationChain(t)
provenancePredicate := map[string]any{
"builder": map[string]any{
"id": "https://example.com/builder-id",
},
"buildType": "https://example.com/build-type",
"invocation": map[string]any{
"configSource": map[string]any{
"digest": map[string]any{
"sha1": "52b004d2afe20c5c80967cc1784e718b52d69dae",
},
"entryPoint": "Dockerfile",
"uri": "https://github.com/moby/buildkit.git#refs/tags/v0.21.0",
},
"parameters": map[string]any{
"frontend": "gateway.v0",
"args": map[string]any{
"build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1",
"cmdline": "docker/dockerfile-upstream:master",
},
},
"environment": map[string]any{
"platform": "linux/amd64",
},
},
"metadata": map[string]any{
"buildInvocationID": "inv-v02",
"buildStartedOn": "2024-01-02T03:04:05Z",
"buildFinishedOn": "2024-01-02T03:05:05Z",
"completeness": map[string]any{
"parameters": true,
"environment": true,
"materials": false,
},
"reproducible": true,
"https://mobyproject.org/buildkit@v1#hermetic": true,
},
}
provenanceBytes := mustMarshalJSON(t, map[string]any{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": slsa02.PredicateSLSAProvenance,
"predicate": provenancePredicate,
})
provenanceDigest := digest.FromBytes(provenanceBytes)
ac.Blobs[provenanceDigest.String()] = &gwpb.Blob{
Descriptor_: &gwpb.Descriptor{
MediaType: "application/vnd.in-toto+json",
Digest: provenanceDigest.String(),
Size: int64(len(provenanceBytes)),
Annotations: map[string]string{
predicateTypeAnnotation: slsa02.PredicateSLSAProvenance,
},
},
Data: provenanceBytes,
}
return ac
}
func newTestAttestationChainWithProvenanceV1(t *testing.T) *gwpb.AttestationChain {
t.Helper()
ac := newTestAttestationChain(t)
provenancePredicate := map[string]any{
"buildDefinition": map[string]any{
"buildType": "https://example.com/build-type-v1",
"externalParameters": map[string]any{
"configSource": map[string]any{
"digest": map[string]any{
"sha1": "9836771d0c5b21cbc7f0c38b81be39c42fc46b7b",
},
"path": "Dockerfile",
"uri": "https://github.com/moby/buildkit.git#refs/heads/master",
},
"request": map[string]any{
"frontend": "gateway.v0",
"args": map[string]any{
"build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1",
"source": "docker/dockerfile-upstream:master",
},
},
},
"internalParameters": map[string]any{},
},
"runDetails": map[string]any{
"builder": map[string]any{
"id": "https://example.com/builder-id-v1",
},
"metadata": map[string]any{
"invocationID": "inv-v1",
"startedOn": "2024-02-03T04:05:06Z",
"finishedOn": "2024-02-03T04:06:06Z",
"buildkit_completeness": map[string]any{
"request": true,
"resolvedDependencies": false,
},
"buildkit_reproducible": true,
"buildkit_hermetic": true,
},
},
}
provenanceBytes := mustMarshalJSON(t, map[string]any{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": slsa1.PredicateSLSAProvenance,
"predicate": provenancePredicate,
})
provenanceDigest := digest.FromBytes(provenanceBytes)
ac.Blobs[provenanceDigest.String()] = &gwpb.Blob{
Descriptor_: &gwpb.Descriptor{
MediaType: "application/vnd.in-toto+json",
Digest: provenanceDigest.String(),
Size: int64(len(provenanceBytes)),
Annotations: map[string]string{
predicateTypeAnnotation: slsa1.PredicateSLSAProvenance,
},
},
Data: provenanceBytes,
}
return ac
}
func mustMarshalJSON(t *testing.T, v any) []byte {
t.Helper()
dt, err := json.Marshal(v)
+152
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
urlpkg "net/url"
"strings"
"testing"
"github.com/containerd/continuity/fs/fstest"
@@ -23,6 +24,8 @@ var policyEvalTests = []func(t *testing.T, sb integration.Sandbox){
testPolicyEvalPrint,
testPolicyEvalFields,
testPolicyEvalLabel,
testPolicyEvalProvenance,
testPolicyEvalProvenanceValidation,
testPolicyEvalHTTP,
}
@@ -220,6 +223,155 @@ decision := {"allow": allow}
require.NoError(t, err, string(out))
}
func testPolicyEvalProvenance(t *testing.T, sb integration.Sandbox) {
// TODO: update after v0.28+ to test with non-master BuildKit
if buildkitTag() != "master" {
t.Skip("policy eval provenance integration requires TEST_BUILDKIT_TAG=master")
}
if !isDockerContainerWorker(sb) {
t.Skip("policy eval provenance integration requires docker-container worker")
}
// Base policy input support.
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
imageRef := createPolicyEvalProvenanceImage(t, sb)
cmd := buildxCmd(sb, withArgs(
"policy",
"eval",
"--print",
"--fields",
"image.provenance",
"docker-image://"+imageRef,
))
var stderr bytes.Buffer
cmd.Stderr = &stderr
out, err := cmd.Output()
expectResolveAttestations := true
t.Logf("buildkit image=%s worker=%s expectResolveAttestations=%v", buildkitImage, sb.Name(), expectResolveAttestations)
if err != nil && strings.Contains(stderr.String(), "maximum attempts reached for resolving policy metadata") {
if expectResolveAttestations {
require.NoError(t, err, stderr.String())
}
t.Skip("policy eval provenance requires BuildKit ResolveAttestations support (currently master-only)")
}
require.NoError(t, err, stderr.String())
var input policy.Input
err = json.Unmarshal(out, &input)
require.NoError(t, err, string(out))
require.NotNil(t, input.Image)
require.True(t, input.Image.HasProvenance, "expected source image to report provenance")
if input.Image.Provenance == nil {
if expectResolveAttestations {
require.NotNil(t, input.Image.Provenance, "expected image provenance to be resolved with master BuildKit")
}
t.Skip("policy eval provenance requires BuildKit ResolveAttestations support (currently master-only)")
}
require.Equal(t, "https://slsa.dev/provenance/v1", input.Image.Provenance.PredicateType)
require.Equal(t, "dockerfile.v0", input.Image.Provenance.Frontend)
}
func testPolicyEvalProvenanceValidation(t *testing.T, sb integration.Sandbox) {
// TODO: update after v0.28+ is released to test with non-master BuildKit
if buildkitTag() != "master" {
t.Skip("policy eval provenance integration requires TEST_BUILDKIT_TAG=master")
}
if !isDockerContainerWorker(sb) {
t.Skip("policy eval provenance integration requires docker-container worker")
}
// Base policy input support.
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
imageRef := createPolicyEvalProvenanceImage(t, sb)
allowPolicyDir := tmpdir(
t,
fstest.CreateFile("policy.rego", []byte(`
package docker
default allow = false
allow if {
input.image.hasProvenance
input.image.provenance.predicateType == "https://slsa.dev/provenance/v1"
input.image.provenance.frontend == "dockerfile.v0"
}
decision := {"allow": allow}
`), 0600),
)
allowCmd := buildxCmd(sb, withDir(allowPolicyDir), withArgs(
"policy",
"eval",
"--filename",
"policy",
"docker-image://"+imageRef,
))
allowOut, err := allowCmd.CombinedOutput()
if err != nil && strings.Contains(string(allowOut), "maximum attempts reached for resolving policy metadata") {
t.Skip("policy eval provenance requires BuildKit ResolveAttestations support (currently master-only)")
}
require.NoError(t, err, string(allowOut))
denyPolicyDir := tmpdir(
t,
fstest.CreateFile("policy.rego", []byte(`
package docker
default allow = false
allow if input.image.provenance.frontend == "not-a-real-frontend"
decision := {"allow": allow}
`), 0600),
)
denyCmd := buildxCmd(sb, withDir(denyPolicyDir), withArgs(
"policy",
"eval",
"--filename",
"policy",
"docker-image://"+imageRef,
))
denyOut, err := denyCmd.CombinedOutput()
if err != nil && strings.Contains(string(denyOut), "maximum attempts reached for resolving policy metadata") {
t.Skip("policy eval provenance requires BuildKit ResolveAttestations support (currently master-only)")
}
require.Error(t, err, string(denyOut))
require.Contains(t, string(denyOut), "policy denied")
}
func createPolicyEvalProvenanceImage(t *testing.T, sb integration.Sandbox) string {
registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)
imageRef := registry + "/buildx/policy-eval-provenance:" + identity.NewID()
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", []byte(`
FROM busybox:latest
RUN echo provenance > /proof
`), 0600),
)
buildCmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--provenance=mode=max,version=v1",
"--output=type=image,name="+imageRef+",push=true",
dir,
))
buildOut, err := buildCmd.CombinedOutput()
require.NoError(t, err, string(buildOut))
return imageRef
}
func testPolicyEvalHTTP(t *testing.T, sb integration.Sandbox) {
resp := &httpserver.Response{Content: []byte("policy-eval-http")}
server := httpserver.NewTestServer(map[string]*httpserver.Response{