Skip to content

Commit

Permalink
Merge pull request #5573 from tonistiigi/oci-aftifact-attestation
Browse files Browse the repository at this point in the history
add OCI artifact version of attestation manifest
  • Loading branch information
tonistiigi authored Jan 10, 2025
2 parents 5035fb2 + 0251a3d commit fe09265
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 26 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ Keys supported by image output:
* `push-by-digest=true`: push unnamed image
* `registry.insecure=true`: push to insecure HTTP registry
* `oci-mediatypes=true`: use OCI mediatypes in configuration JSON instead of Docker's
* `oci-artifact=false`: use OCI artifact format for attestations
* `unpack=true`: unpack image after creation (for use with containerd)
* `dangling-name-prefix=<value>`: name image with `prefix@<digest>`, used for anonymous images
* `name-canonical=true`: add additional canonical name `name@<digest>`
Expand Down
37 changes: 30 additions & 7 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
testPullWithLayerLimit,
testExportAnnotations,
testExportAnnotationsMediaTypes,
testExportAttestations,
testExportAttestationsOCIArtifact,
testExportAttestationsImageManifest,
testExportedImageLabels,
testAttestationDefaultSubject,
testSourceDateEpochLayerTimestamps,
Expand Down Expand Up @@ -8833,7 +8834,15 @@ func testExportAnnotationsMediaTypes(t *testing.T, sb integration.Sandbox) {
require.Equal(t, ocispecs.MediaTypeImageIndex, imgs2.Index.MediaType)
}

