@@ -787,3 +787,206 @@ func TestAddImage_OCI11Referrers(t *testing.T) {
787787 }
788788 t .Logf ("captured %d OCI referrer(s) for %s" , referrerCount , baseTag .Name ())
789789}
790+
791+ // newTestRegistry starts an in-process registry and returns its host and the
792+ // remote.Option needed to talk to it over plain HTTP.
793+ func newTestRegistry (t * testing.T ) (string , []remote.Option ) {
794+ t .Helper ()
795+ srv := httptest .NewServer (registry .New ())
796+ t .Cleanup (srv .Close )
797+ host := strings .TrimPrefix (srv .URL , "http://" )
798+ return host , []remote.Option {remote .WithTransport (srv .Client ().Transport )}
799+ }
800+
801+ // newTestStore creates a fresh OCI layout store rooted in a temp directory.
802+ func newTestStore (t * testing.T ) * store.Layout {
803+ t .Helper ()
804+ s , err := store .NewLayout (t .TempDir ())
805+ if err != nil {
806+ t .Fatalf ("new layout: %v" , err )
807+ }
808+ return s
809+ }
810+
811+ // seedImage pushes a random image to host/repo:tag and returns it, so a test
812+ // can later assert on the exact bytes/digest that were pushed.
813+ func seedImage (t * testing.T , host , repo , tag string , opts ... remote.Option ) v1.Image {
814+ t .Helper ()
815+ img , err := random .Image (1024 , 3 )
816+ if err != nil {
817+ t .Fatalf ("random.Image: %v" , err )
818+ }
819+ ref , err := gname .NewTag (host + "/" + repo + ":" + tag , gname .Insecure )
820+ if err != nil {
821+ t .Fatalf ("new tag: %v" , err )
822+ }
823+ if err := remote .Write (ref , img , opts ... ); err != nil {
824+ t .Fatalf ("remote.Write: %v" , err )
825+ }
826+ return img
827+ }
828+
829+ // TestAddImagePinnedDigestIgnoresMovedTag proves the TOCTOU fix: once a caller
830+ // pins the digest it verified, a tag that moves to different content between
831+ // verification and the fetch cannot substitute its bytes into the store.
832+ func TestAddImagePinnedDigestIgnoresMovedTag (t * testing.T ) {
833+ host , remoteOpts := newTestRegistry (t )
834+ original := seedImage (t , host , "test/pinned" , "v1" , remoteOpts ... )
835+ originalDigest , err := original .Digest ()
836+ if err != nil {
837+ t .Fatalf ("original digest: %v" , err )
838+ }
839+
840+ // Move the tag to different content, exactly as a mutable tag could be
841+ // re-pushed between verification and the pull.
842+ replacement , err := random .Image (1024 , 3 )
843+ if err != nil {
844+ t .Fatalf ("random.Image: %v" , err )
845+ }
846+ ref , err := gname .ParseReference (host + "/test/pinned:v1" )
847+ if err != nil {
848+ t .Fatalf ("parse: %v" , err )
849+ }
850+ if err := remote .Write (ref , replacement , remoteOpts ... ); err != nil {
851+ t .Fatalf ("remote.Write replacement: %v" , err )
852+ }
853+
854+ s := newTestStore (t )
855+ got , err := s .AddImage (context .Background (), host + "/test/pinned:v1" , "" , true ,
856+ originalDigest .String (), false , "" , remoteOpts ... )
857+ if err != nil {
858+ t .Fatalf ("AddImage: %v" , err )
859+ }
860+ if got != originalDigest .String () {
861+ t .Fatalf ("stored digest = %s, want the pinned %s (the moved tag won)" , got , originalDigest .String ())
862+ }
863+ }
864+
865+ // TestAddImageEmptyPinResolvesTag confirms the unpinned path is untouched:
866+ // an empty pinnedDigest still resolves the tag normally.
867+ func TestAddImageEmptyPinResolvesTag (t * testing.T ) {
868+ host , remoteOpts := newTestRegistry (t )
869+ img := seedImage (t , host , "test/unpinned" , "v1" , remoteOpts ... )
870+ want , err := img .Digest ()
871+ if err != nil {
872+ t .Fatalf ("digest: %v" , err )
873+ }
874+
875+ s := newTestStore (t )
876+ got , err := s .AddImage (context .Background (), host + "/test/unpinned:v1" , "" , true , "" , false , "" , remoteOpts ... )
877+ if err != nil {
878+ t .Fatalf ("AddImage: %v" , err )
879+ }
880+ if got != want .String () {
881+ t .Fatalf ("stored digest = %s, want %s" , got , want .String ())
882+ }
883+ }
884+
885+ // TestAddImage_OriginalRefAnnotation verifies that AddImage captures the original,
886+ // fully pullable containerd-style reference (registry/repo:tag) under
887+ // consts.OriginalRefAnnotation, for both single-platform images (writeImage) and
888+ // multi-platform indices (writeIndex), so that provenance survives even if the
889+ // ref/containerd-name annotations are later overwritten by a rewrite.
890+ func TestAddImage_OriginalRefAnnotation (t * testing.T ) {
891+ srv := httptest .NewServer (registry .New ())
892+ t .Cleanup (srv .Close )
893+ host := strings .TrimPrefix (srv .URL , "http://" )
894+
895+ remoteOpts := []remote.Option {
896+ remote .WithTransport (srv .Client ().Transport ),
897+ }
898+
899+ t .Run ("single-platform image" , func (t * testing.T ) {
900+ tag , err := gname .NewTag (host + "/test/image:v1" , gname .Insecure )
901+ if err != nil {
902+ t .Fatalf ("new tag: %v" , err )
903+ }
904+ img , err := random .Image (512 , 2 )
905+ if err != nil {
906+ t .Fatalf ("random image: %v" , err )
907+ }
908+ if err := remote .Write (tag , img , remoteOpts ... ); err != nil {
909+ t .Fatalf ("push image: %v" , err )
910+ }
911+
912+ s , err := store .NewLayout (t .TempDir ())
913+ if err != nil {
914+ t .Fatalf ("new layout: %v" , err )
915+ }
916+ if _ , err := s .AddImage (context .Background (), tag .Name (), "" , false , remoteOpts ... ); err != nil {
917+ t .Fatalf ("AddImage: %v" , err )
918+ }
919+
920+ wantOriginalRef := tag .Name ()
921+ found := false
922+ if err := s .Walk (func (_ string , desc ocispec.Descriptor ) error {
923+ if desc .Annotations [consts .OriginalRefAnnotation ] == wantOriginalRef {
924+ found = true
925+ }
926+ return nil
927+ }); err != nil {
928+ t .Fatalf ("Walk: %v" , err )
929+ }
930+ if ! found {
931+ t .Errorf ("expected an artifact with OriginalRefAnnotation=%q, none found" , wantOriginalRef )
932+ }
933+ })
934+
935+ t .Run ("multi-platform index" , func (t * testing.T ) {
936+ amd64Img , err := random .Image (512 , 2 )
937+ if err != nil {
938+ t .Fatalf ("random image amd64: %v" , err )
939+ }
940+ arm64Img , err := random .Image (512 , 2 )
941+ if err != nil {
942+ t .Fatalf ("random image arm64: %v" , err )
943+ }
944+ idx := mutate .AppendManifests (
945+ empty .Index ,
946+ mutate.IndexAddendum {
947+ Add : amd64Img ,
948+ Descriptor : v1.Descriptor {
949+ MediaType : types .OCIManifestSchema1 ,
950+ Platform : & v1.Platform {OS : "linux" , Architecture : "amd64" },
951+ },
952+ },
953+ mutate.IndexAddendum {
954+ Add : arm64Img ,
955+ Descriptor : v1.Descriptor {
956+ MediaType : types .OCIManifestSchema1 ,
957+ Platform : & v1.Platform {OS : "linux" , Architecture : "arm64" },
958+ },
959+ },
960+ )
961+ tag , err := gname .NewTag (host + "/test/multiarch:v1" , gname .Insecure )
962+ if err != nil {
963+ t .Fatalf ("new tag: %v" , err )
964+ }
965+ if err := remote .WriteIndex (tag , idx , remoteOpts ... ); err != nil {
966+ t .Fatalf ("push index: %v" , err )
967+ }
968+
969+ s , err := store .NewLayout (t .TempDir ())
970+ if err != nil {
971+ t .Fatalf ("new layout: %v" , err )
972+ }
973+ if _ , err := s .AddImage (context .Background (), tag .Name (), "" , false , remoteOpts ... ); err != nil {
974+ t .Fatalf ("AddImage: %v" , err )
975+ }
976+
977+ wantOriginalRef := tag .Name ()
978+ found := false
979+ if err := s .Walk (func (_ string , desc ocispec.Descriptor ) error {
980+ if desc .Annotations [consts .KindAnnotationName ] == consts .KindAnnotationIndex &&
981+ desc .Annotations [consts .OriginalRefAnnotation ] == wantOriginalRef {
982+ found = true
983+ }
984+ return nil
985+ }); err != nil {
986+ t .Fatalf ("Walk: %v" , err )
987+ }
988+ if ! found {
989+ t .Errorf ("expected the index artifact to have OriginalRefAnnotation=%q, none found" , wantOriginalRef )
990+ }
991+ })
992+ }
0 commit comments