Skip to content

Commit fd58680

Browse files
committed
imagetools: support for creating attestations/signatures
Persist attestation manifest and any manifest cosign-based signatures when creating new images. When creating index from single-arch manifests where attestation manifest is not inlined, it can be loaded from referrers API. Note that for this to work the attestation manifest needs to be in artifact type when image was built. Signed-off-by: Tonis Tiigi <[email protected]>
1 parent f4e7893 commit fd58680

File tree

4 files changed

+252
-115
lines changed

4 files changed

+252
-115
lines changed

build/build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
723723
return err
724724
}
725725

726-
dt, desc, _, err := itpull.Combine(ctx, srcs, indexAnnotations, false)
726+
dt, desc, _, err := itpull.Combine(ctx, srcs, indexAnnotations, false, nil)
727727
if err != nil {
728728
return err
729729
}

commands/imagetools/create.go

Lines changed: 6 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"os"
88
"strings"
99

10-
"github.com/containerd/containerd/v2/core/images"
10+
"github.com/containerd/containerd/v2/core/remotes"
1111
"github.com/containerd/platforms"
1212
"github.com/distribution/reference"
1313
"github.com/docker/buildx/builder"
@@ -16,7 +16,6 @@ import (
1616
"github.com/docker/buildx/util/imagetools"
1717
"github.com/docker/buildx/util/progress"
1818
"github.com/docker/cli/cli/command"
19-
"github.com/moby/buildkit/util/attestation"
2019
"github.com/moby/buildkit/util/progress/progressui"
2120
"github.com/opencontainers/go-digest"
2221
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
@@ -169,12 +168,11 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg
169168
return errors.Wrapf(err, "failed to parse annotations")
170169
}
171170

172-
dt, desc, srcMap, err := r.Combine(ctx, srcs, annotations, in.preferIndex)
173-
if err != nil {
174-
return err
175-
}
171+
ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.oci.empty.v1+json", "empty")
172+
ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.dev.cosign.artifact.sig.v1+json", "cosign")
173+
ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.dev.cosign.simplesigning.v1+json", "simplesigning")
176174

177-
dt, desc, manifests, err := filterPlatforms(dt, desc, srcMap, platforms)
175+
dt, desc, manifests, err := r.Combine(ctx, srcs, annotations, in.preferIndex, platforms)
178176
if err != nil {
179177
return err
180178
}
@@ -186,7 +184,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg
186184

187185
// manifests can be nil only if pushing one single-platform desc directly
188186
if manifests == nil {
189-
manifests = []descWithSource{{Descriptor: desc, Source: srcs[0]}}
187+
manifests = []imagetools.DescWithSource{{Descriptor: desc, Source: srcs[0]}}
190188
}
191189

192190
// new resolver cause need new auth
@@ -230,108 +228,6 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg
230228
return err
231229
}
232230

