@@ -990,3 +990,206 @@ func TestAddImage_OriginalRefAnnotation(t *testing.T) {
990990 }
991991 })
992992}
993+
994+ // newTestRegistry starts an in-process registry and returns its host and the
995+ // remote.Option needed to talk to it over plain HTTP.
996+ func newTestRegistry (t * testing.T ) (string , []remote.Option ) {
997+ t .Helper ()
998+ srv := httptest .NewServer (registry .New ())
999+ t .Cleanup (srv .Close )
1000+ host := strings .TrimPrefix (srv .URL , "http://" )
1001+ return host , []remote.Option {remote .WithTransport (srv .Client ().Transport )}
1002+ }
1003+
1004+ // newTestStore creates a fresh OCI layout store rooted in a temp directory.
1005+ func newTestStore (t * testing.T ) * store.Layout {
1006+ t .Helper ()
1007+ s , err := store .NewLayout (t .TempDir ())
1008+ if err != nil {
1009+ t .Fatalf ("new layout: %v" , err )
1010+ }
1011+ return s
1012+ }
1013+
1014+ // seedImage pushes a random image to host/repo:tag and returns it, so a test
1015+ // can later assert on the exact bytes/digest that were pushed.
1016+ func seedImage (t * testing.T , host , repo , tag string , opts ... remote.Option ) v1.Image {
1017+ t .Helper ()
1018+ img , err := random .Image (1024 , 3 )
1019+ if err != nil {
1020+ t .Fatalf ("random.Image: %v" , err )
1021+ }
1022+ ref , err := gname .NewTag (host + "/" + repo + ":" + tag , gname .Insecure )
1023+ if err != nil {
1024+ t .Fatalf ("new tag: %v" , err )
1025+ }
1026+ if err := remote .Write (ref , img , opts ... ); err != nil {
1027+ t .Fatalf ("remote.Write: %v" , err )
1028+ }
1029+ return img
1030+ }
1031+
1032+ // TestAddImagePinnedDigestIgnoresMovedTag proves the TOCTOU fix: once a caller
1033+ // pins the digest it verified, a tag that moves to different content between
1034+ // verification and the fetch cannot substitute its bytes into the store.
1035+ func TestAddImagePinnedDigestIgnoresMovedTag (t * testing.T ) {
1036+ host , remoteOpts := newTestRegistry (t )
1037+ original := seedImage (t , host , "test/pinned" , "v1" , remoteOpts ... )
1038+ originalDigest , err := original .Digest ()
1039+ if err != nil {
1040+ t .Fatalf ("original digest: %v" , err )
1041+ }
1042+
1043+ // Move the tag to different content, exactly as a mutable tag could be
1044+ // re-pushed between verification and the pull.
1045+ replacement , err := random .Image (1024 , 3 )
1046+ if err != nil {
1047+ t .Fatalf ("random.Image: %v" , err )
1048+ }
1049+ ref , err := gname .ParseReference (host + "/test/pinned:v1" )
1050+ if err != nil {
1051+ t .Fatalf ("parse: %v" , err )
1052+ }
1053+ if err := remote .Write (ref , replacement , remoteOpts ... ); err != nil {
1054+ t .Fatalf ("remote.Write replacement: %v" , err )
1055+ }
1056+
1057+ s := newTestStore (t )
1058+ got , err := s .AddImage (context .Background (), host + "/test/pinned:v1" , "" , true ,
1059+ originalDigest .String (), false , "" , remoteOpts ... )
1060+ if err != nil {
1061+ t .Fatalf ("AddImage: %v" , err )
1062+ }
1063+ if got != originalDigest .String () {
1064+ t .Fatalf ("stored digest = %s, want the pinned %s (the moved tag won)" , got , originalDigest .String ())
1065+ }
1066+ }
1067+
1068+ // TestAddImageEmptyPinResolvesTag confirms the unpinned path is untouched:
1069+ // an empty pinnedDigest still resolves the tag normally.
1070+ func TestAddImageEmptyPinResolvesTag (t * testing.T ) {
1071+ host , remoteOpts := newTestRegistry (t )
1072+ img := seedImage (t , host , "test/unpinned" , "v1" , remoteOpts ... )
1073+ want , err := img .Digest ()
1074+ if err != nil {
1075+ t .Fatalf ("digest: %v" , err )
1076+ }
1077+
1078+ s := newTestStore (t )
1079+ got , err := s .AddImage (context .Background (), host + "/test/unpinned:v1" , "" , true , "" , false , "" , remoteOpts ... )
1080+ if err != nil {
1081+ t .Fatalf ("AddImage: %v" , err )
1082+ }
1083+ if got != want .String () {
1084+ t .Fatalf ("stored digest = %s, want %s" , got , want .String ())
1085+ }
1086+ }
1087+
1088+ // TestAddImage_OriginalRefAnnotation verifies that AddImage captures the original,
1089+ // fully pullable containerd-style reference (registry/repo:tag) under
1090+ // consts.OriginalRefAnnotation, for both single-platform images (writeImage) and
1091+ // multi-platform indices (writeIndex), so that provenance survives even if the
1092+ // ref/containerd-name annotations are later overwritten by a rewrite.
1093+ func TestAddImage_OriginalRefAnnotation (t * testing.T ) {
1094+ srv := httptest .NewServer (registry .New ())
1095+ t .Cleanup (srv .Close )
1096+ host := strings .TrimPrefix (srv .URL , "http://" )
1097+
1098+ remoteOpts := []remote.Option {
1099+ remote .WithTransport (srv .Client ().Transport ),
1100+ }
1101+
1102+ t .Run ("single-platform image" , func (t * testing.T ) {
1103+ tag , err := gname .NewTag (host + "/test/image:v1" , gname .Insecure )
1104+ if err != nil {
1105+ t .Fatalf ("new tag: %v" , err )
1106+ }
1107+ img , err := random .Image (512 , 2 )
1108+ if err != nil {
1109+ t .Fatalf ("random image: %v" , err )
1110+ }
1111+ if err := remote .Write (tag , img , remoteOpts ... ); err != nil {
1112+ t .Fatalf ("push image: %v" , err )
1113+ }
1114+
1115+ s , err := store .NewLayout (t .TempDir ())
1116+ if err != nil {
1117+ t .Fatalf ("new layout: %v" , err )
1118+ }
1119+ if _ , err := s .AddImage (context .Background (), tag .Name (), "" , false , remoteOpts ... ); err != nil {
1120+ t .Fatalf ("AddImage: %v" , err )
1121+ }
1122+
1123+ wantOriginalRef := tag .Name ()
1124+ found := false
1125+ if err := s .Walk (func (_ string , desc ocispec.Descriptor ) error {
1126+ if desc .Annotations [consts .OriginalRefAnnotation ] == wantOriginalRef {
1127+ found = true
1128+ }
1129+ return nil
1130+ }); err != nil {
1131+ t .Fatalf ("Walk: %v" , err )
1132+ }
1133+ if ! found {
1134+ t .Errorf ("expected an artifact with OriginalRefAnnotation=%q, none found" , wantOriginalRef )
1135+ }
1136+ })
1137+
1138+ t .Run ("multi-platform index" , func (t * testing.T ) {
1139+ amd64Img , err := random .Image (512 , 2 )
1140+ if err != nil {
1141+ t .Fatalf ("random image amd64: %v" , err )
1142+ }
1143+ arm64Img , err := random .Image (512 , 2 )
1144+ if err != nil {
1145+ t .Fatalf ("random image arm64: %v" , err )
1146+ }
1147+ idx := mutate .AppendManifests (
1148+ empty .Index ,
1149+ mutate.IndexAddendum {
1150+ Add : amd64Img ,
1151+ Descriptor : v1.Descriptor {
1152+ MediaType : types .OCIManifestSchema1 ,
1153+ Platform : & v1.Platform {OS : "linux" , Architecture : "amd64" },
1154+ },
1155+ },
1156+ mutate.IndexAddendum {
1157+ Add : arm64Img ,
1158+ Descriptor : v1.Descriptor {
1159+ MediaType : types .OCIManifestSchema1 ,
1160+ Platform : & v1.Platform {OS : "linux" , Architecture : "arm64" },
1161+ },
1162+ },
1163+ )
1164+ tag , err := gname .NewTag (host + "/test/multiarch:v1" , gname .Insecure )
1165+ if err != nil {
1166+ t .Fatalf ("new tag: %v" , err )
1167+ }
1168+ if err := remote .WriteIndex (tag , idx , remoteOpts ... ); err != nil {
1169+ t .Fatalf ("push index: %v" , err )
1170+ }
1171+
1172+ s , err := store .NewLayout (t .TempDir ())
1173+ if err != nil {
1174+ t .Fatalf ("new layout: %v" , err )
1175+ }
1176+ if _ , err := s .AddImage (context .Background (), tag .Name (), "" , false , remoteOpts ... ); err != nil {
1177+ t .Fatalf ("AddImage: %v" , err )
1178+ }
1179+
1180+ wantOriginalRef := tag .Name ()
1181+ found := false
1182+ if err := s .Walk (func (_ string , desc ocispec.Descriptor ) error {
1183+ if desc .Annotations [consts .KindAnnotationName ] == consts .KindAnnotationIndex &&
1184+ desc .Annotations [consts .OriginalRefAnnotation ] == wantOriginalRef {
1185+ found = true
1186+ }
1187+ return nil
1188+ }); err != nil {
1189+ t .Fatalf ("Walk: %v" , err )
1190+ }
1191+ if ! found {
1192+ t .Errorf ("expected the index artifact to have OriginalRefAnnotation=%q, none found" , wantOriginalRef )
1193+ }
1194+ })
1195+ }
0 commit comments