func testExportAttestations(t *testing.T, sb integration.Sandbox) {
func testExportAttestationsOCIArtifact(t *testing.T, sb integration.Sandbox) {
testExportAttestations(t, sb, true)
}

func testExportAttestationsImageManifest(t *testing.T, sb integration.Sandbox) {
testExportAttestations(t, sb, false)
}

func testExportAttestations(t *testing.T, sb integration.Sandbox, ociArtifact bool) {
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
requiresLinux(t)
c, err := New(sb.Context(), sb.Address())
Expand Down Expand Up @@ -8953,8 +8962,9 @@ func testExportAttestations(t *testing.T, sb integration.Sandbox) {
{
Type: ExporterImage,
Attrs: map[string]string{
"name": strings.Join(targets, ","),
"push": "true",
"name": strings.Join(targets, ","),
"push": "true",
"oci-artifact": strconv.FormatBool(ociArtifact),
},
},
},
Expand Down Expand Up @@ -8984,12 +8994,25 @@ func testExportAttestations(t *testing.T, sb integration.Sandbox) {
for i, att := range atts.Images {
require.Equal(t, ocispecs.MediaTypeImageManifest, att.Desc.MediaType)
require.Equal(t, "unknown/unknown", platforms.Format(*att.Desc.Platform))
require.Equal(t, "unknown/unknown", att.Img.OS+"/"+att.Img.Architecture)
require.Equal(t, attestation.DockerAnnotationReferenceTypeDefault, att.Desc.Annotations[attestation.DockerAnnotationReferenceType])
require.Equal(t, bases[i].Desc.Digest.String(), att.Desc.Annotations[attestation.DockerAnnotationReferenceDigest])
require.Equal(t, 2, len(att.Layers))
require.Equal(t, len(att.Layers), len(att.Img.RootFS.DiffIDs))
require.Equal(t, 0, len(att.Img.History))

if ociArtifact {
subject := att.Manifest.Subject
require.NotNil(t, subject)
require.Equal(t, bases[i].Desc, *subject)
require.Equal(t, "application/vnd.docker.attestation.manifest.v1+json", att.Manifest.ArtifactType)
require.Equal(t, ocispecs.DescriptorEmptyJSON, att.Manifest.Config)
} else {
require.Nil(t, att.Manifest.Subject)
require.Empty(t, att.Manifest.ArtifactType)

// image config is not included in the OCI artifact
require.Equal(t, "unknown/unknown", att.Img.OS+"/"+att.Img.Architecture)
require.Equal(t, len(att.Layers), len(att.Img.RootFS.DiffIDs))
require.Equal(t, 0, len(att.Img.History))
}

var attest intoto.Statement
require.NoError(t, json.Unmarshal(att.LayersRaw[0], &attest))
Expand Down
3 changes: 3 additions & 0 deletions exporter/containerimage/exptypes/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ var (
// Value: bool <true|false>
OptKeyOCITypes ImageExporterOptKey = "oci-mediatypes"

// Use OCI artifact format for the attestation manifest.
OptKeyOCIArtifact ImageExporterOptKey = "oci-artifact"

// Force attestation to be attached.
// Value: bool <true|false>
OptKeyForceInlineAttestations ImageExporterOptKey = "attestation-inline"
Expand Down
6 changes: 6 additions & 0 deletions exporter/containerimage/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type ImageCommitOpts struct {
ImageName string
RefCfg cacheconfig.RefConfig
OCITypes bool
OCIArtifact bool
Annotations AnnotationsGroup
Epoch *time.Time

Expand Down Expand Up @@ -49,6 +50,8 @@ func (c *ImageCommitOpts) Load(ctx context.Context, opt map[string]string) (map[
c.ImageName = v
case exptypes.OptKeyOCITypes:
err = parseBoolWithDefault(&c.OCITypes, k, v, true)
case exptypes.OptKeyOCIArtifact:
err = parseBool(&c.OCIArtifact, k, v)
case exptypes.OptKeyForceInlineAttestations:
err = parseBool(&c.ForceInlineAttestations, k, v)
case exptypes.OptKeyPreferNondistLayers:
Expand All @@ -67,6 +70,9 @@ func (c *ImageCommitOpts) Load(ctx context.Context, opt map[string]string) (map[
if c.RefCfg.Compression.Type.OnlySupportOCITypes() {
c.EnableOCITypes(ctx, c.RefCfg.Compression.Type.String())
}
if c.OCIArtifact && !c.OCITypes {
c.EnableOCITypes(ctx, "oci-artifact")
}

c.Annotations = c.Annotations.Merge(as)

Expand Down
47 changes: 28 additions & 19 deletions exporter/containerimage/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import (
"golang.org/x/sync/errgroup"
)

const attestationManifestArtifactType = "application/vnd.docker.attestation.manifest.v1+json"

type WriterOpt struct {
Snapshotter snapshot.Snapshotter
ContentStore content.Store
Expand Down Expand Up @@ -312,7 +314,7 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, session
return nil, err
}

desc, err := ic.commitAttestationsManifest(ctx, opts, desc.Digest.String(), stmts)
desc, err := ic.commitAttestationsManifest(ctx, opts, *desc, stmts, opts.OCIArtifact)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -553,7 +555,7 @@ func (ic *ImageWriter) commitDistributionManifest(ctx context.Context, opts *Ima
}, &configDesc, nil
}

func (ic *ImageWriter) commitAttestationsManifest(ctx context.Context, opts *ImageCommitOpts, target string, statements []intoto.Statement) (*ocispecs.Descriptor, error) {
func (ic *ImageWriter) commitAttestationsManifest(ctx context.Context, opts *ImageCommitOpts, target ocispecs.Descriptor, statements []intoto.Statement, ociArtifact bool) (*ocispecs.Descriptor, error) {
var (
manifestType = ocispecs.MediaTypeImageManifest
configType = ocispecs.MediaTypeImageConfig
Expand Down Expand Up @@ -588,31 +590,38 @@ func (ic *ImageWriter) commitAttestationsManifest(ctx context.Context, opts *Ima
layers[i] = desc
}

config, err := attestationsConfig(layers)
if err != nil {
return nil, err
}
configDigest := digest.FromBytes(config)
configDesc := ocispecs.Descriptor{
Digest: configDigest,
Size: int64(len(config)),
MediaType: configType,
configDesc := ocispecs.DescriptorEmptyJSON
config := configDesc.Data

if !ociArtifact {
var err error
config, err = attestationsConfig(layers)
if err != nil {
return nil, err
}
configDigest := digest.FromBytes(config)
configDesc = ocispecs.Descriptor{
Digest: configDigest,
Size: int64(len(config)),
MediaType: configType,
}
}

mfst := ocispecs.Manifest{
MediaType: manifestType,
Versioned: specs.Versioned{
SchemaVersion: 2,
},
Config: ocispecs.Descriptor{
Digest: configDigest,
Size: int64(len(config)),
MediaType: configType,
},
Config: configDesc,
}

if ociArtifact {
mfst.ArtifactType = attestationManifestArtifactType
mfst.Subject = &target
}

labels := map[string]string{
"containerd.io/gc.ref.content.0": configDigest.String(),
"containerd.io/gc.ref.content.0": configDesc.Digest.String(),
}
for i, desc := range layers {
desc.Annotations = RemoveInternalLayerAnnotations(desc.Annotations, opts.OCITypes)
Expand All @@ -635,7 +644,7 @@ func (ic *ImageWriter) commitAttestationsManifest(ctx context.Context, opts *Ima
if err := content.WriteBlob(ctx, ic.opt.ContentStore, mfstDigest.String(), bytes.NewReader(mfstJSON), mfstDesc, content.WithLabels((labels))); err != nil {
return nil, done(errors.Wrapf(err, "error writing manifest blob %s", mfstDigest))
}
if err := content.WriteBlob(ctx, ic.opt.ContentStore, configDigest.String(), bytes.NewReader(config), configDesc); err != nil {
if err := content.WriteBlob(ctx, ic.opt.ContentStore, configDesc.Digest.String(), bytes.NewReader(config), configDesc); err != nil {
return nil, done(errors.Wrap(err, "error writing config blob"))
}
done(nil)
Expand All @@ -646,7 +655,7 @@ func (ic *ImageWriter) commitAttestationsManifest(ctx context.Context, opts *Ima
MediaType: manifestType,
Annotations: map[string]string{
attestationTypes.DockerAnnotationReferenceType: attestationTypes.DockerAnnotationReferenceTypeDefault,
attestationTypes.DockerAnnotationReferenceDigest: target,
attestationTypes.DockerAnnotationReferenceDigest: string(target.Digest),
},
}, nil
}
Expand Down

0 comments on commit fe09265

Please sign in to comment.