Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions commands/policy/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
27 changes: 27 additions & 0 deletions commands/policy/eval_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 1 addition & 1 deletion commands/policy/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
41 changes: 41 additions & 0 deletions policy/add_unknowns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
175 changes: 175 additions & 0 deletions policy/provenance.go
Original file line number Diff line number Diff line change
@@ -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 changes: 32 additions & 0 deletions policy/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentionally missing materials atm.

I wanted to make every material type Input so that policy conditions can run in any level of source material in c2164fb .

But I missed that it is not allowed to return different source from a policy callback. So the current patch only works in policy eval and needs a complete redesign.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes policy callback can't return a different source. Looks good for follow-up.

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"`
Expand Down
Loading