policy: image signature verification support

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2026-01-14 09:03:42 -08:00
parent b2697ae933
commit 87d4189039
913 changed files with 164234 additions and 13050 deletions
+177
View File
@@ -0,0 +1,177 @@
package policy
import (
"bytes"
"context"
"encoding/json"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/remotes"
cerrderfs "github.com/containerd/errdefs"
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
policyimage "github.com/moby/policy-helpers/image"
"github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
func (p *Policy) parseSignatures(ctx context.Context, ac *gwpb.AttestationChain, platform *ocispecs.Platform) ([]AttestationSignature, error) {
if ac.Root == "" || ac.AttestationManifest == "" || len(ac.SignatureManifests) == 0 {
return nil, nil
}
root, err := digest.Parse(ac.Root)
if err != nil {
return nil, err
}
att, err := digest.Parse(ac.AttestationManifest)
if err != nil {
return nil, err
}
sigs := make([]digest.Digest, 0, len(ac.SignatureManifests))
for _, sm := range ac.SignatureManifests {
d, err := digest.Parse(sm)
if err != nil {
return nil, err
}
sigs = append(sigs, d)
}
acp := &acProvider{
blobs: ac.Blobs,
signatures: sigs,
att: att,
}
rootBlob, ok := ac.Blobs[root.String()]
if !ok {
return nil, errors.Errorf("root blob %s not found", root)
}
desc := toOCIDescriptor(rootBlob.Descriptor_)
sc, err := policyimage.ResolveSignatureChain(ctx, acp, desc, platform)
if err != nil {
return nil, errors.Wrapf(err, "resolving signature chain for image %s", desc.Digest)
}
if sc.AttestationManifest == nil || sc.SignatureManifest == nil {
return nil, nil
}
if sc.AttestationManifest.Digest != att {
return nil, errors.Errorf("attestation manifest digest mismatch: expected %s, got %s", att, sc.AttestationManifest.Digest)
}
v, err := p.getVerifier()
if err != nil {
return nil, errors.Wrapf(err, "getting policy verifier")
}
siRaw, err := v.VerifyImage(ctx, acp, desc, platform)
if err != nil {
return nil, errors.Wrapf(err, "verifying image signatures")
}
si := AttestationSignature{
raw: siRaw,
Timestamps: siRaw.Timestamps,
IsDHI: siRaw.IsDHI,
DockerReference: siRaw.DockerReference,
}
// TODO: signature type after upstream update
if siRaw.Signer != nil {
si.Signer = &SignerInfo{
CertificateIssuer: siRaw.Signer.CertificateIssuer,
SubjectAlternativeName: siRaw.Signer.SubjectAlternativeName,
Issuer: siRaw.Signer.Issuer,
BuildSignerURI: siRaw.Signer.BuildSignerURI,
BuildSignerDigest: siRaw.Signer.BuildSignerDigest,
RunnerEnvironment: siRaw.Signer.RunnerEnvironment,
SourceRepositoryURI: siRaw.Signer.SourceRepositoryURI,
SourceRepositoryDigest: siRaw.Signer.SourceRepositoryDigest,
SourceRepositoryRef: siRaw.Signer.SourceRepositoryRef,
SourceRepositoryIdentifier: siRaw.Signer.SourceRepositoryIdentifier,
SourceRepositoryOwnerURI: siRaw.Signer.SourceRepositoryOwnerURI,
SourceRepositoryOwnerIdentifier: siRaw.Signer.SourceRepositoryOwnerIdentifier,
BuildConfigURI: siRaw.Signer.BuildConfigURI,
BuildConfigDigest: siRaw.Signer.BuildConfigDigest,
BuildTrigger: siRaw.Signer.BuildTrigger,
RunInvocationURI: siRaw.Signer.RunInvocationURI,
SourceRepositoryVisibilityAtSigning: siRaw.Signer.SourceRepositoryVisibilityAtSigning,
}
}
return []AttestationSignature{si}, nil
}
type acProvider struct {
blobs map[string]*gwpb.Blob
signatures []digest.Digest
att digest.Digest
}
var _ policyimage.ReferrersProvider = &acProvider{}
func (p *acProvider) FetchReferrers(ctx context.Context, dgst digest.Digest, opts ...remotes.FetchReferrersOpt) ([]ocispecs.Descriptor, error) {
if dgst != p.att {
return nil, nil
}
out := make([]ocispecs.Descriptor, 0, len(p.signatures))
for _, d := range p.signatures {
b, ok := p.blobs[d.String()]
if !ok {
continue
}
desc := toOCIDescriptor(b.Descriptor_)
var mfst ocispecs.Manifest
if err := json.Unmarshal(b.Data, &mfst); err != nil {
return nil, errors.Wrapf(err, "unmarshal signature manifest %s", d)
}
desc.ArtifactType = mfst.ArtifactType
// on image manifest assume legacy format
if desc.ArtifactType == "" {
desc.ArtifactType = policyimage.ArtifactTypeCosignSignature
}
out = append(out, desc)
}
return out, nil
}
func (p *acProvider) ReaderAt(ctx context.Context, desc ocispecs.Descriptor) (content.ReaderAt, error) {
b, ok := p.blobs[desc.Digest.String()]
if !ok {
return nil, errors.WithStack(cerrderfs.ErrNotFound)
}
return &readerAt{buf: bytes.NewReader(b.Data)}, nil
}
type readerAt struct {
buf *bytes.Reader
}
var _ content.ReaderAt = &readerAt{}
func (r *readerAt) ReadAt(p []byte, off int64) (n int, err error) {
return r.buf.ReadAt(p, off)
}
func (r *readerAt) Size() int64 {
return int64(r.buf.Len())
}
func (r *readerAt) Close() error {
return nil
}
func toOCIDescriptor(d *gwpb.Descriptor) ocispecs.Descriptor {
return ocispecs.Descriptor{
MediaType: d.MediaType,
Digest: digest.Digest(d.Digest),
Size: d.Size,
}
}
+37 -3
View File
@@ -4,6 +4,7 @@ import (
"time"
"github.com/moby/buildkit/util/gitutil/gitobject"
policytypes "github.com/moby/policy-helpers/types"
)
type Input struct {
@@ -126,16 +127,49 @@ type Image struct {
}
type AttestationSignature struct {
Kind string `json:"kind,omitempty"`
Timestamps []TrustedTimestamp `json:"timestamps,omitempty"`
// CertificateSummary
SignatureType SignatureType `json:"signatureType,omitempty"`
Timestamps []policytypes.TimestampVerificationResult `json:"timestamps,omitempty"`
DockerReference string `json:"dockerReference,omitempty"`
IsDHI bool `json:"isDHI,omitempty"`
Signer *SignerInfo `json:"signer,omitempty"`
raw *policytypes.SignatureInfo
}
type TrustedTimestamp struct {
Tlog bool `json:"tlog,omitempty"`
URI string `json:"uri,omitempty"`
Timestamp time.Time `json:"timestamp,omitzero"`
}
type SignerInfo struct {
// certificate.Summary with deprecated fields removed
CertificateIssuer string `json:"certificateIssuer"`
SubjectAlternativeName string `json:"subjectAlternativeName"`
Issuer string `json:"issuer,omitempty"` // OID 1.3.6.1.4.1.57264.1.8 and 1.3.6.1.4.1.57264.1.1 (Deprecated)
BuildSignerURI string `json:"buildSignerURI,omitempty"` //nolint:tagliatelle // 1.3.6.1.4.1.57264.1.9
BuildSignerDigest string `json:"buildSignerDigest,omitempty"` // 1.3.6.1.4.1.57264.1.10
RunnerEnvironment string `json:"runnerEnvironment,omitempty"` // 1.3.6.1.4.1.57264.1.11
SourceRepositoryURI string `json:"sourceRepositoryURI,omitempty"` //nolint:tagliatelle // 1.3.6.1.4.1.57264.1.12
SourceRepositoryDigest string `json:"sourceRepositoryDigest,omitempty"` // 1.3.6.1.4.1.57264.1.13
SourceRepositoryRef string `json:"sourceRepositoryRef,omitempty"` // 1.3.6.1.4.1.57264.1.14
SourceRepositoryIdentifier string `json:"sourceRepositoryIdentifier,omitempty"` // 1.3.6.1.4.1.57264.1.15
SourceRepositoryOwnerURI string `json:"sourceRepositoryOwnerURI,omitempty"` //nolint:tagliatelle // 1.3.6.1.4.1.57264.1.16
SourceRepositoryOwnerIdentifier string `json:"sourceRepositoryOwnerIdentifier,omitempty"` // 1.3.6.1.4.1.57264.1.17
BuildConfigURI string `json:"buildConfigURI,omitempty"` //nolint:tagliatelle // 1.3.6.1.4.1.57264.1.18
BuildConfigDigest string `json:"buildConfigDigest,omitempty"` // 1.3.6.1.4.1.57264.1.19
BuildTrigger string `json:"buildTrigger,omitempty"` // 1.3.6.1.4.1.57264.1.20
RunInvocationURI string `json:"runInvocationURI,omitempty"` //nolint:tagliatelle // 1.3.6.1.4.1.57264.1.21
SourceRepositoryVisibilityAtSigning string `json:"sourceRepositoryVisibilityAtSigning,omitempty"` // 1.3.6.1.4.1.57264.1.22
}
type SignatureType string
const (
SignatureTypeBundle SignatureType = "bundle-v0.3"
SignatureTypeHashedRecord SignatureType = "hashedreckord"
)
type Local struct {
Name string `json:"name,omitempty"`
}
+51 -14
View File
@@ -9,6 +9,7 @@ import (
"net/url"
"os"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
@@ -17,6 +18,7 @@ import (
"github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/buildx/util/confutil"
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/solver/pb"
moby_buildkit_v1_sourcepolicy "github.com/moby/buildkit/sourcepolicy/pb"
@@ -24,6 +26,7 @@ import (
"github.com/moby/buildkit/util/gitutil"
"github.com/moby/buildkit/util/gitutil/gitobject"
"github.com/moby/buildkit/util/gitutil/gitsign"
policyverifier "github.com/moby/policy-helpers"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/rego"
"github.com/open-policy-agent/opa/v1/topdown/print"
@@ -50,6 +53,9 @@ func debugf(format string, v ...any) {
type Policy struct {
opt Opt
funcs []fun
verifierMu sync.Mutex
verifier *policyverifier.Verifier
}
type state struct {
@@ -70,10 +76,11 @@ type fun struct {
}
type Opt struct {
Files []File
Env Env
Log func(string)
FS func() (fs.StatFS, func() error, error)
Files []File
Env Env
Log func(string)
FS func() (fs.StatFS, func() error, error)
Config *confutil.Config
}
var _ policysession.PolicyCallback = (&Policy{}).CheckPolicy
@@ -91,6 +98,31 @@ func NewPolicy(opt Opt) *Policy {
return p
}
func (p *Policy) getVerifier() (*policyverifier.Verifier, error) {
p.verifierMu.Lock()
defer p.verifierMu.Unlock()
if p.verifier != nil {
return p.verifier, nil
}
root := p.opt.Config.Dir()
confDir := filepath.Join(root, "policy")
if err := p.opt.Config.MkdirAll("policy/tuf", 0o755); err != nil {
return nil, errors.Wrapf(err, "failed to create policy verifier config dir")
}
v, err := policyverifier.NewVerifier(policyverifier.Config{
StateDir: confDir,
})
if err != nil {
return nil, errors.Wrapf(err, "failed to create policy verifier")
}
p.verifier = v
return p.verifier, nil
}
func (p *Policy) initBuiltinFuncs() {
builtinLoadJSON := &rego.Function{
Name: "load_json",
@@ -410,15 +442,15 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
if req.Platform.Variant != "" {
platformStr += "/" + req.Platform.Variant
}
p, err := platforms.Parse(platformStr)
pl, err := platforms.Parse(platformStr)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to parse platform")
}
p = platforms.Normalize(p)
inp.Image.Platform = platforms.Format(p)
inp.Image.OS = p.OS
inp.Image.Architecture = p.Architecture
inp.Image.Variant = p.Variant
pl = platforms.Normalize(pl)
inp.Image.Platform = platforms.Format(pl)
inp.Image.OS = pl.OS
inp.Image.Architecture = pl.Architecture
inp.Image.Variant = pl.Variant
configFields := []string{
"checksum", "labels", "user", "volumes", "workingDir", "env",
@@ -429,7 +461,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
unknowns = append(unknowns, "input.image.checksum")
}
unknowns = append(unknowns, withPrefix(configFields, "input.image.")...)
unknowns = append(unknowns, "input.image.hasProvenance")
unknowns = append(unknowns, "input.image.hasProvenance", "input.image.signatures")
} else {
inp.Image.Checksum = req.Source.Image.Digest
if cfg := req.Source.Image.Config; cfg != nil {
@@ -452,11 +484,16 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
if ac := req.Source.Image.AttestationChain; ac != nil {
inp.Image.HasProvenance = ac.AttestationManifest != ""
signatures, err := p.parseSignatures(ctx, ac, &pl)
if err != nil {
debugf("failed to parse image signatures: %v", err)
} else {
inp.Image.Signatures = signatures
}
} else {
unknowns = append(unknowns, "input.image.hasProvenance")
unknowns = append(unknowns, "input.image.hasProvenance", "input.image.signatures")
}
}
unknowns = append(unknowns, "input.image.signatures")
case "local":
inp.Local = &Local{
Name: refstr,
@@ -611,7 +648,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
next.Image = &gwpb.ResolveSourceImageRequest{}
}
next.Image.NoConfig = false
case "image.hasProvenance":
case "image.hasProvenance", "image.signatures":
if next.Image == nil {
next.Image = &gwpb.ResolveSourceImageRequest{
NoConfig: true,