param([Parameter(Mandatory=$true)][string]$Archive) $ErrorActionPreference = 'Stop' $archivePath = (Resolve-Path -LiteralPath $Archive).Path $archiveDirectory = Split-Path -Parent $archivePath $sidecarPath = $archivePath + '.sha256' if (-not (Test-Path -LiteralPath $sidecarPath -PathType Leaf)) { throw 'Archive checksum sidecar is missing' } $sidecarHash = ((Get-Content -LiteralPath $sidecarPath -Raw).Trim() -split '\s+')[0] $archiveHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant() if ($sidecarHash -ne $archiveHash) { throw 'Archive checksum sidecar does not match' } Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [IO.Compression.ZipFile]::OpenRead($archivePath) try { $forbidden = @($zip.Entries | Where-Object { $_.FullName -match '(^|/)(\.env|node_modules|runtime)(/|$)' -or $_.FullName -match '\.(pem|key|p12|pfx)$' }) if ($forbidden.Count -gt 0) { throw ('Bundle contains forbidden entries: ' + ($forbidden.FullName -join ', ')) } $entryCount = $zip.Entries.Count } finally { $zip.Dispose() } $verificationRoot = Join-Path $archiveDirectory ('.verify-' + [Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $verificationRoot | Out-Null try { Expand-Archive -LiteralPath $archivePath -DestinationPath $verificationRoot $payload = @(Get-ChildItem -LiteralPath $verificationRoot -Directory) if ($payload.Count -ne 1) { throw 'Bundle must contain exactly one top-level directory' } $manifestPath = Join-Path $payload[0].FullName 'deploy-manifest.json' $checksumsPath = Join-Path $payload[0].FullName 'SHA256SUMS' $manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json if ($manifest.schema_version -ne 'server-deployment-bundle-v1' -or $manifest.schema_migration_target -ne 52 -or $manifest.hardware_runtime -ne 'volc_hardware_agent') { throw 'Deployment manifest metadata is invalid' } $verifiedFiles = 0 foreach ($line in Get-Content -LiteralPath $checksumsPath) { if ($line -notmatch '^([0-9a-f]{64}) (.+)$') { throw "Invalid checksum line: $line" } $target = [IO.Path]::GetFullPath((Join-Path $payload[0].FullName $Matches[2])) if (-not $target.StartsWith($payload[0].FullName + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw 'Checksum target escapes bundle root' } if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { throw "Checksum target is missing: $($Matches[2])" } if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne $Matches[1]) { throw "Checksum mismatch: $($Matches[2])" } $verifiedFiles++ } if ($verifiedFiles -ne ($manifest.files.Count + 1)) { throw 'Checksum coverage does not match manifest plus manifest file' } [ordered]@{ archive = $archivePath; sha256 = $archiveHash; zip_entries = $entryCount; verified_files = $verifiedFiles; candidate_version = $manifest.candidate_version; schema_migration_target = $manifest.schema_migration_target } | ConvertTo-Json -Compress } finally { $resolvedVerificationRoot = [IO.Path]::GetFullPath($verificationRoot) $resolvedArchiveDirectory = [IO.Path]::GetFullPath($archiveDirectory) if ($resolvedVerificationRoot.StartsWith($resolvedArchiveDirectory + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -and (Test-Path -LiteralPath $resolvedVerificationRoot)) { Remove-Item -LiteralPath $resolvedVerificationRoot -Recurse -Force } }