@@ -630,8 +630,13 @@ Describe 'Invoke-Voidseal — processor (gate) wiring' {
630630 # AddHardDiskDrive requires the VHD to exist in its state (real Add-VMHardDiskDrive requires the
631631 # file), so we create it on the backend with NewVHD first — mirroring how a real deps.vhdx exists
632632 # on disk before the orchestrator attaches it.
633+ # D3-B (Phase 3): the shared BeforeAll also pre-computes the whole-image hash so each It that
634+ # needs to PASS the verify step can supply DepsImageHash = $script:DepsDiskHash without a per-It
635+ # NewVHD call. Tests that override the hash (mismatch) or omit it (missing) create their own
636+ # local backend + disk so the shared fixture stays unaffected.
633637 $script :DepsDisk = Join-Path $script :TmpRoot (" deps-{0}.vhdx" -f ([guid ]::NewGuid().ToString(' N' )))
634638 & $script :ProcB.NewVHD @ { Path = $script :DepsDisk ; SizeBytes = 1 GB ; Differencing = $false ; Dynamic = $true }
639+ $script :DepsDiskHash = [string ](& $script :ProcB.GetVhdxImageHash @ { Path = $script :DepsDisk }) # the (empty) fixture's whole-image hash
635640 }
636641
637642 It ' releases the SAFE candidates end-to-end: detached OUTPUT outbox -> ReadVhdxRawRegion -> read_outbox.py -> gate' {
@@ -645,9 +650,10 @@ Describe 'Invoke-Voidseal — processor (gate) wiring' {
645650 # fresh backend with the blob + re-create the DEPS disk on it (fake state is per-instance).
646651 $b = New-FakeHyperVBackend - SimulateSelfPowerOff - SimulateOutboxBlob $blob
647652 & $b.NewVHD @ { Path = $script :DepsDisk ; SizeBytes = 1 GB ; Differencing = $false ; Dynamic = $true }
653+ $depsHash = [string ](& $b.GetVhdxImageHash @ { Path = $script :DepsDisk }) # the (empty) fixture's whole-image hash
648654
649655 $report = Invoke-Voidseal - Tier 0 - Profile $script :Proc `
650- - Workload @ { WorkloadMode = ' Disk' ; DepsDiskPath = $script :DepsDisk } `
656+ - Workload @ { WorkloadMode = ' Disk' ; DepsDiskPath = $script :DepsDisk ; DepsImageHash = $depsHash } `
651657 - Name ' sbx-proc-outbox' - ArtifactRoot $script :ProcArt - Destination $script :ProcDest `
652658 - WorkloadTimeoutSeconds 0 - BootPollDelaySeconds 0 - Backend $b
653659
@@ -670,9 +676,10 @@ Describe 'Invoke-Voidseal — processor (gate) wiring' {
670676
671677 $b = New-FakeHyperVBackend - SimulateSelfPowerOff - SimulateOutboxBlob $bad
672678 & $b.NewVHD @ { Path = $script :DepsDisk ; SizeBytes = 1 GB ; Differencing = $false ; Dynamic = $true }
679+ $depsHash = [string ](& $b.GetVhdxImageHash @ { Path = $script :DepsDisk }) # the (empty) fixture's whole-image hash
673680
674681 $report = Invoke-Voidseal - Tier 0 - Profile $script :Proc `
675- - Workload @ { WorkloadMode = ' Disk' ; DepsDiskPath = $script :DepsDisk } `
682+ - Workload @ { WorkloadMode = ' Disk' ; DepsDiskPath = $script :DepsDisk ; DepsImageHash = $depsHash } `
676683 - Name ' sbx-proc-tamper' - ArtifactRoot $script :ProcArt - Destination $script :ProcDest `
677684 - WorkloadTimeoutSeconds 0 - BootPollDelaySeconds 0 - Backend $b
678685
@@ -687,9 +694,10 @@ Describe 'Invoke-Voidseal — processor (gate) wiring' {
687694 It ' DENY-on-timeout: a hung processor (SimulateNeverOff) -> gate does NOT run, Released stays $null' {
688695 $b = New-FakeHyperVBackend - SimulateNeverOff # never self-powers-off -> Wait-WorkloadComplete force-stops -> TimedOut
689696 & $b.NewVHD @ { Path = $script :DepsDisk ; SizeBytes = 1 GB ; Differencing = $false ; Dynamic = $true }
697+ $depsHash = [string ](& $b.GetVhdxImageHash @ { Path = $script :DepsDisk }) # the (empty) fixture's whole-image hash
690698
691699 $report = Invoke-Voidseal - Tier 0 - Profile $script :Proc `
692- - Workload @ { WorkloadMode = ' Disk' ; DepsDiskPath = $script :DepsDisk } `
700+ - Workload @ { WorkloadMode = ' Disk' ; DepsDiskPath = $script :DepsDisk ; DepsImageHash = $depsHash } `
693701 - Name ' sbx-proc-timeout' - ArtifactRoot $script :ProcArt - Destination $script :ProcDest `
694702 - WorkloadTimeoutSeconds 0 - BootPollDelaySeconds 0 - Backend $b
695703
@@ -724,6 +732,44 @@ Describe 'Invoke-Voidseal — processor (gate) wiring' {
724732 ($null -eq $gateRanField -or -not [bool ]$gateRanField.Value ) |
725733 Should - BeTrue - Because ' GateRan is never stamped when the gate did not run'
726734 }
735+
736+ It ' DENY-on-deps-mismatch: a deps disk whose hash != the expected (builder-recorded) hash is REFUSED (no attach, no run)' {
737+ $b = New-FakeHyperVBackend - SimulateSelfPowerOff
738+ & $b.NewVHD @ { Path = $script :DepsDisk ; SizeBytes = 1 GB ; Differencing = $false ; Dynamic = $true }
739+ # wrong hash — 64 zero hex chars; the actual hash of an empty VHDX is non-zero
740+ $report = Invoke-Voidseal - Tier 0 - Profile $script :Proc `
741+ - Workload @ { WorkloadMode = ' Disk' ; DepsDiskPath = $script :DepsDisk ; DepsImageHash = (' 0' * 64 ) } `
742+ - Name ' sbx-proc-depsmismatch' - ArtifactRoot $script :ProcArt - Destination $script :ProcDest `
743+ - WorkloadTimeoutSeconds 0 - BootPollDelaySeconds 0 - Backend $b
744+ @ ($report.States ) | Should -Not - Contain ' SEALED' - Because ' a tampered/substituted deps disk aborts BEFORE the seal'
745+ $report.Error | Should -Match ' (?i)deps.*(integrity|hash)|integrity check' - Because ' the abort names the deps integrity failure'
746+ $report.Released | Should - BeNullOrEmpty
747+ }
748+
749+ It ' DENY-on-missing-deps-hash: a deps disk with NO DepsImageHash is REFUSED (mandatory verification)' {
750+ $b = New-FakeHyperVBackend - SimulateSelfPowerOff
751+ & $b.NewVHD @ { Path = $script :DepsDisk ; SizeBytes = 1 GB ; Differencing = $false ; Dynamic = $true }
752+ # NO DepsImageHash supplied — should trigger mandatory-verification refusal
753+ $report = Invoke-Voidseal - Tier 0 - Profile $script :Proc `
754+ - Workload @ { WorkloadMode = ' Disk' ; DepsDiskPath = $script :DepsDisk } `
755+ - Name ' sbx-proc-nodepshash' - ArtifactRoot $script :ProcArt - Destination $script :ProcDest `
756+ - WorkloadTimeoutSeconds 0 - BootPollDelaySeconds 0 - Backend $b
757+ @ ($report.States ) | Should -Not - Contain ' SEALED'
758+ $report.Error | Should -Match ' (?i)DepsImageHash|no .*hash|unverified' - Because ' a deps disk without a verified hash is refused'
759+ }
760+
761+ It ' GC invariant (D3-D): the verified deps.vhdx is NOT in CreatedDisks -> teardown LEAVES it (builder-owned, reusable)' {
762+ $b = New-FakeHyperVBackend - SimulateSelfPowerOff
763+ & $b.NewVHD @ { Path = $script :DepsDisk ; SizeBytes = 1 GB ; Differencing = $false ; Dynamic = $true }
764+ $depsHash = [string ](& $b.GetVhdxImageHash @ { Path = $script :DepsDisk }) # detached -> hashable
765+ $report = Invoke-Voidseal - Tier 0 - Profile $script :Proc `
766+ - Workload @ { WorkloadMode = ' Disk' ; DepsDiskPath = $script :DepsDisk ; DepsImageHash = $depsHash } `
767+ - Name ' sbx-proc-depsgc' - ArtifactRoot $script :ProcArt - Destination $script :ProcDest `
768+ - WorkloadTimeoutSeconds 0 - BootPollDelaySeconds 0 - Backend $b
769+ @ ($report.States ) | Should - Contain ' SEALED' - Because ' a verified deps disk attaches + the run proceeds'
770+ @ ($report.Descriptor.CreatedDisks ) | Should -Not - Contain $script :DepsDisk - Because ' the builder-owned deps.vhdx is never in CreatedDisks; the Reaper leaves it'
771+ $report.Descriptor.DepsImageHash | Should - Be $depsHash - Because ' the verified hash is recorded on the descriptor'
772+ }
727773}
728774
729775# ===========================================================================
0 commit comments