Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add OCI artifact version of attestation manifest #5573

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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"

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