1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
|
package attestation
import (
"context"
"encoding/json"
"os"
"path"
"strings"
"github.com/containerd/continuity/fs"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/moby/buildkit/exporter"
gatewaypb "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/solver/result"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
// Unbundle iterates over all provided result attestations and un-bundles any
// bundled attestations by loading them from the provided refs map.
func Unbundle(ctx context.Context, s session.Group, bundled []exporter.Attestation) ([]exporter.Attestation, error) {
if err := Validate(bundled); err != nil {
return nil, err
}
eg, ctx := errgroup.WithContext(ctx)
unbundled := make([][]exporter.Attestation, len(bundled))
for i, att := range bundled {
i, att := i, att
eg.Go(func() error {
switch att.Kind {
case gatewaypb.AttestationKindInToto:
if strings.HasPrefix(att.InToto.PredicateType, "https://slsa.dev/provenance/") {
if att.ContentFunc == nil {
// provenance may only be set buildkit-side using ContentFunc
return errors.New("frontend may not set provenance attestations")
}
}
unbundled[i] = append(unbundled[i], att)
case gatewaypb.AttestationKindBundle:
if att.ContentFunc != nil {
return errors.New("attestation bundle cannot have callback")
}
if att.Ref == nil {
return errors.Errorf("no ref provided for attestation bundle")
}
mount, err := att.Ref.Mount(ctx, true, s)
if err != nil {
return err
}
lm := snapshot.LocalMounter(mount)
src, err := lm.Mount()
if err != nil {
return err
}
defer lm.Unmount()
atts, err := unbundle(ctx, src, att)
if err != nil {
return err
}
for _, att := range atts {
if strings.HasPrefix(att.InToto.PredicateType, "https://slsa.dev/provenance/") {
return errors.New("frontend may not bundle provenance attestations")
}
}
unbundled[i] = append(unbundled[i], atts...)
}
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, err
}
var joined []exporter.Attestation
for _, atts := range unbundled {
joined = append(joined, atts...)
}
joined = sort(joined)
if err := Validate(joined); err != nil {
return nil, err
}
return joined, nil
}
func sort(atts []exporter.Attestation) []exporter.Attestation {
isCore := make([]bool, len(atts))
for i, att := range atts {
name, ok := att.Metadata[result.AttestationSBOMCore]
if !ok {
continue
}
if n, _, _ := strings.Cut(att.Path, "."); n != string(name) {
continue
}
isCore[i] = true
}
result := make([]exporter.Attestation, 0, len(atts))
for i, att := range atts {
if isCore[i] {
result = append(result, att)
}
}
for i, att := range atts {
if !isCore[i] {
result = append(result, att)
}
}
return result
}
func unbundle(ctx context.Context, root string, bundle exporter.Attestation) ([]exporter.Attestation, error) {
dir, err := fs.RootPath(root, bundle.Path)
if err != nil {
return nil, err
}
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var unbundled []exporter.Attestation
for _, entry := range entries {
p, err := fs.RootPath(dir, entry.Name())
if err != nil {
return nil, err
}
f, err := os.Open(p)
if err != nil {
return nil, err
}
dec := json.NewDecoder(f)
var stmt intoto.Statement
if err := dec.Decode(&stmt); err != nil {
return nil, errors.Wrap(err, "cannot decode in-toto statement")
}
if bundle.InToto.PredicateType != "" && stmt.PredicateType != bundle.InToto.PredicateType {
return nil, errors.Errorf("bundle entry %s does not match required predicate type %s", stmt.PredicateType, bundle.InToto.PredicateType)
}
predicate, err := json.Marshal(stmt.Predicate)
if err != nil {
return nil, err
}
subjects := make([]result.InTotoSubject, len(stmt.Subject))
for i, subject := range stmt.Subject {
subjects[i] = result.InTotoSubject{
Kind: gatewaypb.InTotoSubjectKindRaw,
Name: subject.Name,
Digest: result.FromDigestMap(subject.Digest),
}
}
unbundled = append(unbundled, exporter.Attestation{
Kind: gatewaypb.AttestationKindInToto,
Metadata: bundle.Metadata,
Path: path.Join(bundle.Path, entry.Name()),
ContentFunc: func() ([]byte, error) { return predicate, nil },
InToto: result.InTotoAttestation{
PredicateType: stmt.PredicateType,
Subjects: subjects,
},
})
}
return unbundled, nil
}
func Validate(atts []exporter.Attestation) error {
for _, att := range atts {
if err := validate(att); err != nil {
return err
}
}
return nil
}
func validate(att exporter.Attestation) error {
if att.Kind != gatewaypb.AttestationKindBundle && att.Path == "" {
return errors.New("attestation does not have set path")
}
if att.Ref == nil && att.ContentFunc == nil {
return errors.New("attestation does not have available content")
}
return nil
}
|