Skip to content

Commit 06e5e97

Browse files
committed
feat(scaffold): seam #1 — deps verify-before-attach (mandatory whole-image-hash match; fail-closed on mismatch/absence; deps.vhdx stays builder-owned)
1 parent 2bf4dc4 commit 06e5e97

2 files changed

Lines changed: 73 additions & 5 deletions

File tree

scripts/Invoke-Voidseal.ps1

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,8 +445,30 @@ function Invoke-Voidseal {
445445
$depsDiskPath = [string](Get-WorkloadField -Workload $Workload -Name 'DepsDiskPath')
446446
if ([string]::IsNullOrWhiteSpace($depsDiskPath) -and $resolved.ContainsKey('DepsDiskPath')) { $depsDiskPath = [string]$resolved['DepsDiskPath'] }
447447
if ($resolved['Network'] -eq 'None' -and -not [string]::IsNullOrWhiteSpace($depsDiskPath)) {
448-
$null = & $Backend.AddHardDiskDrive @{ VMName = $Name; Path = $depsDiskPath } # existing method; no read-only attach (D3)
449-
Set-DescriptorField -Descriptor $descriptor -Name 'DepsDiskPath' -Value $depsDiskPath
448+
# PHASE 3 — VERIFY-BEFORE-ATTACH (supply-chain integrity; Pass-5 + Pass-4 P1 "offline != trustworthy").
449+
# The expected whole-image hash comes from the TRUSTED caller (who ran the builder + captured its
450+
# WholeImageHash) via -Workload.DepsImageHash (fallback: the resolved profile). Re-hash deps.vhdx in
451+
# USER-SPACE (GetVhdxImageHash — raw bytes, NEVER Mount-VHD) while it is still DETACHED (before this
452+
# AddHardDiskDrive), and REFUSE on a missing OR mismatched hash — fail-closed: no unverified or
453+
# tampered/substituted dependency disk ever attaches. (Live: GetVhdxImageHash is qemu-img-free
454+
# streaming SHA-256 — re-resolve at fire; the mock fake returns SHA-256 of the recorded deps bytes.)
455+
$expectedHash = [string](Get-WorkloadField -Workload $Workload -Name 'DepsImageHash')
456+
if ([string]::IsNullOrWhiteSpace($expectedHash) -and $resolved.ContainsKey('DepsImageHash')) { $expectedHash = [string]$resolved['DepsImageHash'] }
457+
if ([string]::IsNullOrWhiteSpace($expectedHash)) {
458+
throw "Invoke-Voidseal: '$Name' attaches a DEPS disk ('$depsDiskPath') but no DepsImageHash was supplied. A deps disk MUST carry the builder's recorded whole-image hash (pass -Workload.DepsImageHash) — refusing to attach an UNVERIFIED dependency disk. Failing closed."
459+
}
460+
$expectedHash = $expectedHash.Trim().ToLowerInvariant()
461+
$actualHash = ([string](& $Backend.GetVhdxImageHash @{ Path = $depsDiskPath })).Trim().ToLowerInvariant()
462+
if ($actualHash -ne $expectedHash) {
463+
throw "Invoke-Voidseal: '$Name' DEPS disk integrity check FAILED — '$depsDiskPath' hashes to '$actualHash' but the expected (builder-recorded) hash is '$expectedHash'. The dependency disk was tampered or substituted — refusing to attach. Failing closed."
464+
}
465+
# Verified -> attach + RECORD (DepsDiskPath + the verified DepsImageHash) BEFORE Lock-Sandbox so
466+
# Assert-Sealed accepts it as an expected disk. NOT added to CreatedDisks (builder-owned, reusable
467+
# across runs — D3-D; teardown's Remove-Sandbox leaves the deps.vhdx FILE on disk). No read-only
468+
# attach (Hyper-V can't host-enforce guest-RO — D3).
469+
$null = & $Backend.AddHardDiskDrive @{ VMName = $Name; Path = $depsDiskPath }
470+
Set-DescriptorField -Descriptor $descriptor -Name 'DepsDiskPath' -Value $depsDiskPath
471+
Set-DescriptorField -Descriptor $descriptor -Name 'DepsImageHash' -Value $actualHash
450472
$report.Descriptor = $descriptor
451473
}
452474
}

tests/InvokeVoidseal.Tests.ps1

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = 1GB; 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 = 1GB; 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 = 1GB; 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 = 1GB; 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 = 1GB; 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 = 1GB; 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 = 1GB; 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

Comments
 (0)