@@ -34,6 +34,7 @@ var imagetoolsTests = []func(t *testing.T, sb integration.Sandbox){
3434 testImagetoolsCreatePlatformFilter ,
3535 testImagetoolsOCILayoutInspect ,
3636 testImagetoolsOCILayoutCreateSourceAndTarget ,
37+ testImagetoolsOCILayoutReferrers ,
3738 testImagetoolsOCILayoutMergeSources ,
3839 testImagetoolsOCILayoutTargetDigest ,
3940 testImagetoolsAppend ,
@@ -450,6 +451,121 @@ func testImagetoolsOCILayoutCreateSourceAndTarget(t *testing.T, sb integration.S
450451 require .Equal (t , sourceDigest , digest .FromBytes (dt ))
451452}
452453
454+ // testImagetoolsOCILayoutReferrers verifies standalone referrers are recorded
455+ // directly in OCI layout index.json with a subject annotation, while reachable
456+ // attestation manifests are not duplicated there.
457+ func testImagetoolsOCILayoutReferrers (t * testing.T , sb integration.Sandbox ) {
458+ if ! isDockerContainerWorker (sb ) {
459+ t .Skip ("only testing with docker-container worker, imagetools only runs on docker-container" )
460+ }
461+
462+ dir := createDockerfileWithArches (t , "amd64" , "arm64" )
463+ registry1 , err := sb .NewRegistry ()
464+ if errors .Is (err , integration .ErrRequirements ) {
465+ t .Skip (err .Error ())
466+ }
467+ require .NoError (t , err )
468+ registry2 , err := sb .NewRegistry ()
469+ require .NoError (t , err )
470+
471+ source := registry1 + "/buildx/imtools-oci-layout-referrers-src:latest"
472+ out , err := buildCmd (sb , withArgs (
473+ "--output" , "type=image,name=" + source + ",push=true,oci-mediatypes=true,oci-artifact=true" ,
474+ "--platform=linux/amd64,linux/arm64" ,
475+ "--provenance=mode=min" ,
476+ dir ,
477+ ))
478+ require .NoError (t , err , string (out ))
479+
480+ cmd := buildxCmd (sb , withArgs ("imagetools" , "inspect" , source , "--raw" ))
481+ dt , err := cmd .CombinedOutput ()
482+ require .NoError (t , err , string (dt ))
483+
484+ var srcIdx ocispecs.Index
485+ err = json .Unmarshal (dt , & srcIdx )
486+ require .NoError (t , err )
487+
488+ var attestations []ocispecs.Descriptor
489+ for _ , mfst := range srcIdx .Manifests {
490+ if mfst .Annotations ["vnd.docker.reference.type" ] == "attestation-manifest" {
491+ attestations = append (attestations , mfst )
492+ }
493+ }
494+ require .Len (t , attestations , 2 )
495+
496+ signatures := make ([]ocispecs.Descriptor , 0 , len (attestations ))
497+ for _ , attestation := range attestations {
498+ signatures = append (signatures , pushFakeSignatureReferrer (t , source , attestation ))
499+ }
500+
501+ layoutPath := filepath .Join (dir , "layout-referrers" )
502+ layoutRef := "oci-layout://" + layoutPath + ":latest"
503+ cmd = buildxCmd (sb , withArgs ("imagetools" , "create" , "-t" , layoutRef , source ))
504+ dt , err = cmd .CombinedOutput ()
505+ require .NoError (t , err , string (dt ))
506+
507+ idxBytes , err := os .ReadFile (filepath .Join (layoutPath , "index.json" ))
508+ require .NoError (t , err )
509+
510+ var layoutIdx ocispecs.Index
511+ err = json .Unmarshal (idxBytes , & layoutIdx )
512+ require .NoError (t , err )
513+
514+ directReferrers := map [digest.Digest ]ocispecs.Descriptor {}
515+ directReferrerCount := 0
516+ for _ , desc := range layoutIdx .Manifests {
517+ if desc .Annotations ["io.containerd.manifest.subject" ] != "" {
518+ directReferrerCount ++
519+ directReferrers [desc .Digest ] = desc
520+ }
521+ }
522+ require .Len (t , directReferrers , directReferrerCount )
523+ require .Len (t , directReferrers , len (signatures ))
524+ for i , sig := range signatures {
525+ desc , ok := directReferrers [sig .Digest ]
526+ require .True (t , ok )
527+ require .Equal (t , attestations [i ].Digest .String (), desc .Annotations ["io.containerd.manifest.subject" ])
528+ }
529+ for _ , attestation := range attestations {
530+ _ , ok := directReferrers [attestation .Digest ]
531+ require .False (t , ok )
532+ }
533+
534+ target := registry2 + "/buildx/imtools-oci-layout-referrers-dst:latest"
535+ cmd = buildxCmd (sb , withArgs ("imagetools" , "create" , "-t" , target , layoutRef ))
536+ dt , err = cmd .CombinedOutput ()
537+ require .NoError (t , err , string (dt ))
538+
539+ cmd = buildxCmd (sb , withArgs ("imagetools" , "inspect" , target , "--raw" ))
540+ dt , err = cmd .CombinedOutput ()
541+ require .NoError (t , err , string (dt ))
542+
543+ var dstIdx ocispecs.Index
544+ err = json .Unmarshal (dt , & dstIdx )
545+ require .NoError (t , err )
546+
547+ copiedAttestations := map [digest.Digest ]struct {}{}
548+ for _ , mfst := range dstIdx .Manifests {
549+ if mfst .Annotations ["vnd.docker.reference.type" ] == "attestation-manifest" {
550+ copiedAttestations [mfst .Digest ] = struct {}{}
551+ }
552+ }
553+ require .Len (t , copiedAttestations , len (attestations ))
554+
555+ for _ , sig := range signatures {
556+ cmd = buildxCmd (sb , withArgs ("imagetools" , "inspect" , target + "@" + sig .Digest .String (), "--raw" ))
557+ dt , err = cmd .CombinedOutput ()
558+ require .NoError (t , err , string (dt ))
559+
560+ var sigManifest ocispecs.Manifest
561+ err = json .Unmarshal (dt , & sigManifest )
562+ require .NoError (t , err )
563+ require .NotNil (t , sigManifest .Subject )
564+ _ , ok := copiedAttestations [sigManifest .Subject .Digest ]
565+ require .True (t , ok )
566+ }
567+ }
568+
453569// testImagetoolsOCILayoutMergeSources verifies create merges registry and local OCI layout sources.
454570func testImagetoolsOCILayoutMergeSources (t * testing.T , sb integration.Sandbox ) {
455571 if ! isDockerContainerWorker (sb ) {
@@ -871,7 +987,6 @@ func testImagetoolsCopyAttestationWithSignature(t *testing.T, sb integration.San
871987 require .NotNil (t , signatureManifest .Subject )
872988 require .Equal (t , attestationDesc .Digest , signatureManifest .Subject .Digest )
873989 require .Equal (t , "dsse-envelope" , signatureManifest .Annotations ["dev.sigstore.bundle.content" ])
874-
875990 }
876991
877992 // Only attestation signatures should be present after the copy. The
0 commit comments