policy: add artifact attestation builtin support

Add artifact_attestation(http, filename) and wire verifier support for
artifact bundle checks. Add docker_github_builder_bundle helper rule.

Handle runtime unknown http.checksum after eval so metadata resolve is
requested when checksum is missing.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2026-02-20 08:56:39 -08:00
parent 7d3b6988c2
commit ac9e4c512c
7 changed files with 266 additions and 9 deletions
+5
View File
@@ -21,3 +21,8 @@ docker_github_builder_signature(sig, repo) if {
sig.signer.runnerEnvironment == "github-hosted"
count(sig.timestamps) > 0
}
docker_github_builder_bundle(http, filename, repo) if {
sig := artifact_attestation(http, filename)
docker_github_builder_signature(sig, repo)
}
+86 -3
View File
@@ -17,9 +17,10 @@ import (
)
const (
funcLoadJSON = "load_json"
funcVerifyGitSignature = "verify_git_signature"
funcPinImage = "pin_image"
funcLoadJSON = "load_json"
funcVerifyGitSignature = "verify_git_signature"
funcPinImage = "pin_image"
funcArtifactAttestation = "artifact_attestation"
)
func (p *Policy) initBuiltinFuncs() {
@@ -78,6 +79,88 @@ func (p *Policy) initBuiltinFuncs() {
})
},
})
artifactAttestation := &rego.Function{
Name: funcArtifactAttestation,
Decl: types.NewFunction(
types.Args(
types.A,
types.S,
),
types.A,
),
Memoize: false,
}
p.funcs = append(p.funcs, fun{
decl: artifactAttestation,
impl: func(s *state) func(*rego.Rego) {
return rego.Function2(artifactAttestation, func(bctx rego.BuiltinContext, a1 *ast.Term, a2 *ast.Term) (*ast.Term, error) {
return p.builtinArtifactAttestationImpl(bctx, a1, a2, s)
})
},
})
}
func (p *Policy) builtinArtifactAttestationImpl(bctx rego.BuiltinContext, a1, a2 *ast.Term, s *state) (*ast.Term, error) {
inp := s.Input
if inp.HTTP == nil {
return nil, nil
}
obja, ok := a1.Value.(ast.Object)
if !ok {
return nil, errors.Errorf("%s: expected object, got %T", funcArtifactAttestation, a1.Value)
}
httpValue, err := ast.InterfaceToValue(inp.HTTP)
if err != nil {
return nil, errors.Wrapf(err, "%s: failed converting object to interface", funcArtifactAttestation)
}
if obja.Compare(httpValue) != 0 {
return nil, errors.Errorf("%s: first argument is not the same as input http", funcArtifactAttestation)
}
path, ok := a2.Value.(ast.String)
if !ok {
return nil, errors.Errorf("%s: expected string path, got %T", funcArtifactAttestation, a2.Value)
}
if inp.HTTP.Checksum == "" {
s.addUnknown(funcArtifactAttestation)
return nil, nil
}
dgst, err := digest.Parse(inp.HTTP.Checksum)
if err != nil {
return nil, errors.Wrapf(err, "%s: invalid checksum", funcArtifactAttestation)
}
bundleBytes, err := p.readFile(string(path), 8*1024*1024)
if err != nil {
return nil, err
}
if p.opt.VerifierProvider == nil {
return nil, errors.Errorf("%s: policy verifier is not configured", funcArtifactAttestation)
}
v, err := p.opt.VerifierProvider()
if err != nil {
return nil, errors.Wrapf(err, "%s: getting policy verifier", funcArtifactAttestation)
}
siRaw, err := v.VerifyArtifact(bctx.Context, dgst, bundleBytes)
if err != nil {
return nil, nil
}
si := toAttestationSignature(siRaw)
astVal, err := ast.InterfaceToValue(si)
if err != nil {
return nil, errors.Wrapf(err, "%s: failed converting verification result", funcArtifactAttestation)
}
return ast.NewTerm(astVal), nil
}
func (p *Policy) builtinPinImageImpl(_ rego.BuiltinContext, a1, a2 *ast.Term, s *state) (*ast.Term, error) {
+129
View File
@@ -0,0 +1,129 @@
package policy
import (
"context"
"errors"
"io/fs"
"testing"
"testing/fstest"
"time"
policyverifier "github.com/moby/policy-helpers"
policytypes "github.com/moby/policy-helpers/types"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/rego"
"github.com/opencontainers/go-digest"
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
"github.com/stretchr/testify/require"
)
func TestBuiltinArtifactAttestationImpl(t *testing.T) {
t.Run("success", func(t *testing.T) {
const bundlePath = "bundle.sigstore"
bundleBytes := []byte("bundle-bytes")
dgst := digest.FromString("artifact-bytes")
st := &state{Input: Input{HTTP: &HTTP{Checksum: dgst.String()}}}
expectedTS := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)
expectedRaw := &policytypes.SignatureInfo{
Kind: policytypes.KindDockerGithubBuilder,
SignatureType: policytypes.SignatureBundleV03,
Signer: &certificate.Summary{
CertificateIssuer: "CN=sigstore-intermediate,O=sigstore.dev",
SubjectAlternativeName: "https://github.com/docker/buildx/.github/workflows/release.yml@refs/tags/v0.31.1",
Extensions: certificate.Extensions{
Issuer: "https://token.actions.githubusercontent.com",
RunnerEnvironment: "github-hosted",
SourceRepositoryURI: "https://github.com/docker/buildx",
SourceRepositoryRef: "refs/tags/v0.31.1",
},
},
Timestamps: []policytypes.TimestampVerificationResult{{
Type: "rekor",
URI: "https://rekor.sigstore.dev",
Timestamp: expectedTS,
}},
}
p := NewPolicy(Opt{
FS: func() (fs.StatFS, func() error, error) {
return fstest.MapFS{
bundlePath: &fstest.MapFile{Data: bundleBytes},
}, func() error { return nil }, nil
},
VerifierProvider: func() (PolicyVerifier, error) {
return &mockPolicyVerifier{
verifyArtifact: func(_ context.Context, gotDigest digest.Digest, gotBundle []byte, _ ...policyverifier.ArtifactVerifyOpt) (*policytypes.SignatureInfo, error) {
require.Equal(t, dgst, gotDigest)
require.Equal(t, bundleBytes, gotBundle)
return expectedRaw, nil
},
}, nil
},
})
httpVal, err := ast.InterfaceToValue(st.Input.HTTP)
require.NoError(t, err)
term, err := p.builtinArtifactAttestationImpl(
rego.BuiltinContext{Context: t.Context()},
ast.NewTerm(httpVal),
ast.StringTerm(bundlePath),
st,
)
require.NoError(t, err)
require.NotNil(t, term)
expectedVal, err := ast.InterfaceToValue(toAttestationSignature(expectedRaw))
require.NoError(t, err)
require.Equal(t, 0, term.Value.Compare(expectedVal))
})
t.Run("verify failure returns undefined", func(t *testing.T) {
const bundlePath = "bundle.sigstore"
st := &state{Input: Input{HTTP: &HTTP{Checksum: digest.FromString("artifact-bytes").String()}}}
p := NewPolicy(Opt{
FS: func() (fs.StatFS, func() error, error) {
return fstest.MapFS{bundlePath: &fstest.MapFile{Data: []byte("bundle")}}, func() error { return nil }, nil
},
VerifierProvider: func() (PolicyVerifier, error) {
return &mockPolicyVerifier{
verifyArtifact: func(context.Context, digest.Digest, []byte, ...policyverifier.ArtifactVerifyOpt) (*policytypes.SignatureInfo, error) {
return nil, errors.New("verification failed")
},
}, nil
},
})
httpVal, err := ast.InterfaceToValue(st.Input.HTTP)
require.NoError(t, err)
term, err := p.builtinArtifactAttestationImpl(
rego.BuiltinContext{Context: t.Context()},
ast.NewTerm(httpVal),
ast.StringTerm(bundlePath),
st,
)
require.NoError(t, err)
require.Nil(t, term)
})
t.Run("missing checksum adds unknown", func(t *testing.T) {
st := &state{Input: Input{HTTP: &HTTP{}}}
p := NewPolicy(Opt{})
httpVal, err := ast.InterfaceToValue(st.Input.HTTP)
require.NoError(t, err)
term, err := p.builtinArtifactAttestationImpl(
rego.BuiltinContext{Context: t.Context()},
ast.NewTerm(httpVal),
ast.StringTerm("bundle.sigstore"),
st,
)
require.NoError(t, err)
require.Nil(t, term)
require.Contains(t, st.Unknowns, funcArtifactAttestation)
})
}
+10 -3
View File
@@ -22,6 +22,7 @@ import (
type PolicyVerifier interface {
VerifyImage(context.Context, policyimage.ReferrersProvider, ocispecs.Descriptor, *ocispecs.Platform) (*policytypes.SignatureInfo, error)
VerifyArtifact(context.Context, digest.Digest, []byte, ...policyverifier.ArtifactVerifyOpt) (*policytypes.SignatureInfo, error)
}
type PolicyVerifierProvider func() (PolicyVerifier, error)
@@ -124,6 +125,14 @@ func parseSignatures(ctx context.Context, getVerifier PolicyVerifierProvider, ac
return nil, errors.Wrapf(err, "verifying image signatures")
}
return []AttestationSignature{toAttestationSignature(siRaw)}, nil
}
func toAttestationSignature(siRaw *policytypes.SignatureInfo) AttestationSignature {
if siRaw == nil {
return AttestationSignature{}
}
si := AttestationSignature{
raw: siRaw,
Timestamps: siRaw.Timestamps,
@@ -133,8 +142,6 @@ func parseSignatures(ctx context.Context, getVerifier PolicyVerifierProvider, ac
SignatureKind: toSignatureKind(siRaw.Kind),
}
// TODO: signature type after upstream update
if siRaw.Signer != nil {
si.Signer = &SignerInfo{
CertificateIssuer: siRaw.Signer.CertificateIssuer,
@@ -157,7 +164,7 @@ func parseSignatures(ctx context.Context, getVerifier PolicyVerifierProvider, ac
}
}
return []AttestationSignature{si}, nil
return si
}
type acProvider struct {
+3 -2
View File
@@ -64,10 +64,11 @@ func TestRuntimeUnknownInputRefs(t *testing.T) {
st := &state{
Unknowns: map[string]struct{}{
funcVerifyGitSignature: {},
funcVerifyGitSignature: {},
funcArtifactAttestation: {},
},
}
require.Equal(t, []string{"git.commit"}, runtimeUnknownInputRefs(st))
require.Equal(t, []string{"git.commit", "http.checksum"}, runtimeUnknownInputRefs(st))
}
func TestMissingInputRefsWithRuntimeUnknowns(t *testing.T) {
+19
View File
@@ -288,6 +288,22 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
if err != nil {
return nil, nil, err
}
rtUnk := runtimeUnknownInputRefs(st)
if len(rtUnk) > 0 {
next := &gwpb.ResolveSourceMetaRequest{
Source: req.Source.Source,
Platform: req.Platform,
}
if err := AddUnknownsWithLogger(p.opt.Log, next, rtUnk); err != nil {
return nil, nil, err
}
if next.Image != nil || next.Git != nil || hasHTTPUnknowns(rtUnk) {
p.log(logrus.InfoLevel, "policy decision for source %s: resolve missing fields %+v", sourceName(req), summarizeUnknownsForLog(rtUnk))
return nil, next, nil
}
}
if len(rs) == 0 {
return nil, nil, errors.Errorf("policy returned zero result")
}
@@ -781,6 +797,9 @@ func runtimeUnknownInputRefs(st *state) []string {
if _, ok := st.Unknowns[funcVerifyGitSignature]; ok {
out = append(out, "git.commit")
}
if _, ok := st.Unknowns[funcArtifactAttestation]; ok {
out = append(out, "http.checksum")
}
return out
}
+14 -1
View File
@@ -5,6 +5,7 @@ import (
"crypto/sha1" //nolint:gosec // used for git object checksums in tests
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"testing"
"time"
@@ -13,6 +14,7 @@ import (
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"
policyverifier "github.com/moby/policy-helpers"
policyimage "github.com/moby/policy-helpers/image"
policytypes "github.com/moby/policy-helpers/types"
"github.com/opencontainers/go-digest"
@@ -896,13 +898,24 @@ func gitObjectSHA1(objType string, raw []byte) string {
}
type mockPolicyVerifier struct {
verifyImage func(context.Context, policyimage.ReferrersProvider, ocispecs.Descriptor, *ocispecs.Platform) (*policytypes.SignatureInfo, error)
verifyImage func(context.Context, policyimage.ReferrersProvider, ocispecs.Descriptor, *ocispecs.Platform) (*policytypes.SignatureInfo, error)
verifyArtifact func(context.Context, digest.Digest, []byte, ...policyverifier.ArtifactVerifyOpt) (*policytypes.SignatureInfo, error)
}
func (m *mockPolicyVerifier) VerifyImage(ctx context.Context, provider policyimage.ReferrersProvider, desc ocispecs.Descriptor, platform *ocispecs.Platform) (*policytypes.SignatureInfo, error) {
if m.verifyImage == nil {
return nil, errors.New("unexpected VerifyImage call")
}
return m.verifyImage(ctx, provider, desc, platform)
}
func (m *mockPolicyVerifier) VerifyArtifact(ctx context.Context, dgst digest.Digest, bundle []byte, opts ...policyverifier.ArtifactVerifyOpt) (*policytypes.SignatureInfo, error) {
if m.verifyArtifact == nil {
return nil, errors.New("unexpected VerifyArtifact call")
}
return m.verifyArtifact(ctx, dgst, bundle, opts...)
}
func newTestAttestationChain(t *testing.T) *gwpb.AttestationChain {
t.Helper()