@@ -18,6 +18,10 @@ final class ReleaseImageContinuationTest extends TestCase
1818
1919 private const DIGEST = 'sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc ' ;
2020
21+ private const AMD64 = 'sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee ' ;
22+
23+ private const ARM64 = 'sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff ' ;
24+
2125 protected function setUp (): void
2226 {
2327 $ this ->directory = sys_get_temp_dir ().'/release-continuation- ' .bin2hex (random_bytes (8 ));
@@ -36,6 +40,12 @@ protected function setUp(): void
3640 echo '500 Internal Server Error' >&2
3741 exit 1
3842 fi
43+ store="$CALL_LOG-store-${3##*:}"
44+ if [ -f "$store" ] && [ "$(cat "$store")" != "$DOCKER_DEFAULT_PLATFORM" ]; then
45+ echo "cannot overwrite digest ${3##*@}" >&2
46+ exit 1
47+ fi
48+ printf '%s\n' "$DOCKER_DEFAULT_PLATFORM" > "$store"
3949 exit 0
4050fi
4151if [ "$1" = run ]; then
@@ -50,6 +60,11 @@ protected function setUp(): void
5060test "$2" = imagetools
5161if [ "$3" = create ]; then exit 0; fi
5262test "$3" = inspect
63+ if [ "$4" = --raw ]; then
64+ test "$5" = "durableworkflow/server@$INDEX_DIGEST"
65+ printf '%s\n' "$INDEX_JSON"
66+ exit 0
67+ fi
5368if [ "$4" = --format ]; then
5469 if [ "$STATE" = wrong_arm_config ]; then
5570 case "$5" in *linux/arm64*) printf '%s\n' "$BAD_METADATA"; exit 0 ;; esac
@@ -245,17 +260,7 @@ public static function trustPullOutcomes(): array
245260 #[DataProvider('trustPullOutcomes ' )]
246261 public function test_only_pulls_are_retried_and_trust_uses_the_cached_digest (string $ state , int $ status , int $ pulls , int $ runs ): void
247262 {
248- $ workflow = Yaml ::parse (file_get_contents (dirname (__DIR__ , 2 ).'/.github/workflows/release.yml ' ));
249- $ steps = array_column ($ workflow ['jobs ' ]['publish ' ]['steps ' ], null , 'name ' );
250- $ step = $ steps ['Verify published image trust stores ' ];
251- $ script = $ this ->directory .'/trust.sh ' ;
252- file_put_contents ($ script , $ step ['run ' ]);
253- $ result = $ this ->runProcess (['bash ' , $ script ], [
254- 'STATE ' => $ state , 'RELEASE_IMAGE_PLATFORMS ' => 'linux/amd64,linux/arm64 ' ,
255- 'RELEASE_IMAGE ' => 'durableworkflow/server@ ' .self ::DIGEST ,
256- 'DW_PULL_ATTEMPTS ' => $ step ['env ' ]['DW_PULL_ATTEMPTS ' ] ?? '3 ' ,
257- 'DW_PULL_BACKOFF_SECONDS ' => '0 ' ,
258- ]);
263+ $ result = $ this ->runTrustStep (['STATE ' => $ state ]);
259264 self ::assertSame ($ status , $ result ['status ' ], $ result ['text ' ]);
260265 self ::assertSame ($ pulls , substr_count ($ result ['calls ' ], 'pull --quiet ' ));
261266 self ::assertSame ($ runs , substr_count ($ result ['calls ' ], 'run --rm ' ));
@@ -269,6 +274,79 @@ public function test_only_pulls_are_retried_and_trust_uses_the_cached_digest(str
269274 }
270275 }
271276
277+ public function test_mock_reproduces_the_same_index_cross_platform_overwrite (): void
278+ {
279+ $ result = $ this ->runProcess (['bash ' , '-c ' , <<<'SH'
280+ set -e
281+ DOCKER_DEFAULT_PLATFORM=linux/amd64 docker pull --quiet "$RELEASE_IMAGE"
282+ DOCKER_DEFAULT_PLATFORM=linux/arm64 docker pull --quiet "$RELEASE_IMAGE"
283+ SH], ['RELEASE_IMAGE ' => 'durableworkflow/server@ ' .self ::DIGEST ]);
284+ self ::assertSame (1 , $ result ['status ' ]);
285+ self ::assertStringContainsString ('cannot overwrite digest ' .self ::DIGEST , $ result ['text ' ]);
286+ }
287+
288+ public function test_trust_runs_both_verified_children_without_overwriting_the_local_index (): void
289+ {
290+ $ result = $ this ->runTrustStep ();
291+ self ::assertSame (0 , $ result ['status ' ], $ result ['text ' ]);
292+ self ::assertStringContainsString ('buildx imagetools inspect --raw durableworkflow/server@ ' .self ::DIGEST , $ result ['calls ' ]);
293+ foreach (['linux/amd64 ' => self ::AMD64 , 'linux/arm64 ' => self ::ARM64 ] as $ platform => $ digest ) {
294+ self ::assertStringContainsString ('pull --quiet durableworkflow/server@ ' .$ digest , $ result ['calls ' ]);
295+ self ::assertStringContainsString ('run --rm --pull never --platform ' .$ platform .' --network none --entrypoint server-verify-trust durableworkflow/server@ ' .$ digest , $ result ['calls ' ]);
296+ }
297+ self ::assertStringNotContainsString ('pull --quiet durableworkflow/server@ ' .self ::DIGEST , $ result ['calls ' ]);
298+ self ::assertSame (2 , substr_count ($ result ['calls ' ], 'pull --quiet ' ));
299+ self ::assertSame (2 , substr_count ($ result ['calls ' ], 'run --rm ' ));
300+ }
301+
302+ public static function invalidTrustIndexes (): iterable
303+ {
304+ $ amd64 = ['mediaType ' => 'application/vnd.oci.image.manifest.v1+json ' , 'digest ' => self ::AMD64 ,
305+ 'platform ' => ['os ' => 'linux ' , 'architecture ' => 'amd64 ' ]];
306+ $ arm64 = ['mediaType ' => 'application/vnd.oci.image.manifest.v1+json ' , 'digest ' => self ::ARM64 ,
307+ 'platform ' => ['os ' => 'linux ' , 'architecture ' => 'arm64 ' ]];
308+ yield 'missing amd64 ' => [[$ arm64 ]];
309+ yield 'missing arm64 ' => [[$ amd64 ]];
310+ yield 'wrong operating system ' => [[$ amd64 , array_replace ($ arm64 , ['platform ' => ['os ' => 'windows ' , 'architecture ' => 'arm64 ' ]])]];
311+ yield 'wrong architecture ' => [[$ amd64 , array_replace ($ arm64 , ['platform ' => ['os ' => 'linux ' , 'architecture ' => 's390x ' ]])]];
312+ yield 'foreign attestation is not an arm64 child ' => [[$ amd64 , array_replace ($ arm64 , ['platform ' => ['os ' => 'unknown ' , 'architecture ' => 'unknown ' ]])]];
313+ yield 'ambiguous platform ' => [[$ amd64 , $ arm64 , $ arm64 ]];
314+ yield 'invalid child digest ' => [[$ amd64 , array_replace ($ arm64 , ['digest ' => 'latest ' ])]];
315+ yield 'nested index is not a platform image ' => [[$ amd64 , array_replace ($ arm64 , ['mediaType ' => 'application/vnd.oci.image.index.v1+json ' ])]];
316+ }
317+
318+ #[DataProvider('invalidTrustIndexes ' )]
319+ public function test_unverified_platform_selection_fails_before_any_pull_or_trust (array $ manifests ): void
320+ {
321+ $ result = $ this ->runTrustStep (['INDEX_JSON ' => json_encode (['manifests ' => $ manifests ], JSON_THROW_ON_ERROR )]);
322+ self ::assertNotSame (0 , $ result ['status ' ]);
323+ self ::assertStringNotContainsString ('pull --quiet ' , $ result ['calls ' ]);
324+ self ::assertStringNotContainsString ('run --rm ' , $ result ['calls ' ]);
325+ }
326+
327+ public function test_mutable_trust_reference_fails_before_registry_or_container_access (): void
328+ {
329+ $ result = $ this ->runTrustStep (['RELEASE_IMAGE ' => 'durableworkflow/server:latest ' ]);
330+ self ::assertSame (1 , $ result ['status ' ]);
331+ self ::assertSame ('' , $ result ['calls ' ]);
332+ }
333+
334+ private function runTrustStep (array $ environment = []): array
335+ {
336+ $ workflow = Yaml ::parse (file_get_contents (dirname (__DIR__ , 2 ).'/.github/workflows/release.yml ' ));
337+ $ steps = array_column ($ workflow ['jobs ' ]['publish ' ]['steps ' ], null , 'name ' );
338+ $ step = $ steps ['Verify published image trust stores ' ];
339+ $ script = $ this ->directory .'/trust.sh ' ;
340+ file_put_contents ($ script , $ step ['run ' ]);
341+
342+ return $ this ->runProcess (['bash ' , $ script ], array_replace ([
343+ 'RELEASE_IMAGE_PLATFORMS ' => 'linux/amd64,linux/arm64 ' ,
344+ 'RELEASE_IMAGE ' => 'durableworkflow/server@ ' .self ::DIGEST ,
345+ 'DW_PULL_ATTEMPTS ' => $ step ['env ' ]['DW_PULL_ATTEMPTS ' ] ?? '3 ' ,
346+ 'DW_PULL_BACKOFF_SECONDS ' => '0 ' ,
347+ ], $ environment ));
348+ }
349+
272350 private function context (array $ overrides = []): array
273351 {
274352 return array_replace ([
@@ -311,6 +389,12 @@ private function runProcess(array $command, array $environment): array
311389 'PATH ' => $ this ->directory .': ' .getenv ('PATH ' ), 'DOCKER ' => $ this ->directory .'/docker ' ,
312390 'GITHUB_OUTPUT ' => $ this ->directory .'/output ' , 'CALL_LOG ' => $ this ->directory .'/calls ' ,
313391 'STATE ' => 'valid ' , 'INDEX_DIGEST ' => self ::DIGEST ,
392+ 'INDEX_JSON ' => json_encode (['schemaVersion ' => 2 , 'manifests ' => [
393+ ['mediaType ' => 'application/vnd.oci.image.manifest.v1+json ' , 'digest ' => self ::AMD64 ,
394+ 'platform ' => ['os ' => 'linux ' , 'architecture ' => 'amd64 ' ]],
395+ ['mediaType ' => 'application/vnd.oci.image.manifest.v1+json ' , 'digest ' => self ::ARM64 ,
396+ 'platform ' => ['os ' => 'linux ' , 'architecture ' => 'arm64 ' ]],
397+ ]], JSON_THROW_ON_ERROR ),
314398 'METADATA ' => json_encode (['config ' => ['Labels ' => $ labels ]], JSON_THROW_ON_ERROR ),
315399 'BAD_METADATA ' => json_encode (['config ' => ['Labels ' => $ badLabels ]], JSON_THROW_ON_ERROR ),
316400 ], $ environment );
0 commit comments