233-
type descWithSource struct {
234-
ocispecs.Descriptor
235-
Source *imagetools.Source
236-
}
237-
238-
func filterPlatforms(dt []byte, desc ocispecs.Descriptor, srcMap map[digest.Digest]*imagetools.Source, plats []ocispecs.Platform) ([]byte, ocispecs.Descriptor, []descWithSource, error) {
239-
if len(plats) == 0 {
240-
return dt, desc, nil, nil
241-
}
242-
243-
matcher := platforms.Any(plats...)
244-
245-
if !images.IsIndexType(desc.MediaType) {
246-
var mfst ocispecs.Manifest
247-
if err := json.Unmarshal(dt, &mfst); err != nil {
248-
return nil, ocispecs.Descriptor{}, nil, errors.Wrapf(err, "failed to parse manifest")
249-
}
250-
if desc.Platform == nil {
251-
return nil, ocispecs.Descriptor{}, nil, errors.Errorf("cannot filter platforms from a manifest without platform information")
252-
}
253-
if !matcher.Match(*desc.Platform) {
254-
return nil, ocispecs.Descriptor{}, nil, errors.Errorf("input platform %s does not match any of the provided platforms", platforms.Format(*desc.Platform))
255-
}
256-
return dt, desc, nil, nil
257-
}
258-
259-
var idx ocispecs.Index
260-
if err := json.Unmarshal(dt, &idx); err != nil {
261-
return nil, ocispecs.Descriptor{}, nil, errors.Wrapf(err, "failed to parse index")
262-
}
263-
264-
var manifestMap = map[digest.Digest]ocispecs.Descriptor{}
265-
for _, m := range idx.Manifests {
266-
manifestMap[m.Digest] = m
267-
}
268-
var references = map[digest.Digest]struct{}{}
269-
for _, m := range idx.Manifests {
270-
if refType, ok := m.Annotations[attestation.DockerAnnotationReferenceType]; ok && refType == attestation.DockerAnnotationReferenceTypeDefault {
271-
dgstStr, ok := m.Annotations[attestation.DockerAnnotationReferenceDigest]
272-
if !ok {
273-
continue
274-
}
275-
dgst, err := digest.Parse(dgstStr)
276-
if err != nil {
277-
continue
278-
}
279-
subject, ok := manifestMap[dgst]
280-
if !ok {
281-
continue
282-
}
283-
if subject.Platform == nil || matcher.Match(*subject.Platform) {
284-
references[m.Digest] = struct{}{}
285-
}
286-
}
287-
}
288-
289-
var mfsts []ocispecs.Descriptor
290-
var mfstsWithSource []descWithSource
291-
292-
for _, m := range idx.Manifests {
293-
_, isRef := references[m.Digest]
294-
if isRef || m.Platform == nil || matcher.Match(*m.Platform) {
295-
src, ok := srcMap[m.Digest]
296-
if !ok {
297-
defaultSource, ok := srcMap[desc.Digest]
298-
if !ok {
299-
return nil, ocispecs.Descriptor{}, nil, errors.Errorf("internal error: no source found for %s", m.Digest)
300-
}
301-
src = defaultSource
302-
}
303-
mfsts = append(mfsts, m)
304-
mfstsWithSource = append(mfstsWithSource, descWithSource{
305-
Descriptor: m,
306-
Source: src,
307-
})
308-
}
309-
}
310-
if len(mfsts) == len(idx.Manifests) {
311-
// all platforms matched, no need to rewrite index
312-
return dt, desc, mfstsWithSource, nil
313-
}
314-
315-
if len(mfsts) == 0 {
316-
return nil, ocispecs.Descriptor{}, nil, errors.Errorf("none of the manifests match the provided platforms")
317-
}
318-
319-
idx.Manifests = mfsts
320-
idxBytes, err := json.MarshalIndent(&idx, "", " ")
321-
if err != nil {
322-
return nil, ocispecs.Descriptor{}, nil, errors.Wrap(err, "failed to marshal index")
323-
}
324-
325-
desc = ocispecs.Descriptor{
326-
MediaType: desc.MediaType,
327-
Size: int64(len(idxBytes)),
328-
Digest: digest.FromBytes(idxBytes),
329-
Annotations: desc.Annotations,
330-
}
331-
332-
return idxBytes, desc, mfstsWithSource, nil
333-
}
334-
335231
func parseSources(in []string) ([]*imagetools.Source, error) {
336232
out := make([]*imagetools.Source, len(in))
337233
for i, in := range in {

util/imagetools/create.go

Lines changed: 199 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/containerd/platforms"
1616
"github.com/distribution/reference"
1717
"github.com/moby/buildkit/exporter/containerimage/exptypes"
18+
"github.com/moby/buildkit/util/attestation"
1819
"github.com/moby/buildkit/util/contentutil"
1920
"github.com/opencontainers/go-digest"
2021
"github.com/opencontainers/image-spec/specs-go"
@@ -23,12 +24,34 @@ import (
2324
"golang.org/x/sync/errgroup"
2425
)
2526

27+
const (
28+
artifactTypeAttestationManifest = "application/vnd.docker.attestation.manifest.v1+json"
29+
artifactTypeCosignSignature = "application/vnd.dev.cosign.artifact.sig.v1+json"
30+
)
31+
32+
var supportedArtifactTypes = map[string]struct{}{
33+
artifactTypeAttestationManifest: {},
34+
artifactTypeCosignSignature: {},
35+
}
36+
2637
type Source struct {
2738
Desc ocispecs.Descriptor
2839
Ref reference.Named
2940
}
3041

31-
func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann map[exptypes.AnnotationKey]string, preferIndex bool) ([]byte, ocispecs.Descriptor, map[digest.Digest]*Source, error) {
42+
func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann map[exptypes.AnnotationKey]string, preferIndex bool, platforms []ocispecs.Platform) ([]byte, ocispecs.Descriptor, []DescWithSource, error) {
43+
dt, desc, srcMap, err := r.combine(ctx, srcs, ann, preferIndex)
44+
if err != nil {
45+
return nil, ocispecs.Descriptor{}, nil, err
46+
}
47+
dt, desc, mfstsWithSource, err := r.filterPlatforms(ctx, dt, desc, srcMap, platforms)
48+
if err != nil {
49+
return nil, ocispecs.Descriptor{}, nil, err
50+
}
51+
return dt, desc, mfstsWithSource, nil
52+
}
53+
54+
func (r *Resolver) combine(ctx context.Context, srcs []*Source, ann map[exptypes.AnnotationKey]string, preferIndex bool) ([]byte, ocispecs.Descriptor, map[digest.Digest]*Source, error) {
3255
eg, ctx := errgroup.WithContext(ctx)
3356

3457
dts := make([][]byte, len(srcs))
@@ -250,7 +273,28 @@ func (r *Resolver) Copy(ctx context.Context, src *Source, dest reference.Named)
250273
source, repo := u.Hostname(), strings.TrimPrefix(u.Path, "/")
251274
desc.Annotations["containerd.io/distribution.source."+source] = repo
252275

253-
err = contentutil.CopyChain(ctx, contentutil.FromPusher(p), contentutil.FromFetcher(f), desc)
276+
referrersFetcher, ok := f.(remotes.ReferrersFetcher)
277+
if !ok {
278+
return errors.Errorf("fetcher for %s does not support referrers", src.Ref.String())
279+
}
280+
281+
opts := []contentutil.CopyOption{
282+
contentutil.WithReferrers(func(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
283+
descs, err := referrersFetcher.FetchReferrers(ctx, desc.Digest, "")
284+
if err != nil {
285+
return nil, err
286+
}
287+
var filtered []ocispecs.Descriptor
288+
for _, d := range descs {
289+
if _, ok := supportedArtifactTypes[d.ArtifactType]; ok {
290+
filtered = append(filtered, d)
291+
}
292+
}
293+
return filtered, nil
294+
}),
295+
}
296+
297+
err = contentutil.CopyChain(ctx, contentutil.FromPusher(p), contentutil.FromFetcher(f), desc, opts...)
254298
if err != nil {
255299
return err
256300
}
@@ -288,6 +332,159 @@ func (r *Resolver) loadPlatform(ctx context.Context, p2 *ocispecs.Platform, in s
288332
return nil
289333
}
290334

335+
type DescWithSource struct {
336+
ocispecs.Descriptor
337+
Source *Source
338+
}
339+
340+
func (r *Resolver) filterPlatforms(ctx context.Context, dt []byte, desc ocispecs.Descriptor, srcMap map[digest.Digest]*Source, plats []ocispecs.Platform) ([]byte, ocispecs.Descriptor, []DescWithSource, error) {
341+
matcher := platforms.Any(plats...)
342+
if len(plats) == 0 {
343+
matcher = platforms.All
344+
}
345+
346+
if !images.IsIndexType(desc.MediaType) {
347+
var mfst ocispecs.Manifest
348+
if err := json.Unmarshal(dt, &mfst); err != nil {
349+
return nil, ocispecs.Descriptor{}, nil, errors.Wrapf(err, "failed to parse manifest")
350+
}
351+
if desc.Platform == nil {
352+
return nil, ocispecs.Descriptor{}, nil, errors.Errorf("cannot filter platforms from a manifest without platform information")
353+
}
354+
if !matcher.Match(*desc.Platform) {
355+
return nil, ocispecs.Descriptor{}, nil, errors.Errorf("input platform %s does not match any of the provided platforms", platforms.Format(*desc.Platform))
356+
}
357+
return dt, desc, nil, nil
358+
}
359+
360+
var idx ocispecs.Index
361+
if err := json.Unmarshal(dt, &idx); err != nil {
362+
return nil, ocispecs.Descriptor{}, nil, errors.Wrapf(err, "failed to parse index")
363+
}
364+
365+
var manifestMap = map[digest.Digest]ocispecs.Descriptor{}
366+
for _, m := range idx.Manifests {
367+
manifestMap[m.Digest] = m
368+
}
369+
var references = map[digest.Digest]ocispecs.Descriptor{}
370+
var matchedManifests = map[digest.Digest]struct{}{}
371+
for _, m := range idx.Manifests {
372+
if m.Platform == nil || matcher.Match(*m.Platform) {
373+
matchedManifests[m.Digest] = struct{}{}
374+
}
375+
if refType, ok := m.Annotations[attestation.DockerAnnotationReferenceType]; ok && refType == attestation.DockerAnnotationReferenceTypeDefault {
376+
dgstStr, ok := m.Annotations[attestation.DockerAnnotationReferenceDigest]
377+
if !ok {
378+
continue
379+
}
380+
dgst, err := digest.Parse(dgstStr)
381+
if err != nil {
382+
continue
383+
}
384+
subject, ok := manifestMap[dgst]
385+
if !ok {
386+
continue
387+
}
388+
if subject.Platform == nil || matcher.Match(*subject.Platform) {
389+
references[m.Digest] = subject
390+
}
391+
}
392+
}
393+
394+
var mfsts []ocispecs.Descriptor
395+
var mfstsWithSource []DescWithSource
396+
397+
for _, m := range idx.Manifests {
398+
_, isRef := references[m.Digest]
399+
if isRef || m.Platform == nil || matcher.Match(*m.Platform) {
400+
src, ok := srcMap[m.Digest]
401+
if !ok {
402+
defaultSource, ok := srcMap[desc.Digest]
403+
if !ok {
404+
return nil, ocispecs.Descriptor{}, nil, errors.Errorf("internal error: no source found for %s", m.Digest)
405+
}
406+
src = defaultSource
407+
}
408+
mfsts = append(mfsts, m)
409+
mfstsWithSource = append(mfstsWithSource, DescWithSource{
410+
Descriptor: m,
411+
Source: src,
412+
})
413+
}
414+
}
415+
416+
if len(mfsts) == 0 {
417+
return nil, ocispecs.Descriptor{}, nil, errors.Errorf("none of the manifests match the provided platforms")
418+
}
419+
420+
// try to pull in attestation manifest via referrer if one exists
421+
addedRef := false
422+
for d := range matchedManifests {
423+
hasRef := false
424+
for _, subject := range references {
425+
if subject.Digest == d {
426+
hasRef = true
427+
break
428+
}
429+
}
430+
if hasRef {
431+
continue
432+
}
433+
f, err := r.resolver().Fetcher(ctx, srcMap[d].Ref.String())
434+
if err != nil {
435+
return nil, ocispecs.Descriptor{}, nil, err
436+
}
437+
rf, ok := f.(remotes.ReferrersFetcher)
438+
if !ok {
439+
return nil, ocispecs.Descriptor{}, nil, errors.Errorf("fetcher for %s does not support referrers", srcMap[d].Ref.String())
440+
}
441+
refs, err := rf.FetchReferrers(ctx, d, artifactTypeAttestationManifest)
442+
if err != nil {
443+
if errors.Is(err, errdefs.ErrNotFound) {
444+
continue
445+
}
446+
return nil, ocispecs.Descriptor{}, nil, err
447+
}
448+
for _, ref := range refs {
449+
if _, ok := references[ref.Digest]; ok {
450+
continue
451+
}
452+
ref.Platform = &ocispecs.Platform{
453+
OS: "unknown", Architecture: "unknown",
454+
}
455+
if ref.Annotations == nil {
456+
ref.Annotations = map[string]string{}
457+
}
458+
ref.Annotations[attestation.DockerAnnotationReferenceType] = attestation.DockerAnnotationReferenceTypeDefault
459+
ref.Annotations[attestation.DockerAnnotationReferenceDigest] = d.String()
460+
ref.ArtifactType = ""
461+
mfsts = append(mfsts, ref)
462+
addedRef = true
463+
break
464+
}
465+
}
466+
467+
if len(mfsts) == len(idx.Manifests) && !addedRef {
468+
// all platforms matched, no need to rewrite index
469+
return dt, desc, mfstsWithSource, nil
470+
}
471+
472+
idx.Manifests = mfsts
473+
idxBytes, err := json.MarshalIndent(&idx, "", " ")
474+
if err != nil {
475+
return nil, ocispecs.Descriptor{}, nil, errors.Wrap(err, "failed to marshal index")
476+
}
477+
478+
desc = ocispecs.Descriptor{
479+
MediaType: desc.MediaType,
480+
Size: int64(len(idxBytes)),
481+
Digest: digest.FromBytes(idxBytes),
482+
Annotations: desc.Annotations,
483+
}
484+
485+
return idxBytes, desc, mfstsWithSource, nil
486+
}
487+
291488
func detectMediaType(dt []byte) (string, error) {
292489
var mfst struct {
293490
MediaType string `json:"mediaType"`

0 commit comments

Comments
 (0)