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:
@@ -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
@@ -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 := ®o.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) {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user