Files
waggle-os/app/scripts/sign-windows-artifact.test.ps1
Oleg Maslov b20b138fe4 moving
2026-09-02 10:14:22 +02:00

2773 lines
122 KiB
PowerShell

#Requires -Version 7.0
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$wrapperPath = Join-Path $PSScriptRoot 'sign-windows-artifact.ps1'
. $wrapperPath -ArtifactPath 'C:\unused.exe'
$passed = 0
function Assert-Equal {
param($Actual, $Expected, [string]$Label)
if ($Actual -cne $Expected) { throw "$Label expected '$Expected', got '$Actual'." }
$script:passed++
}
function Assert-Throws {
param([scriptblock]$Action, [string]$Pattern, [string]$Label)
try {
& $Action
} catch {
if ($_.Exception.Message -notmatch $Pattern) {
throw "$Label threw the wrong error: $($_.Exception.Message)"
}
$script:passed++
return
}
throw "$Label did not throw."
}
function Write-JsonNoBom {
param([string]$Path, [object]$Value)
[IO.File]::WriteAllText(
$Path,
($Value | ConvertTo-Json -Depth 32 -Compress),
[Text.UTF8Encoding]::new($false)
)
}
function New-SyntheticPe {
param([string]$Path, [byte]$Marker = 0)
[IO.Directory]::CreateDirectory((Split-Path $Path -Parent)) | Out-Null
$bytes = [byte[]]::new(128)
$bytes[0] = 0x4D
$bytes[1] = 0x5A
[BitConverter]::GetBytes([uint32]0x40).CopyTo($bytes, 0x3C)
$bytes[0x40] = 0x50
$bytes[0x41] = 0x45
$bytes[0x42] = 0
$bytes[0x43] = 0
$bytes[127] = $Marker
[IO.File]::WriteAllBytes($Path, $bytes)
}
function New-SyntheticPatchablePe {
param([string]$Path)
[IO.Directory]::CreateDirectory((Split-Path $Path -Parent)) | Out-Null
$bytes = [byte[]]::new(256)
$bytes[0] = 0x4D
$bytes[1] = 0x5A
[BitConverter]::GetBytes([uint32]0x40).CopyTo($bytes, 0x3C)
$bytes[0x40] = 0x50
$bytes[0x41] = 0x45
$token = [Text.Encoding]::ASCII.GetBytes('__TAURI_BUNDLE_TYPE_VAR_UNK')
[Array]::Copy($token, 0, $bytes, 160, $token.Length)
[IO.File]::WriteAllBytes($Path, $bytes)
}
function Get-Sha256 {
param([string]$Path)
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
}
function Get-RealSha256 {
param([string]$Path)
$stream = [IO.File]::Open(
$Path,
[IO.FileMode]::Open,
[IO.FileAccess]::Read,
[IO.FileShare]::Read
)
$algorithm = [Security.Cryptography.SHA256]::Create()
try {
return ([BitConverter]::ToString($algorithm.ComputeHash($stream)) -replace '-', '')
} finally {
$algorithm.Dispose()
$stream.Dispose()
}
}
function New-PendingLedger {
param([string]$SessionId, [string]$ManifestSha256, [object[]]$Slots)
return [pscustomobject][ordered]@{
schemaVersion = 3
sessionId = $SessionId
manifestSha256 = $ManifestSha256
state = 'open'
terminalAtUtc = $null
terminalReceiptSha256 = $null
entries = @($Slots | ForEach-Object {
[pscustomobject][ordered]@{
slotId = [string]$_.id
order = [int]$_.order
kind = [string]$_.kind
status = 'pending'
attempts = 0
reservationId = $null
path = $null
preSignSha256 = $null
reservedAtUtc = $null
completedAtUtc = $null
postSignSha256 = $null
signerSubject = $null
}
})
}
}
function Close-PrebuiltTreeLease {
param([AllowNull()] [object]$Lease)
if ($null -eq $Lease) { return }
$locksProperty = $Lease.PSObject.Properties['Locks']
$locks = if ($null -eq $locksProperty) { @($Lease) } else { @($locksProperty.Value) }
foreach ($lock in $locks) {
if ($null -ne $lock) { $lock.Dispose() }
}
}
function Assert-FreshProcessModuleIsolation {
param(
[Parameter(Mandatory = $true)] [string]$HostPath,
[Parameter(Mandatory = $true)] [string]$Label,
[Parameter(Mandatory = $true)] [string]$Root
)
$hostRoot = Join-Path $Root ([IO.Path]::GetFileNameWithoutExtension($HostPath))
$hostileModuleRoot = Join-Path $hostRoot 'hostile-modules'
$markerPath = Join-Path $hostRoot 'hostile-module-loaded.txt'
$childPath = Join-Path $hostRoot 'module-isolation-child.ps1'
[IO.Directory]::CreateDirectory($hostileModuleRoot) | Out-Null
foreach ($moduleName in @(
'Microsoft.PowerShell.Security',
'Microsoft.PowerShell.Management',
'Microsoft.PowerShell.Utility'
)) {
$moduleRoot = Join-Path $hostileModuleRoot $moduleName
[IO.Directory]::CreateDirectory($moduleRoot) | Out-Null
$modulePath = Join-Path $moduleRoot "$moduleName.psm1"
$manifestPath = Join-Path $moduleRoot "$moduleName.psd1"
[IO.File]::WriteAllText(
$modulePath,
"[IO.File]::AppendAllText('$($markerPath.Replace("'", "''"))', '$moduleName')`n" +
"function Get-AuthenticodeSignature { throw 'hostile command' }`n" +
"function Get-Acl { throw 'hostile command' }`n" +
"function Set-Acl { throw 'hostile command' }`n" +
"function Get-FileHash { throw 'hostile command' }`n" +
'Export-ModuleMember -Function *',
[Text.UTF8Encoding]::new($false)
)
New-ModuleManifest -Path $manifestPath -RootModule "$moduleName.psm1" `
-ModuleVersion '99.0.0' -FunctionsToExport '*' | Out-Null
}
$childSource = @'
param([string]$WrapperPath, [string]$HostileModuleRoot, [string]$MarkerPath)
$ErrorActionPreference = 'Stop'
[Environment]::SetEnvironmentVariable('PSModulePath', $HostileModuleRoot, 'Process')
$global:PSModuleAutoLoadingPreference = 'All'
. $WrapperPath -ArtifactPath 'C:\unused.exe'
$trustedRoot = [IO.Path]::GetFullPath([IO.Path]::Combine($PSHOME, 'Modules'))
$expected = @{
'Get-AuthenticodeSignature' = 'Microsoft.PowerShell.Security'
'Get-Acl' = 'Microsoft.PowerShell.Security'
'Set-Acl' = 'Microsoft.PowerShell.Security'
}
if ($PSVersionTable.PSEdition -ceq 'Core') {
$expected['Get-FileHash'] = 'Microsoft.PowerShell.Utility'
}
foreach ($entry in $expected.GetEnumerator()) {
$command = Get-Command $entry.Key -CommandType Cmdlet -ErrorAction Stop
$expectedPath = [IO.Path]::Combine($trustedRoot, $entry.Value, "$($entry.Value).psd1")
if (-not [string]::Equals(
[IO.Path]::GetFullPath([string]$command.Module.Path),
[IO.Path]::GetFullPath($expectedPath),
[StringComparison]::OrdinalIgnoreCase
)) {
throw "Command '$($entry.Key)' resolved outside the trusted module root."
}
}
if ([IO.File]::Exists($MarkerPath)) { throw 'A hostile PowerShell module was loaded.' }
'module-isolation-ok'
'@
[IO.File]::WriteAllText($childPath, $childSource, [Text.UTF8Encoding]::new($false))
$output = & $HostPath -NoLogo -NoProfile -NonInteractive `
-ExecutionPolicy Bypass -File $childPath `
-WrapperPath $wrapperPath -HostileModuleRoot $hostileModuleRoot `
-MarkerPath $markerPath 2>&1
if ($LASTEXITCODE -ne 0 -or @($output | Where-Object { $_ -ceq 'module-isolation-ok' }).Count -ne 1) {
throw "$Label hostile PSModulePath isolation failed: $($output -join ' | ')"
}
if (Test-Path -LiteralPath $markerPath) {
throw "$Label imported a hostile PowerShell module."
}
$script:passed++
}
function Start-SynchronizedChild {
param(
[Parameter(Mandatory = $true)] [string]$PowerShellPath,
[Parameter(Mandatory = $true)] [string]$ScriptPath,
[Parameter(Mandatory = $true)] [hashtable]$Arguments
)
$argumentList = [Collections.Generic.List[string]]::new()
foreach ($argument in @(
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
'-File', ('"' + $ScriptPath + '"')
)) {
$argumentList.Add($argument)
}
foreach ($entry in $Arguments.GetEnumerator()) {
$argumentList.Add("-$($entry.Key)")
$argumentList.Add('"' + ([string]$entry.Value).Replace('"', '\"') + '"')
}
return Start-Process -FilePath $PowerShellPath -ArgumentList @($argumentList) `
-WindowStyle Hidden -PassThru
}
function Wait-ForChildBarriers {
param([string[]]$Paths, [string]$Label)
$deadline = [DateTimeOffset]::UtcNow.AddSeconds(30)
while (@($Paths | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Leaf) }).Count -ne 0) {
if ([DateTimeOffset]::UtcNow -gt $deadline) {
throw "Timed out waiting for $Label child barriers."
}
Start-Sleep -Milliseconds 20
}
}
$releaseRoot = 'D:\repo\app\src-tauri\target\x86_64-pc-windows-msvc\release'
$resourcesRoot = 'D:\repo\app\src-tauri\resources'
$tempRoot = 'D:\repo\app\src-tauri\target\.signing-temp\run-123456'
$version = '0.2.0'
$installedTauriRoot = Join-Path `
(Split-Path $PSScriptRoot -Parent) 'node_modules\@tauri-apps\cli'
$installedNativeRoot = Join-Path `
(Split-Path $PSScriptRoot -Parent) 'node_modules\@tauri-apps\cli-win32-x64-msvc'
foreach ($binding in @(
@((Join-Path $installedTauriRoot 'tauri.js'), $TauriCliSha256, 'installed Tauri entry'),
@((Join-Path $installedTauriRoot 'main.js'), $TauriCliMainSha256, 'installed Tauri main'),
@((Join-Path $installedTauriRoot 'index.js'), $TauriCliIndexSha256, 'installed Tauri loader'),
@((Join-Path $installedNativeRoot 'package.json'), $TauriNativePackageSha256, 'installed Tauri native package'),
@((Join-Path $installedNativeRoot 'cli.win32-x64-msvc.node'), $TauriNativeBinarySha256, 'installed Tauri native binary')
)) {
Assert-Equal (Get-Sha256 $binding[0]) $binding[1] $binding[2]
}
$wrapperSource = Get-Content -Raw -LiteralPath $wrapperPath
if ($wrapperSource -match "ValidateSet\('Callback',\s*'Build'\)" -or
$wrapperSource -match 'function\s+Invoke-WaggleSigningBuild' -or
$wrapperSource -match 'function\s+Assert-WaggleSigningBuildComplete' -or
$wrapperSource -match "Mode\s+-ceq\s+'Build'") {
throw 'Production Artifact Signing still exposes the rejected local Build issuer.'
}
if ($wrapperSource -notmatch "ValidateSet\('Callback',\s*'Package'\)" -or
$wrapperSource -notmatch 'function\s+Invoke-WaggleSigningPackage' -or
$wrapperSource -notmatch 'function\s+Assert-WaggleSigningPackageComplete') {
throw 'Production Artifact Signing lacks the hosted-only Package issuer.'
}
foreach ($portableParameter in @(
'PortableToolchainRoot', 'PortableNodePath',
'PortableGitPath', 'PortableSevenZipPath'
)) {
if ($wrapperSource -notmatch "\[string\]\`$$portableParameter") {
throw "Hosted Package mode lacks the explicit $portableParameter input."
}
$passed++
}
$portableEnvironmentNames = @(
'WAGGLE_SIGNING_PORTABLE_TOOLCHAIN_ROOT',
'WAGGLE_SIGNING_PORTABLE_NODE_PATH',
'WAGGLE_SIGNING_PORTABLE_GIT_PATH',
'WAGGLE_SIGNING_PORTABLE_SEVEN_ZIP_PATH'
)
foreach ($portableEnvironmentName in $portableEnvironmentNames) {
if ($wrapperSource -notmatch [Regex]::Escape($portableEnvironmentName)) {
throw "Signing callbacks do not receive $portableEnvironmentName."
}
$passed++
}
if ($wrapperSource -notmatch 'function\s+Get-WagglePortableToolchain' -or
$wrapperSource -notmatch 'portableToolchainRoot' -or
$wrapperSource -notmatch 'sevenZipDllPath' -or
$wrapperSource -notmatch 'sevenZipDllSha256') {
throw 'Portable signing tools are not path-, manifest-, and closure-bound.'
}
$passed++
if ($wrapperSource -notmatch [Regex]::Escape(
'CN=.NET, O=Microsoft Corporation, L=Redmond, S=Washington, C=US'
) -or
$wrapperSource -notmatch
'Assert-MicrosoftAuthenticodeFile\s+\$dotnet\s+''\.NET host''\s+\$null\s+\$DotNetPublisher') {
throw '.NET 8 validation is not bound to the exact repository-pinned .NET signer.'
}
$passed++
if ($wrapperSource -notmatch 'Restore-WaggleReplacedArtifact\s+`?\s*-ArtifactPath') {
throw 'Production signing callback does not use the verified rollback path.'
}
$passed++
foreach ($hostBoundaryName in @(
'GITHUB_ACTIONS', 'GITHUB_EVENT_NAME', 'RUNNER_ENVIRONMENT',
'GITHUB_REPOSITORY', 'GITHUB_SHA', 'GITHUB_REF',
'GITHUB_REF_TYPE', 'GITHUB_REF_NAME', 'GITHUB_WORKFLOW_REF',
'GITHUB_WORKFLOW_SHA'
)) {
if ($wrapperSource -notmatch [Regex]::Escape($hostBoundaryName)) {
throw "Hosted Package mode does not bind $hostBoundaryName."
}
$passed++
}
foreach ($hostBoundaryPattern in @(
"GITHUB_EVENT_NAME\s*=\s*'push'",
'GITHUB_REF\s*=\s*\$expectedRef',
'GITHUB_WORKFLOW_REF\s*=\s*\$expectedWorkflowRef',
'GITHUB_WORKFLOW_SHA\s*=\s*\$ExpectedRevision'
)) {
if ($wrapperSource -notmatch $hostBoundaryPattern) {
throw "Hosted Package mode lacks exact boundary binding: $hostBoundaryPattern"
}
$passed++
}
$validBoundaryRevision = '0123456789abcdef0123456789abcdef01234567'
$validBoundaryVersion = '0.2.0'
$validHostedBoundary = [ordered]@{
GITHUB_ACTIONS = 'true'
GITHUB_EVENT_NAME = 'push'
RUNNER_ENVIRONMENT = 'github-hosted'
GITHUB_REPOSITORY = 'marolinik/waggle-os'
GITHUB_SHA = $validBoundaryRevision
GITHUB_REF = 'refs/tags/v0.2.0'
GITHUB_REF_TYPE = 'tag'
GITHUB_REF_NAME = 'v0.2.0'
GITHUB_WORKFLOW_REF = 'marolinik/waggle-os/.github/workflows/release.yml@refs/tags/v0.2.0'
GITHUB_WORKFLOW_SHA = $validBoundaryRevision
}
$savedHostedBoundary = @{}
foreach ($name in $validHostedBoundary.Keys) {
$savedHostedBoundary[$name] = [Environment]::GetEnvironmentVariable($name)
}
try {
foreach ($name in $validHostedBoundary.Keys) {
[Environment]::SetEnvironmentVariable($name, $validHostedBoundary[$name])
}
Assert-WaggleHostedSigningBoundary `
-ExpectedRevision $validBoundaryRevision `
-ExpectedVersion $validBoundaryVersion
$passed++
foreach ($name in $validHostedBoundary.Keys) {
[Environment]::SetEnvironmentVariable($name, "invalid-$name")
Assert-Throws {
Assert-WaggleHostedSigningBoundary `
-ExpectedRevision $validBoundaryRevision `
-ExpectedVersion $validBoundaryVersion
} "exact $([Regex]::Escape($name)) boundary evidence" `
"hosted signing rejects mutated $name"
[Environment]::SetEnvironmentVariable($name, $validHostedBoundary[$name])
}
} finally {
foreach ($name in $validHostedBoundary.Keys) {
[Environment]::SetEnvironmentVariable($name, $savedHostedBoundary[$name])
}
}
$wrapperTokens = $null
$wrapperParseErrors = $null
$wrapperAst = [Management.Automation.Language.Parser]::ParseFile(
$wrapperPath, [ref]$wrapperTokens, [ref]$wrapperParseErrors
)
if (@($wrapperParseErrors).Count -ne 0) {
throw "Signing wrapper does not parse: $($wrapperParseErrors[0].Message)"
}
$packageFunctions = @($wrapperAst.FindAll({
param($node)
$node -is [Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -ceq 'Invoke-WaggleSigningPackage'
}, $true))
if ($packageFunctions.Count -ne 1 -or
$packageFunctions[0].Extent.Text -match
'BuildScriptPaths|ViteCliPath|bundleNode|buildSidecar|bundleNativeDeps|stageSidecarDeps|tauri:build') {
throw 'Hosted Package mode still contains the rejected local source-build closure.'
}
$passed++
$packageSource = $packageFunctions[0].Extent.Text
if ([regex]::Matches($packageSource, 'New-WagglePrebuiltWorkCopy').Count -ne 2 -or
[regex]::Matches($packageSource, 'Clear-WaggleRegeneratedRoots').Count -ne 2 -or
$packageSource -notmatch "SetEnvironmentVariable\('CARGO_TARGET_DIR',\s*\`$unsignedWorkRoot\)" -or
$packageSource -notmatch "SetEnvironmentVariable\('CARGO_TARGET_DIR',\s*\`$signingWorkRoot\)" -or
$packageSource -match "SetEnvironmentVariable\('CARGO_TARGET_DIR',\s*\`$(?:unsigned|signing)SourceRoot\)" -or
$packageSource -notmatch '(?s)New-WaggleSigningManifest.*?\$preflightEvidenceRoot.*?\$receipt' -or
$packageSource -notmatch '(?s)Unsigned hosted prebuilt input.*?Remove-Item\s+-LiteralPath\s+\$unsignedWorkRoot.*?Signing hosted prebuilt input' -or
$packageSource -notmatch '(?s)Unsigned private work tree.*?Clear-WaggleRegeneratedRoots.*?Unsigned immutable NSIS preflight package' -or
$packageSource -notmatch '(?s)Signing private work tree.*?Clear-WaggleRegeneratedRoots.*?Signed NSIS bundle' -or
[regex]::Matches($packageSource, 'Assert-WaggleHostedDiskCapacity').Count -ne 2) {
throw 'Hosted Package mode does not isolate immutable receipt inputs in private work copies.'
}
$passed++
if ($packageSource -notmatch '(?s)Open-ReadLock\s+\$overridePath.*?Unsigned immutable NSIS preflight package' -or
$wrapperSource -notmatch '(?s)function\s+Open-WagglePackageToolchainLocks.*?app/src-tauri/icons/icon\.ico.*?status\s+--porcelain=v1.*?Assert-WaggleHostedSigningBoundary') {
throw 'Hosted Package mode does not lock exact-HEAD packaging inputs and its unsigned override.'
}
$passed++
if ($wrapperSource -notmatch "(?s)if\s*\(\`$replaced\)\s*\{.*?Rollback backup is missing") {
throw 'Signing callback does not report a missing required rollback backup as a rollback failure.'
}
$passed++
if ($wrapperSource -notmatch "PSModuleAutoLoadingPreference\s*=\s*'None'" -or
$wrapperSource -notmatch "SetEnvironmentVariable\(\s*'PSModulePath'") {
throw 'Production Artifact Signing does not harden PowerShell module resolution before trust checks.'
}
$passed++
if ($wrapperSource -match 'NpmCliPath\s+run|npm\s+run\s+tauri:build') {
throw 'Production signing build still invokes the PATH-dependent npm build script.'
}
$passed++
foreach ($environmentName in @(
'NODE_OPTIONS', 'NODE_PATH', 'NAPI_RS_NATIVE_LIBRARY_PATH',
'NAPI_RS_FORCE_WASI', 'npm_config_node_options'
)) {
if ($wrapperSource -notmatch "SetEnvironmentVariable\('$environmentName', \`$null\)") {
throw "Production signing build does not sanitize $environmentName."
}
$passed++
}
if ($packageSource -notmatch '(?s)foreach\s*\(\$name\s+in\s+\$environmentNames\).*?\$savedEnvironment\[\$name\].*?finally\s*\{.*?SetEnvironmentVariable\(\$name,\s*\$savedEnvironment\[\$name\]\)' -or
$wrapperSource -notmatch "@\('-PortableToolchainRoot',\s*\`$ToolchainRoot\)" -or
$wrapperSource -notmatch "@\('-PortableNodePath',\s*\`$Node\)" -or
$wrapperSource -notmatch "@\('-PortableGitPath',\s*\`$Git\)" -or
$wrapperSource -notmatch "@\('-PortableSevenZipPath',\s*\`$SevenZip\)") {
throw 'Portable toolchain inputs are not forwarded and transactionally restored.'
}
$passed++
& {
$handoffPath = Join-Path $PSScriptRoot 'new-windows-signing-handoff.ps1'
. $handoffPath -SourceTargetRoot 'C:\unused' -DestinationRoot 'C:\unused'
Assert-Equal $MaxTargetFileCount 18000 'handoff target file-count production bound'
Assert-Equal $MaxTargetBytes ([long]768MB) 'handoff target byte production bound'
Assert-Equal $MaxResourceFileCount 17500 'handoff resource file-count production bound'
Assert-Equal $MaxResourceBytes ([long]600MB) 'handoff resource byte production bound'
$handoffFixtureRoot = Join-Path `
([IO.Path]::GetTempPath()) `
"waggle-handoff-test-$([Guid]::NewGuid().ToString('N'))"
[IO.Directory]::CreateDirectory($handoffFixtureRoot) | Out-Null
$handoffReparsePath = $null
try {
$sourceRoot = Join-Path $handoffFixtureRoot 'source'
$sourceDependency = Join-Path $sourceRoot 'release\deps\waggle.exe'
$sourceMain = Join-Path $sourceRoot 'release\waggle.exe'
$sourceResource = Join-Path $sourceRoot 'resources\service.js'
$sourceResourceTwo = Join-Path $sourceRoot 'resources\model.json'
$sourceNsis = Join-Path $sourceRoot 'nsis\makensis.exe'
$sourceNsisDll = Join-Path $sourceRoot 'nsis\plugin.dll'
[IO.Directory]::CreateDirectory((Split-Path $sourceDependency -Parent)) | Out-Null
[IO.Directory]::CreateDirectory((Split-Path $sourceResource -Parent)) | Out-Null
[IO.Directory]::CreateDirectory((Split-Path $sourceNsis -Parent)) | Out-Null
New-SyntheticPe $sourceDependency 211
New-Item -ItemType HardLink -Path $sourceMain -Target $sourceDependency | Out-Null
[IO.File]::WriteAllText($sourceResource, 'receipt-bound-service')
[IO.File]::WriteAllText($sourceResourceTwo, '{"model":"fixture"}')
[IO.File]::WriteAllText($sourceNsis, 'fixture-makensis')
[IO.File]::WriteAllText($sourceNsisDll, 'fixture-plugin')
$targetMappings = @(
[pscustomobject]@{
Source = $sourceMain
Path = "$TargetTriple\release\waggle.exe"
},
[pscustomobject]@{
Source = $sourceDependency
Path = "$TargetTriple\release\deps\waggle.exe"
},
[pscustomobject]@{ Source = $sourceResource; Path = 'resources\service.js' },
[pscustomobject]@{ Source = $sourceResourceTwo; Path = 'resources\model.json' }
)
$resourceMappings = @(
[pscustomobject]@{ Source = $sourceResource; Path = 'service.js' },
[pscustomobject]@{ Source = $sourceResourceTwo; Path = 'model.json' }
)
$nsisMappings = @(
[pscustomobject]@{ Source = $sourceNsis; Path = 'makensis.exe' },
[pscustomobject]@{ Source = $sourceNsisDll; Path = 'plugins\plugin.dll' }
)
$expectedTarget = New-HandoffInventory $targetMappings 'Fixture target'
$expectedResources = New-HandoffInventory $resourceMappings 'Fixture resources'
$expectedNsis = New-HandoffInventory $nsisMappings 'Fixture NSIS'
$sourceRevision = 'a' * 40
$checkerSha256 = 'B' * 64
$transactionArguments = @{
DestinationParentRoot = $handoffFixtureRoot
TargetMappings = $targetMappings
ResourceMappings = $resourceMappings
NsisMappings = $nsisMappings
ExpectedTargetInventory = $expectedTarget
ExpectedResourcesInventory = $expectedResources
ExpectedNsisInventory = $expectedNsis
SourceRevision = $sourceRevision
CheckerSha256 = $checkerSha256
TargetFileCountLimit = 8
TargetByteLimit = 1MB
ResourceFileCountLimit = 4
ResourceByteLimit = 1MB
NsisFileCountLimit = 4
NsisByteLimit = 1MB
}
$acceptedDestination = Join-Path $handoffFixtureRoot 'accepted'
$accepted = Invoke-HandoffDestinationTransaction `
-DestinationRoot $acceptedDestination @transactionArguments
Assert-Equal `
(Test-Path -LiteralPath $accepted.ReceiptPath -PathType Leaf) $true `
'handoff transaction publishes a receipt'
Assert-Equal `
(Get-HandoffFileSha256 $accepted.ReceiptPath) $accepted.ReceiptSha256 `
'handoff receipt digest binds exact bytes'
$acceptedReceipt = Get-Content -Raw -LiteralPath $accepted.ReceiptPath |
ConvertFrom-Json -Depth 32 -DateKind String
Assert-Equal $acceptedReceipt.schemaVersion 1 'handoff receipt schema'
Assert-Equal $acceptedReceipt.repository $ApprovedRepository 'handoff receipt repository'
Assert-Equal $acceptedReceipt.sourceRevision $sourceRevision `
'handoff receipt source revision'
Assert-Equal $acceptedReceipt.targetInventory.sha256 $expectedTarget.sha256 `
'handoff receipt target inventory'
Assert-Equal $acceptedReceipt.resourcesInventory.sha256 $expectedResources.sha256 `
'handoff receipt resource inventory'
Assert-Equal $acceptedReceipt.nsisInventory.sha256 $expectedNsis.sha256 `
'handoff receipt NSIS inventory'
$acceptedRelease = Join-Path `
$accepted.PrebuiltRoot "$TargetTriple\release"
Assert-HandoffCargoPair $acceptedRelease 'Accepted staged Cargo output'
$passed++
foreach ($boundProbe in @(
[pscustomobject]@{ Name = 'target-count'; Overrides = @{ TargetFileCountLimit = 3 } },
[pscustomobject]@{
Name = 'target-bytes'
Overrides = @{
TargetByteLimit = ([long](Assert-HandoffMappingBounds `
$targetMappings 8 1MB 'Fixture target bytes')) - 1
}
},
[pscustomobject]@{ Name = 'resource-count'; Overrides = @{ ResourceFileCountLimit = 1 } },
[pscustomobject]@{ Name = 'nsis-count'; Overrides = @{ NsisFileCountLimit = 1 } }
)) {
$probeDestination = Join-Path $handoffFixtureRoot ([string]$boundProbe.Name)
$probeArguments = @{} + $transactionArguments
foreach ($override in $boundProbe.Overrides.GetEnumerator()) {
$probeArguments[$override.Key] = $override.Value
}
Assert-Throws {
Invoke-HandoffDestinationTransaction `
-DestinationRoot $probeDestination @probeArguments | Out-Null
} 'deterministic file-count or byte bound' `
"handoff $($boundProbe.Name) rejection"
Assert-Equal (Test-Path -LiteralPath $probeDestination) $false `
"handoff $($boundProbe.Name) leaves no destination"
}
$tamperedDestination = Join-Path $handoffFixtureRoot 'tampered-stage'
Assert-Throws {
Invoke-HandoffDestinationTransaction `
-DestinationRoot $tamperedDestination @transactionArguments `
-FinalSourceAssertion {
param([string]$StagedRoot)
[IO.File]::WriteAllText(
(Join-Path $StagedRoot 'resources\service.js'),
'tampered-after-copy'
)
} | Out-Null
} 'inventory changed during handoff staging' `
'handoff tampered staged inventory rejection'
Assert-Equal (Test-Path -LiteralPath $tamperedDestination) $false `
'handoff tamper rollback removes the entire destination'
$collisionMappings = @($resourceMappings) + @(
[pscustomobject]@{ Source = $sourceResource; Path = 'SERVICE.JS' }
)
Assert-Throws {
New-HandoffInventory $collisionMappings 'Fixture collision' | Out-Null
} 'duplicate path or case-insensitive collision' `
'handoff case-insensitive collision rejection'
Assert-Throws {
Invoke-HandoffDestinationTransaction `
-DestinationRoot (Join-Path $handoffFixtureRoot '..\escape') `
@transactionArguments | Out-Null
} 'safe, fully qualified local Windows path|absent direct child' `
'handoff unsafe destination rejection'
$adsRoot = Join-Path $handoffFixtureRoot 'ads-tree'
[IO.Directory]::CreateDirectory($adsRoot) | Out-Null
$adsFile = Join-Path $adsRoot 'payload.bin'
[IO.File]::WriteAllText($adsFile, 'visible')
[IO.File]::WriteAllText("${adsFile}:hidden", 'hidden')
Assert-Throws {
Get-HandoffTreeFiles $adsRoot 'Fixture ADS tree' | Out-Null
} 'alternate data stream' 'handoff ADS rejection'
$reparseTarget = Join-Path $handoffFixtureRoot 'reparse-target'
$reparseRoot = Join-Path $handoffFixtureRoot 'reparse-tree'
[IO.Directory]::CreateDirectory($reparseTarget) | Out-Null
[IO.Directory]::CreateDirectory($reparseRoot) | Out-Null
[IO.File]::WriteAllText((Join-Path $reparseTarget 'payload.bin'), 'outside')
$handoffReparsePath = Join-Path $reparseRoot 'linked'
New-Item -ItemType Junction -Path $handoffReparsePath `
-Target $reparseTarget | Out-Null
Assert-Throws {
Get-HandoffTreeFiles $reparseRoot 'Fixture reparse tree' | Out-Null
} 'reparse point|linked directory' 'handoff reparse rejection'
$git = [string](
Get-Command git.exe -CommandType Application -ErrorAction Stop |
Select-Object -First 1 -ExpandProperty Source
)
$repoFixture = Join-Path $handoffFixtureRoot 'repo'
[IO.Directory]::CreateDirectory($repoFixture) | Out-Null
& $git -C $repoFixture init --quiet
& $git -C $repoFixture config user.email 'handoff-test@invalid.example'
& $git -C $repoFixture config user.name 'Waggle Handoff Test'
for ($index = 0; $index -lt 101; $index++) {
[IO.File]::WriteAllText(
(Join-Path $repoFixture ("input-{0:D3}.txt" -f $index)),
"canonical-$index",
[Text.UTF8Encoding]::new($false)
)
}
& $git -C $repoFixture add -- .
& $git -C $repoFixture commit --quiet -m 'fixture'
& $git -C $repoFixture tag v1.0.0
$fixtureRevision = [string](& $git -C $repoFixture rev-parse HEAD)
$trackedFixture = @('input-000.txt')
Assert-HandoffRepositoryState `
$git $repoFixture $fixtureRevision '1.0.0' $trackedFixture
$passed++
[IO.File]::AppendAllText((Join-Path $repoFixture 'input-000.txt'), 'dirty')
Assert-Throws {
Assert-HandoffRepositoryState `
$git $repoFixture $fixtureRevision '1.0.0' $trackedFixture
} 'clean exact tagged' 'handoff dirty repository rejection'
& $git -C $repoFixture checkout --quiet -- input-000.txt
& $git -C $repoFixture tag -d v1.0.0 | Out-Null
Assert-Throws {
Assert-HandoffRepositoryState `
$git $repoFixture $fixtureRevision '1.0.0' $trackedFixture 2>$null
} 'clean exact tagged' 'handoff missing release tag rejection'
& $git -C $repoFixture tag v1.0.0
& $git -C $repoFixture update-index --skip-worktree input-000.txt
Assert-Throws {
Assert-HandoffRepositoryState `
$git $repoFixture $fixtureRevision '1.0.0' $trackedFixture
} 'non-default tracked state' 'handoff skip-worktree index rejection'
& $git -C $repoFixture update-index --no-skip-worktree input-000.txt
} finally {
if ($null -ne $handoffReparsePath -and
(Test-Path -LiteralPath $handoffReparsePath)) {
[IO.Directory]::Delete($handoffReparsePath)
}
if (Test-Path -LiteralPath $handoffFixtureRoot) {
foreach ($fixtureItem in @(Get-ChildItem `
-LiteralPath $handoffFixtureRoot -Force -Recurse)) {
if (($fixtureItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -eq 0) {
$fixtureItem.Attributes = [IO.FileAttributes]::Normal
}
}
[IO.Directory]::Delete($handoffFixtureRoot, $true)
}
}
}
& {
$portableFixtureParent = Join-Path `
([IO.Path]::GetTempPath()) `
"waggle-portable-tools-$([Guid]::NewGuid().ToString('N'))"
[IO.Directory]::CreateDirectory($portableFixtureParent) | Out-Null
$portableRoot = New-PrivateDirectory (Join-Path $portableFixtureParent 'private')
$node = Join-Path $portableRoot 'node\node.exe'
$git = Join-Path $portableRoot 'git\cmd\git.exe'
$gitRuntime = Join-Path $portableRoot 'git\mingw64\bin\git.exe'
$gitDependency = Join-Path $portableRoot 'git\mingw64\bin\libcrypto-3-x64.dll'
$sevenZip = Join-Path $portableRoot 'sevenzip\7z.exe'
$sevenZipDll = Join-Path $portableRoot 'sevenzip\7z.dll'
$outsideNode = Join-Path $portableFixtureParent 'outside-node.exe'
foreach ($path in @(
$node, $git, $gitRuntime, $gitDependency,
$sevenZip, $sevenZipDll, $outsideNode
)) {
[IO.Directory]::CreateDirectory((Split-Path $path -Parent)) | Out-Null
[IO.File]::WriteAllText($path, "fixture:$([IO.Path]::GetFileName($path))")
}
function Get-FileHash {
param([string]$LiteralPath, [string]$Algorithm)
if ($null -ne (Get-Variable receiptPath -ErrorAction SilentlyContinue) -and
[string]::Equals(
[IO.Path]::GetFullPath($LiteralPath),
[IO.Path]::GetFullPath($receiptPath),
[StringComparison]::OrdinalIgnoreCase
)) {
return [pscustomobject]@{ Hash = Get-RealSha256 $LiteralPath }
}
$content = [IO.File]::ReadAllText($LiteralPath)
$hash = if ($content -match 'tampered') {
'0' * 64
} else {
switch ([IO.Path]::GetFileName($LiteralPath)) {
'node.exe' { $NodeSha256 }
'git.exe' {
if ($LiteralPath -match 'mingw64') { $GitRuntimeSha256 } else { $GitSha256 }
}
'7z.exe' { $SevenZipSha256 }
'7z.dll' { $SevenZipDllSha256 }
'node.zip' { $NodeArchiveSha256 }
'mingit.zip' { $GitArchiveSha256 }
'sevenzip.exe' { $SevenZipArchiveSha256 }
default { 'F' * 64 }
}
}
return [pscustomobject]@{ Hash = $hash }
}
$archiveRoot = Join-Path $portableFixtureParent 'downloads'
[IO.Directory]::CreateDirectory($archiveRoot) | Out-Null
$nodeArchive = Join-Path $archiveRoot 'node.zip'
$gitArchive = Join-Path $archiveRoot 'mingit.zip'
$sevenZipArchive = Join-Path $archiveRoot 'sevenzip.exe'
foreach ($archive in @($nodeArchive, $gitArchive, $sevenZipArchive)) {
[IO.File]::WriteAllText($archive, 'fixture:vendor-archive')
}
$portableInventory = New-WagglePrebuiltInventory -Root $portableRoot
$receiptPath = Join-Path $portableFixtureParent 'portable-receipt.json'
Write-WaggleJsonNoBom $receiptPath ([ordered]@{
schemaVersion = 1
portableToolchainRoot = $portableRoot
archives = [ordered]@{
node = [ordered]@{ path = $nodeArchive; sha256 = $NodeArchiveSha256 }
git = [ordered]@{ path = $gitArchive; sha256 = $GitArchiveSha256 }
sevenZip = [ordered]@{ path = $sevenZipArchive; sha256 = $SevenZipArchiveSha256 }
}
inventory = $portableInventory
})
$portableReceiptJson = [IO.File]::ReadAllText($receiptPath)
$receiptSha256 = Get-RealSha256 $receiptPath
$savedPortableFileCount = $PortableToolchainFileCount
$savedPortableInventorySha256 = $PortableToolchainInventorySha256
$PortableToolchainFileCount = @($portableInventory.entries).Count
$PortableToolchainInventorySha256 = $portableInventory.sha256
function Get-AuthenticodeSignature {
param([string]$LiteralPath)
$content = [IO.File]::ReadAllText($LiteralPath)
$subject = switch ([IO.Path]::GetFileName($LiteralPath)) {
'node.exe' { $NodePublisher }
'git.exe' {
if ($content -match 'wrong-publisher') { $MicrosoftPublisher } else { $GitPublisher }
}
'dotnet.exe' {
if ($content -match 'wrong-publisher') { $MicrosoftPublisher } else { $DotNetPublisher }
}
default { $MicrosoftPublisher }
}
$oids = [Security.Cryptography.OidCollection]::new()
[void]$oids.Add([Security.Cryptography.Oid]::new($CodeSigningOid))
$eku = [Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new(
$oids,
$false
)
return [pscustomobject]@{
Status = [Management.Automation.SignatureStatus]::Valid
SignatureType = 'Authenticode'
SignerCertificate = [pscustomobject]@{
Subject = $subject
Extensions = @($eku)
}
}
}
$portableEnvironmentNames = @(
'WAGGLE_SIGNING_PORTABLE_TOOLCHAIN_ROOT',
'WAGGLE_SIGNING_PORTABLE_NODE_PATH',
'WAGGLE_SIGNING_PORTABLE_GIT_PATH',
'WAGGLE_SIGNING_PORTABLE_SEVEN_ZIP_PATH',
'WAGGLE_SIGNING_PORTABLE_RECEIPT_PATH',
'WAGGLE_SIGNING_PORTABLE_RECEIPT_SHA256',
'WAGGLE_SIGNING_MANIFEST_PATH',
'WAGGLE_SIGNING_MANIFEST_SHA256'
)
$savedPortableEnvironment = @{}
foreach ($name in $portableEnvironmentNames) {
$savedPortableEnvironment[$name] = [Environment]::GetEnvironmentVariable($name)
}
try {
$portable = Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath $receiptSha256
Assert-Equal $portable.NodePath $node 'portable Node path validation'
Assert-Equal $portable.GitPath $git 'portable Git path validation'
Assert-Equal $portable.GitRuntimePath $gitRuntime `
'portable Git runtime closure validation'
Assert-Equal $portable.SevenZipDllPath $sevenZipDll `
'portable 7-Zip closure validation'
foreach ($lock in $portable.Locks) { $lock.Dispose() }
[IO.File]::AppendAllText(
$receiptPath, ' ', [Text.UTF8Encoding]::new($false)
)
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath $receiptSha256
} 'does not match its handoff SHA-256' `
'portable raw receipt byte tamper with stale digest rejection'
[IO.File]::WriteAllText(
$receiptPath, $portableReceiptJson, [Text.UTF8Encoding]::new($false)
)
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath ('E' * 64)
} 'does not match its handoff SHA-256' 'portable receipt handoff digest rejection'
$mutatedReceipt = $portableReceiptJson | ConvertFrom-Json -Depth 32
$mutatedReceipt.schemaVersion = 2
Write-WaggleJsonNoBom $receiptPath $mutatedReceipt
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath `
(Get-RealSha256 $receiptPath)
} 'schemaVersion must be 1' 'portable receipt schema rejection'
[IO.File]::WriteAllText(
$receiptPath, $portableReceiptJson, [Text.UTF8Encoding]::new($false)
)
$mutatedReceipt = $portableReceiptJson | ConvertFrom-Json -Depth 32
$mutatedReceipt.portableToolchainRoot = $portableFixtureParent
Write-WaggleJsonNoBom $receiptPath $mutatedReceipt
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath `
(Get-RealSha256 $receiptPath)
} 'receipt root' 'portable receipt root substitution rejection'
[IO.File]::WriteAllText(
$receiptPath, $portableReceiptJson, [Text.UTF8Encoding]::new($false)
)
$mutatedReceipt = $portableReceiptJson | ConvertFrom-Json -Depth 32
$mutatedReceipt.archives.node.sha256 = '0' * 64
Write-WaggleJsonNoBom $receiptPath $mutatedReceipt
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath `
(Get-RealSha256 $receiptPath)
} 'exact distinct repository-pinned handoff' `
'portable receipt recomputed archive hash rejection'
[IO.File]::WriteAllText(
$receiptPath, $portableReceiptJson, [Text.UTF8Encoding]::new($false)
)
$mutatedReceipt = $portableReceiptJson | ConvertFrom-Json -Depth 32
$mutatedReceipt.archives.node.path = $gitArchive
Write-WaggleJsonNoBom $receiptPath $mutatedReceipt
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath `
(Get-RealSha256 $receiptPath)
} 'exact distinct repository-pinned handoff|does not match its pinned SHA-256' `
'portable receipt recomputed archive path rejection'
[IO.File]::WriteAllText(
$receiptPath, $portableReceiptJson, [Text.UTF8Encoding]::new($false)
)
$mutatedReceipt = $portableReceiptJson | ConvertFrom-Json -Depth 32
$mutatedReceipt.inventory.sha256 = '0' * 64
Write-WaggleJsonNoBom $receiptPath $mutatedReceipt
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath `
(Get-RealSha256 $receiptPath)
} 'repository-pinned full closure' `
'portable receipt recomputed inventory aggregate rejection'
[IO.File]::WriteAllText(
$receiptPath, $portableReceiptJson, [Text.UTF8Encoding]::new($false)
)
$mutatedReceipt = $portableReceiptJson | ConvertFrom-Json -Depth 32
$mutatedReceipt.inventory.entries[0].sha256 = '0' * 64
Write-WaggleJsonNoBom $receiptPath $mutatedReceipt
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath `
(Get-RealSha256 $receiptPath)
} 'inventory aggregate SHA-256 digest is invalid' `
'portable receipt recomputed inventory entry rejection'
[IO.File]::WriteAllText(
$receiptPath, $portableReceiptJson, [Text.UTF8Encoding]::new($false)
)
$mutatedReceipt = $portableReceiptJson | ConvertFrom-Json -Depth 32
$mutatedReceipt.inventory.entries = @($mutatedReceipt.inventory.entries)[1..(
@($mutatedReceipt.inventory.entries).Count - 1
)]
Write-WaggleJsonNoBom $receiptPath $mutatedReceipt
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath `
(Get-RealSha256 $receiptPath)
} 'repository-pinned full closure' `
'portable receipt recomputed inventory full-count rejection'
[IO.File]::WriteAllText(
$receiptPath, $portableReceiptJson, [Text.UTF8Encoding]::new($false)
)
$unexpectedDependency = Join-Path $portableRoot 'git\mingw64\bin\injected.dll'
[IO.File]::WriteAllText($unexpectedDependency, 'unexpected')
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath $receiptSha256
} 'missing, extra, or unexpected files' `
'portable unexpected adjacent dependency rejection'
[IO.File]::Delete($unexpectedDependency)
[IO.File]::WriteAllText($gitDependency, 'tampered adjacent dependency')
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath $receiptSha256
} 'inventory SHA-256 digest and size' `
'portable mutated adjacent dependency rejection'
[IO.File]::WriteAllText($gitDependency, 'fixture:libcrypto-3-x64.dll')
[IO.File]::Delete($gitDependency)
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath $receiptSha256
} 'missing, extra, or unexpected files' `
'portable missing adjacent dependency rejection'
[IO.File]::WriteAllText($gitDependency, 'fixture:libcrypto-3-x64.dll')
[IO.File]::WriteAllText($node, 'tampered')
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath $receiptSha256
} 'inventory SHA-256 digest and size' 'portable Node tamper rejection'
[IO.File]::WriteAllText($node, 'fixture:node.exe')
[IO.File]::WriteAllText($git, 'wrong-publisher')
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath $receiptSha256
} 'Git executable.*approved publisher' 'portable Git publisher rejection'
[IO.File]::WriteAllText($git, 'fixture:git.exe')
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $outsideNode $git $sevenZip $receiptPath $receiptSha256
} 'contained by the private portable toolchain root' `
'portable tool path containment rejection'
Assert-Throws {
Get-WagglePortableToolchain `
$portableRoot $node $git $sevenZip $receiptPath $receiptSha256 `
-DisallowedRoots @($portableFixtureParent)
} 'overlaps.*boundary' 'portable root overlap rejection'
[Environment]::SetEnvironmentVariable(
'WAGGLE_SIGNING_PORTABLE_TOOLCHAIN_ROOT', $portableRoot
)
[Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_NODE_PATH', $node)
[Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_GIT_PATH', $git)
[Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_SEVEN_ZIP_PATH', $sevenZip)
[Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_RECEIPT_PATH', $receiptPath)
[Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_RECEIPT_SHA256', $receiptSha256)
[Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_PATH', $null)
[Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_SHA256', $null)
Assert-Throws {
Get-WaggleSigningToolchain
} 'active receipt-bound signing session' `
'portable callback outside receipt-bound session rejection'
$resolvedPortable = Get-WaggleSigningToolchain -AllowPortableBeforeManifest
Assert-Equal $resolvedPortable.PortableToolchainRoot $portableRoot `
'Package setup resolves exact portable root'
foreach ($lock in $resolvedPortable.Locks) { $lock.Dispose() }
[Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_NODE_PATH', $null)
Assert-Throws {
Get-WaggleSigningToolchain -AllowPortableBeforeManifest
} 'must provide the exact root, Node, Git, 7-Zip, receipt path, and receipt SHA-256' `
'partial portable environment rejection'
$dotnetFixture = Join-Path $portableRoot 'dotnet.exe'
[IO.File]::WriteAllText($dotnetFixture, 'wrong-publisher')
Assert-Throws {
Assert-MicrosoftAuthenticodeFile `
$dotnetFixture '.NET host' $null $DotNetPublisher
} 'approved publisher' 'generic Microsoft signer rejected for .NET host'
[IO.File]::WriteAllText($dotnetFixture, 'exact-dotnet-publisher')
Assert-MicrosoftAuthenticodeFile `
$dotnetFixture '.NET host' $null $DotNetPublisher
$script:passed++
} finally {
$PortableToolchainFileCount = $savedPortableFileCount
$PortableToolchainInventorySha256 = $savedPortableInventorySha256
foreach ($name in $portableEnvironmentNames) {
[Environment]::SetEnvironmentVariable($name, $savedPortableEnvironment[$name])
}
[IO.Directory]::Delete($portableFixtureParent, $true)
}
}
Assert-Equal `
(Get-ArtifactPolicyKind 'D:\repo\app\src-tauri\target\x86_64-pc-windows-msvc\release\waggle.exe' $releaseRoot $resourcesRoot $tempRoot $version) `
'PE' 'main executable policy'
Assert-Equal `
(Get-ArtifactPolicyKind 'D:\repo\app\src-tauri\target\x86_64-pc-windows-msvc\release\bundle\nsis\Waggle_0.2.0_x64-setup.exe' $releaseRoot $resourcesRoot $tempRoot $version) `
'PE' 'NSIS installer policy'
Assert-Throws {
Get-ArtifactPolicyKind 'D:\repo\app\src-tauri\target\x86_64-pc-windows-msvc\release\bundle\msi\Waggle_0.2.0_x64_en-US.msi' $releaseRoot $resourcesRoot $tempRoot $version
} 'approved Tauri release manifest' 'MSI rejection for NSIS-only production signing'
Assert-Throws {
Get-ArtifactPolicyKind 'D:\repo\app\src-tauri\target\x86_64-pc-windows-msvc\release\wix\x64\wix\WixUIExtension.dll' $releaseRoot $resourcesRoot $tempRoot $version
} 'approved Tauri release manifest' 'WiX rejection for NSIS-only production signing'
Assert-Equal `
(Get-ArtifactPolicyKind 'D:\repo\app\src-tauri\resources\native\vec0.dll' $releaseRoot $resourcesRoot $tempRoot $version) `
'PE' 'resource policy'
Assert-Equal `
(Get-ArtifactPolicyKind 'D:\repo\app\src-tauri\target\.signing-temp\run-123456\nst9DA7.tmp' $releaseRoot $resourcesRoot $tempRoot $version) `
'PE' 'NSIS uninstaller policy'
Assert-Throws {
Get-ArtifactPolicyKind 'D:\outside\malware.exe' $releaseRoot $resourcesRoot $tempRoot $version
} 'outside every approved' 'outside-root rejection'
Assert-Throws {
Get-ArtifactPolicyKind 'D:\repo\app\src-tauri\target\x86_64-pc-windows-msvc\release-evil\waggle.exe' $releaseRoot $resourcesRoot $tempRoot $version
} 'outside every approved' 'prefix-collision rejection'
Assert-Throws {
Get-ArtifactPolicyKind 'D:\repo\app\src-tauri\target\.signing-temp\run-123456\evil.tmp' $releaseRoot $resourcesRoot $tempRoot $version
} 'outside every approved' 'wrong temp name rejection'
Assert-Throws {
Get-ArtifactPolicyKind 'D:\repo\app\src-tauri\resources\node.exe' $releaseRoot $resourcesRoot $tempRoot $version
} 'resource manifest' 'vendor binary rejection'
Assert-Throws {
Get-TrustedPath 'D:\safe\..\outside.exe' 'Traversal probe'
} 'safe, fully qualified' 'traversal rejection'
Assert-Throws {
Get-TrustedPath '\\server\share\outside.exe' 'UNC probe'
} 'safe, fully qualified' 'UNC rejection'
Assert-Throws {
Get-TrustedPath '\\?\C:\outside.exe' 'Device probe'
} 'safe, fully qualified' 'device path rejection'
Assert-Throws {
Get-TrustedPath 'D:\outside.exe:stream' 'ADS probe'
} 'safe, fully qualified' 'ADS rejection'
$probeRoot = Join-Path ([IO.Path]::GetTempPath()) "waggle-signing-policy-$([Guid]::NewGuid().ToString('N'))"
$targetDirectory = Join-Path $probeRoot 'target'
$junctionPath = Join-Path $probeRoot 'junction'
$fakeToolDirectory = Join-Path $probeRoot 'x64'
$hardLinkReleaseRoot = Join-Path $probeRoot 'release'
try {
[IO.Directory]::CreateDirectory($targetDirectory) | Out-Null
[IO.File]::WriteAllText((Join-Path $targetDirectory 'payload.exe'), 'not a PE')
New-Item -ItemType Junction -Path $junctionPath -Target $targetDirectory | Out-Null
Assert-Throws {
Get-TrustedPath (Join-Path $junctionPath 'payload.exe') 'Junction probe'
} 'reparse point|linked filesystem' 'junction rejection'
[IO.Directory]::CreateDirectory($fakeToolDirectory) | Out-Null
$patchableMain = Join-Path $probeRoot 'patchable-waggle.exe'
New-SyntheticPatchablePe $patchableMain
$expectedPatchedBytes = [IO.File]::ReadAllBytes($patchableMain)
$expectedToken = [Text.Encoding]::ASCII.GetBytes('__TAURI_BUNDLE_TYPE_VAR_NSS')
[Array]::Copy($expectedToken, 0, $expectedPatchedBytes, 160, $expectedToken.Length)
$expectedPatchedHash = [Convert]::ToHexString(
[Security.Cryptography.SHA256]::HashData($expectedPatchedBytes)
)
Assert-Equal `
(Get-NsisPatchedMainSha256 $patchableMain) `
$expectedPatchedHash `
'Tauri NSIS main patch pre-authorization hash'
$missingPatchToken = Join-Path $probeRoot 'missing-patch-token.exe'
New-SyntheticPe $missingPatchToken
Assert-Throws {
Get-NsisPatchedMainSha256 $missingPatchToken
} 'exactly one unpatched' 'missing Tauri bundle token rejection'
$duplicatePatchToken = Join-Path $probeRoot 'duplicate-patch-token.exe'
$duplicateBytes = [byte[]]::new(320)
$sourceBytes = [IO.File]::ReadAllBytes($patchableMain)
[Array]::Copy($sourceBytes, $duplicateBytes, $sourceBytes.Length)
$unknownToken = [Text.Encoding]::ASCII.GetBytes('__TAURI_BUNDLE_TYPE_VAR_UNK')
[Array]::Copy($unknownToken, 0, $duplicateBytes, 270, $unknownToken.Length)
[IO.File]::WriteAllBytes($duplicatePatchToken, $duplicateBytes)
Assert-Throws {
Get-NsisPatchedMainSha256 $duplicatePatchToken
} 'exactly one unpatched' 'duplicate Tauri bundle token rejection'
$fakeSignTool = Join-Path $fakeToolDirectory 'signtool.exe'
$fakeDlib = Join-Path $fakeToolDirectory 'Azure.CodeSigning.Dlib.dll'
[IO.File]::WriteAllText($fakeSignTool, 'impostor')
[IO.File]::WriteAllText($fakeDlib, 'impostor')
Assert-Throws {
Assert-MicrosoftAuthenticodeFile `
(Get-TrustedPath $fakeSignTool 'Fake SignTool') 'Fake SignTool' $null
} 'not validly Authenticode-signed by the approved publisher' 'fake SignTool rejection'
Assert-Throws {
Assert-MicrosoftAuthenticodeFile `
(Get-TrustedPath $fakeDlib 'Fake dlib') 'Fake dlib' $ArtifactSigningDlibSha256
} 'not validly Authenticode-signed by the approved publisher' 'fake dlib rejection'
$currentPowerShell = Get-TrustedPath (Get-Process -Id $PID).Path 'Current PowerShell'
Assert-MicrosoftAuthenticodeFile $currentPowerShell 'Current PowerShell' $null
$passed++
$approvedPowerShell = Get-ApprovedPowerShell7Path
Assert-ApprovedPowerShell7Path $approvedPowerShell
$passed++
Assert-FreshProcessModuleIsolation `
$approvedPowerShell 'PowerShell 7' $probeRoot
if (Test-Path -LiteralPath $SystemPowerShellPath -PathType Leaf) {
Assert-FreshProcessModuleIsolation `
$SystemPowerShellPath 'Windows PowerShell' $probeRoot
}
$privateDirectory = New-PrivateDirectory (Join-Path $probeRoot 'private')
if (-not (Get-Acl -LiteralPath $privateDirectory).AreAccessRulesProtected) {
throw 'Private signing directory test still inherits permissions.'
}
$passed++
if ((New-PrivateDirectory $privateDirectory) -cne $privateDirectory) {
throw 'Private signing directory hardening is not idempotent.'
}
$passed++
$readLock = Open-ReadLock $fakeSignTool
try {
Assert-Throws {
$writeProbe = [IO.File]::Open(
$fakeSignTool,
[IO.FileMode]::Open,
[IO.FileAccess]::Write,
[IO.FileShare]::None
)
$writeProbe.Dispose()
} 'used by another process|cannot access' 'read-lock write rejection'
} finally {
$readLock.Dispose()
}
$hardLinkDeps = Join-Path $hardLinkReleaseRoot 'deps'
[IO.Directory]::CreateDirectory($hardLinkDeps) | Out-Null
$hardLinkDependency = Join-Path $hardLinkDeps 'waggle.exe'
$hardLinkMain = Join-Path $hardLinkReleaseRoot 'waggle.exe'
[IO.File]::WriteAllText($hardLinkDependency, 'cargo executable probe')
New-Item -ItemType HardLink -Path $hardLinkMain -Target $hardLinkDependency | Out-Null
$originalSystemRoot = $env:SystemRoot
try {
$env:SystemRoot = $probeRoot
Assert-Equal `
(Get-SystemFsutilPath) `
'C:\Windows\System32\fsutil.exe' `
'fixed fsutil path ignores environment'
} finally {
$env:SystemRoot = $originalSystemRoot
}
Assert-ApprovedHardLinkTopology $hardLinkMain $hardLinkReleaseRoot
$passed++
$detachedReleaseRoot = Join-Path $probeRoot 'detached-release'
[IO.Directory]::CreateDirectory($detachedReleaseRoot) | Out-Null
$detachedMain = Join-Path $detachedReleaseRoot 'waggle.exe'
[IO.File]::WriteAllText($detachedMain, 'detached executable probe')
Assert-Throws {
Assert-ApprovedHardLinkTopology $detachedMain $detachedReleaseRoot
} 'must have.*hard-link topology' 'detached Cargo main rejection'
Assert-ApprovedHardLinkTopology $detachedMain $detachedReleaseRoot -AllowDetachedMain
$passed++
$rogueHardLink = Join-Path $probeRoot 'rogue.exe'
New-Item -ItemType HardLink -Path $rogueHardLink -Target $hardLinkDependency | Out-Null
Assert-Throws {
Assert-ApprovedHardLinkTopology $hardLinkMain $hardLinkReleaseRoot
} 'unexpected hard-link sibling' 'rogue hard-link rejection'
} finally {
if (Test-Path -LiteralPath $junctionPath) { [IO.Directory]::Delete($junctionPath) }
if (Test-Path -LiteralPath $probeRoot) { [IO.Directory]::Delete($probeRoot, $true) }
}
$prebuiltFixtureRoot = Join-Path `
([IO.Path]::GetTempPath()) `
"waggle-prebuilt-tree-$([Guid]::NewGuid().ToString('N'))"
$reparseDirectory = $null
try {
$seedRoot = Join-Path $prebuiltFixtureRoot 'seed'
[IO.Directory]::CreateDirectory((Join-Path $seedRoot 'Bin')) | Out-Null
[IO.Directory]::CreateDirectory((Join-Path $seedRoot 'resources')) | Out-Null
[IO.File]::WriteAllBytes(
(Join-Path $seedRoot 'Bin\Payload.bin'),
[Text.Encoding]::UTF8.GetBytes('immutable-payload-one')
)
[IO.File]::WriteAllBytes(
(Join-Path $seedRoot 'resources\data.bin'),
[Text.Encoding]::UTF8.GetBytes('immutable-payload-two')
)
$expectedInventory = New-WagglePrebuiltInventory -Root $seedRoot
if (@($expectedInventory.entries).Count -ne 2 -or
[string]$expectedInventory.sha256 -notmatch '^[0-9A-Fa-f]{64}$') {
throw 'Prebuilt inventory lacks its exact canonical entries and aggregate digest.'
}
$passed++
$treeLease = Open-WaggleValidatedPrebuiltTree `
-Root $seedRoot -ExpectedInventory $expectedInventory -Label 'Pristine prebuilt tree'
try {
foreach ($entry in @($expectedInventory.entries)) {
$lockedPath = Join-Path $seedRoot ([string]$entry.path)
Assert-Throws {
$writeProbe = [IO.File]::Open(
$lockedPath,
[IO.FileMode]::Open,
[IO.FileAccess]::Write,
[IO.FileShare]::None
)
$writeProbe.Dispose()
} 'used by another process|cannot access' 'validated prebuilt tree holds every file read-locked'
}
} finally {
Close-PrebuiltTreeLease $treeLease
}
$mutablePath = Join-Path $seedRoot 'Bin\Payload.bin'
$immutablePath = Join-Path $seedRoot 'resources\data.bin'
$mutableLease = Open-WaggleValidatedPrebuiltTree `
-Root $seedRoot -ExpectedInventory $expectedInventory `
-Label 'Signing-mutable prebuilt tree' -MutablePaths @($mutablePath)
try {
$mutableWriteProbe = [IO.File]::Open(
$mutablePath,
[IO.FileMode]::Open,
[IO.FileAccess]::Write,
[IO.FileShare]::None
)
$mutableWriteProbe.Dispose()
$passed++
Assert-Throws {
$immutableWriteProbe = [IO.File]::Open(
$immutablePath,
[IO.FileMode]::Open,
[IO.FileAccess]::Write,
[IO.FileShare]::None
)
$immutableWriteProbe.Dispose()
} 'used by another process|cannot access' 'mutable tree keeps immutable files read-locked'
} finally {
Close-PrebuiltTreeLease $mutableLease
}
foreach ($passName in @('unsigned-generated-roots', 'signing-generated-roots')) {
$generatedTreeRoot = Join-Path $prebuiltFixtureRoot $passName
$generatedReleaseRoot = Join-Path `
$generatedTreeRoot 'x86_64-pc-windows-msvc\release'
$generatedNsisRoot = Join-Path $generatedReleaseRoot 'nsis'
$generatedBundleNsisRoot = Join-Path $generatedReleaseRoot 'bundle\nsis'
$generatedNsisSiblingRoot = Join-Path $generatedReleaseRoot 'nsis-evil'
$generatedBundleSiblingRoot = Join-Path $generatedReleaseRoot 'bundle\nsis-old'
$generatedResourceRoot = Join-Path $generatedTreeRoot 'resources'
foreach ($directory in @(
$generatedNsisRoot, $generatedBundleNsisRoot,
$generatedNsisSiblingRoot, $generatedBundleSiblingRoot,
$generatedResourceRoot
)) {
[IO.Directory]::CreateDirectory($directory) | Out-Null
}
$generatedFiles = @(
[pscustomobject]@{
Path = Join-Path $generatedNsisRoot 'x64\Plugins\fixture.dll'
Content = 'tauri-nsis-output'
}
[pscustomobject]@{
Path = Join-Path $generatedBundleNsisRoot 'Waggle_fixture_x64-setup.exe'
Content = 'tauri-installer-output'
}
[pscustomobject]@{
Path = Join-Path $generatedNsisSiblingRoot 'sibling.dll'
Content = 'locked-nsis-sibling'
}
[pscustomobject]@{
Path = Join-Path $generatedBundleSiblingRoot 'sibling.exe'
Content = 'locked-bundle-sibling'
}
[pscustomobject]@{
Path = Join-Path $generatedResourceRoot 'data.bin'
Content = 'locked-resource'
}
)
foreach ($file in $generatedFiles) {
[IO.Directory]::CreateDirectory((Split-Path ([string]$file.Path) -Parent)) | Out-Null
[IO.File]::WriteAllBytes(
[string]$file.Path, [Text.Encoding]::UTF8.GetBytes([string]$file.Content)
)
}
$generatedInventory = New-WagglePrebuiltInventory -Root $generatedTreeRoot
$generatedLease = Open-WaggleValidatedPrebuiltTree `
-Root $generatedTreeRoot -ExpectedInventory $generatedInventory `
-Label "$passName private work tree" `
-RegeneratedRoots @($generatedNsisRoot, $generatedBundleNsisRoot) `
-CargoReleaseRelativePath 'x86_64-pc-windows-msvc\release'
try {
Clear-WaggleRegeneratedRoots $generatedLease
Assert-Equal `
(Test-Path -LiteralPath $generatedNsisRoot) $false `
"$passName clears the exact Tauri NSIS work root"
Assert-Equal `
(Test-Path -LiteralPath $generatedBundleNsisRoot) $false `
"$passName clears the exact Tauri installer output root"
Assert-Equal `
(Test-Path -LiteralPath $generatedNsisSiblingRoot -PathType Container) $true `
"$passName preserves the nsis-evil sibling root"
Assert-Equal `
(Test-Path -LiteralPath $generatedBundleSiblingRoot -PathType Container) $true `
"$passName preserves the bundle nsis-old sibling root"
foreach ($lockedFile in @(
(Join-Path $generatedNsisSiblingRoot 'sibling.dll'),
(Join-Path $generatedBundleSiblingRoot 'sibling.exe'),
(Join-Path $generatedResourceRoot 'data.bin')
)) {
Assert-Throws {
$writeProbe = [IO.File]::Open(
$lockedFile,
[IO.FileMode]::Open,
[IO.FileAccess]::Write,
[IO.FileShare]::None
)
$writeProbe.Dispose()
} 'used by another process|cannot access' `
"$passName retains the non-generated work-tree lock for $lockedFile"
}
} finally {
Close-PrebuiltTreeLease $generatedLease
}
}
$copySeed = {
param([string]$Name)
$destination = Join-Path $prebuiltFixtureRoot $Name
[IO.Directory]::CreateDirectory($destination) | Out-Null
Copy-Item -Path (Join-Path $seedRoot '*') -Destination $destination -Recurse
return $destination
}
$extraRoot = & $copySeed 'extra'
[IO.File]::WriteAllText((Join-Path $extraRoot 'unexpected.bin'), 'unexpected')
Assert-Throws {
Open-WaggleValidatedPrebuiltTree `
-Root $extraRoot -ExpectedInventory $expectedInventory -Label 'Extra-file tree'
} 'extra|unexpected|inventory' 'prebuilt tree extra-file rejection'
$missingRoot = & $copySeed 'missing'
[IO.File]::Delete((Join-Path $missingRoot 'resources\data.bin'))
Assert-Throws {
Open-WaggleValidatedPrebuiltTree `
-Root $missingRoot -ExpectedInventory $expectedInventory -Label 'Missing-file tree'
} 'missing|inventory' 'prebuilt tree missing-file rejection'
$mutatedRoot = & $copySeed 'same-size-mutation'
$mutatedPath = Join-Path $mutatedRoot 'Bin\Payload.bin'
$originalLength = (Get-Item -LiteralPath $mutatedPath).Length
[IO.File]::WriteAllBytes(
$mutatedPath,
[Text.Encoding]::UTF8.GetBytes('tampered-payload-one!')
)
Assert-Equal `
(Get-Item -LiteralPath $mutatedPath).Length `
$originalLength `
'same-size prebuilt mutation fixture'
Assert-Throws {
Open-WaggleValidatedPrebuiltTree `
-Root $mutatedRoot -ExpectedInventory $expectedInventory -Label 'Mutated tree'
} 'SHA-256|digest|inventory' 'prebuilt tree same-size mutation rejection'
$collisionInventory = $expectedInventory | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$collisionEntry = $collisionInventory.entries[0] | ConvertTo-Json -Depth 8 |
ConvertFrom-Json -Depth 8 -DateKind String
$collisionEntry.path = ([string]$collisionEntry.path).ToUpperInvariant()
$collisionInventory.entries = @($collisionInventory.entries) + @($collisionEntry)
Assert-Throws {
Open-WaggleValidatedPrebuiltTree `
-Root $seedRoot -ExpectedInventory $collisionInventory -Label 'Case-collision inventory'
} 'case-insensitive|case collision|duplicate canonical' 'prebuilt inventory case-collision rejection'
$traversalInventory = $expectedInventory | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$traversalInventory.entries[0].path = '..\outside.bin'
Assert-Throws {
Open-WaggleValidatedPrebuiltTree `
-Root $seedRoot -ExpectedInventory $traversalInventory -Label 'Traversal inventory'
} 'relative|traversal|canonical|unsafe' 'prebuilt inventory traversal rejection'
$adsRoot = & $copySeed 'alternate-data-stream'
$adsCarrier = Join-Path $adsRoot 'Bin\Payload.bin'
[IO.File]::WriteAllText("${adsCarrier}:waggle-probe", 'hidden-content')
Assert-Throws {
Open-WaggleValidatedPrebuiltTree `
-Root $adsRoot -ExpectedInventory $expectedInventory -Label 'ADS tree'
} 'alternate data stream|ADS|named stream' 'prebuilt tree ADS rejection'
$reparseRoot = & $copySeed 'reparse'
$reparseDirectory = Join-Path $reparseRoot 'resources'
$reparseTarget = Join-Path $prebuiltFixtureRoot 'reparse-target'
Move-Item -LiteralPath $reparseDirectory -Destination $reparseTarget
New-Item -ItemType Junction -Path $reparseDirectory -Target $reparseTarget | Out-Null
Assert-Throws {
Open-WaggleValidatedPrebuiltTree `
-Root $reparseRoot -ExpectedInventory $expectedInventory -Label 'Reparse tree'
} 'reparse point|linked filesystem|junction' 'prebuilt tree reparse rejection'
$unapprovedLinkRoot = & $copySeed 'unapproved-hardlink'
$unapprovedLink = Join-Path $unapprovedLinkRoot 'rogue-link.bin'
Copy-Item -LiteralPath (Join-Path $unapprovedLinkRoot 'resources\data.bin') `
-Destination $unapprovedLink
$unapprovedLinkInventory = New-WagglePrebuiltInventory -Root $unapprovedLinkRoot
[IO.File]::Delete($unapprovedLink)
New-Item -ItemType HardLink -Path $unapprovedLink `
-Target (Join-Path $unapprovedLinkRoot 'resources\data.bin') | Out-Null
Assert-Throws {
Open-WaggleValidatedPrebuiltTree `
-Root $unapprovedLinkRoot -ExpectedInventory $unapprovedLinkInventory `
-Label 'Unapproved hard-link tree'
} 'hard-link|hardlink|link topology' 'prebuilt tree unapproved hard-link rejection'
$approvedLinkRoot = Join-Path $prebuiltFixtureRoot 'approved-hardlink'
$approvedDependency = Join-Path $approvedLinkRoot 'release\deps\waggle.exe'
$approvedMain = Join-Path $approvedLinkRoot 'release\waggle.exe'
[IO.Directory]::CreateDirectory((Split-Path $approvedDependency -Parent)) | Out-Null
New-SyntheticPe $approvedDependency 91
New-Item -ItemType HardLink -Path $approvedMain -Target $approvedDependency | Out-Null
$approvedLinkInventory = New-WagglePrebuiltInventory -Root $approvedLinkRoot
$approvedLinkLease = Open-WaggleValidatedPrebuiltTree `
-Root $approvedLinkRoot -ExpectedInventory $approvedLinkInventory `
-Label 'Approved Cargo hard-link tree'
try {
Assert-ApprovedHardLinkTopology `
$approvedMain (Join-Path $approvedLinkRoot 'release')
$passed++
} finally {
Close-PrebuiltTreeLease $approvedLinkLease
}
$combinedSourceRoot = Join-Path $prebuiltFixtureRoot 'combined-source'
$combinedReleaseRelative = 'x86_64-pc-windows-msvc\release'
$combinedDependency = Join-Path `
$combinedSourceRoot "$combinedReleaseRelative\deps\waggle.exe"
$combinedMain = Join-Path $combinedSourceRoot "$combinedReleaseRelative\waggle.exe"
$combinedResource = Join-Path $combinedSourceRoot 'resources\service.js'
[IO.Directory]::CreateDirectory((Split-Path $combinedDependency -Parent)) | Out-Null
[IO.Directory]::CreateDirectory((Split-Path $combinedResource -Parent)) | Out-Null
New-SyntheticPe $combinedDependency 92
New-Item -ItemType HardLink -Path $combinedMain -Target $combinedDependency | Out-Null
[IO.File]::WriteAllText($combinedResource, 'receipt-bound-sidecar')
$combinedInventory = New-WagglePrebuiltInventory `
-Root $combinedSourceRoot -CargoReleaseRelativePath $combinedReleaseRelative
$requiredDiskBytes = Assert-WaggleHostedDiskCapacity `
-Path $combinedSourceRoot -Inventory $combinedInventory -AvailableBytes ([long]::MaxValue)
Assert-Throws {
Assert-WaggleHostedDiskCapacity `
-Path $combinedSourceRoot -Inventory $combinedInventory `
-AvailableBytes ($requiredDiskBytes - 1)
} 'insufficient free disk space' 'hosted signing disk-capacity fail-fast'
$passed++
$combinedSourceLease = Open-WaggleValidatedPrebuiltTree `
-Root $combinedSourceRoot -ExpectedInventory $combinedInventory `
-Label 'Combined hosted input tree' `
-CargoReleaseRelativePath $combinedReleaseRelative
$combinedWorkRoot = Join-Path $prebuiltFixtureRoot 'combined-work'
try {
$sourceHashBeforeCopy = Get-Sha256 $combinedMain
New-WagglePrebuiltWorkCopy `
-SourceRoot $combinedSourceRoot -DestinationRoot $combinedWorkRoot `
-Inventory $combinedInventory -Label 'Combined hosted input tree' `
-CargoReleaseRelativePath $combinedReleaseRelative | Out-Null
Assert-Equal (Get-Sha256 $combinedMain) $sourceHashBeforeCopy `
'work-copy creation leaves receipt source unchanged'
$workMain = Join-Path $combinedWorkRoot "$combinedReleaseRelative\waggle.exe"
$workDependency = Join-Path `
$combinedWorkRoot "$combinedReleaseRelative\deps\waggle.exe"
Assert-ApprovedHardLinkTopology `
$workMain (Join-Path $combinedWorkRoot $combinedReleaseRelative)
$workLinks = @(Get-HardLinkPaths $workMain)
if ($workLinks.Count -ne 2 -or $workLinks -notcontains $workMain -or
$workLinks -notcontains $workDependency -or
$workLinks -contains $combinedMain -or $workLinks -contains $combinedDependency) {
throw 'Work copy did not create a destination-local Cargo hard-link pair.'
}
$passed++
$combinedWorkLease = Open-WaggleValidatedPrebuiltTree `
-Root $combinedWorkRoot -ExpectedInventory $combinedInventory `
-Label 'Combined mutable private work tree' `
-MutablePaths @($workMain, $workDependency) `
-CargoReleaseRelativePath $combinedReleaseRelative
try {
$writeProbe = [IO.File]::Open(
$workMain,
[IO.FileMode]::Open,
[IO.FileAccess]::Write,
[IO.FileShare]::None
)
$writeProbe.Dispose()
Assert-Throws {
$writeProbe = [IO.File]::Open(
(Join-Path $combinedWorkRoot 'resources\service.js'),
[IO.FileMode]::Open,
[IO.FileAccess]::Write,
[IO.FileShare]::None
)
$writeProbe.Dispose()
} 'used by another process|cannot access|being used|denied' `
'private work tree locks immutable resources while Cargo pair stays mutable'
} finally {
Close-PrebuiltTreeLease $combinedWorkLease
}
} finally {
Close-PrebuiltTreeLease $combinedSourceLease
if (Test-Path -LiteralPath $combinedWorkRoot) {
[IO.Directory]::Delete($combinedWorkRoot, $true)
}
}
$resourcesInventory = New-WagglePrebuiltInventory `
-Root (Join-Path $seedRoot 'resources')
$hostedReceiptPath = Join-Path $prebuiltFixtureRoot 'build-receipt.json'
$hostedReceipt = [pscustomobject][ordered]@{
schemaVersion = 1
repository = 'marolinik/waggle-os'
sourceRevision = 'a' * 40
targetTriple = 'x86_64-pc-windows-msvc'
targetInventory = $expectedInventory
resourcesInventory = $resourcesInventory
checker = [pscustomobject]@{ exitCode = 0; sha256 = 'A' * 64 }
}
Write-JsonNoBom $hostedReceiptPath $hostedReceipt
$hostedReceiptLease = Get-WaggleHostedBuildReceipt `
$hostedReceiptPath (Get-Sha256 $hostedReceiptPath) ('a' * 40)
$hostedReceiptLease.Lock.Dispose()
$passed++
$mismatchedReceipt = $hostedReceipt | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$mismatchedReceipt.resourcesInventory.entries[0].sha256 = 'B' * 64
$mismatchedReceipt.resourcesInventory.sha256 = Get-WaggleInventorySha256 `
@($mismatchedReceipt.resourcesInventory.entries)
Write-JsonNoBom $hostedReceiptPath $mismatchedReceipt
Assert-Throws {
Get-WaggleHostedBuildReceipt `
$hostedReceiptPath (Get-Sha256 $hostedReceiptPath) ('a' * 40)
} 'resource projection.*does not match' `
'hosted receipt rejects independently valid mismatched resource inventory'
$payloadProjection = @(
[pscustomobject]@{ path = 'waggle.exe'; size = 128; sha256 = 'C' * 64 },
[pscustomobject]@{
path = 'resources\data.bin'
size = [long]$resourcesInventory.entries[0].size
sha256 = [string]$resourcesInventory.entries[0].sha256
}
)
Assert-WaggleUnsignedPayloadResourceProjection `
$payloadProjection $resourcesInventory
$passed++
$tamperedPayloadProjection = $payloadProjection | ConvertTo-Json -Depth 8 |
ConvertFrom-Json -Depth 8 -DateKind String
$tamperedPayloadProjection[1].sha256 = 'D' * 64
Assert-Throws {
Assert-WaggleUnsignedPayloadResourceProjection `
$tamperedPayloadProjection $resourcesInventory
} 'resource projection.*does not match' `
'unsigned payload rejects resource digest mismatch against build receipt'
$preflightSigningRoot = Join-Path $prebuiltFixtureRoot 'preflight-signing'
$preflightUnsignedRoot = Join-Path $prebuiltFixtureRoot 'preflight-unsigned'
$preflightRelative = 'resources\service.js'
$preflightSigningPath = Join-Path $preflightSigningRoot $preflightRelative
$preflightUnsignedPath = Join-Path $preflightUnsignedRoot $preflightRelative
[IO.Directory]::CreateDirectory((Split-Path $preflightSigningPath -Parent)) | Out-Null
[IO.Directory]::CreateDirectory((Split-Path $preflightUnsignedPath -Parent)) | Out-Null
[IO.File]::WriteAllText($preflightSigningPath, 'signing-copy')
[IO.File]::WriteAllText($preflightUnsignedPath, 'unsigned-copy')
Assert-Equal `
(Resolve-WagglePreflightFixedPath `
$preflightSigningPath $preflightSigningRoot $preflightUnsignedRoot) `
([IO.Path]::GetFullPath($preflightUnsignedPath)) `
'manifest preflight resolves same relative path from unsigned work tree'
Assert-Throws {
Resolve-WagglePreflightFixedPath `
(Join-Path $prebuiltFixtureRoot 'outside.bin') `
$preflightSigningRoot $preflightUnsignedRoot
} 'escaped.*signing target root' 'manifest preflight rejects signing-root escape'
$rollbackRoot = Join-Path $prebuiltFixtureRoot 'rollback'
$rollbackDependency = Join-Path $rollbackRoot 'release\deps\waggle.exe'
$rollbackMain = Join-Path $rollbackRoot 'release\waggle.exe'
[IO.Directory]::CreateDirectory((Split-Path $rollbackDependency -Parent)) | Out-Null
New-SyntheticPe $rollbackDependency 101
New-Item -ItemType HardLink -Path $rollbackMain -Target $rollbackDependency | Out-Null
$expectedOriginalSha256 = Get-Sha256 $rollbackMain
$expectedHardLinkPaths = @(Get-HardLinkPaths $rollbackMain)
$replacement = Join-Path $rollbackRoot 'signed-replacement.exe'
$backup = Join-Path $rollbackRoot 'original.backup'
New-SyntheticPe $replacement 102
[IO.File]::Replace($replacement, $rollbackMain, $backup, $true)
if ((Get-Sha256 $rollbackMain) -ceq $expectedOriginalSha256) {
throw 'Rollback test fixture did not replace the original artifact.'
}
$passed++
Restore-WaggleReplacedArtifact `
-ArtifactPath $rollbackMain -BackupPath $backup `
-ExpectedOriginalSha256 $expectedOriginalSha256 `
-ExpectedHardLinkPaths $expectedHardLinkPaths
Assert-Equal (Get-Sha256 $rollbackMain) $expectedOriginalSha256 'rollback restores exact main bytes'
Assert-Equal (Get-Sha256 $rollbackDependency) $expectedOriginalSha256 'rollback preserves exact dependency bytes'
Assert-ApprovedHardLinkTopology `
$rollbackMain (Join-Path $rollbackRoot 'release')
$passed++
$regularRollbackArtifact = Join-Path $rollbackRoot 'regular-artifact.dll'
$regularRollbackReplacement = Join-Path $rollbackRoot 'regular-replacement.dll'
$regularRollbackBackup = Join-Path $rollbackRoot 'regular.backup'
New-SyntheticPe $regularRollbackArtifact 106
New-SyntheticPe $regularRollbackReplacement 107
$regularOriginalHash = Get-Sha256 $regularRollbackArtifact
[IO.File]::Replace(
$regularRollbackReplacement,
$regularRollbackArtifact,
$regularRollbackBackup,
$true
)
Restore-WaggleReplacedArtifact `
-ArtifactPath $regularRollbackArtifact -BackupPath $regularRollbackBackup `
-ExpectedOriginalSha256 $regularOriginalHash `
-ExpectedHardLinkPaths @($regularRollbackArtifact)
Assert-Equal `
(Get-Sha256 $regularRollbackArtifact) $regularOriginalHash `
'rollback restores exact regular-artifact bytes'
$missingBackupArtifact = Join-Path $rollbackRoot 'missing-backup-artifact.exe'
$missingBackup = Join-Path $rollbackRoot 'missing.backup'
New-SyntheticPe $missingBackupArtifact 103
$missingArtifactHash = Get-Sha256 $missingBackupArtifact
Assert-Throws {
Restore-WaggleReplacedArtifact `
-ArtifactPath $missingBackupArtifact -BackupPath $missingBackup `
-ExpectedOriginalSha256 ('0' * 64) `
-ExpectedHardLinkPaths @($missingBackupArtifact)
} 'backup.*missing|does not exist' 'rollback missing-backup rejection'
Assert-Equal `
(Get-Sha256 $missingBackupArtifact) $missingArtifactHash `
'missing rollback backup leaves replacement unchanged'
$corruptBackupArtifact = Join-Path $rollbackRoot 'corrupt-backup-artifact.exe'
$corruptBackup = Join-Path $rollbackRoot 'corrupt.backup'
New-SyntheticPe $corruptBackupArtifact 104
New-SyntheticPe $corruptBackup 105
$corruptArtifactHash = Get-Sha256 $corruptBackupArtifact
Assert-Throws {
Restore-WaggleReplacedArtifact `
-ArtifactPath $corruptBackupArtifact -BackupPath $corruptBackup `
-ExpectedOriginalSha256 ('0' * 64) `
-ExpectedHardLinkPaths @($corruptBackupArtifact)
} 'backup.*SHA-256|backup.*digest|original.*digest' 'rollback corrupt-backup rejection'
Assert-Equal `
(Get-Sha256 $corruptBackupArtifact) $corruptArtifactHash `
'corrupt rollback backup leaves replacement unchanged'
Assert-Throws {
Open-WaggleValidatedPrebuiltTree `
-Root $seedRoot -ExpectedInventory $expectedInventory `
-Label 'Overlapping prebuilt tree' -DisallowedRoots @($prebuiltFixtureRoot)
} 'overlap|disallowed root|must be distinct' 'prebuilt tree root-overlap rejection'
} finally {
if ($null -ne $reparseDirectory -and (Test-Path -LiteralPath $reparseDirectory)) {
[IO.Directory]::Delete($reparseDirectory)
}
if (Test-Path -LiteralPath $prebuiltFixtureRoot) {
[IO.Directory]::Delete($prebuiltFixtureRoot, $true)
}
}
$sessionFixtureRoot = Join-Path `
([IO.Path]::GetTempPath()) `
"waggle-signing-session-$([Guid]::NewGuid().ToString('N'))"
$environmentNames = @(
'WAGGLE_SIGNING_MANIFEST_PATH', 'WAGGLE_SIGNING_MANIFEST_SHA256',
'WAGGLE_SIGNING_SESSION_ID', 'CARGO_TARGET_DIR', 'TEMP', 'TMP',
'WAGGLE_NSIS_SIGNING_TEMP_ROOT', 'NODE_OPTIONS', 'NODE_PATH',
'WAGGLE_SIGNING_PORTABLE_TOOLCHAIN_ROOT',
'WAGGLE_SIGNING_PORTABLE_NODE_PATH',
'WAGGLE_SIGNING_PORTABLE_GIT_PATH',
'WAGGLE_SIGNING_PORTABLE_SEVEN_ZIP_PATH',
'NAPI_RS_NATIVE_LIBRARY_PATH', 'NAPI_RS_FORCE_WASI',
'npm_config_node_options', 'TARGET_ARCH'
)
$savedEnvironment = @{}
foreach ($name in $environmentNames) {
$savedEnvironment[$name] = [Environment]::GetEnvironmentVariable($name)
}
try {
$sessionId = '0123456789abcdef0123456789abcdef'
$tauriRoot = Join-Path $sessionFixtureRoot 'app\src-tauri'
$resourcesRoot = Join-Path $tauriRoot 'resources'
$targetRoot = New-PrivateDirectory `
(Join-Path $tauriRoot "target\.signing-builds\run-$sessionId")
$releaseRoot = Join-Path $targetRoot 'x86_64-pc-windows-msvc\release'
[IO.Directory]::CreateDirectory($resourcesRoot) | Out-Null
[IO.File]::WriteAllText((Join-Path $resourcesRoot 'service.js'), 'sidecar')
[IO.Directory]::CreateDirectory($releaseRoot) | Out-Null
$sessionDirectory = New-PrivateDirectory `
(Join-Path $tauriRoot "target\.signing-sessions\run-$sessionId")
$tempRoot = New-PrivateDirectory `
(Join-Path $tauriRoot "target\.signing-temp\run-$sessionId")
$configPath = Join-Path $tauriRoot 'tauri.conf.json'
$overridePath = Join-Path $sessionDirectory 'tauri.signing-override.json'
Write-JsonNoBom $configPath ([ordered]@{ version = '0.2.0' })
Write-WaggleSigningOverride `
$overridePath $wrapperPath $resourcesRoot | Out-Null
$unsignedOverride = Get-Content -Raw $overridePath | ConvertFrom-Json -Depth 16
Assert-Equal $unsignedOverride.bundle.targets[0] 'nsis' 'unsigned preflight targets NSIS only'
Assert-Equal $unsignedOverride.build.beforeBuildCommand '' 'unsigned preflight disables nested build'
Assert-Equal $unsignedOverride.build.beforeBundleCommand '' 'unsigned preflight disables nested bundle hook'
$unsignedResourceMap = @($unsignedOverride.bundle.resources.PSObject.Properties)
Assert-Equal $unsignedResourceMap.Count 1 'unsigned override maps one complete resource tree'
Assert-Equal `
([IO.Path]::GetFullPath([string]$unsignedResourceMap[0].Name)) `
([IO.Path]::GetFullPath($resourcesRoot)) `
'unsigned override resource source'
Assert-Equal ([string]$unsignedResourceMap[0].Value) 'resources' `
'unsigned override resource destination'
if ($null -ne $unsignedOverride.bundle.windows.PSObject.Properties['signCommand']) {
throw 'Unsigned preflight override unexpectedly contains a signing command.'
}
$passed++
Write-WaggleSigningOverride `
$overridePath $wrapperPath $resourcesRoot -EnableSigning | Out-Null
$signedOverride = Get-Content -Raw $overridePath | ConvertFrom-Json -Depth 16
Assert-Equal $signedOverride.bundle.windows.signCommand.cmd `
$SystemPowerShellPath 'signed override uses canonical bootstrap host'
Assert-Equal `
(@($signedOverride.bundle.windows.signCommand.args | Where-Object { $_ -ceq '%1' }).Count) `
1 'signed override contains one artifact placeholder'
Assert-Equal $signedOverride.build.beforeBundleCommand '' 'signed override disables nested bundle hook'
$invalidOverride = $signedOverride | ConvertTo-Json -Depth 16 |
ConvertFrom-Json -Depth 16
$invalidOverride.bundle.resources = [ordered]@{
(Join-Path $resourcesRoot 'native') = 'resources'
}
Write-JsonNoBom $overridePath $invalidOverride
Assert-Throws {
Assert-WaggleSigningOverrideContract `
$overridePath $wrapperPath $resourcesRoot
} 'exact receipt-bound resource tree' `
'signed override rejects receipt resource subroot substitution'
Write-WaggleSigningOverride `
$overridePath $wrapperPath $resourcesRoot -EnableSigning | Out-Null
$toolDirectory = Join-Path $sessionFixtureRoot 'tools'
[IO.Directory]::CreateDirectory($toolDirectory) | Out-Null
$toolNames = @(
'wrapper.ps1', 'tauri.js', 'tauri-main.js', 'tauri-index.js',
'tauri-package.json', 'tauri-native-package.json', 'tauri-native.node',
'vite.js', 'vite-package.json', 'bundle-node.mjs', 'build-sidecar.mjs',
'bundle-native-deps.mjs', 'stage-sidecar-deps.mjs', 'makensis.exe',
'git.exe', 'git-runtime.exe', 'node.exe', 'npm-cli.js', '7z.exe', 'signtool.exe',
'artifact-signing.nupkg'
)
$toolPaths = @{}
foreach ($toolName in $toolNames) {
$toolPath = Join-Path $toolDirectory $toolName
[IO.File]::WriteAllText($toolPath, "fixture:$toolName")
$toolPaths[$toolName] = $toolPath
}
$context = [pscustomobject]@{
RepoRoot = $sessionFixtureRoot
AppRoot = Join-Path $sessionFixtureRoot 'app'
TauriRoot = $tauriRoot
ResourcesRoot = $resourcesRoot
TargetRoot = $targetRoot
ReleaseRoot = $releaseRoot
ConfigPath = $configPath
OverrideConfigPath = $overridePath
WrapperPath = $wrapperPath
TauriCliPath = $toolPaths['tauri.js']
TauriCliPackagePath = $toolPaths['tauri-package.json']
TauriCliMainPath = $toolPaths['tauri-main.js']
TauriCliIndexPath = $toolPaths['tauri-index.js']
TauriNativePackagePath = $toolPaths['tauri-native-package.json']
TauriNativeBinaryPath = $toolPaths['tauri-native.node']
ViteCliPath = $toolPaths['vite.js']
VitePackagePath = $toolPaths['vite-package.json']
WebRoot = Join-Path $sessionFixtureRoot 'apps\web'
BuildScriptPaths = [ordered]@{
bundleNode = $toolPaths['bundle-node.mjs']
buildSidecar = $toolPaths['build-sidecar.mjs']
bundleNativeDeps = $toolPaths['bundle-native-deps.mjs']
stageSidecarDeps = $toolPaths['stage-sidecar-deps.mjs']
}
MakensisPath = $toolPaths['makensis.exe']
GitPath = $toolPaths['git.exe']
GitRuntimePath = $toolPaths['git-runtime.exe']
PortableToolchainRoot = $null
NodePath = $toolPaths['node.exe']
NpmCliPath = $toolPaths['npm-cli.js']
SevenZipPath = $toolPaths['7z.exe']
SevenZipDllPath = $toolPaths['7z.exe']
SignToolPath = $toolPaths['signtool.exe']
ArtifactSigningPackagePath = $toolPaths['artifact-signing.nupkg']
SourceRevision = 'a' * 40
TauriCliVersion = '2.10.1'
TauriCliSha256 = Get-Sha256 $toolPaths['tauri.js']
TauriCliPackageSha256 = Get-Sha256 $toolPaths['tauri-package.json']
TauriCliMainSha256 = Get-Sha256 $toolPaths['tauri-main.js']
TauriCliIndexSha256 = Get-Sha256 $toolPaths['tauri-index.js']
TauriNativePackageSha256 = Get-Sha256 $toolPaths['tauri-native-package.json']
TauriNativeBinarySha256 = Get-Sha256 $toolPaths['tauri-native.node']
ViteCliSha256 = Get-Sha256 $toolPaths['vite.js']
VitePackageSha256 = Get-Sha256 $toolPaths['vite-package.json']
ViteVersion = '6.4.3'
BuildScriptHashes = [ordered]@{
bundleNode = Get-Sha256 $toolPaths['bundle-node.mjs']
buildSidecar = Get-Sha256 $toolPaths['build-sidecar.mjs']
bundleNativeDeps = Get-Sha256 $toolPaths['bundle-native-deps.mjs']
stageSidecarDeps = Get-Sha256 $toolPaths['stage-sidecar-deps.mjs']
}
MakensisSha256 = Get-Sha256 $toolPaths['makensis.exe']
GitSha256 = Get-Sha256 $toolPaths['git.exe']
GitRuntimeSha256 = Get-Sha256 $toolPaths['git-runtime.exe']
NodeSha256 = Get-Sha256 $toolPaths['node.exe']
NpmCliSha256 = Get-Sha256 $toolPaths['npm-cli.js']
SevenZipSha256 = Get-Sha256 $toolPaths['7z.exe']
SevenZipDllSha256 = Get-Sha256 $toolPaths['7z.exe']
SignToolSha256 = Get-Sha256 $toolPaths['signtool.exe']
ArtifactSigningPackageSha256 = Get-Sha256 $toolPaths['artifact-signing.nupkg']
ArtifactSigningX64ManifestSha256 = 'B' * 64
}
$fixedPaths = @(Get-ExpectedNsisFixedPaths $context '0.2.0')
$packagedPaths = @(Get-ExpectedNsisPackagedPaths)
$slots = [Collections.Generic.List[object]]::new()
for ($index = 0; $index -lt $fixedPaths.Count; $index++) {
New-SyntheticPe $fixedPaths[$index] ([byte]($index + 1))
$slots.Add([pscustomobject][ordered]@{
id = 'fixed-{0:d2}' -f ($index + 1)
order = $index + 1
kind = 'fixed'
maxUses = 1
path = $fixedPaths[$index]
packagedPath = $packagedPaths[$index]
preSignSha256 = Get-Sha256 $fixedPaths[$index]
})
}
$slots.Add([pscustomobject][ordered]@{
id = 'nsis-uninstaller'
order = 13
kind = 'generated-nsis-uninstaller'
maxUses = 1
pathPattern = $NsisUninstallerPattern
evidencePath = Join-Path $sessionDirectory 'signed-evidence\13-nsis-uninstaller.exe'
})
$slots.Add([pscustomobject][ordered]@{
id = 'nsis-installer'
order = 14
kind = 'generated-nsis-installer'
maxUses = 1
path = Join-Path $releaseRoot 'bundle\nsis\Waggle_0.2.0_x64-setup.exe'
})
$manifestPath = Join-Path $sessionDirectory 'manifest.json'
$ledgerPath = Join-Path $sessionDirectory 'callback-ledger.json'
$baseManifest = [pscustomobject][ordered]@{
schemaVersion = 1
mode = 'nsis'
sessionId = $sessionId
sourceRevision = $context.SourceRevision
createdAtUtc = [DateTimeOffset]::UtcNow.AddMinutes(-1).ToString('O')
expiresAtUtc = [DateTimeOffset]::UtcNow.AddHours(1).ToString('O')
repoRoot = $context.RepoRoot
tauriRoot = $tauriRoot
targetRoot = $targetRoot
releaseRoot = $releaseRoot
resourcesRoot = $resourcesRoot
tempRoot = $tempRoot
ledgerPath = $ledgerPath
appVersion = '0.2.0'
payloads = @(
[pscustomobject]@{ path = 'waggle.exe'; sha256 = '1' * 64; size = 128 }
) + @(1..9 | ForEach-Object {
[pscustomobject]@{ path = "resources\fixture-$_.bin"; sha256 = '2' * 64; size = $_ }
})
toolchain = [pscustomobject][ordered]@{
wrapperPath = $context.WrapperPath
wrapperSha256 = Get-Sha256 $context.WrapperPath
tauriConfigPath = $configPath
tauriConfigSha256 = Get-Sha256 $configPath
tauriOverrideConfigPath = $overridePath
tauriOverrideConfigSha256 = Get-Sha256 $overridePath
tauriCliPath = $context.TauriCliPath
tauriCliSha256 = $context.TauriCliSha256
tauriCliPackagePath = $context.TauriCliPackagePath
tauriCliPackageSha256 = $context.TauriCliPackageSha256
tauriCliMainPath = $context.TauriCliMainPath
tauriCliMainSha256 = $context.TauriCliMainSha256
tauriCliIndexPath = $context.TauriCliIndexPath
tauriCliIndexSha256 = $context.TauriCliIndexSha256
tauriNativePackagePath = $context.TauriNativePackagePath
tauriNativePackageSha256 = $context.TauriNativePackageSha256
tauriNativeBinaryPath = $context.TauriNativeBinaryPath
tauriNativeBinarySha256 = $context.TauriNativeBinarySha256
tauriCliVersion = $context.TauriCliVersion
viteCliPath = $context.ViteCliPath
viteCliSha256 = $context.ViteCliSha256
vitePackagePath = $context.VitePackagePath
vitePackageSha256 = $context.VitePackageSha256
viteVersion = $context.ViteVersion
buildScripts = [pscustomobject][ordered]@{
bundleNode = [pscustomobject]@{ path = $context.BuildScriptPaths.bundleNode; sha256 = $context.BuildScriptHashes.bundleNode }
buildSidecar = [pscustomobject]@{ path = $context.BuildScriptPaths.buildSidecar; sha256 = $context.BuildScriptHashes.buildSidecar }
bundleNativeDeps = [pscustomobject]@{ path = $context.BuildScriptPaths.bundleNativeDeps; sha256 = $context.BuildScriptHashes.bundleNativeDeps }
stageSidecarDeps = [pscustomobject]@{ path = $context.BuildScriptPaths.stageSidecarDeps; sha256 = $context.BuildScriptHashes.stageSidecarDeps }
}
makensisPath = $context.MakensisPath
makensisSha256 = $context.MakensisSha256
gitPath = $context.GitPath
gitSha256 = $context.GitSha256
gitRuntimePath = $context.GitRuntimePath
gitRuntimeSha256 = $context.GitRuntimeSha256
nodePath = $context.NodePath
nodeSha256 = $context.NodeSha256
npmCliPath = $context.NpmCliPath
npmCliSha256 = $context.NpmCliSha256
sevenZipPath = $context.SevenZipPath
sevenZipSha256 = $context.SevenZipSha256
signToolPath = $context.SignToolPath
signToolSha256 = $context.SignToolSha256
artifactSigningPackagePath = $context.ArtifactSigningPackagePath
artifactSigningPackageSha256 = $context.ArtifactSigningPackageSha256
artifactSigningX64ManifestSha256 = $context.ArtifactSigningX64ManifestSha256
}
slots = @($slots)
}
$writeSessionFixture = {
param([object]$Manifest)
Write-JsonNoBom $manifestPath $Manifest
$manifestHash = Get-Sha256 $manifestPath
Write-JsonNoBom $ledgerPath (New-PendingLedger $sessionId $manifestHash $Manifest.slots)
$env:WAGGLE_SIGNING_MANIFEST_SHA256 = $manifestHash
return $manifestHash
}
$env:WAGGLE_SIGNING_MANIFEST_PATH = $manifestPath
$env:WAGGLE_SIGNING_SESSION_ID = $sessionId
$env:CARGO_TARGET_DIR = $targetRoot
$env:TEMP = $tempRoot
$env:TMP = $tempRoot
$env:WAGGLE_NSIS_SIGNING_TEMP_ROOT = $tempRoot
[void](& $writeSessionFixture $baseManifest)
$loadedSession = Get-WaggleSigningSession $context
try {
Assert-Equal $loadedSession.Id $sessionId 'valid manifest session load'
foreach ($lockedPath in @(
$context.TauriCliMainPath,
$context.TauriCliIndexPath,
$context.TauriNativeBinaryPath
)) {
Assert-Throws {
$writeProbe = [IO.File]::Open(
$lockedPath,
[IO.FileMode]::Open,
[IO.FileAccess]::Write,
[IO.FileShare]::None
)
$writeProbe.Dispose()
} 'used by another process|cannot access' 'loaded session locks complete Tauri runtime closure'
}
} finally {
foreach ($lock in $loadedSession.Locks) { $lock.Dispose() }
}
$portableContext = $context | Select-Object *
$portableContext.PortableToolchainRoot = $toolDirectory
$portableContext | Add-Member `
-NotePropertyName PortableToolchainReceiptPath `
-NotePropertyValue $toolPaths['artifact-signing.nupkg']
$portableContext | Add-Member `
-NotePropertyName PortableToolchainReceiptSha256 `
-NotePropertyValue (Get-Sha256 $toolPaths['artifact-signing.nupkg'])
$portableContext | Add-Member `
-NotePropertyName PortableToolchainInventorySha256 `
-NotePropertyValue $PortableToolchainInventorySha256
$portableContext | Add-Member `
-NotePropertyName PortableToolchainLocks `
-NotePropertyValue ([Collections.Generic.List[IDisposable]]::new())
$portableContext.SevenZipDllPath = $toolPaths['7z.exe']
$portableContext.SevenZipDllSha256 = Get-Sha256 $toolPaths['7z.exe']
$portableManifest = $baseManifest | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$portableManifest | Add-Member -NotePropertyName buildReceipt -NotePropertyValue (
[pscustomobject]@{
schemaVersion = 1
repository = 'marolinik/waggle-os'
sourceRevision = $context.SourceRevision
targetTriple = 'x86_64-pc-windows-msvc'
}
)
$portableManifest.toolchain | Add-Member `
-NotePropertyName portableToolchainRoot -NotePropertyValue $toolDirectory
$portableManifest.toolchain | Add-Member `
-NotePropertyName sevenZipDllPath -NotePropertyValue $portableContext.SevenZipDllPath
$portableManifest.toolchain | Add-Member `
-NotePropertyName sevenZipDllSha256 -NotePropertyValue $portableContext.SevenZipDllSha256
$portableManifest.toolchain | Add-Member `
-NotePropertyName portableToolchainReceiptPath `
-NotePropertyValue $portableContext.PortableToolchainReceiptPath
$portableManifest.toolchain | Add-Member `
-NotePropertyName portableToolchainReceiptSha256 `
-NotePropertyValue $portableContext.PortableToolchainReceiptSha256
$portableManifest.toolchain | Add-Member `
-NotePropertyName portableToolchainInventorySha256 `
-NotePropertyValue $PortableToolchainInventorySha256
$portableManifest.toolchain | Add-Member `
-NotePropertyName portableToolchainFileCount `
-NotePropertyValue $PortableToolchainFileCount
$env:WAGGLE_SIGNING_PORTABLE_TOOLCHAIN_ROOT = $toolDirectory
$env:WAGGLE_SIGNING_PORTABLE_NODE_PATH = $context.NodePath
$env:WAGGLE_SIGNING_PORTABLE_GIT_PATH = $context.GitPath
$env:WAGGLE_SIGNING_PORTABLE_SEVEN_ZIP_PATH = $context.SevenZipPath
$env:WAGGLE_SIGNING_PORTABLE_RECEIPT_PATH = `
$portableContext.PortableToolchainReceiptPath
$env:WAGGLE_SIGNING_PORTABLE_RECEIPT_SHA256 = `
$portableContext.PortableToolchainReceiptSha256
[void](& $writeSessionFixture $portableManifest)
$portableSession = Get-WaggleSigningSession $portableContext
try {
Assert-Equal $portableSession.Id $sessionId `
'receipt-bound portable manifest session load'
} finally {
foreach ($lock in $portableSession.Locks) { $lock.Dispose() }
}
$wrongPortableRootManifest = $portableManifest | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$wrongPortableRootManifest.toolchain.portableToolchainRoot = `
Join-Path $sessionFixtureRoot 'substituted-tools'
[void](& $writeSessionFixture $wrongPortableRootManifest)
Assert-Throws {
Get-WaggleSigningSession $portableContext
} 'Portable signing toolchain root does not match' `
'portable manifest root substitution rejection'
$wrongPortableHashManifest = $portableManifest | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$wrongPortableHashManifest.toolchain.sevenZipDllSha256 = '0' * 64
[void](& $writeSessionFixture $wrongPortableHashManifest)
Assert-Throws {
Get-WaggleSigningSession $portableContext
} '7-Zip runtime library manifest SHA-256' `
'portable manifest closure digest substitution rejection'
[void](& $writeSessionFixture $portableManifest)
$env:WAGGLE_SIGNING_PORTABLE_NODE_PATH = $context.GitPath
Assert-Throws {
Get-WaggleSigningSession $portableContext
} 'WAGGLE_SIGNING_PORTABLE_NODE_PATH does not match' `
'portable callback environment substitution rejection'
$env:WAGGLE_SIGNING_PORTABLE_NODE_PATH = $context.NodePath
$unboundPortableManifest = $portableManifest | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$unboundPortableManifest.PSObject.Properties.Remove('buildReceipt')
[void](& $writeSessionFixture $unboundPortableManifest)
Assert-Throws {
Get-WaggleSigningSession $portableContext
} 'missing required property.*buildReceipt' `
'portable callback without hosted receipt rejection'
foreach ($portableEnvironmentName in @(
'WAGGLE_SIGNING_PORTABLE_TOOLCHAIN_ROOT',
'WAGGLE_SIGNING_PORTABLE_NODE_PATH',
'WAGGLE_SIGNING_PORTABLE_GIT_PATH',
'WAGGLE_SIGNING_PORTABLE_SEVEN_ZIP_PATH',
'WAGGLE_SIGNING_PORTABLE_RECEIPT_PATH',
'WAGGLE_SIGNING_PORTABLE_RECEIPT_SHA256'
)) {
[Environment]::SetEnvironmentVariable($portableEnvironmentName, $null)
}
[void](& $writeSessionFixture $baseManifest)
$validManifestHash = $env:WAGGLE_SIGNING_MANIFEST_SHA256
$env:WAGGLE_SIGNING_MANIFEST_SHA256 = '0' * 64
Assert-Throws {
Get-WaggleSigningSession $context
} 'manifest does not match.*SHA256|manifest.*WAGGLE_SIGNING_MANIFEST_SHA256' 'wrong manifest digest rejection'
$env:WAGGLE_SIGNING_MANIFEST_SHA256 = $validManifestHash
$unsupportedManifest = $baseManifest | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$unsupportedManifest.mode = 'msi'
[void](& $writeSessionFixture $unsupportedManifest)
Assert-Throws {
Get-WaggleSigningSession $context
} 'NSIS-only' 'unsupported manifest mode rejection'
$expiredManifest = $baseManifest | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$expiredManifest.createdAtUtc = [DateTimeOffset]::UtcNow.AddHours(-2).ToString('O')
$expiredManifest.expiresAtUtc = [DateTimeOffset]::UtcNow.AddHours(-1).ToString('O')
[void](& $writeSessionFixture $expiredManifest)
Assert-Throws {
Get-WaggleSigningSession $context
} 'expired|four-hour' 'expired manifest rejection'
$wrongToolManifest = $baseManifest | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$wrongToolManifest.toolchain.wrapperSha256 = '0' * 64
[void](& $writeSessionFixture $wrongToolManifest)
Assert-Throws {
Get-WaggleSigningSession $context
} 'wrapper.*SHA-256' 'wrong wrapper digest rejection'
$wrongNativeManifest = $baseManifest | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$wrongNativeManifest.toolchain.tauriNativeBinarySha256 = '0' * 64
[void](& $writeSessionFixture $wrongNativeManifest)
Assert-Throws {
Get-WaggleSigningSession $context
} 'native CLI binary.*SHA-256|native CLI binary.*digest' 'wrong Tauri native digest rejection'
Write-JsonNoBom $overridePath ([ordered]@{
build = @{ beforeBuildCommand = '' }
bundle = @{ active = $true; targets = @('msi'); windows = @{
signCommand = @{ cmd = $SystemPowerShellPath; args = @(
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
'-File', $wrapperPath, '-ArtifactPath', '%1'
) }
} }
})
$wrongOverrideManifest = $baseManifest | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$wrongOverrideManifest.toolchain.tauriOverrideConfigSha256 = Get-Sha256 $overridePath
[void](& $writeSessionFixture $wrongOverrideManifest)
Assert-Throws {
Get-WaggleSigningSession $context
} 'NSIS-only callback contract' 'MSI signing override rejection'
Write-WaggleSigningOverride $overridePath $wrapperPath -EnableSigning | Out-Null
[void](& $writeSessionFixture $baseManifest)
$env:TEMP = Join-Path $sessionFixtureRoot 'wrong-temp'
Assert-Throws {
Get-WaggleSigningSession $context
} 'TEMP path does not match' 'cross-session TEMP rejection'
$env:TEMP = $tempRoot
[void](& $writeSessionFixture $baseManifest)
$legacyLedger = [pscustomobject]@{
schemaVersion = 1
sessionId = $sessionId
manifestSha256 = $env:WAGGLE_SIGNING_MANIFEST_SHA256
nextOrder = 1
entries = @()
}
Assert-Throws {
Assert-WaggleSigningLedgerState `
([pscustomobject]@{
Id = $sessionId
ManifestSha256 = $env:WAGGLE_SIGNING_MANIFEST_SHA256
Manifest = $baseManifest
TempRoot = $tempRoot
}) `
$legacyLedger
} 'active manifest session|schemaVersion' 'legacy empty ledger rejection'
$lifecycleDirectory = New-PrivateDirectory (Join-Path $sessionFixtureRoot 'lifecycle')
$lifecycleTemp = New-PrivateDirectory (Join-Path $lifecycleDirectory 'temp')
$lifecycleSlots = [Collections.Generic.List[object]]::new()
$signedHost = Get-TrustedPath (Get-Process -Id $PID).Path 'Signed lifecycle host' -AllowHardLink
for ($index = 0; $index -lt 14; $index++) {
$path = if ($index -eq 0) {
$signedHost
} else {
$candidate = Join-Path $lifecycleDirectory "slot-$($index + 1).exe"
New-SyntheticPe $candidate ([byte]($index + 20))
$candidate
}
$lifecycleSlots.Add([pscustomobject][ordered]@{
id = 'lifecycle-{0:d2}' -f ($index + 1)
order = $index + 1
kind = 'fixed'
maxUses = 1
path = $path
preSignSha256 = Get-Sha256 $path
})
}
$lifecycleLedgerPath = Join-Path $lifecycleDirectory 'callback-ledger.json'
$lifecycleSession = [pscustomobject]@{
Id = 'abcdef0123456789abcdef0123456789'
ManifestSha256 = 'C' * 64
Manifest = [pscustomobject]@{ slots = @($lifecycleSlots) }
TempRoot = $lifecycleTemp
SessionDirectory = $lifecycleDirectory
LedgerPath = $lifecycleLedgerPath
ManifestPath = Join-Path $lifecycleDirectory 'manifest.json'
Context = [pscustomobject]@{ SourceRevision = 'a' * 40 }
}
Write-JsonNoBom $lifecycleLedgerPath `
(New-PendingLedger $lifecycleSession.Id $lifecycleSession.ManifestSha256 $lifecycleSlots)
$lifecycleLedger = Get-Content -Raw $lifecycleLedgerPath |
ConvertFrom-Json -Depth 32 -DateKind String
Assert-WaggleSigningLedgerState $lifecycleSession $lifecycleLedger
$passed++
Assert-Throws {
Reserve-WaggleSigningCallback $lifecycleSession $lifecycleSlots[1].path
} 'callback phase' 'cross-phase callback rejection'
$ledgerBeforeInvalidMutation = Get-Content -Raw -LiteralPath $lifecycleLedgerPath
Assert-Throws {
Invoke-WaggleLedgerMutation $lifecycleSession {
param($ledger)
$ledger.entries[0].attempts = 99
}
} 'pristine|reservation metadata' 'invalid ledger mutation rejection'
Assert-Equal `
(Get-Content -Raw -LiteralPath $lifecycleLedgerPath) `
$ledgerBeforeInvalidMutation `
'invalid mutation leaves ledger unchanged'
$reservation = Reserve-WaggleSigningCallback $lifecycleSession $lifecycleSlots[0].path
Assert-Throws {
Reserve-WaggleSigningCallback $lifecycleSession $lifecycleSlots[0].path
} 'already consumed|state must be' 'duplicate callback rejection'
Assert-Throws {
Reserve-WaggleSigningCallback $lifecycleSession $lifecycleSlots[1].path
} 'callback phase|state must be' 'reserved callback blocks later slot'
Complete-WaggleSigningCallback $lifecycleSession $reservation $lifecycleSlots[0].path | Out-Null
$completedLedger = Get-Content -Raw -LiteralPath $lifecycleLedgerPath |
ConvertFrom-Json -Depth 32 -DateKind String
Assert-Equal $completedLedger.entries[0].status 'completed' 'callback completion state'
[IO.File]::Copy($signedHost, $lifecycleSlots[2].path, $true)
$lifecycleSlots[2].preSignSha256 = Get-Sha256 $lifecycleSlots[2].path
$phaseReservation = Reserve-WaggleSigningCallback `
$lifecycleSession $lifecycleSlots[2].path
Assert-Equal $phaseReservation.order 3 'resource callback may arrive out of manifest order'
Complete-WaggleSigningCallback `
$lifecycleSession $phaseReservation $lifecycleSlots[2].path | Out-Null
$secondReservation = Reserve-WaggleSigningCallback $lifecycleSession $lifecycleSlots[1].path
Assert-Equal $secondReservation.order 2 'next ordered callback reservation'
$failureReceiptPath = Fail-WaggleSigningSession `
$lifecycleSession $secondReservation 'fixture_failure' 'fixture error' 'restored-original'
Assert-Equal `
(Test-Path -LiteralPath $failureReceiptPath -PathType Leaf) `
$true `
'failed callback publishes durable failure receipt'
$failedLedger = Get-Content -Raw -LiteralPath $lifecycleLedgerPath |
ConvertFrom-Json -Depth 32 -DateKind String
Assert-Equal $failedLedger.state 'failed' 'callback failure terminal ledger state'
Assert-Equal $failedLedger.entries[1].status 'failed' 'reserved callback becomes failed'
$failedLedgerBeforeRetry = Get-Content -Raw -LiteralPath $lifecycleLedgerPath
Assert-Throws {
Reserve-WaggleSigningCallback $lifecycleSession $lifecycleSlots[2].path
} 'receipt already exists|terminal' 'failed session callback retry rejection'
Assert-Equal `
(Get-Content -Raw -LiteralPath $lifecycleLedgerPath) `
$failedLedgerBeforeRetry `
'failed session retry leaves ledger unchanged'
$completeDirectory = New-PrivateDirectory (Join-Path $sessionFixtureRoot 'complete-ledger')
$completeTemp = New-PrivateDirectory (Join-Path $completeDirectory 'temp')
$completeSlots = [Collections.Generic.List[object]]::new()
$completePayloads = [Collections.Generic.List[object]]::new()
for ($index = 0; $index -lt 12; $index++) {
$completePath = Join-Path $completeDirectory "complete-$($index + 1).exe"
New-SyntheticPe $completePath ([byte]($index + 40))
$completeSlots.Add([pscustomobject][ordered]@{
id = 'complete-{0:d2}' -f ($index + 1)
order = $index + 1
kind = 'fixed'
maxUses = 1
path = $completePath
packagedPath = $packagedPaths[$index]
preSignSha256 = Get-Sha256 $completePath
})
$completePayloads.Add([pscustomobject][ordered]@{
path = $packagedPaths[$index]
sha256 = Get-Sha256 $completePath
size = [long](Get-Item -LiteralPath $completePath).Length
extractedPath = $completePath
})
}
$completeUninstallerPath = Join-Path $completeTemp 'nstABCD.tmp'
New-SyntheticPe $completeUninstallerPath 80
$completeEvidencePath = Join-Path $completeDirectory 'signed-evidence\13-nsis-uninstaller.exe'
New-SyntheticPe $completeEvidencePath 81
$completeSlots.Add([pscustomobject][ordered]@{
id = 'complete-13'; order = 13; kind = 'generated-nsis-uninstaller'; maxUses = 1
pathPattern = $NsisUninstallerPattern; evidencePath = $completeEvidencePath
})
$completeInstallerPath = Join-Path $completeDirectory 'Waggle_0.2.0_x64-setup.exe'
New-SyntheticPe $completeInstallerPath 82
$completeSlots.Add([pscustomobject][ordered]@{
id = 'complete-14'; order = 14; kind = 'generated-nsis-installer'; maxUses = 1
path = $completeInstallerPath
})
$completeLedgerPath = Join-Path $completeDirectory 'callback-ledger.json'
$completeSession = [pscustomobject]@{
Id = 'fedcba9876543210fedcba9876543210'
ManifestSha256 = 'D' * 64
Manifest = [pscustomobject]@{
slots = @($completeSlots)
payloads = @($completePayloads | ForEach-Object {
[pscustomobject]@{ path = $_.path; sha256 = $_.sha256; size = $_.size }
})
}
TempRoot = $completeTemp
SessionDirectory = $completeDirectory
LedgerPath = $completeLedgerPath
ManifestPath = Join-Path $completeDirectory 'manifest.json'
Context = [pscustomobject]@{
SourceRevision = 'b' * 40
SignToolPath = $toolPaths['signtool.exe']
SevenZipPath = $toolPaths['7z.exe']
}
}
$completeLedger = New-PendingLedger `
$completeSession.Id $completeSession.ManifestSha256 @($completeSlots)
$reservedAt = [DateTimeOffset]::UtcNow.AddMinutes(-2).ToString('O')
$completedAt = [DateTimeOffset]::UtcNow.AddMinutes(-1).ToString('O')
foreach ($entry in $completeLedger.entries) {
$slot = $completeSlots[[int]$entry.order - 1]
$entryPath = if ([int]$entry.order -eq 13) {
$completeUninstallerPath
} else {
[string]$slot.path
}
$entry.status = 'completed'
$entry.attempts = 1
$entry.reservationId = [Guid]::NewGuid().ToString('N')
$entry.path = $entryPath
$entry.preSignSha256 = Get-Sha256 $entryPath
$entry.reservedAtUtc = $reservedAt
$entry.completedAtUtc = $completedAt
$entry.postSignSha256 = if ([int]$entry.order -eq 13) {
Get-Sha256 $completeEvidencePath
} else {
Get-Sha256 $entryPath
}
$entry.signerSubject = 'CN=Fixture signer'
}
Write-JsonNoBom $completeLedgerPath $completeLedger
$loadedCompleteLedger = Get-CompletedWaggleSigningLedger $completeSession
Assert-Equal $loadedCompleteLedger.entries.Count 14 'complete finalizer accepts exact 14-slot ledger'
$changedPayloads = @($completePayloads | ForEach-Object {
[pscustomobject]@{
path = $_.path; sha256 = $_.sha256; size = $_.size; extractedPath = $_.extractedPath
}
})
$changedPayloads[0].sha256 = '0' * 64
Assert-Throws {
Assert-WaggleFinalPayloadBindings `
$completeSession $loadedCompleteLedger $changedPayloads { param($Path, $SignTool) }
} 'payload bytes differ' 'same-name changed packaged payload rejection'
$completeLedger.entries[13].status = 'pending'
$completeLedger.entries[13].attempts = 0
foreach ($field in @(
'reservationId', 'path', 'preSignSha256', 'reservedAtUtc',
'completedAtUtc', 'postSignSha256', 'signerSubject'
)) {
$completeLedger.entries[13].$field = $null
}
Write-JsonNoBom $completeLedgerPath $completeLedger
Assert-Throws {
Get-CompletedWaggleSigningLedger $completeSession
} 'incomplete' 'finalizer rejects incomplete callback ledger'
Write-JsonNoBom $completeLedgerPath $loadedCompleteLedger
$finalResult = Assert-WaggleSigningPackageComplete `
$completeSession @($completePayloads) { param($Path, $SignTool) }
Assert-Equal `
(Test-Path -LiteralPath $finalResult.ReceiptPath -PathType Leaf) `
$true `
'finalizer publishes regular provenance receipt'
$sealedLedger = Get-Content -Raw -LiteralPath $completeLedgerPath |
ConvertFrom-Json -Depth 32 -DateKind String
Assert-Equal $sealedLedger.state 'sealed' 'finalizer seals only after receipt publication'
Assert-Equal `
$sealedLedger.terminalReceiptSha256 `
(Get-Sha256 $finalResult.ReceiptPath) `
'sealed ledger binds exact provenance receipt digest'
$receiptFailureDirectory = New-PrivateDirectory `
(Join-Path $sessionFixtureRoot 'receipt-ledger-recovery')
$receiptFailureLedgerPath = Join-Path $receiptFailureDirectory 'callback-ledger.json'
$receiptFailureSession = [pscustomobject]@{
Id = '11111111111111111111111111111111'
ManifestSha256 = 'F' * 64
Manifest = [pscustomobject]@{ slots = @($completeSlots) }
TempRoot = $completeTemp
SessionDirectory = $receiptFailureDirectory
LedgerPath = $receiptFailureLedgerPath
ManifestPath = Join-Path $receiptFailureDirectory 'manifest.json'
Context = [pscustomobject]@{ SourceRevision = 'b' * 40 }
}
Write-JsonNoBom $receiptFailureLedgerPath `
(New-PendingLedger $receiptFailureSession.Id $receiptFailureSession.ManifestSha256 @($completeSlots))
$ledgerBlocker = [IO.FileStream]::new(
$receiptFailureLedgerPath,
[IO.FileMode]::Open,
[IO.FileAccess]::Read,
[IO.FileShare]::Read
)
try {
Assert-Throws {
Fail-WaggleSigningSession `
$receiptFailureSession $null 'forced-ledger-failure' `
'forced receipt-before-ledger recovery fixture'
} 'replace|access|being used|used by another process' `
'ledger replace failure occurs after durable receipt publication'
} finally {
$ledgerBlocker.Dispose()
}
$pendingReceiptPath = Join-Path $receiptFailureDirectory 'failure-receipt.json'
$terminalIntentPath = Join-Path $receiptFailureDirectory 'terminal-intent.json'
Assert-Equal `
(Test-Path -LiteralPath $pendingReceiptPath -PathType Leaf) $true `
'ledger failure leaves a durable pending terminal receipt'
Assert-Equal `
(Test-Path -LiteralPath $terminalIntentPath -PathType Leaf) $true `
'ledger failure leaves a durable terminal intent'
$receiptFailureLedger = Get-Content -Raw -LiteralPath $receiptFailureLedgerPath |
ConvertFrom-Json -Depth 32 -DateKind String
Assert-Equal $receiptFailureLedger.state 'open' 'receipt failure leaves ledger open'
Assert-Throws {
Invoke-WaggleLedgerMutation $receiptFailureSession { param($ledger) }
} 'pending terminal receipt' 'pending terminal intent blocks ordinary ledger mutation'
$pendingReceiptBytes = [IO.File]::ReadAllBytes($pendingReceiptPath)
$pendingReceipt = Get-Content -Raw -LiteralPath $pendingReceiptPath |
ConvertFrom-Json -Depth 32 -DateKind String
$pendingReceiptSha256 = Get-Sha256 $pendingReceiptPath
[IO.File]::WriteAllText(
$pendingReceiptPath,
'{"schemaVersion":1,"status":"failed","tampered":true}',
[Text.UTF8Encoding]::new($false)
)
Assert-Throws {
Fail-WaggleSigningSession `
$receiptFailureSession $null 'forced-ledger-failure' `
'forced receipt-before-ledger recovery fixture'
} 'intent|digest|receipt' 'tampered pending terminal receipt is rejected'
[IO.File]::WriteAllBytes($pendingReceiptPath, $pendingReceiptBytes)
$recoveredReceiptPath = Fail-WaggleSigningSession `
$receiptFailureSession $null 'forced-ledger-failure' `
'forced receipt-before-ledger recovery fixture'
$recoveredLedger = Get-Content -Raw -LiteralPath $receiptFailureLedgerPath |
ConvertFrom-Json -Depth 32 -DateKind String
Assert-Equal $recoveredLedger.state 'failed' 'pending terminal receipt recovers ledger state'
Assert-Equal `
$recoveredLedger.terminalAtUtc $pendingReceipt.terminalAtUtc `
'recovery reuses the original durable terminal timestamp'
Assert-Equal `
$recoveredLedger.terminalReceiptSha256 $pendingReceiptSha256 `
'recovery reuses the original durable terminal receipt digest'
$recoveredBytes = [IO.File]::ReadAllBytes($recoveredReceiptPath)
[IO.File]::WriteAllText($recoveredReceiptPath, '{}', [Text.UTF8Encoding]::new($false))
Assert-Throws {
Fail-WaggleSigningSession `
$receiptFailureSession $null 'forced-ledger-failure' `
'forced receipt-before-ledger recovery fixture'
} 'intent|digest|receipt' 'post-commit terminal receipt tampering is rejected'
[IO.File]::WriteAllBytes($recoveredReceiptPath, $recoveredBytes)
$intentOnlyDirectory = New-PrivateDirectory `
(Join-Path $sessionFixtureRoot 'intent-only-recovery')
$intentOnlyLedgerPath = Join-Path $intentOnlyDirectory 'callback-ledger.json'
$intentOnlySession = [pscustomobject]@{
Id = '12121212121212121212121212121212'
ManifestSha256 = '1' * 64
Manifest = [pscustomobject]@{ slots = @($completeSlots) }
TempRoot = $completeTemp
SessionDirectory = $intentOnlyDirectory
LedgerPath = $intentOnlyLedgerPath
ManifestPath = Join-Path $intentOnlyDirectory 'manifest.json'
Context = [pscustomobject]@{ SourceRevision = 'b' * 40 }
}
Write-JsonNoBom $intentOnlyLedgerPath `
(New-PendingLedger $intentOnlySession.Id $intentOnlySession.ManifestSha256 @($completeSlots))
$intentOnlyTerminalAt = [DateTimeOffset]::UtcNow.AddMinutes(-3).ToString('O')
$intentOnlyReceipt = [pscustomobject][ordered]@{
schemaVersion = 1
status = 'failed'
sessionId = $intentOnlySession.Id
sourceRevision = $intentOnlySession.Context.SourceRevision
manifestPath = $intentOnlySession.ManifestPath
manifestSha256 = $intentOnlySession.ManifestSha256
failureCode = 'intent-only-failure'
failureMessage = 'recover the receipt from the durable terminal intent'
failedSlotId = $null
rollbackOutcome = 'not-required'
terminalAtUtc = $intentOnlyTerminalAt
}
$intentOnlyStagedReceiptPath = Join-Path $intentOnlyDirectory 'staged-receipt.tmp'
Write-WaggleDurableJsonNew $intentOnlyStagedReceiptPath $intentOnlyReceipt
$intentOnlyReceiptSha256 = Get-Sha256 $intentOnlyStagedReceiptPath
[IO.File]::Delete($intentOnlyStagedReceiptPath)
Write-WaggleDurableJsonNew `
(Join-Path $intentOnlyDirectory 'terminal-intent.json') `
([pscustomobject][ordered]@{
schemaVersion = 1
state = 'failed'
sessionId = $intentOnlySession.Id
manifestSha256 = $intentOnlySession.ManifestSha256
terminalAtUtc = $intentOnlyTerminalAt
receiptSha256 = $intentOnlyReceiptSha256
})
Assert-Throws {
Fail-WaggleSigningSession `
$intentOnlySession $null 'intent-only-failure' 'tampered recovery candidate'
} 'intent|candidate' 'intent-only recovery rejects a changed terminal candidate'
Assert-Equal `
(Test-Path -LiteralPath (Join-Path $intentOnlyDirectory 'failure-receipt.json')) `
$false `
'intent-only candidate mismatch does not publish a receipt'
$intentOnlyOpenLedger = Get-Content -Raw -LiteralPath $intentOnlyLedgerPath |
ConvertFrom-Json -Depth 32 -DateKind String
Assert-Equal $intentOnlyOpenLedger.state 'open' 'intent-only candidate mismatch leaves ledger open'
$intentOnlyRecoveredReceiptPath = Fail-WaggleSigningSession `
$intentOnlySession $null 'intent-only-failure' `
'recover the receipt from the durable terminal intent'
$intentOnlyRecoveredLedger = Get-Content -Raw -LiteralPath $intentOnlyLedgerPath |
ConvertFrom-Json -Depth 32 -DateKind String
Assert-Equal $intentOnlyRecoveredLedger.state 'failed' 'intent-only retry terminalizes the ledger'
Assert-Equal `
$intentOnlyRecoveredLedger.terminalAtUtc $intentOnlyTerminalAt `
'intent-only retry reuses the durable terminal timestamp'
Assert-Equal `
(Get-Sha256 $intentOnlyRecoveredReceiptPath) $intentOnlyReceiptSha256 `
'intent-only retry publishes the exact intent-bound receipt bytes'
$concurrencyRoot = New-PrivateDirectory `
(Join-Path $sessionFixtureRoot 'concurrent-reservation')
$concurrencyLedgerPath = Join-Path $concurrencyRoot 'callback-ledger.json'
$concurrencySessionPath = Join-Path $concurrencyRoot 'session.json'
$concurrencySession = [pscustomobject]@{
Id = '22222222222222222222222222222222'
ManifestSha256 = '2' * 64
Manifest = [pscustomobject]@{ slots = @($lifecycleSlots) }
TempRoot = $lifecycleTemp
SessionDirectory = $concurrencyRoot
LedgerPath = $concurrencyLedgerPath
ManifestPath = Join-Path $concurrencyRoot 'manifest.json'
Context = [pscustomobject]@{ SourceRevision = 'a' * 40 }
}
Write-JsonNoBom $concurrencyLedgerPath `
(New-PendingLedger `
$concurrencySession.Id $concurrencySession.ManifestSha256 @($lifecycleSlots))
Write-JsonNoBom $concurrencySessionPath $concurrencySession
$reservationChildPath = Join-Path $concurrencyRoot 'reserve-child.ps1'
$reservationChildSource = @'
param(
[string]$WrapperPath, [string]$SessionPath, [string]$CallbackArtifactPath,
[string]$ReadyPath, [string]$GoPath, [string]$ResultPath
)
$ErrorActionPreference = 'Stop'
. $WrapperPath -ArtifactPath 'C:\unused.exe'
$session = Get-Content -Raw -LiteralPath $SessionPath |
ConvertFrom-Json -Depth 32 -DateKind String
[IO.File]::WriteAllText($ReadyPath, 'ready')
while (-not [IO.File]::Exists($GoPath)) { Start-Sleep -Milliseconds 10 }
try {
$reservation = Reserve-WaggleSigningCallback $session $CallbackArtifactPath
[IO.File]::WriteAllText($ResultPath, "reserved:$($reservation.reservationId)")
} catch {
[IO.File]::WriteAllText($ResultPath, "error:$($_.Exception.Message)")
}
'@
[IO.File]::WriteAllText(
$reservationChildPath, $reservationChildSource, [Text.UTF8Encoding]::new($false)
)
$reservationGoPath = Join-Path $concurrencyRoot 'go'
$reservationReadyPaths = @(
(Join-Path $concurrencyRoot 'ready-1'),
(Join-Path $concurrencyRoot 'ready-2')
)
$reservationResultPaths = @(
(Join-Path $concurrencyRoot 'result-1'),
(Join-Path $concurrencyRoot 'result-2')
)
$reservationProcesses = @()
for ($index = 0; $index -lt 2; $index++) {
$reservationProcesses += Start-SynchronizedChild `
-PowerShellPath $approvedPowerShell -ScriptPath $reservationChildPath `
-Arguments @{
WrapperPath = $wrapperPath
SessionPath = $concurrencySessionPath
CallbackArtifactPath = [string]$lifecycleSlots[0].path
ReadyPath = $reservationReadyPaths[$index]
GoPath = $reservationGoPath
ResultPath = $reservationResultPaths[$index]
}
}
Wait-ForChildBarriers $reservationReadyPaths 'reservation race'
[IO.File]::WriteAllText($reservationGoPath, 'go')
foreach ($process in $reservationProcesses) {
if (-not $process.WaitForExit(30000)) {
$process.Kill()
throw 'Timed out waiting for the duplicate reservation race.'
}
Assert-Equal $process.ExitCode 0 'reservation race child exit code'
}
$reservationResults = @($reservationResultPaths | ForEach-Object {
[IO.File]::ReadAllText($_)
})
Assert-Equal `
@($reservationResults | Where-Object { $_ -cmatch '^reserved:[0-9a-f]{32}$' }).Count `
1 `
"concurrent duplicate reservation has exactly one winner [$($reservationResults -join ' | ')]"
Assert-Equal `
@($reservationResults | Where-Object {
$_ -cmatch '^error:.*(already consumed|state must be|reservation)'
}).Count `
1 `
'concurrent duplicate reservation has exactly one rejected loser'
$concurrencyLedger = Get-Content -Raw -LiteralPath $concurrencyLedgerPath |
ConvertFrom-Json -Depth 32 -DateKind String
Assert-WaggleSigningLedgerState $concurrencySession $concurrencyLedger
Assert-Equal $concurrencyLedger.entries[0].status 'reserved' 'reservation race persists one reservation'
Assert-Equal $concurrencyLedger.entries[0].attempts 1 'reservation race consumes one attempt'
$terminalRoot = New-PrivateDirectory `
(Join-Path $sessionFixtureRoot 'concurrent-terminal')
$terminalLedgerPath = Join-Path $terminalRoot 'callback-ledger.json'
$terminalSessionPath = Join-Path $terminalRoot 'session.json'
$terminalSession = [pscustomobject]@{
Id = '33333333333333333333333333333333'
ManifestSha256 = '3' * 64
Manifest = [pscustomobject]@{ slots = @($completeSlots) }
TempRoot = $completeTemp
SessionDirectory = $terminalRoot
LedgerPath = $terminalLedgerPath
ManifestPath = Join-Path $terminalRoot 'manifest.json'
Context = [pscustomobject]@{ SourceRevision = 'b' * 40 }
}
$terminalLedger = $loadedCompleteLedger | ConvertTo-Json -Depth 32 |
ConvertFrom-Json -Depth 32 -DateKind String
$terminalLedger.sessionId = $terminalSession.Id
$terminalLedger.manifestSha256 = $terminalSession.ManifestSha256
$terminalLedger.state = 'open'
$terminalLedger.terminalAtUtc = $null
$terminalLedger.terminalReceiptSha256 = $null
Write-JsonNoBom $terminalLedgerPath $terminalLedger
Write-JsonNoBom $terminalSessionPath $terminalSession
$terminalChildPath = Join-Path $terminalRoot 'terminal-child.ps1'
$terminalChildSource = @'
param(
[string]$WrapperPath, [string]$SessionPath, [string]$State,
[string]$ReadyPath, [string]$GoPath, [string]$ResultPath
)
$ErrorActionPreference = 'Stop'
. $WrapperPath -ArtifactPath 'C:\unused.exe'
$session = Get-Content -Raw -LiteralPath $SessionPath |
ConvertFrom-Json -Depth 32 -DateKind String
[IO.File]::WriteAllText($ReadyPath, 'ready')
while (-not [IO.File]::Exists($GoPath)) { Start-Sleep -Milliseconds 10 }
$receipt = [pscustomobject][ordered]@{
schemaVersion = 1
status = $State
sessionId = $session.Id
sourceRevision = $session.Context.SourceRevision
manifestSha256 = $session.ManifestSha256
terminalAtUtc = [DateTimeOffset]::UtcNow.ToString('O')
failureCode = if ($State -ceq 'failed') { 'race-fixture' } else { $null }
failureMessage = if ($State -ceq 'failed') { 'race fixture failure' } else { $null }
failedSlotId = $null
rollbackOutcome = if ($State -ceq 'failed') { 'not-required' } else { $null }
manifestPath = if ($State -ceq 'sealed') { $session.ManifestPath } else { $null }
callbackLedgerPath = if ($State -ceq 'sealed') { $session.LedgerPath } else { $null }
installerPath = if ($State -ceq 'sealed') { [string]$session.Manifest.slots[13].path } else { $null }
installerSha256 = if ($State -ceq 'sealed') { 'A' * 64 } else { $null }
signerSubject = if ($State -ceq 'sealed') { 'CN=Race Fixture' } else { $null }
payloadManifestSha256 = if ($State -ceq 'sealed') { 'B' * 64 } else { $null }
artifactBindings = if ($State -ceq 'sealed') { @(
[pscustomobject]@{ kind = 'fixture'; sha256 = 'C' * 64 },
[pscustomobject]@{ kind = 'fixture'; sha256 = 'D' * 64 }
) } else { $null }
}
try {
$path = Set-WaggleSigningTerminalState `
-Session $session -State $State -Receipt $receipt `
-LedgerMutation { param($ledger) }
[IO.File]::WriteAllText($ResultPath, "terminal:${State}:$path")
} catch {
[IO.File]::WriteAllText($ResultPath, "error:$($_.Exception.Message)")
}
'@
[IO.File]::WriteAllText(
$terminalChildPath, $terminalChildSource, [Text.UTF8Encoding]::new($false)
)
$terminalGoPath = Join-Path $terminalRoot 'go'
$terminalReadyPaths = @(
(Join-Path $terminalRoot 'ready-failed'),
(Join-Path $terminalRoot 'ready-sealed')
)
$terminalResultPaths = @(
(Join-Path $terminalRoot 'result-failed'),
(Join-Path $terminalRoot 'result-sealed')
)
$terminalStates = @('failed', 'sealed')
$terminalProcesses = @()
for ($index = 0; $index -lt 2; $index++) {
$terminalProcesses += Start-SynchronizedChild `
-PowerShellPath $approvedPowerShell -ScriptPath $terminalChildPath `
-Arguments @{
WrapperPath = $wrapperPath
SessionPath = $terminalSessionPath
State = $terminalStates[$index]
ReadyPath = $terminalReadyPaths[$index]
GoPath = $terminalGoPath
ResultPath = $terminalResultPaths[$index]
}
}
Wait-ForChildBarriers $terminalReadyPaths 'terminal fail-vs-seal race'
[IO.File]::WriteAllText($terminalGoPath, 'go')
foreach ($process in $terminalProcesses) {
if (-not $process.WaitForExit(30000)) {
$process.Kill()
throw 'Timed out waiting for the terminal fail-vs-seal race.'
}
Assert-Equal $process.ExitCode 0 'terminal race child exit code'
}
$terminalResults = @($terminalResultPaths | ForEach-Object {
[IO.File]::ReadAllText($_)
})
Assert-Equal `
@($terminalResults | Where-Object { $_ -cmatch '^terminal:(failed|sealed):' }).Count `
1 `
'terminal fail-vs-seal race has exactly one winner'
Assert-Equal `
@($terminalResults | Where-Object {
$_ -cmatch '^error:.*(terminal|opposite|different)'
}).Count `
1 `
'terminal fail-vs-seal race has exactly one rejected loser'
$terminalReceiptPaths = @(
(Join-Path $terminalRoot 'failure-receipt.json'),
(Join-Path $terminalRoot 'provenance-receipt.json')
)
$publishedTerminalReceipts = @($terminalReceiptPaths | Where-Object {
Test-Path -LiteralPath $_ -PathType Leaf
})
Assert-Equal $publishedTerminalReceipts.Count 1 'terminal race publishes one receipt'
$terminalLedgerAfterRace = Get-Content -Raw -LiteralPath $terminalLedgerPath |
ConvertFrom-Json -Depth 32 -DateKind String
Assert-WaggleSigningLedgerState $terminalSession $terminalLedgerAfterRace
$expectedTerminalState = if (
[IO.Path]::GetFileName($publishedTerminalReceipts[0]) -ceq 'failure-receipt.json'
) { 'failed' } else { 'sealed' }
Assert-Equal `
$terminalLedgerAfterRace.state $expectedTerminalState `
'terminal race ledger matches its sole receipt'
Assert-Equal `
$terminalLedgerAfterRace.terminalReceiptSha256 `
(Get-Sha256 $publishedTerminalReceipts[0]) `
'terminal race ledger binds its sole receipt digest'
} finally {
foreach ($name in $environmentNames) {
[Environment]::SetEnvironmentVariable($name, $savedEnvironment[$name])
}
if (Test-Path -LiteralPath $sessionFixtureRoot) {
[IO.Directory]::Delete($sessionFixtureRoot, $true)
}
}
Write-Host "Artifact Signing policy tests passed: $passed"