policy: image signature verification support
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
@@ -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
@@ -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
@@ -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 := ®o.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,
|
||||
|
||||
Reference in New Issue
Block a user