Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -8725,7 +8726,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 @@ -8845,8 +8854,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 @@ -8876,12 +8886,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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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
Loading