<# .SYNOPSIS Verify an DezhNebesht release is authentic and unmodified BEFORE installing (AFTA ยง6 / FPT_TUD_EXT.1). .DESCRIPTION 1. Verifies the RSA signature of RELEASE.manifest with the trusted PUBLIC certificate (proves the manifest was produced by the holder of the release key โ€” authenticity). 2. Re-hashes every file in -ReleaseDir and compares against the manifest (proves no file was added, removed, or altered โ€” integrity). Exits non-zero on ANY failure, so it can gate an installer/deploy step. Runs on Windows PowerShell 5.1 and PowerShell 7. .PARAMETER ReleaseDir Folder containing the artifacts plus RELEASE.manifest and RELEASE.manifest.sig. .PARAMETER CerPath Path to the trusted PUBLIC certificate (.cer/.crt) exported from the release cert. Pin this with the deployer, NOT alongside the release it verifies. .EXAMPLE ./verify-release.ps1 -ReleaseDir ./publish -CerPath ./release.cer #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ReleaseDir, [Parameter(Mandatory)] [string]$CerPath ) $ErrorActionPreference = 'Stop' $tab = [char]9 $manifestPath = Join-Path $ReleaseDir 'RELEASE.manifest' $sigPath = Join-Path $ReleaseDir 'RELEASE.manifest.sig' # release-public.cer is the (public) verification material shipped with the package, not part of the # signed payload โ€” it is written AFTER the manifest, so exclude it from the "unexpected file" check. $skip = @('RELEASE.manifest','RELEASE.manifest.sig','release-public.cer') foreach ($p in @($ReleaseDir, $CerPath, $manifestPath, $sigPath)) { if (-not (Test-Path $p)) { Write-Error "Missing: $p"; exit 2 } } # 1) Verify the manifest signature with the trusted public certificate. $manifestBytes = [System.IO.File]::ReadAllBytes($manifestPath) $sig = [Convert]::FromBase64String((Get-Content -Raw $sigPath).Trim()) $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CerPath) $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($cert) $sigOk = $rsa.VerifyData($manifestBytes, $sig, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) if (-not $sigOk) { Write-Error "SIGNATURE INVALID - manifest is not signed by the trusted release key. Do NOT install." exit 1 } Write-Host "Manifest signature: VALID (cert: $($cert.Subject))" -ForegroundColor Green # 2) Re-hash every file and compare against the manifest. $root = (Resolve-Path $ReleaseDir).Path $expected = @{} Get-Content $manifestPath | Where-Object { $_ -notmatch '^#' -and $_.Trim() } | ForEach-Object { $parts = $_ -split $tab, 2 $expected[$parts[1]] = $parts[0].ToLower() } $problems = 0 foreach ($rel in $expected.Keys) { $full = Join-Path $root $rel if (-not (Test-Path $full)) { Write-Warning "MISSING: $rel"; $problems++; continue } $actual = (Get-FileHash -Algorithm SHA256 -Path $full).Hash.ToLower() if ($actual -ne $expected[$rel]) { Write-Warning "ALTERED: $rel"; $problems++ } } Get-ChildItem -Path $root -Recurse -File | Where-Object { $skip -notcontains $_.Name } | ForEach-Object { $rel = $_.FullName.Substring($root.Length).TrimStart('\','/').Replace('\','/') if (-not $expected.ContainsKey($rel)) { Write-Warning "UNEXPECTED: $rel"; $problems++ } } if ($problems -gt 0) { Write-Error "$problems integrity problem(s) - release does NOT match the signed manifest. Do NOT install." exit 1 } Write-Host "Integrity: all $($expected.Count) files match the signed manifest." -ForegroundColor Green Write-Host "Release is AUTHENTIC and UNMODIFIED - safe to install." -ForegroundColor Green exit 0