[CmdletBinding()] param( [ValidateSet('Callback', 'Package')] [string]$Mode = 'Callback', [AllowEmptyString()] [string]$ArtifactPath = '', [AllowEmptyString()] [string]$ArtifactSigningPackageSource = '', [AllowEmptyString()] [string]$UnsignedInputRoot = '', [AllowEmptyString()] [string]$SigningInputRoot = '', [AllowEmptyString()] [string]$BuildReceiptPath = '', [AllowEmptyString()] [string]$BuildReceiptSha256 = '', [AllowEmptyString()] [string]$PortableToolchainRoot = '', [AllowEmptyString()] [string]$PortableNodePath = '', [AllowEmptyString()] [string]$PortableGitPath = '', [AllowEmptyString()] [string]$PortableSevenZipPath = '', [AllowEmptyString()] [string]$PortableToolchainReceiptPath = '', [AllowEmptyString()] [string]$PortableToolchainReceiptSha256 = '', [switch]$TrustedPowerShellHost ) $trustedModuleRoot = [IO.Path]::GetFullPath([IO.Path]::Combine($PSHOME, 'Modules')) [Environment]::SetEnvironmentVariable('PSModulePath', $trustedModuleRoot, 'Process') $global:PSModuleAutoLoadingPreference = 'None' foreach ($moduleName in @( 'Microsoft.PowerShell.Security', 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Utility' )) { $moduleManifest = [IO.Path]::Combine( $trustedModuleRoot, $moduleName, "$moduleName.psd1" ) if (-not [IO.File]::Exists($moduleManifest)) { throw "Trusted PowerShell module is missing: $moduleManifest" } Microsoft.PowerShell.Core\Import-Module ` $moduleManifest -Force -Scope Global -ErrorAction Stop } if ($PSVersionTable.PSEdition -ceq 'Desktop') { $appxManifest = [IO.Path]::Combine($trustedModuleRoot, 'Appx', 'Appx.psd1') if ([IO.File]::Exists($appxManifest)) { Microsoft.PowerShell.Core\Import-Module ` $appxManifest -Force -Scope Global -ErrorAction Stop } } $trustedCommandModules = [ordered]@{ 'Get-AuthenticodeSignature' = 'Microsoft.PowerShell.Security' 'Get-Acl' = 'Microsoft.PowerShell.Security' 'Set-Acl' = 'Microsoft.PowerShell.Security' 'Get-FileHash' = 'Microsoft.PowerShell.Utility' } foreach ($entry in $trustedCommandModules.GetEnumerator()) { $command = $ExecutionContext.SessionState.InvokeCommand.GetCommand( [string]$entry.Key, [Management.Automation.CommandTypes]::All ) $expectedModulePath = [IO.Path]::Combine( $trustedModuleRoot, [string]$entry.Value, "$([string]$entry.Value).psd1" ) if ($null -eq $command -or -not [string]::Equals( [IO.Path]::GetFullPath([string]$command.Module.Path), [IO.Path]::GetFullPath($expectedModulePath), [StringComparison]::OrdinalIgnoreCase )) { throw "PowerShell command '$($entry.Key)' did not resolve from the trusted module root." } } Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $MicrosoftPublisher = 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US' $MicrosoftWindowsPublisher = 'CN=Microsoft Windows, O=Microsoft Corporation, L=Redmond, S=Washington, C=US' $DotNetPublisher = 'CN=.NET, O=Microsoft Corporation, L=Redmond, S=Washington, C=US' $NodePublisher = 'CN=OpenJS Foundation, O=OpenJS Foundation, L=San Francisco, S=California, C=US' $GitPublisher = 'CN=Johannes Schindelin, O=Johannes Schindelin, S=Nordrhein-Westfalen, C=DE' $ArtifactSigningEndpoint = 'https://weu.codesigning.azure.net/' $ArtifactSigningAccount = 'waggleos-egzakta-signing' $ArtifactSigningProfile = 'waggleos-public-trust' $ArtifactSigningClientVersion = '1.0.128.0' $ArtifactSigningDlibSha256 = '2D4C1BBC87467B3AC25BBC49DF58CC8B36A0F92B3E21AA98BBBAD08A4D7C98BA' $ArtifactSigningPackageSha256 = '74BD7D27E6CE1051409C38D9B46BC8DF0400ECD643D51FFBF2AC00869061E40B' $ArtifactSigningX64ManifestSha256 = '7B78EF94C8B5939281F7AA364EA6406716F844D893DAA89AF601933D0BA1DB6E' $SignToolSha256 = '431EE314C83988CACDA86606356FD321B75AE0093481B97E3B738E99C412F2A0' $TauriCliVersion = '2.10.1' $TauriCliSha256 = '0DD6EC63C7C63A993FDE20955E291D833C03F3760E63E0EE21E83482F6C0B43A' $TauriCliPackageSha256 = '15A3A9383E8EDF7AD3D5117DBD8B9A6D75EE36654D3F71D2D0AD48E294EBAB98' $TauriCliMainSha256 = '49DF414A16784E3711D5582D55C5C9E537ACEB1108C5ECFC6A17CDC2F5259B4D' $TauriCliIndexSha256 = 'F6A7556765D3ED2DD40F9FCF609CEB7D3646FCCBB7FA01EEB9B7931F8BC8BB4F' $TauriNativePackageSha256 = '6CB1DD193C36BAA11679ED9403FC564AA1E06A2BFAA066A146555D93BD07A783' $TauriNativeBinarySha256 = 'F7289148FDF4CE6CE527D34C63A0055C87DE4FFE0A98475F3998D527225B1443' $MakensisSha256 = '42850802704ECB11163F7E0329D35EE54BD288953200D4966E226D572848CFC5' $NsisClosureSha256 = '1FC822D1A183552A80ADEA01B0BF456F462B90518256EF1FE9EDFA22D76CD85A' $GitSha256 = '34A408843194BE320D8A87A3C12CD5C7D2E08D03B24567A41DB32E21D12569D2' $GitRuntimeSha256 = '755D4896D35663D0FF08924F84507F35236B83D240635B512C519BF43CC71A87' $NodePath = 'C:\Program Files\nodejs\node.exe' $NodeSha256 = 'AE1A50511BE58E987483FDBC12125407443926D2D394669ADE2352776E920DD3' $SevenZipPath = 'C:\Program Files\7-Zip\7z.exe' $SevenZipSha256 = '4CD7D776C686427226A151789D2D61F0B2ED2C392148CC4E69C0238362FAFECF' $SevenZipDllSha256 = '5BD20FB38499D95C39594F41D4781B6181B3304B7F1F4D06B0182F514E7EAA74' $NodeArchiveSha256 = '7C93E9D92BF68C07182B471AA187E35EE6CD08EF0F24AB060DFFF605FCC1C57C' $GitArchiveSha256 = 'C2C955A21FA99889D83F485F24FA5D9A38FFFC2D509D4022385510E11C26B250' $SevenZipArchiveSha256 = '78AFA2A1C773CAF3CF7EDF62F857D2A8A5DA55FB0FFF5DA416074C0D28B2B55F' $PortableToolchainFileCount = 2495 $PortableToolchainInventorySha256 = 'D64F897D4E1A7F07FE9BA62D6AF062EF9F0E41C595CDAF2F3C4F73991BBEA0F5' $ApprovedPublisher = 'CN=EGZAKTA DOO BEOGRAD, O=EGZAKTA DOO BEOGRAD, L=Amsterdam, C=NL' $CodeSigningOid = '1.3.6.1.5.5.7.3.3' $SystemPowerShellPath = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' $SignToolPath = 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe' $GitPath = 'C:\Program Files\Git\cmd\git.exe' $NsisUninstallerPattern = '^nst[0-9A-F]{4}\.tmp$' function Get-TrustedPath { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Label, [ValidateSet('Leaf', 'Container')] [string]$PathType = 'Leaf', [switch]$AllowHardLink ) if ([string]::IsNullOrWhiteSpace($Path) -or $Path -match '[\x00-\x1F\x7F]' -or $Path -notmatch '^[A-Za-z]:[\\/]' -or $Path -match '^[\\/]{2}' -or $Path -match '^[\\/]{2}[?.][\\/]' -or $Path -match '(^|[\\/])\.\.?(?:[\\/]|$)' -or $Path.Substring(2) -match ':' -or @($Path -split '[\\/]' | Where-Object { $_ -match '[. ]$' }).Count -gt 0) { throw "$Label must use a safe, fully qualified local Windows path." } $fullPath = [IO.Path]::GetFullPath($Path) $testPathType = if ($PathType -eq 'Leaf') { 'Leaf' } else { 'Container' } if (-not (Test-Path -LiteralPath $fullPath -PathType $testPathType)) { throw "$Label does not exist as a $($PathType.ToLowerInvariant()): $fullPath" } $root = [IO.Path]::GetPathRoot($fullPath) $relative = $fullPath.Substring($root.Length) $current = $root foreach ($component in @($relative -split '[\\/]' | Where-Object { $_ })) { $current = Join-Path $current $component $item = Get-Item -LiteralPath $current -Force if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "$Label traverses a reparse point: $current" } $linkTypeProperty = $item.PSObject.Properties['LinkType'] $linkType = if ($null -eq $linkTypeProperty) { '' } else { [string]$linkTypeProperty.Value } if (-not [string]::IsNullOrEmpty($linkType) -and -not ($AllowHardLink -and $linkType -ceq 'HardLink')) { throw "$Label traverses a linked filesystem object: $current" } } $resolved = (Resolve-Path -LiteralPath $fullPath).ProviderPath if (-not [string]::Equals( [IO.Path]::GetFullPath($resolved), $fullPath, [StringComparison]::OrdinalIgnoreCase )) { throw "$Label resolves to an unexpected path." } return $fullPath } function Get-ContainedRelativePath { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Root ) $relative = [IO.Path]::GetRelativePath($Root, $Path) if ([IO.Path]::IsPathRooted($relative) -or $relative -eq '..' -or $relative.StartsWith("..$([IO.Path]::DirectorySeparatorChar)", [StringComparison]::Ordinal)) { return $null } return $relative.Replace('/', '\') } function Get-ArtifactPolicyKind { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$ReleaseRoot, [Parameter(Mandatory = $true)] [string]$ResourcesRoot, [AllowNull()] [string]$NsisTempRoot, [Parameter(Mandatory = $true)] [string]$Version ) $releaseRelative = Get-ContainedRelativePath $Path $ReleaseRoot if ($null -ne $releaseRelative) { $allowedPeFiles = @( 'waggle.exe', 'nsis\x64\Plugins\x86-unicode\NSISdl.dll', 'nsis\x64\Plugins\x86-unicode\StartMenu.dll', 'nsis\x64\Plugins\x86-unicode\System.dll', 'nsis\x64\Plugins\x86-unicode\nsDialogs.dll', 'nsis\x64\Plugins\x86-unicode\additional\nsis_tauri_utils.dll', "bundle\nsis\Waggle_${Version}_x64-setup.exe" ) if ($allowedPeFiles -contains $releaseRelative) { return 'PE' } throw "Artifact is not in the approved Tauri release manifest: $releaseRelative" } $resourceRelative = Get-ContainedRelativePath $Path $ResourcesRoot if ($null -ne $resourceRelative) { $allowedResourceFiles = @( 'native\vec0.dll', 'native\onnxruntime\onnxruntime.dll', 'node_modules\@img\sharp-win32-x64\lib\libvips-42.dll', 'node_modules\@img\sharp-win32-x64\lib\libvips-cpp-8.18.3.dll', 'node_modules\onnxruntime-node\bin\napi-v3\win32\x64\onnxruntime.dll', 'node_modules\sqlite-vec-windows-x64\vec0.dll' ) if ($allowedResourceFiles -contains $resourceRelative) { return 'PE' } throw "Artifact is not in the approved Tauri resource manifest: $resourceRelative" } if ($null -ne $NsisTempRoot) { $tempRelative = Get-ContainedRelativePath $Path $NsisTempRoot if ($null -ne $tempRelative -and $tempRelative -notmatch '[\\/]' -and $tempRelative -cmatch '^nst[0-9A-F]{4}\.tmp$') { return 'PE' } } throw 'Artifact is outside every approved Waggle signing root.' } function Assert-PeFile { param([Parameter(Mandatory = $true)] [string]$Path) $stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) try { if ($stream.Length -lt 64) { throw 'PE file is too short.' } $reader = [IO.BinaryReader]::new($stream) if ($reader.ReadUInt16() -ne 0x5A4D) { throw 'Artifact lacks the PE MZ header.' } $stream.Position = 0x3C $peOffset = $reader.ReadUInt32() if ($peOffset -lt 64 -or $peOffset -gt 4MB -or $peOffset + 4 -gt $stream.Length) { throw 'Artifact has an invalid PE header offset.' } $stream.Position = $peOffset if ($reader.ReadUInt32() -ne 0x00004550) { throw 'Artifact lacks the PE signature.' } } finally { $stream.Dispose() } } function Assert-MsiFile { param([Parameter(Mandatory = $true)] [string]$Path) $expected = [byte[]](0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1) $stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) try { if ($stream.Length -lt $expected.Length) { throw 'MSI file is too short.' } $actual = [byte[]]::new($expected.Length) if ($stream.Read($actual, 0, $actual.Length) -ne $actual.Length) { throw 'Could not read the MSI header.' } if (-not [Linq.Enumerable]::SequenceEqual($actual, $expected)) { throw 'Artifact lacks the MSI compound-file header.' } } finally { $stream.Dispose() } } function Assert-ApprovedAuthenticodeFile { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Label, [AllowNull()] [string]$ExpectedSha256, [Parameter(Mandatory = $true)] [string]$ExpectedPublisher, [switch]$AllowCatalog ) $signature = Get-AuthenticodeSignature -LiteralPath $Path $signatureType = [string]$signature.SignatureType $approvedSignatureType = $signatureType -ceq 'Authenticode' -or ($AllowCatalog -and $signatureType -ceq 'Catalog') if ($signature.Status -ne [Management.Automation.SignatureStatus]::Valid -or -not $approvedSignatureType -or $null -eq $signature.SignerCertificate -or -not [string]::Equals( [string]$signature.SignerCertificate.Subject, $ExpectedPublisher, [StringComparison]::Ordinal )) { throw "$Label is not validly Authenticode-signed by the approved publisher." } $hasCodeSigningEku = @( $signature.SignerCertificate.Extensions | Where-Object { $_ -is [Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension] } | ForEach-Object { $_.EnhancedKeyUsages } | Where-Object { $_.Value -eq $CodeSigningOid } ).Count -gt 0 if (-not $hasCodeSigningEku) { throw "$Label lacks the Code Signing EKU." } if (-not [string]::IsNullOrEmpty($ExpectedSha256)) { $actualHash = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash if (-not [string]::Equals($actualHash, $ExpectedSha256, [StringComparison]::OrdinalIgnoreCase)) { throw "$Label does not match the pinned SHA-256 digest." } } } function Assert-MicrosoftAuthenticodeFile { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Label, [AllowNull()] [string]$ExpectedSha256, [string]$ExpectedPublisher = $MicrosoftPublisher, [switch]$AllowCatalog ) Assert-ApprovedAuthenticodeFile ` $Path $Label $ExpectedSha256 $ExpectedPublisher -AllowCatalog:$AllowCatalog } function Assert-ApprovedPowerShell7Path { param([Parameter(Mandatory = $true)] [string]$Path) $programFiles = [Environment]::GetFolderPath( [Environment+SpecialFolder]::ProgramFiles ) $standardPath = [IO.Path]::GetFullPath((Join-Path $programFiles 'PowerShell\7\pwsh.exe')) $windowsAppsPattern = '^[A-Za-z]:\\Program Files\\WindowsApps\\Microsoft\.PowerShell_\d+\.\d+\.\d+\.\d+_x64__8wekyb3d8bbwe\\pwsh\.exe$' if (-not [string]::Equals($Path, $standardPath, [StringComparison]::OrdinalIgnoreCase) -and $Path -notmatch $windowsAppsPattern) { throw 'PowerShell 7 must come from the canonical Microsoft installation path.' } if ([Version](Get-Item -LiteralPath $Path).VersionInfo.FileVersion -lt [Version]'7.5.0') { throw 'Artifact Signing requires PowerShell 7.5 or newer for non-coercing JSON validation.' } Assert-MicrosoftAuthenticodeFile $Path 'PowerShell 7 host' $null } function Get-ApprovedPowerShell7Path { $programFiles = [Environment]::GetFolderPath( [Environment+SpecialFolder]::ProgramFiles ) $candidates = @() $standardPath = Join-Path $programFiles 'PowerShell\7\pwsh.exe' if (Test-Path -LiteralPath $standardPath -PathType Leaf) { $candidates += $standardPath } if ($PSVersionTable.PSEdition -ceq 'Core') { $currentHost = (Get-Process -Id $PID).Path if (-not [string]::IsNullOrWhiteSpace($currentHost)) { $candidates += $currentHost } } if ($PSVersionTable.PSEdition -ceq 'Desktop') { $windowsAppsRoot = Join-Path $programFiles 'WindowsApps' $candidates += @( Microsoft.PowerShell.Core\Get-Module Appx | ForEach-Object { Appx\Get-AppxPackage -Name Microsoft.PowerShell -ErrorAction SilentlyContinue } | Where-Object { [string]$_.Architecture -ceq 'X64' -and $_.InstallLocation -like "$windowsAppsRoot\Microsoft.PowerShell_*" } | Sort-Object Version -Descending | ForEach-Object { Join-Path $_.InstallLocation 'pwsh.exe' } | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } ) } foreach ($candidate in $candidates) { try { $trustedCandidate = Get-TrustedPath $candidate 'PowerShell 7 host' -AllowHardLink Assert-ApprovedPowerShell7Path $trustedCandidate return $trustedCandidate } catch { continue } } throw 'No approved, Microsoft-signed 64-bit PowerShell 7 host is installed.' } function Invoke-TrustedPowerShellRelaunch { param( [Parameter(Mandatory = $true)] [ValidateSet('Callback', 'Package')] [string]$LaunchMode, [string]$Path = '', [string]$PackageSource = '', [string]$ToolchainRoot = '', [string]$Node = '', [string]$Git = '', [string]$SevenZip = '', [string]$ToolchainReceipt = '', [string]$ToolchainReceiptSha256 = '' ) $systemHost = Get-TrustedPath $SystemPowerShellPath 'Windows PowerShell bootstrap' -AllowHardLink $actualHost = Get-TrustedPath (Get-Process -Id $PID).Path 'Current bootstrap host' -AllowHardLink if (-not [string]::Equals($actualHost, $systemHost, [StringComparison]::OrdinalIgnoreCase)) { throw 'Artifact Signing bootstrap must run under canonical Windows PowerShell.' } Assert-MicrosoftAuthenticodeFile ` $systemHost 'Windows PowerShell bootstrap' $null $MicrosoftWindowsPublisher -AllowCatalog $powerShell7 = Get-ApprovedPowerShell7Path $arguments = @( '-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $PSCommandPath, '-Mode', $LaunchMode, '-TrustedPowerShellHost' ) if ($LaunchMode -ceq 'Callback') { if ([string]::IsNullOrWhiteSpace($Path)) { throw 'Signing callback requires an artifact path.' } $arguments += @('-ArtifactPath', $Path) } else { foreach ($pair in @( @('-UnsignedInputRoot', $UnsignedInputRoot), @('-SigningInputRoot', $SigningInputRoot), @('-BuildReceiptPath', $BuildReceiptPath), @('-BuildReceiptSha256', $BuildReceiptSha256), @('-ArtifactSigningPackageSource', $PackageSource), @('-PortableToolchainRoot', $ToolchainRoot), @('-PortableNodePath', $Node), @('-PortableGitPath', $Git), @('-PortableSevenZipPath', $SevenZip), @('-PortableToolchainReceiptPath', $ToolchainReceipt), @('-PortableToolchainReceiptSha256', $ToolchainReceiptSha256) )) { if (-not [string]::IsNullOrWhiteSpace([string]$pair[1])) { $arguments += @([string]$pair[0], [string]$pair[1]) } } } & $powerShell7 @arguments if ($LASTEXITCODE -ne 0) { throw "Trusted PowerShell 7 signing process failed with exit code $LASTEXITCODE." } } function New-PrivateDirectory { param([Parameter(Mandatory = $true)] [string]$Path) [IO.Directory]::CreateDirectory($Path) | Out-Null $inheritance = [Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [Security.AccessControl.InheritanceFlags]::ObjectInherit $propagation = [Security.AccessControl.PropagationFlags]::None $allow = [Security.AccessControl.AccessControlType]::Allow $fullControl = [Security.AccessControl.FileSystemRights]::FullControl $identities = @( [Security.Principal.WindowsIdentity]::GetCurrent().User, [Security.Principal.SecurityIdentifier]::new('S-1-5-18'), [Security.Principal.SecurityIdentifier]::new('S-1-5-32-544') ) $approvedSids = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) foreach ($identity in $identities) { [void]$approvedSids.Add($identity.Value) } $isApprovedAcl = { param([Security.AccessControl.DirectorySecurity]$Acl) if (-not $Acl.AreAccessRulesProtected -or -not $approvedSids.Contains( $Acl.GetOwner([Security.Principal.SecurityIdentifier]).Value )) { return $false } $rules = @($Acl.GetAccessRules( $true, $true, [Security.Principal.SecurityIdentifier] )) if ($rules.Count -ne $approvedSids.Count) { return $false } foreach ($rule in $rules) { if ($rule.IsInherited -or $rule.AccessControlType -ne $allow -or -not $approvedSids.Contains($rule.IdentityReference.Value) -or ($rule.FileSystemRights -band $fullControl) -ne $fullControl) { return $false } } return $true } $existing = Get-Acl -LiteralPath $Path if (-not (& $isApprovedAcl $existing)) { $security = $existing $security.SetAccessRuleProtection($true, $false) $security.SetOwner([Security.Principal.WindowsIdentity]::GetCurrent().User) $existingRules = @($security.GetAccessRules( $true, $true, [Security.Principal.SecurityIdentifier] )) foreach ($existingRule in $existingRules) { $security.RemoveAccessRuleSpecific($existingRule) } foreach ($identity in $identities) { $rule = [Security.AccessControl.FileSystemAccessRule]::new( $identity, $fullControl, $inheritance, $propagation, $allow ) [void]$security.AddAccessRule($rule) } Set-Acl -LiteralPath $Path -AclObject $security } $trustedPath = Get-TrustedPath $Path 'Private signing directory' 'Container' $applied = Get-Acl -LiteralPath $trustedPath if (-not (& $isApprovedAcl $applied)) { throw 'Private signing directory permissions do not match the approved principals.' } return $trustedPath } function Open-ReadLock { param([Parameter(Mandatory = $true)] [string]$Path) return [IO.File]::Open( $Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read ) } function Get-WagglePrebuiltFiles { param( [Parameter(Mandatory = $true)] [string]$Root, [Parameter(Mandatory = $true)] [string]$Label ) $trustedRoot = Get-TrustedPath $Root $Label 'Container' $pending = [Collections.Generic.Queue[string]]::new() $files = [Collections.Generic.List[string]]::new() $pending.Enqueue($trustedRoot) while ($pending.Count -gt 0) { $directory = $pending.Dequeue() foreach ($item in @(Get-ChildItem -LiteralPath $directory -Force)) { if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "$Label contains a reparse point or linked filesystem object: $($item.FullName)" } if ($item.PSIsContainer) { $pending.Enqueue((Get-TrustedPath $item.FullName $Label 'Container')) } else { $files.Add((Get-TrustedPath $item.FullName $Label -AllowHardLink)) } } } return [pscustomobject]@{ Root = $trustedRoot; Files = @($files) } } function Get-WaggleInventorySha256 { param([Parameter(Mandatory = $true)] [object[]]$Entries) $canonical = @($Entries | ForEach-Object { [ordered]@{ path = [string]$_.path size = [long]$_.size sha256 = ([string]$_.sha256).ToUpperInvariant() } }) | ConvertTo-Json -Depth 8 -Compress return [Convert]::ToHexString( [Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($canonical)) ) } function Assert-WaggleCanonicalInventoryEntries { param( [Parameter(Mandatory = $true)] [object[]]$Entries, [Parameter(Mandatory = $true)] [string]$Label ) $exactPaths = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) $foldedPaths = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $orderedPaths = [Collections.Generic.List[string]]::new() foreach ($entry in $Entries) { $relativePath = [string](Get-RequiredPropertyValue $entry 'path' "$Label entry") $sha256 = [string](Get-RequiredPropertyValue $entry 'sha256' "$Label entry") $size = [long](Get-RequiredPropertyValue $entry 'size' "$Label entry") if ([string]::IsNullOrWhiteSpace($relativePath) -or $relativePath -match '[\x00-\x1F\x7F]' -or $relativePath -match '(^|[\\/])\.\.?(?:[\\/]|$)' -or [IO.Path]::IsPathRooted($relativePath) -or $relativePath.Contains('/') -or $relativePath.Contains(':') -or @($relativePath -split '\\' | Where-Object { $_ -match '[. ]$' }).Count -ne 0) { throw "$Label contains an unsafe or non-canonical relative path." } if (-not $exactPaths.Add($relativePath) -or -not $foldedPaths.Add($relativePath)) { throw "$Label contains a duplicate canonical path or case-insensitive case collision." } if ($sha256 -notmatch '^[0-9A-Fa-f]{64}$' -or $size -lt 0) { throw "$Label contains an invalid SHA-256 digest or file size." } $orderedPaths.Add($relativePath) } $sortedPaths = [Collections.Generic.List[string]]::new() foreach ($path in $orderedPaths) { $sortedPaths.Add($path) } $sortedPaths.Sort([StringComparer]::Ordinal) if ([string]::Join("`n", $orderedPaths) -cne [string]::Join("`n", $sortedPaths)) { throw "$Label entries must be sorted by ordinal canonical path." } } function New-WagglePrebuiltInventory { param( [Parameter(Mandatory = $true)] [string]$Root, [string]$CargoReleaseRelativePath = 'release' ) $tree = Get-WagglePrebuiltFiles $Root 'Prebuilt input tree' $entries = [Collections.Generic.List[object]]::new() foreach ($path in $tree.Files) { $streams = @(Get-Item -LiteralPath $path -Stream * -ErrorAction Stop) if ($streams.Count -ne 1 -or [string]$streams[0].Stream -cne ':$DATA') { throw 'Prebuilt input tree contains an alternate data stream (ADS).' } $relative = (Get-ContainedRelativePath $path $tree.Root).Replace('/', '\') if ($null -eq $relative) { throw 'Prebuilt input tree path escaped its root.' } $item = Get-Item -LiteralPath $path -Force $linkTypeProperty = $item.PSObject.Properties['LinkType'] $linkType = if ($null -eq $linkTypeProperty) { '' } else { [string]$linkTypeProperty.Value } if (-not [string]::IsNullOrEmpty($linkType)) { if ($linkType -cne 'HardLink') { throw 'Prebuilt input tree contains an unsupported linked filesystem object.' } Assert-ApprovedHardLinkTopology ` $path (Join-Path $tree.Root $CargoReleaseRelativePath) } $lock = Open-ReadLock $path try { $entries.Add([pscustomobject][ordered]@{ path = $relative size = [long]$item.Length sha256 = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash }) } finally { $lock.Dispose() } } $sortedEntries = @($entries) [Array]::Sort($sortedEntries, [Comparison[object]]{ param($left, $right) return [StringComparer]::Ordinal.Compare([string]$left.path, [string]$right.path) }) Assert-WaggleCanonicalInventoryEntries $sortedEntries 'Prebuilt input inventory' return [pscustomobject][ordered]@{ entries = $sortedEntries sha256 = Get-WaggleInventorySha256 $sortedEntries } } function Test-WagglePathsOverlap { param( [Parameter(Mandatory = $true)] [string]$Left, [Parameter(Mandatory = $true)] [string]$Right ) $leftPath = [IO.Path]::GetFullPath($Left).TrimEnd('\') $rightPath = [IO.Path]::GetFullPath($Right).TrimEnd('\') return [string]::Equals($leftPath, $rightPath, [StringComparison]::OrdinalIgnoreCase) -or $leftPath.StartsWith("$rightPath\", [StringComparison]::OrdinalIgnoreCase) -or $rightPath.StartsWith("$leftPath\", [StringComparison]::OrdinalIgnoreCase) } function New-WagglePrebuiltWorkCopy { param( [Parameter(Mandatory = $true)] [string]$SourceRoot, [Parameter(Mandatory = $true)] [string]$DestinationRoot, [Parameter(Mandatory = $true)] [object]$Inventory, [Parameter(Mandatory = $true)] [string]$Label, [string]$CargoReleaseRelativePath = 'x86_64-pc-windows-msvc\release' ) $source = Get-TrustedPath $SourceRoot "$Label source" 'Container' $destination = [IO.Path]::GetFullPath($DestinationRoot) if ((Test-WagglePathsOverlap $source $destination) -or (Test-Path -LiteralPath $destination)) { throw "$Label destination must be absent and distinct from its source." } $root = New-PrivateDirectory $destination $entries = @((Get-RequiredPropertyValue $Inventory 'entries' "$Label inventory")) Assert-WaggleCanonicalInventoryEntries $entries "$Label inventory" $releaseRelative = $CargoReleaseRelativePath $mainRelative = "$releaseRelative\waggle.exe" $dependencyRelative = "$releaseRelative\deps\waggle.exe" $mainEntry = @($entries | Where-Object { [string]$_.path -ceq $mainRelative }) $dependencyEntry = @($entries | Where-Object { [string]$_.path -ceq $dependencyRelative }) if ($mainEntry.Count -ne 1 -or $dependencyEntry.Count -ne 1 -or [long]$mainEntry[0].size -ne [long]$dependencyEntry[0].size -or -not [string]::Equals( [string]$mainEntry[0].sha256, [string]$dependencyEntry[0].sha256, [StringComparison]::OrdinalIgnoreCase )) { throw "$Label inventory lacks the exact equal-byte Cargo executable pair." } try { foreach ($entry in $entries) { $relative = [string]$entry.path if ($relative -ceq $mainRelative) { continue } $sourcePath = Get-TrustedPath ` (Join-Path $source $relative) "$Label source file" -AllowHardLink $destinationPath = Join-Path $root $relative [IO.Directory]::CreateDirectory((Split-Path $destinationPath -Parent)) | Out-Null [IO.File]::Copy($sourcePath, $destinationPath, $false) } $dependencyPath = Join-Path $root $dependencyRelative $mainPath = Join-Path $root $mainRelative New-Item -ItemType HardLink -Path $mainPath -Target $dependencyPath | Out-Null $copyInventory = New-WagglePrebuiltInventory ` -Root $root -CargoReleaseRelativePath $CargoReleaseRelativePath if (-not [string]::Equals( [string]$copyInventory.sha256, [string](Get-RequiredPropertyValue $Inventory 'sha256' "$Label inventory"), [StringComparison]::OrdinalIgnoreCase )) { throw "$Label work copy does not match the receipt-bound inventory." } Assert-ApprovedHardLinkTopology ` $mainPath (Join-Path $root $releaseRelative) return $root } catch { if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue } throw } } function Assert-WaggleHostedDiskCapacity { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [object]$Inventory, [long]$AvailableBytes = -1 ) $entries = @((Get-RequiredPropertyValue $Inventory 'entries' 'Hosted build target inventory')) $inventoryBytes = [long]0 foreach ($entry in $entries) { $size = [long](Get-RequiredPropertyValue $entry 'size' 'Hosted build inventory entry') if ($size -lt 0 -or $inventoryBytes -gt [long]::MaxValue - $size) { throw 'Hosted build target inventory has an invalid aggregate size.' } $inventoryBytes += $size } $safetyBytes = [Math]::Max(2GB, [long][Math]::Ceiling($inventoryBytes / 2.0)) $requiredBytes = $inventoryBytes + $safetyBytes if ($AvailableBytes -lt 0) { $root = [IO.Path]::GetPathRoot([IO.Path]::GetFullPath($Path)) $AvailableBytes = [IO.DriveInfo]::new($root).AvailableFreeSpace } if ($AvailableBytes -lt $requiredBytes) { throw "Hosted signing has insufficient free disk space: requires $requiredBytes bytes, available $AvailableBytes bytes." } return $requiredBytes } function New-WagglePreflightEvidenceCopy { param( [Parameter(Mandatory = $true)] [object]$Context, [Parameter(Mandatory = $true)] [string]$DestinationRoot ) $root = New-PrivateDirectory $DestinationRoot try { $fixedPaths = @(Get-ExpectedNsisFixedPaths $Context '0.0.0') $mainSource = Get-TrustedPath $fixedPaths[0] 'Unsigned preflight main' -AllowHardLink $mainRelative = Get-ContainedRelativePath $mainSource $Context.TargetRoot if ($null -eq $mainRelative) { throw 'Unsigned preflight main escaped its target root.' } $mainDestination = Join-Path $root $mainRelative $dependencyDestination = Join-Path ` (Split-Path $mainDestination -Parent) 'deps\waggle.exe' [IO.Directory]::CreateDirectory((Split-Path $dependencyDestination -Parent)) | Out-Null [IO.File]::Copy($mainSource, $dependencyDestination, $false) New-Item -ItemType HardLink -Path $mainDestination -Target $dependencyDestination | Out-Null Assert-ApprovedHardLinkTopology ` $mainDestination (Split-Path $mainDestination -Parent) foreach ($sourcePathValue in @($fixedPaths | Select-Object -Skip 1)) { $sourcePath = Get-TrustedPath ` $sourcePathValue 'Unsigned preflight evidence source' -AllowHardLink $relative = Get-ContainedRelativePath $sourcePath $Context.TargetRoot if ($null -eq $relative) { throw 'Unsigned preflight evidence escaped its target root.' } $destination = Join-Path $root $relative [IO.Directory]::CreateDirectory((Split-Path $destination -Parent)) | Out-Null [IO.File]::Copy($sourcePath, $destination, $false) if ((Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash -cne (Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256).Hash) { throw 'Unsigned preflight evidence copy failed digest verification.' } } return $root } catch { if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue } throw } } function Open-WaggleValidatedPrebuiltTree { param( [Parameter(Mandatory = $true)] [string]$Root, [Parameter(Mandatory = $true)] [object]$ExpectedInventory, [Parameter(Mandatory = $true)] [string]$Label, [string[]]$DisallowedRoots = @(), [string[]]$MutablePaths = @(), [string[]]$RegeneratedRoots = @(), [string]$CargoReleaseRelativePath = 'release' ) $trustedRoot = Get-TrustedPath $Root $Label 'Container' foreach ($disallowedRoot in $DisallowedRoots) { $overlaps = if ([string]::IsNullOrWhiteSpace($disallowedRoot)) { $false } else { Test-WagglePathsOverlap $trustedRoot ([IO.Path]::GetFullPath($disallowedRoot)) } if (-not [string]::IsNullOrWhiteSpace($disallowedRoot) -and $overlaps) { throw "$Label must be distinct from every disallowed root; root overlap is forbidden." } } $expectedEntries = @((Get-RequiredPropertyValue $ExpectedInventory 'entries' "$Label inventory")) $expectedSha256 = [string](Get-RequiredPropertyValue ` $ExpectedInventory 'sha256' "$Label inventory") Assert-WaggleCanonicalInventoryEntries $expectedEntries "$Label inventory" if ($expectedSha256 -notmatch '^[0-9A-Fa-f]{64}$' -or -not [string]::Equals( (Get-WaggleInventorySha256 $expectedEntries), $expectedSha256, [StringComparison]::OrdinalIgnoreCase )) { throw "$Label inventory aggregate SHA-256 digest is invalid." } $tree = Get-WagglePrebuiltFiles $trustedRoot $Label $actualPaths = [Collections.Generic.List[string]]::new() foreach ($path in $tree.Files) { $relative = Get-ContainedRelativePath $path $trustedRoot if ($null -eq $relative) { throw "$Label path escaped its root." } $actualPaths.Add($relative.Replace('/', '\')) } $actualPaths.Sort([StringComparer]::Ordinal) $expectedPaths = @($expectedEntries | ForEach-Object { [string]$_.path }) if ($actualPaths.Count -ne $expectedPaths.Count -or [string]::Join("`n", $actualPaths) -cne [string]::Join("`n", $expectedPaths)) { throw "$Label contains missing, extra, or unexpected files relative to its inventory." } $mutableSet = [Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase ) foreach ($mutablePathValue in @($MutablePaths)) { $mutablePath = Get-TrustedPath ` $mutablePathValue "$Label mutable file" -AllowHardLink if ($null -eq (Get-ContainedRelativePath $mutablePath $trustedRoot) -or -not $mutableSet.Add($mutablePath)) { throw "$Label mutable-file roster contains an escaped or duplicate path." } } $regeneratedRootList = [Collections.Generic.List[string]]::new() foreach ($regeneratedRootValue in @($RegeneratedRoots)) { $regeneratedRoot = Get-TrustedPath ` $regeneratedRootValue "$Label regenerated root" 'Container' $relativeRoot = Get-ContainedRelativePath $regeneratedRoot $trustedRoot if ($null -eq $relativeRoot -or $relativeRoot -ceq '.') { throw "$Label regenerated-root roster contains an escaped or unsafe root." } foreach ($existingRoot in $regeneratedRootList) { if (Test-WagglePathsOverlap $regeneratedRoot $existingRoot) { throw "$Label regenerated-root roster contains duplicate or overlapping roots." } } $regeneratedRootList.Add($regeneratedRoot) } $matchedMutableSet = [Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase ) $matchedRegeneratedRoots = [Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase ) $locks = [Collections.Generic.List[IDisposable]]::new() try { for ($index = 0; $index -lt $expectedEntries.Count; $index++) { $entry = $expectedEntries[$index] $path = Get-TrustedPath (Join-Path $trustedRoot ([string]$entry.path)) $Label -AllowHardLink $streams = @(Get-Item -LiteralPath $path -Stream * -ErrorAction Stop) if ($streams.Count -ne 1 -or [string]$streams[0].Stream -cne ':$DATA') { throw "$Label contains an alternate data stream (ADS)." } $item = Get-Item -LiteralPath $path -Force $linkTypeProperty = $item.PSObject.Properties['LinkType'] $linkType = if ($null -eq $linkTypeProperty) { '' } else { [string]$linkTypeProperty.Value } if (-not [string]::IsNullOrEmpty($linkType)) { if ($linkType -cne 'HardLink') { throw "$Label contains an unsupported link." } Assert-ApprovedHardLinkTopology ` $path (Join-Path $trustedRoot $CargoReleaseRelativePath) } $lock = Open-ReadLock $path try { if ([long]$item.Length -ne [long]$entry.size -or -not [string]::Equals( (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash, [string]$entry.sha256, [StringComparison]::OrdinalIgnoreCase )) { throw "$Label file does not match its inventory SHA-256 digest and size." } if ($mutableSet.Contains($path)) { [void]$matchedMutableSet.Add($path) } $isRegeneratedOutput = $false foreach ($regeneratedRoot in $regeneratedRootList) { if ($null -ne (Get-ContainedRelativePath $path $regeneratedRoot)) { $isRegeneratedOutput = $true [void]$matchedRegeneratedRoots.Add($regeneratedRoot) break } } if (-not $mutableSet.Contains($path) -and -not $isRegeneratedOutput) { $locks.Add($lock) $lock = $null } } finally { if ($null -ne $lock) { $lock.Dispose() } } } if ($matchedMutableSet.Count -ne $mutableSet.Count) { throw "$Label mutable-file roster does not match its exact inventory." } if ($matchedRegeneratedRoots.Count -ne $regeneratedRootList.Count) { throw "$Label regenerated-root roster does not match its exact inventory." } return [pscustomobject]@{ Root = $trustedRoot Inventory = $ExpectedInventory Locks = $locks MutablePaths = @($mutableSet) RegeneratedRoots = @($regeneratedRootList) } } catch { foreach ($lock in $locks) { $lock.Dispose() } throw } } function Clear-WaggleRegeneratedRoots { param([Parameter(Mandatory = $true)] [object]$Lease) $leaseRoot = Get-TrustedPath $Lease.Root 'Validated prebuilt work root' 'Container' foreach ($rootValue in @($Lease.RegeneratedRoots)) { $trustedRoot = Get-TrustedPath ` ([string]$rootValue) 'Validated Tauri-generated output root' 'Container' $relativeRoot = Get-ContainedRelativePath $trustedRoot $leaseRoot if ($null -eq $relativeRoot -or $relativeRoot -ceq '.') { throw 'Tauri-generated output root escaped its validated private work root.' } [IO.Directory]::Delete($trustedRoot, $true) if (Test-Path -LiteralPath $trustedRoot) { throw 'Tauri-generated output root could not be cleared before packaging.' } } } function Get-NsisPatchedMainSha256 { param([Parameter(Mandatory = $true)] [string]$Path) $trustedPath = Get-TrustedPath $Path 'Tauri main executable' -AllowHardLink $lock = Open-ReadLock $trustedPath try { $bytes = [IO.File]::ReadAllBytes($trustedPath) $unknownToken = [Text.Encoding]::ASCII.GetBytes('__TAURI_BUNDLE_TYPE_VAR_UNK') $nsisToken = [Text.Encoding]::ASCII.GetBytes('__TAURI_BUNDLE_TYPE_VAR_NSS') $msiToken = [Text.Encoding]::ASCII.GetBytes('__TAURI_BUNDLE_TYPE_VAR_MSI') $findOffsets = { param([byte[]]$Needle) $offsets = [Collections.Generic.List[int]]::new() for ($offset = 0; $offset -le $bytes.Length - $Needle.Length; $offset++) { $matches = $true for ($index = 0; $index -lt $Needle.Length; $index++) { if ($bytes[$offset + $index] -ne $Needle[$index]) { $matches = $false break } } if ($matches) { $offsets.Add($offset) } } return @($offsets) } $unknownOffsets = @(& $findOffsets $unknownToken) if ($unknownOffsets.Count -ne 1 -or @(& $findOffsets $nsisToken).Count -ne 0 -or @(& $findOffsets $msiToken).Count -ne 0) { throw 'Tauri main executable must contain exactly one unpatched bundle-type token.' } $patched = [byte[]]$bytes.Clone() [Array]::Copy($nsisToken, 0, $patched, $unknownOffsets[0], $nsisToken.Length) return [Convert]::ToHexString( [Security.Cryptography.SHA256]::HashData($patched) ) } finally { $lock.Dispose() } } function Get-RequiredPropertyValue { param( [Parameter(Mandatory = $true)] [object]$InputObject, [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [string]$Label ) $property = $InputObject.PSObject.Properties[$Name] if ($null -eq $property -or $null -eq $property.Value) { throw "$Label is missing required property '$Name'." } return $property.Value } function Assert-ExactCanonicalPathValue { param( [Parameter(Mandatory = $true)] [string]$Actual, [Parameter(Mandatory = $true)] [string]$Expected, [Parameter(Mandatory = $true)] [string]$Label ) if ([string]::IsNullOrWhiteSpace($Actual) -or $Actual -match '[\x00-\x1F\x7F]' -or $Actual -notmatch '^[A-Za-z]:[\\/]' -or $Actual -match '^[\\/]{2}' -or $Actual -match '(^|[\\/])\.\.?(?:[\\/]|$)' -or $Actual.Substring(2) -match ':' -or @($Actual -split '[\\/]' | Where-Object { $_ -match '[. ]$' }).Count -gt 0) { throw "$Label must be a safe, fully qualified local Windows path." } if (-not [string]::Equals( [IO.Path]::GetFullPath($Actual), [IO.Path]::GetFullPath($Expected), [StringComparison]::OrdinalIgnoreCase )) { throw "$Label does not match the signing session." } } function Get-WaggleSigningSlotPhase { param([Parameter(Mandatory = $true)] [int]$Order) if ($Order -eq 1) { return 1 } if ($Order -ge 2 -and $Order -le 7) { return 2 } if ($Order -ge 8 -and $Order -le 12) { return 3 } if ($Order -eq 13) { return 4 } if ($Order -eq 14) { return 5 } throw 'Signing callback slot order is outside the exact NSIS phase roster.' } function Assert-PrivateDirectoryAcl { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Label ) $directory = Get-TrustedPath $Path $Label 'Container' $acl = Get-Acl -LiteralPath $directory $allow = [Security.AccessControl.AccessControlType]::Allow $fullControl = [Security.AccessControl.FileSystemRights]::FullControl $approvedSids = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) foreach ($identity in @( [Security.Principal.WindowsIdentity]::GetCurrent().User, [Security.Principal.SecurityIdentifier]::new('S-1-5-18'), [Security.Principal.SecurityIdentifier]::new('S-1-5-32-544') )) { [void]$approvedSids.Add($identity.Value) } if (-not $acl.AreAccessRulesProtected -or -not $approvedSids.Contains( $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value )) { throw "$Label must have protected private signing permissions." } $rules = @($acl.GetAccessRules( $true, $true, [Security.Principal.SecurityIdentifier] )) if ($rules.Count -ne $approvedSids.Count) { throw "$Label contains an unexpected access rule." } foreach ($rule in $rules) { if ($rule.IsInherited -or $rule.AccessControlType -ne $allow -or -not $approvedSids.Contains($rule.IdentityReference.Value) -or ($rule.FileSystemRights -band $fullControl) -ne $fullControl) { throw "$Label contains an unapproved access rule." } } return $directory } function Assert-WagglePinnedToolFile { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Label, [Parameter(Mandatory = $true)] [string]$ExpectedSha256, [AllowNull()] [string]$ExpectedPublisher, [switch]$AllowCatalog, [switch]$AllowHardLink ) $trustedPath = Get-TrustedPath $Path $Label -AllowHardLink:$AllowHardLink if ([string]::IsNullOrEmpty($ExpectedPublisher)) { if ((Get-FileHash -LiteralPath $trustedPath -Algorithm SHA256).Hash -cne $ExpectedSha256) { throw "$Label does not match the pinned SHA-256 digest." } } else { Assert-ApprovedAuthenticodeFile ` $trustedPath $Label $ExpectedSha256 $ExpectedPublisher ` -AllowCatalog:$AllowCatalog } return $trustedPath } function Open-WagglePortableToolchainReceipt { param( [Parameter(Mandatory = $true)] [string]$Root, [Parameter(Mandatory = $true)] [string]$ReceiptPath, [Parameter(Mandatory = $true)] [string]$ReceiptSha256, [string[]]$DisallowedRoots = @() ) if ($ReceiptSha256 -notmatch '^[0-9A-Fa-f]{64}$') { throw 'Portable signing toolchain receipt SHA-256 is invalid.' } $trustedRoot = Assert-PrivateDirectoryAcl $Root 'Portable signing toolchain root' $trustedReceipt = Get-TrustedPath $ReceiptPath 'Portable signing toolchain receipt' if (Test-WagglePathsOverlap $trustedRoot $trustedReceipt) { throw 'Portable signing toolchain receipt must be outside its toolchain root.' } foreach ($disallowedRoot in @($DisallowedRoots | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })) { if (Test-WagglePathsOverlap $trustedReceipt ([string]$disallowedRoot)) { throw 'Portable signing toolchain receipt overlaps a protected signing boundary.' } } $locks = [Collections.Generic.List[IDisposable]]::new() try { $receiptLock = Open-ReadLock $trustedReceipt $locks.Add($receiptLock) if (-not [string]::Equals( (Get-FileHash -LiteralPath $trustedReceipt -Algorithm SHA256).Hash, $ReceiptSha256, [StringComparison]::OrdinalIgnoreCase )) { throw 'Portable signing toolchain receipt does not match its handoff SHA-256.' } try { $receipt = Get-Content -Raw -LiteralPath $trustedReceipt | ConvertFrom-Json -Depth 32 -DateKind String } catch { throw 'Portable signing toolchain receipt is not valid JSON.' } if ([int](Get-RequiredPropertyValue ` $receipt 'schemaVersion' 'Portable signing toolchain receipt') -ne 1) { throw 'Portable signing toolchain receipt schemaVersion must be 1.' } Assert-ExactCanonicalPathValue ` ([string](Get-RequiredPropertyValue ` $receipt 'portableToolchainRoot' 'Portable signing toolchain receipt')) ` $trustedRoot 'Portable signing toolchain receipt root' $archives = Get-RequiredPropertyValue ` $receipt 'archives' 'Portable signing toolchain receipt' $archiveBindings = @( @('node', $NodeArchiveSha256), @('git', $GitArchiveSha256), @('sevenZip', $SevenZipArchiveSha256) ) $archivePaths = [Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase ) foreach ($binding in $archiveBindings) { $archive = Get-RequiredPropertyValue ` $archives ([string]$binding[0]) 'Portable toolchain archives' $archivePath = Get-TrustedPath ` ([string](Get-RequiredPropertyValue ` $archive 'path' 'Portable toolchain vendor archive')) ` 'Portable toolchain vendor archive' $archiveHash = [string](Get-RequiredPropertyValue ` $archive 'sha256' 'Portable toolchain vendor archive') if ($archiveHash -cne [string]$binding[1] -or -not $archivePaths.Add($archivePath) -or (Test-WagglePathsOverlap $trustedRoot $archivePath) -or [string]::Equals( $trustedReceipt, $archivePath, [StringComparison]::OrdinalIgnoreCase )) { throw 'Portable toolchain vendor archives are not the exact distinct repository-pinned handoff.' } foreach ($disallowedRoot in @($DisallowedRoots | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })) { if (Test-WagglePathsOverlap $archivePath ([string]$disallowedRoot)) { throw 'Portable toolchain vendor archive overlaps a protected signing boundary.' } } $archiveLock = Open-ReadLock $archivePath if ((Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash -cne $archiveHash) { $archiveLock.Dispose() throw 'Portable toolchain vendor archive does not match its pinned SHA-256.' } $locks.Add($archiveLock) } $inventory = Get-RequiredPropertyValue ` $receipt 'inventory' 'Portable signing toolchain receipt' $inventoryEntries = @((Get-RequiredPropertyValue ` $inventory 'entries' 'Portable signing toolchain inventory')) $inventoryHash = [string](Get-RequiredPropertyValue ` $inventory 'sha256' 'Portable signing toolchain inventory') if ($inventoryEntries.Count -ne $PortableToolchainFileCount -or $inventoryHash -cne $PortableToolchainInventorySha256) { throw 'Portable signing toolchain inventory does not match the repository-pinned full closure.' } $treeLease = Open-WaggleValidatedPrebuiltTree ` $trustedRoot $inventory 'Portable signing toolchain full closure' ` ($DisallowedRoots + @($trustedReceipt) + @($archivePaths)) foreach ($treeLock in $treeLease.Locks) { $locks.Add($treeLock) } $treeLease.Locks.Clear() return [pscustomobject]@{ Receipt = $receipt ReceiptPath = $trustedReceipt ReceiptSha256 = $ReceiptSha256.ToUpperInvariant() Inventory = $inventory InventorySha256 = $inventoryHash Locks = $locks } } catch { foreach ($lock in $locks) { $lock.Dispose() } throw } } function Get-WagglePortableToolchain { param( [Parameter(Mandatory = $true)] [string]$Root, [Parameter(Mandatory = $true)] [string]$Node, [Parameter(Mandatory = $true)] [string]$Git, [Parameter(Mandatory = $true)] [string]$SevenZip, [Parameter(Mandatory = $true)] [string]$ReceiptPath, [Parameter(Mandatory = $true)] [string]$ReceiptSha256, [string[]]$DisallowedRoots = @() ) $trustedRoot = Assert-PrivateDirectoryAcl $Root 'Portable signing toolchain root' foreach ($disallowedRoot in @($DisallowedRoots | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })) { if (Test-WagglePathsOverlap $trustedRoot ([string]$disallowedRoot)) { throw 'Portable signing toolchain root overlaps a repository, prebuilt, receipt, or signing-temp boundary.' } } $receiptLease = Open-WagglePortableToolchainReceipt ` $trustedRoot $ReceiptPath $ReceiptSha256 -DisallowedRoots $DisallowedRoots $receiptLeaseTransferred = $false try { $rootItems = @(Get-ChildItem -LiteralPath $trustedRoot -Force -Recurse) $rootFiles = @($rootItems | Where-Object { -not $_.PSIsContainer }) $rootBytes = [long](($rootFiles | Measure-Object -Property Length -Sum).Sum) if ($rootItems.Count -gt 20000 -or $rootBytes -gt 1GB) { throw 'Portable signing toolchain root exceeds the bounded 20,000-item or 1-GiB trust envelope.' } foreach ($item in $rootItems) { $linkProperty = $item.PSObject.Properties['LinkType'] $linkType = if ($null -eq $linkProperty) { '' } else { [string]$linkProperty.Value } if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or -not [string]::IsNullOrEmpty($linkType)) { throw 'Portable signing toolchain root contains a linked or reparse filesystem object.' } } $requestedTools = [ordered]@{ Node = @($Node, 'Portable Node.js runtime') Git = @($Git, 'Portable Git executable') SevenZip = @($SevenZip, 'Portable 7-Zip inventory tool') } $trustedTools = @{} $uniquePaths = [Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase ) foreach ($entry in $requestedTools.GetEnumerator()) { $trustedPath = Get-TrustedPath ([string]$entry.Value[0]) ([string]$entry.Value[1]) $relativePath = Get-ContainedRelativePath $trustedPath $trustedRoot if ([string]::IsNullOrEmpty($relativePath)) { throw "$($entry.Value[1]) must be a regular non-reparse file contained by the private portable toolchain root." } if (-not $uniquePaths.Add($trustedPath)) { throw 'Portable signing tool paths must be distinct regular files.' } $trustedTools[[string]$entry.Key] = $trustedPath } $sevenZipDll = Get-TrustedPath ` (Join-Path (Split-Path $trustedTools.SevenZip -Parent) '7z.dll') ` 'Portable 7-Zip runtime library' $sevenZipDllRelative = Get-ContainedRelativePath $sevenZipDll $trustedRoot if ([string]::IsNullOrEmpty($sevenZipDllRelative) -or -not $uniquePaths.Add($sevenZipDll)) { throw 'Portable 7-Zip runtime library must be a distinct regular file contained by the private portable toolchain root.' } $gitRuntime = Get-TrustedPath ` (Join-Path (Split-Path (Split-Path $trustedTools.Git -Parent) -Parent) ` 'mingw64\bin\git.exe') ` 'Portable Git runtime' $gitRuntimeRelative = Get-ContainedRelativePath $gitRuntime $trustedRoot if ([string]::IsNullOrEmpty($gitRuntimeRelative) -or -not $uniquePaths.Add($gitRuntime)) { throw 'Portable Git runtime must be a distinct regular file contained by the private portable toolchain root.' } $trustedTools.Node = Assert-WagglePinnedToolFile ` $trustedTools.Node 'Portable Node.js runtime' $NodeSha256 $NodePublisher $trustedTools.Git = Assert-WagglePinnedToolFile ` $trustedTools.Git 'Portable Git executable' $GitSha256 $GitPublisher $gitRuntime = Assert-WagglePinnedToolFile ` $gitRuntime 'Portable Git runtime' $GitRuntimeSha256 $GitPublisher $trustedTools.SevenZip = Assert-WagglePinnedToolFile ` $trustedTools.SevenZip 'Portable 7-Zip inventory tool' $SevenZipSha256 $null $sevenZipDll = Assert-WagglePinnedToolFile ` $sevenZipDll 'Portable 7-Zip runtime library' $SevenZipDllSha256 $null $result = [pscustomobject]@{ PortableToolchainRoot = $trustedRoot NodePath = $trustedTools.Node GitPath = $trustedTools.Git GitRuntimePath = $gitRuntime SevenZipPath = $trustedTools.SevenZip SevenZipDllPath = $sevenZipDll ReceiptPath = $receiptLease.ReceiptPath ReceiptSha256 = $receiptLease.ReceiptSha256 InventorySha256 = $receiptLease.InventorySha256 Locks = $receiptLease.Locks } $receiptLeaseTransferred = $true return $result } finally { if (-not $receiptLeaseTransferred) { foreach ($lock in $receiptLease.Locks) { $lock.Dispose() } } } } function Get-WaggleSigningToolchain { param( [string[]]$DisallowedRoots = @(), [switch]$AllowPortableBeforeManifest ) $portableValues = [ordered]@{ Root = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_TOOLCHAIN_ROOT') Node = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_NODE_PATH') Git = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_GIT_PATH') SevenZip = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_SEVEN_ZIP_PATH') Receipt = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_RECEIPT_PATH') ReceiptSha256 = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_RECEIPT_SHA256') } $providedCount = @($portableValues.Values | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }).Count if ($providedCount -ne 0) { if ($providedCount -ne $portableValues.Count) { throw 'Portable signing toolchain environment must provide the exact root, Node, Git, 7-Zip, receipt path, and receipt SHA-256.' } if (-not $AllowPortableBeforeManifest -and ([Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_PATH') -notmatch '^[A-Za-z]:[\\/]' -or [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_SHA256') -notmatch '^[0-9A-Fa-f]{64}$')) { throw 'Portable signing tools are valid only inside an active receipt-bound signing session.' } return Get-WagglePortableToolchain ` $portableValues.Root $portableValues.Node $portableValues.Git ` $portableValues.SevenZip $portableValues.Receipt ` $portableValues.ReceiptSha256 -DisallowedRoots $DisallowedRoots } $systemNode = Assert-WagglePinnedToolFile ` $NodePath 'Node.js runtime' $NodeSha256 $NodePublisher -AllowHardLink $systemGit = Assert-WagglePinnedToolFile ` $GitPath 'Git executable' $GitSha256 $GitPublisher -AllowHardLink $systemGitRuntime = Assert-WagglePinnedToolFile ` (Join-Path (Split-Path (Split-Path $systemGit -Parent) -Parent) ` 'mingw64\bin\git.exe') ` 'Git runtime' $GitRuntimeSha256 $GitPublisher -AllowHardLink $systemSevenZip = Assert-WagglePinnedToolFile ` $SevenZipPath '7-Zip inventory tool' $SevenZipSha256 $null $systemSevenZipDll = Assert-WagglePinnedToolFile ` (Join-Path (Split-Path $systemSevenZip -Parent) '7z.dll') ` '7-Zip runtime library' $SevenZipDllSha256 $null return [pscustomobject]@{ PortableToolchainRoot = $null NodePath = $systemNode GitPath = $systemGit GitRuntimePath = $systemGitRuntime SevenZipPath = $systemSevenZip SevenZipDllPath = $systemSevenZipDll ReceiptPath = $null ReceiptSha256 = $null InventorySha256 = $null Locks = [Collections.Generic.List[IDisposable]]::new() } } function Assert-WagglePortableToolchainEnvironment { param([Parameter(Mandatory = $true)] [object]$Context) if ($null -eq $Context.PortableToolchainRoot) { return } foreach ($binding in @( @('WAGGLE_SIGNING_PORTABLE_TOOLCHAIN_ROOT', $Context.PortableToolchainRoot), @('WAGGLE_SIGNING_PORTABLE_NODE_PATH', $Context.NodePath), @('WAGGLE_SIGNING_PORTABLE_GIT_PATH', $Context.GitPath), @('WAGGLE_SIGNING_PORTABLE_SEVEN_ZIP_PATH', $Context.SevenZipPath), @('WAGGLE_SIGNING_PORTABLE_RECEIPT_PATH', $Context.PortableToolchainReceiptPath) )) { Assert-ExactCanonicalPathValue ` ([Environment]::GetEnvironmentVariable([string]$binding[0])) ` ([string]$binding[1]) ([string]$binding[0]) } if ([Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_PORTABLE_RECEIPT_SHA256') -cne $Context.PortableToolchainReceiptSha256) { throw 'WAGGLE_SIGNING_PORTABLE_RECEIPT_SHA256 does not match the signing session.' } } function Assert-WagglePortableSessionBootstrap { param( [Parameter(Mandatory = $true)] [object]$Toolchain, [Parameter(Mandatory = $true)] [string]$RepoRoot, [Parameter(Mandatory = $true)] [string]$TauriRoot ) if ($null -eq $Toolchain.PortableToolchainRoot) { return } $sessionId = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_SESSION_ID') $manifestPathValue = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_PATH') $manifestSha256 = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_SHA256') if ($sessionId -notmatch '^[0-9a-f]{32}$' -or $manifestSha256 -notmatch '^[0-9A-Fa-f]{64}$') { throw 'Portable signing callback is not inside an active receipt-bound signing session.' } $sessionDirectory = Assert-PrivateDirectoryAcl ` (Join-Path $TauriRoot "target\.signing-sessions\run-$sessionId") ` 'Portable signing callback session directory' $expectedManifestPath = Join-Path $sessionDirectory 'manifest.json' Assert-ExactCanonicalPathValue ` $manifestPathValue $expectedManifestPath 'Portable signing callback manifest path' $manifestPath = Get-TrustedPath $manifestPathValue 'Portable signing callback manifest' $lock = Open-ReadLock $manifestPath try { if ((Get-FileHash -LiteralPath $manifestPath -Algorithm SHA256).Hash -cne $manifestSha256.ToUpperInvariant()) { throw 'Portable signing callback manifest does not match its session digest.' } try { $manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json -Depth 32 -DateKind String } catch { throw 'Portable signing callback manifest is not valid JSON.' } if ([int](Get-RequiredPropertyValue $manifest 'schemaVersion' 'Signing manifest') -ne 1 -or [string](Get-RequiredPropertyValue $manifest 'mode' 'Signing manifest') -cne 'nsis' -or [string](Get-RequiredPropertyValue $manifest 'sessionId' 'Signing manifest') -cne $sessionId) { throw 'Portable signing callback manifest does not bind the active NSIS session.' } Assert-ExactCanonicalPathValue ` ([string](Get-RequiredPropertyValue $manifest 'repoRoot' 'Signing manifest')) ` $RepoRoot 'Portable signing callback repository root' $sourceRevision = [string](Get-RequiredPropertyValue ` $manifest 'sourceRevision' 'Signing manifest') $receipt = Get-RequiredPropertyValue ` $manifest 'buildReceipt' 'Receipt-bound signing manifest' if ($sourceRevision -notmatch '^[0-9a-f]{40}$' -or [int](Get-RequiredPropertyValue $receipt 'schemaVersion' 'Hosted build receipt') -ne 1 -or [string](Get-RequiredPropertyValue $receipt 'repository' 'Hosted build receipt') -cne 'marolinik/waggle-os' -or [string](Get-RequiredPropertyValue $receipt 'sourceRevision' 'Hosted build receipt') -cne $sourceRevision -or [string](Get-RequiredPropertyValue $receipt 'targetTriple' 'Hosted build receipt') -cne 'x86_64-pc-windows-msvc') { throw 'Portable signing callback manifest is not bound to the approved hosted receipt.' } $manifestToolchain = Get-RequiredPropertyValue $manifest 'toolchain' 'Signing manifest' foreach ($binding in @( @('portableToolchainRoot', $Toolchain.PortableToolchainRoot), @('nodePath', $Toolchain.NodePath), @('gitPath', $Toolchain.GitPath), @('gitRuntimePath', $Toolchain.GitRuntimePath), @('sevenZipPath', $Toolchain.SevenZipPath), @('sevenZipDllPath', $Toolchain.SevenZipDllPath), @('portableToolchainReceiptPath', $Toolchain.ReceiptPath) )) { Assert-ExactCanonicalPathValue ` ([string](Get-RequiredPropertyValue ` $manifestToolchain ([string]$binding[0]) 'Signing manifest toolchain')) ` ([string]$binding[1]) "Portable signing callback $($binding[0])" } foreach ($binding in @( @('nodeSha256', $NodeSha256), @('gitSha256', $GitSha256), @('gitRuntimeSha256', $GitRuntimeSha256), @('sevenZipSha256', $SevenZipSha256), @('sevenZipDllSha256', $SevenZipDllSha256), @('portableToolchainReceiptSha256', $Toolchain.ReceiptSha256), @('portableToolchainInventorySha256', $PortableToolchainInventorySha256) )) { if ([string](Get-RequiredPropertyValue ` $manifestToolchain ([string]$binding[0]) 'Signing manifest toolchain') -cne [string]$binding[1]) { throw "Portable signing callback $($binding[0]) is not repository-pinned." } } if ([int](Get-RequiredPropertyValue ` $manifestToolchain 'portableToolchainFileCount' 'Signing manifest toolchain') -ne $PortableToolchainFileCount) { throw 'Portable signing callback full closure file count is not repository-pinned.' } } finally { $lock.Dispose() } } function Get-WaggleSigningContext { param([switch]$AllowPortableBeforeManifest) $sessionId = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_SESSION_ID') if ($sessionId -notmatch '^[0-9a-f]{32}$') { throw 'WAGGLE_SIGNING_SESSION_ID must be one lowercase 128-bit session id.' } $scriptPath = Get-TrustedPath $PSCommandPath 'Signing wrapper' $appRoot = Split-Path (Split-Path $scriptPath -Parent) -Parent $repoRoot = Get-TrustedPath (Split-Path $appRoot -Parent) 'Repository root' 'Container' $tauriRoot = Get-TrustedPath (Join-Path $appRoot 'src-tauri') 'Tauri root' 'Container' $resourcesRootValue = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_RESOURCES_ROOT') if ([string]::IsNullOrWhiteSpace($resourcesRootValue)) { throw 'WAGGLE_SIGNING_RESOURCES_ROOT must identify the receipt-bound resource tree.' } $resourcesRoot = Get-TrustedPath $resourcesRootValue 'Tauri resources root' 'Container' $cargoTargetRoot = [Environment]::GetEnvironmentVariable('CARGO_TARGET_DIR') if ([string]::IsNullOrWhiteSpace($cargoTargetRoot)) { throw 'CARGO_TARGET_DIR must identify the isolated signing package root.' } $targetRoot = Assert-PrivateDirectoryAcl $cargoTargetRoot 'Signing package target root' $releaseRoot = Get-TrustedPath ` (Join-Path $targetRoot 'x86_64-pc-windows-msvc\release') ` 'Tauri release root' 'Container' $configPath = Get-TrustedPath (Join-Path $tauriRoot 'tauri.conf.json') 'Tauri config' $sessionDirectory = Join-Path ` (Join-Path $tauriRoot 'target\.signing-sessions') ` "run-$sessionId" $overrideConfigPath = Get-TrustedPath ` (Join-Path $sessionDirectory 'tauri.signing-override.json') ` 'Tauri signing override config' $tauriCliPath = Get-TrustedPath ` (Join-Path $appRoot 'node_modules\@tauri-apps\cli\tauri.js') ` 'Tauri CLI' $tauriCliPackagePath = Get-TrustedPath ` (Join-Path $appRoot 'node_modules\@tauri-apps\cli\package.json') ` 'Tauri CLI package manifest' $tauriCliMainPath = Get-TrustedPath ` (Join-Path $appRoot 'node_modules\@tauri-apps\cli\main.js') ` 'Tauri CLI main module' $tauriCliIndexPath = Get-TrustedPath ` (Join-Path $appRoot 'node_modules\@tauri-apps\cli\index.js') ` 'Tauri CLI native loader' $tauriNativePackagePath = Get-TrustedPath ` (Join-Path $appRoot 'node_modules\@tauri-apps\cli-win32-x64-msvc\package.json') ` 'Tauri native package manifest' $tauriNativeBinaryPath = Get-TrustedPath ` (Join-Path $appRoot 'node_modules\@tauri-apps\cli-win32-x64-msvc\cli.win32-x64-msvc.node') ` 'Tauri native CLI binary' $tauriVersion = [string]( Get-Content -Raw -LiteralPath $tauriCliPackagePath | ConvertFrom-Json ).version if ($tauriVersion -cne $TauriCliVersion) { throw "Tauri CLI must be the pinned version $TauriCliVersion." } $tauriNativeVersion = [string]( Get-Content -Raw -LiteralPath $tauriNativePackagePath | ConvertFrom-Json ).version if ($tauriNativeVersion -cne $TauriCliVersion) { throw "Tauri native CLI must be the pinned version $TauriCliVersion." } $makensisPath = Get-TrustedPath ` (Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'tauri\NSIS\Bin\makensis.exe') ` 'makensis' $nsisRoot = Get-TrustedPath ` (Split-Path (Split-Path $makensisPath -Parent) -Parent) ` 'NSIS compiler root' 'Container' $toolchain = Get-WaggleSigningToolchain ` -DisallowedRoots @( $repoRoot, $targetRoot, $resourcesRoot, [Environment]::GetEnvironmentVariable('TEMP'), [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_PATH') ) ` -AllowPortableBeforeManifest:$AllowPortableBeforeManifest if (-not $AllowPortableBeforeManifest) { Assert-WagglePortableSessionBootstrap $toolchain $repoRoot $tauriRoot } $git = $toolchain.GitPath $gitLock = Open-ReadLock $git try { if ((Get-FileHash -LiteralPath $git -Algorithm SHA256).Hash -cne $GitSha256) { throw 'Git executable does not match the pinned SHA-256 digest.' } $sourceRevision = [string](& $git -C $repoRoot rev-parse --verify HEAD) if ($LASTEXITCODE -ne 0 -or $sourceRevision -notmatch '^[0-9a-f]{40}$') { throw 'Could not resolve the exact repository source revision.' } $status = @(& $git -C $repoRoot status --porcelain=v1 --untracked-files=all) if ($LASTEXITCODE -ne 0 -or $status.Count -ne 0) { throw 'Signing requires a clean exact repository revision.' } } finally { $gitLock.Dispose() } return [pscustomobject]@{ RepoRoot = $repoRoot AppRoot = $appRoot TauriRoot = $tauriRoot ResourcesRoot = $resourcesRoot TargetRoot = $targetRoot ReleaseRoot = $releaseRoot ConfigPath = $configPath OverrideConfigPath = $overrideConfigPath WrapperPath = $scriptPath TauriCliPath = $tauriCliPath TauriCliPackagePath = $tauriCliPackagePath TauriCliMainPath = $tauriCliMainPath TauriCliIndexPath = $tauriCliIndexPath TauriNativePackagePath = $tauriNativePackagePath TauriNativeBinaryPath = $tauriNativeBinaryPath MakensisPath = $makensisPath NsisRoot = $nsisRoot GitPath = $git GitRuntimePath = $toolchain.GitRuntimePath PortableToolchainRoot = $toolchain.PortableToolchainRoot PortableToolchainReceiptPath = $toolchain.ReceiptPath PortableToolchainReceiptSha256 = $toolchain.ReceiptSha256 PortableToolchainInventorySha256 = $toolchain.InventorySha256 PortableToolchainLocks = $toolchain.Locks SignToolPath = Get-TrustedPath $SignToolPath 'SignTool' -AllowHardLink ArtifactSigningPackagePath = Get-TrustedPath ` (Join-Path $tauriRoot 'target\.artifact-signing-tools\Microsoft.ArtifactSigning.Client.1.0.128.nupkg') ` 'Artifact Signing package' NodePath = $toolchain.NodePath SevenZipPath = $toolchain.SevenZipPath SevenZipDllPath = $toolchain.SevenZipDllPath SourceRevision = $sourceRevision TauriCliVersion = $TauriCliVersion TauriCliSha256 = $TauriCliSha256 TauriCliPackageSha256 = $TauriCliPackageSha256 TauriCliMainSha256 = $TauriCliMainSha256 TauriCliIndexSha256 = $TauriCliIndexSha256 TauriNativePackageSha256 = $TauriNativePackageSha256 TauriNativeBinarySha256 = $TauriNativeBinarySha256 MakensisSha256 = $MakensisSha256 NsisClosureSha256 = $NsisClosureSha256 GitSha256 = $GitSha256 GitRuntimeSha256 = $GitRuntimeSha256 NodeSha256 = $NodeSha256 SevenZipSha256 = $SevenZipSha256 SevenZipDllSha256 = $SevenZipDllSha256 SignToolSha256 = $SignToolSha256 ArtifactSigningPackageSha256 = $ArtifactSigningPackageSha256 ArtifactSigningX64ManifestSha256 = $ArtifactSigningX64ManifestSha256 } } function Get-ExpectedNsisFixedPaths { param( [Parameter(Mandatory = $true)] [object]$Context, [Parameter(Mandatory = $true)] [string]$Version ) return @( Join-Path $Context.ReleaseRoot 'waggle.exe' Join-Path $Context.ResourcesRoot 'native\vec0.dll' Join-Path $Context.ResourcesRoot 'native\onnxruntime\onnxruntime.dll' Join-Path $Context.ResourcesRoot 'node_modules\@img\sharp-win32-x64\lib\libvips-42.dll' Join-Path $Context.ResourcesRoot 'node_modules\@img\sharp-win32-x64\lib\libvips-cpp-8.18.3.dll' Join-Path $Context.ResourcesRoot 'node_modules\onnxruntime-node\bin\napi-v3\win32\x64\onnxruntime.dll' Join-Path $Context.ResourcesRoot 'node_modules\sqlite-vec-windows-x64\vec0.dll' Join-Path $Context.ReleaseRoot 'nsis\x64\Plugins\x86-unicode\NSISdl.dll' Join-Path $Context.ReleaseRoot 'nsis\x64\Plugins\x86-unicode\StartMenu.dll' Join-Path $Context.ReleaseRoot 'nsis\x64\Plugins\x86-unicode\System.dll' Join-Path $Context.ReleaseRoot 'nsis\x64\Plugins\x86-unicode\nsDialogs.dll' Join-Path $Context.ReleaseRoot 'nsis\x64\Plugins\x86-unicode\additional\nsis_tauri_utils.dll' ) | ForEach-Object { [IO.Path]::GetFullPath($_) } } function Get-ExpectedNsisPackagedPaths { return @( 'waggle.exe' 'resources\native\vec0.dll' 'resources\native\onnxruntime\onnxruntime.dll' 'resources\node_modules\@img\sharp-win32-x64\lib\libvips-42.dll' 'resources\node_modules\@img\sharp-win32-x64\lib\libvips-cpp-8.18.3.dll' 'resources\node_modules\onnxruntime-node\bin\napi-v3\win32\x64\onnxruntime.dll' 'resources\node_modules\sqlite-vec-windows-x64\vec0.dll' '$PLUGINSDIR\NSISdl.dll' '$PLUGINSDIR\StartMenu.dll' '$PLUGINSDIR\System.dll' '$PLUGINSDIR\nsDialogs.dll' '$PLUGINSDIR\nsis_tauri_utils.dll' ) } function Add-ValidatedSessionFileLock { param( [Parameter(Mandatory = $true)] [Collections.Generic.List[IDisposable]]$Locks, [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$ExpectedSha256, [Parameter(Mandatory = $true)] [string]$Label, [switch]$AllowHardLink ) if ($ExpectedSha256 -notmatch '^[0-9A-Fa-f]{64}$') { throw "$Label manifest SHA-256 is invalid." } $trustedPath = Get-TrustedPath $Path $Label -AllowHardLink:$AllowHardLink $lock = Open-ReadLock $trustedPath try { if (-not [string]::Equals( (Get-FileHash -LiteralPath $trustedPath -Algorithm SHA256).Hash, $ExpectedSha256, [StringComparison]::OrdinalIgnoreCase )) { throw "$Label does not match the signing manifest SHA-256 digest." } $Locks.Add($lock) $lock = $null } finally { if ($null -ne $lock) { $lock.Dispose() } } return $trustedPath } function Assert-WaggleSigningOverrideContract { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$WrapperPath, [AllowNull()] [string]$ResourcesRoot = $null, [switch]$Unsigned ) try { $override = Get-Content -Raw -LiteralPath $Path | ConvertFrom-Json -Depth 16 } catch { throw 'Tauri signing override is not valid JSON.' } $targets = @((Get-RequiredPropertyValue ` (Get-RequiredPropertyValue $override 'bundle' 'Tauri signing override') ` 'targets' 'Tauri signing override bundle')) $bundle = $override.bundle $windows = Get-RequiredPropertyValue $bundle 'windows' 'Tauri signing override bundle' $signCommand = $windows.PSObject.Properties['signCommand'] $expectedArgs = @( '-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $WrapperPath, '-ArtifactPath', '%1' ) if ([bool](Get-RequiredPropertyValue $bundle 'active' 'Tauri signing override bundle') -ne $true -or $targets.Count -ne 1 -or [string]$targets[0] -cne 'nsis' -or [string](Get-RequiredPropertyValue ` (Get-RequiredPropertyValue $override 'build' 'Tauri signing override') ` 'beforeBuildCommand' 'Tauri signing override build') -cne '' -or [string](Get-RequiredPropertyValue ` (Get-RequiredPropertyValue $override 'build' 'Tauri signing override') ` 'beforeBundleCommand' 'Tauri signing override build') -cne '' -or $null -ne $windows.PSObject.Properties['certificateThumbprint'] -or $null -ne $windows.PSObject.Properties['timestampUrl']) { throw 'Tauri signing override must be the exact fail-closed NSIS-only callback contract.' } if (-not [string]::IsNullOrWhiteSpace($ResourcesRoot)) { $resourceMap = Get-RequiredPropertyValue ` $bundle 'resources' 'Tauri signing override bundle' $resourceEntries = @($resourceMap.PSObject.Properties) $trustedResourcesRoot = Get-TrustedPath ` $ResourcesRoot 'Tauri signing override resource root' 'Container' if ($resourceEntries.Count -ne 1 -or -not [string]::Equals( [IO.Path]::GetFullPath([string]$resourceEntries[0].Name), $trustedResourcesRoot, [StringComparison]::OrdinalIgnoreCase ) -or [string]$resourceEntries[0].Value -cne 'resources') { throw 'Tauri signing override must map the exact receipt-bound resource tree.' } } elseif ($null -ne $bundle.PSObject.Properties['resources']) { throw 'Tauri signing override unexpectedly contains an unbound resource map.' } if ($Unsigned) { if ($null -ne $signCommand) { throw 'Unsigned Tauri override cannot contain a signing callback.' } return } if ($null -eq $signCommand) { throw 'Signed Tauri override must contain the signing callback.' } $actualCommand = $signCommand.Value $actualArgs = @((Get-RequiredPropertyValue ` $actualCommand 'args' 'Tauri signing override signCommand')) if ([string](Get-RequiredPropertyValue ` $actualCommand 'cmd' 'Tauri signing override signCommand') -cne $SystemPowerShellPath -or $actualArgs.Count -ne $expectedArgs.Count -or [string]::Join("`n", $actualArgs) -cne [string]::Join("`n", $expectedArgs)) { throw 'Tauri signing override must be the exact fail-closed NSIS-only callback contract.' } } function Get-WaggleSigningSession { param([Parameter(Mandatory = $true)] [object]$Context) Assert-WagglePortableToolchainEnvironment $Context $manifestPathValue = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_PATH') $manifestSha256 = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_SHA256') $sessionId = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_SESSION_ID') if ([string]::IsNullOrWhiteSpace($manifestPathValue)) { throw 'WAGGLE_SIGNING_MANIFEST_PATH is required for production signing.' } if ($manifestSha256 -notmatch '^[0-9A-Fa-f]{64}$') { throw 'WAGGLE_SIGNING_MANIFEST_SHA256 must be one exact SHA-256 digest.' } if ($sessionId -notmatch '^[0-9a-f]{32}$') { throw 'WAGGLE_SIGNING_SESSION_ID must be one lowercase 128-bit session id.' } $sessionDirectory = Join-Path ` (Join-Path $Context.TauriRoot 'target\.signing-sessions') ` "run-$sessionId" $expectedManifestPath = Join-Path $sessionDirectory 'manifest.json' Assert-ExactCanonicalPathValue ` $manifestPathValue $expectedManifestPath 'Signing manifest path' $sessionDirectory = Assert-PrivateDirectoryAcl $sessionDirectory 'Signing session directory' $manifestPath = Get-TrustedPath $manifestPathValue 'Signing manifest' $locks = [Collections.Generic.List[IDisposable]]::new() $portableLocksProperty = $Context.PSObject.Properties['PortableToolchainLocks'] if ($null -ne $portableLocksProperty -and $null -ne $portableLocksProperty.Value) { foreach ($portableLock in @($portableLocksProperty.Value)) { $locks.Add($portableLock) } $portableLocksProperty.Value.Clear() } try { $manifestLock = Open-ReadLock $manifestPath $locks.Add($manifestLock) if (-not [string]::Equals( (Get-FileHash -LiteralPath $manifestPath -Algorithm SHA256).Hash, $manifestSha256, [StringComparison]::OrdinalIgnoreCase )) { throw 'Signing manifest does not match WAGGLE_SIGNING_MANIFEST_SHA256.' } try { $manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json -Depth 32 -DateKind String } catch { throw 'Signing manifest is not valid JSON.' } if ([int](Get-RequiredPropertyValue $manifest 'schemaVersion' 'Signing manifest') -ne 1) { throw 'Signing manifest schemaVersion must be 1.' } if ([string](Get-RequiredPropertyValue $manifest 'mode' 'Signing manifest') -cne 'nsis') { throw 'Production signing is NSIS-only.' } if ([string](Get-RequiredPropertyValue $manifest 'sessionId' 'Signing manifest') -cne $sessionId) { throw 'Signing manifest session id does not match WAGGLE_SIGNING_SESSION_ID.' } if ([string](Get-RequiredPropertyValue $manifest 'sourceRevision' 'Signing manifest') -cne [string]$Context.SourceRevision) { throw 'Signing manifest source revision does not match the clean repository HEAD.' } if ($null -ne $Context.PortableToolchainRoot) { $buildReceipt = Get-RequiredPropertyValue ` $manifest 'buildReceipt' 'Receipt-bound signing manifest' if ([int](Get-RequiredPropertyValue $buildReceipt 'schemaVersion' 'Hosted build receipt') -ne 1 -or [string](Get-RequiredPropertyValue $buildReceipt 'repository' 'Hosted build receipt') -cne 'marolinik/waggle-os' -or [string](Get-RequiredPropertyValue $buildReceipt 'sourceRevision' 'Hosted build receipt') -cne [string]$Context.SourceRevision -or [string](Get-RequiredPropertyValue $buildReceipt 'targetTriple' 'Hosted build receipt') -cne 'x86_64-pc-windows-msvc') { throw 'Portable signing callback is not bound to the approved hosted build receipt.' } } $createdAt = [DateTimeOffset]::MinValue $expiresAt = [DateTimeOffset]::MinValue if (-not [DateTimeOffset]::TryParseExact( [string](Get-RequiredPropertyValue $manifest 'createdAtUtc' 'Signing manifest'), 'O', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$createdAt ) -or -not [DateTimeOffset]::TryParseExact( [string](Get-RequiredPropertyValue $manifest 'expiresAtUtc' 'Signing manifest'), 'O', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$expiresAt )) { throw 'Signing manifest timestamps must use the round-trip UTC format.' } $now = [DateTimeOffset]::UtcNow if ($createdAt -gt $now.AddMinutes(5) -or $expiresAt -le $now -or $expiresAt -le $createdAt -or $expiresAt -gt $createdAt.AddHours(4)) { throw 'Signing manifest is expired or outside the four-hour signing window.' } Assert-ExactCanonicalPathValue ` ([string](Get-RequiredPropertyValue $manifest 'repoRoot' 'Signing manifest')) ` $Context.RepoRoot 'Signing manifest repository root' Assert-ExactCanonicalPathValue ` ([string](Get-RequiredPropertyValue $manifest 'tauriRoot' 'Signing manifest')) ` $Context.TauriRoot 'Signing manifest Tauri root' Assert-ExactCanonicalPathValue ` ([string](Get-RequiredPropertyValue $manifest 'releaseRoot' 'Signing manifest')) ` $Context.ReleaseRoot 'Signing manifest release root' Assert-ExactCanonicalPathValue ` ([string](Get-RequiredPropertyValue $manifest 'resourcesRoot' 'Signing manifest')) ` $Context.ResourcesRoot 'Signing manifest resources root' Assert-ExactCanonicalPathValue ` ([string](Get-RequiredPropertyValue $manifest 'targetRoot' 'Signing manifest')) ` $Context.TargetRoot 'Signing manifest target root' $expectedTempRoot = Join-Path ` (Join-Path $Context.TauriRoot 'target\.signing-temp') ` "run-$sessionId" $tempRootValue = [string](Get-RequiredPropertyValue $manifest 'tempRoot' 'Signing manifest') Assert-ExactCanonicalPathValue $tempRootValue $expectedTempRoot 'Signing manifest session temp root' $tempRoot = Assert-PrivateDirectoryAcl $tempRootValue 'Signing session temp root' foreach ($name in @('TEMP', 'TMP', 'WAGGLE_NSIS_SIGNING_TEMP_ROOT')) { $environmentPath = [Environment]::GetEnvironmentVariable($name) if ([string]::IsNullOrWhiteSpace($environmentPath)) { throw "$name must identify the signing session temp root." } Assert-ExactCanonicalPathValue $environmentPath $tempRoot "$name path" } $expectedLedgerPath = Join-Path $sessionDirectory 'callback-ledger.json' $ledgerPathValue = [string](Get-RequiredPropertyValue $manifest 'ledgerPath' 'Signing manifest') Assert-ExactCanonicalPathValue $ledgerPathValue $expectedLedgerPath 'Signing callback ledger path' $ledgerPath = Get-TrustedPath $ledgerPathValue 'Signing callback ledger' $config = Get-Content -Raw -LiteralPath $Context.ConfigPath | ConvertFrom-Json $version = [string]$config.version if ($version -notmatch '^\d+\.\d+\.\d+$' -or [string](Get-RequiredPropertyValue $manifest 'appVersion' 'Signing manifest') -cne $version) { throw 'Signing manifest app version does not match tauri.conf.json.' } $toolchain = Get-RequiredPropertyValue $manifest 'toolchain' 'Signing manifest' $toolBindings = @( @('wrapperPath', 'wrapperSha256', $Context.WrapperPath, $null, 'Signing wrapper', $false), @('tauriConfigPath', 'tauriConfigSha256', $Context.ConfigPath, $null, 'Tauri config', $false), @('tauriOverrideConfigPath', 'tauriOverrideConfigSha256', $Context.OverrideConfigPath, $null, 'Tauri signing override config', $false), @('tauriCliPath', 'tauriCliSha256', $Context.TauriCliPath, $Context.TauriCliSha256, 'Tauri CLI', $false), @('tauriCliPackagePath', 'tauriCliPackageSha256', $Context.TauriCliPackagePath, $Context.TauriCliPackageSha256, 'Tauri CLI package manifest', $false), @('tauriCliMainPath', 'tauriCliMainSha256', $Context.TauriCliMainPath, $Context.TauriCliMainSha256, 'Tauri CLI main module', $false), @('tauriCliIndexPath', 'tauriCliIndexSha256', $Context.TauriCliIndexPath, $Context.TauriCliIndexSha256, 'Tauri CLI native loader', $false), @('tauriNativePackagePath', 'tauriNativePackageSha256', $Context.TauriNativePackagePath, $Context.TauriNativePackageSha256, 'Tauri native package manifest', $false), @('tauriNativeBinaryPath', 'tauriNativeBinarySha256', $Context.TauriNativeBinaryPath, $Context.TauriNativeBinarySha256, 'Tauri native CLI binary', $false), @('makensisPath', 'makensisSha256', $Context.MakensisPath, $Context.MakensisSha256, 'makensis', $false), @('gitPath', 'gitSha256', $Context.GitPath, $Context.GitSha256, 'Git executable', $true), @('gitRuntimePath', 'gitRuntimeSha256', $Context.GitRuntimePath, $Context.GitRuntimeSha256, 'Git runtime', $true), @('nodePath', 'nodeSha256', $Context.NodePath, $Context.NodeSha256, 'Node.js runtime', $true), @('sevenZipPath', 'sevenZipSha256', $Context.SevenZipPath, $Context.SevenZipSha256, '7-Zip inventory tool', $false), @('signToolPath', 'signToolSha256', $Context.SignToolPath, $Context.SignToolSha256, 'SignTool', $true), @('artifactSigningPackagePath', 'artifactSigningPackageSha256', $Context.ArtifactSigningPackagePath, $Context.ArtifactSigningPackageSha256, 'Artifact Signing package', $false) ) foreach ($binding in $toolBindings) { $manifestToolPath = [string](Get-RequiredPropertyValue $toolchain $binding[0] 'Signing manifest toolchain') $manifestToolHash = [string](Get-RequiredPropertyValue $toolchain $binding[1] 'Signing manifest toolchain') Assert-ExactCanonicalPathValue $manifestToolPath ([string]$binding[2]) "$($binding[4]) path" if ($null -ne $binding[3] -and $manifestToolHash -cne [string]$binding[3]) { throw "$($binding[4]) manifest SHA-256 is not the repository-pinned digest." } Add-ValidatedSessionFileLock ` $locks ([string]$binding[2]) $manifestToolHash ([string]$binding[4]) ` -AllowHardLink:([bool]$binding[5]) | Out-Null } if ($null -ne $Context.PortableToolchainRoot) { Assert-ExactCanonicalPathValue ` ([string](Get-RequiredPropertyValue ` $toolchain 'portableToolchainRoot' 'Signing manifest toolchain')) ` $Context.PortableToolchainRoot 'Portable signing toolchain root' $sevenZipDllPath = [string](Get-RequiredPropertyValue ` $toolchain 'sevenZipDllPath' 'Signing manifest toolchain') $sevenZipDllHash = [string](Get-RequiredPropertyValue ` $toolchain 'sevenZipDllSha256' 'Signing manifest toolchain') Assert-ExactCanonicalPathValue ` $sevenZipDllPath $Context.SevenZipDllPath '7-Zip runtime library path' if ($sevenZipDllHash -cne $Context.SevenZipDllSha256) { throw '7-Zip runtime library manifest SHA-256 is not the repository-pinned digest.' } Add-ValidatedSessionFileLock ` $locks $Context.SevenZipDllPath $sevenZipDllHash ` '7-Zip runtime library' | Out-Null Assert-ExactCanonicalPathValue ` ([string](Get-RequiredPropertyValue ` $toolchain 'portableToolchainReceiptPath' 'Signing manifest toolchain')) ` $Context.PortableToolchainReceiptPath ` 'Portable signing toolchain receipt path' if ([string](Get-RequiredPropertyValue ` $toolchain 'portableToolchainReceiptSha256' 'Signing manifest toolchain') -cne $Context.PortableToolchainReceiptSha256 -or [string](Get-RequiredPropertyValue ` $toolchain 'portableToolchainInventorySha256' 'Signing manifest toolchain') -cne $PortableToolchainInventorySha256 -or [int](Get-RequiredPropertyValue ` $toolchain 'portableToolchainFileCount' 'Signing manifest toolchain') -ne $PortableToolchainFileCount) { throw 'Portable signing toolchain manifest does not bind the repository-pinned full closure.' } } if ([string](Get-RequiredPropertyValue $toolchain 'tauriCliVersion' 'Signing manifest toolchain') -cne [string]$Context.TauriCliVersion) { throw 'Signing manifest Tauri CLI version is not the repository-pinned version.' } if ([string](Get-RequiredPropertyValue $toolchain 'artifactSigningX64ManifestSha256' 'Signing manifest toolchain') -cne [string]$Context.ArtifactSigningX64ManifestSha256) { throw 'Signing manifest Artifact Signing x64 closure digest is not repository-pinned.' } Assert-WaggleSigningOverrideContract ` $Context.OverrideConfigPath $Context.WrapperPath $Context.ResourcesRoot $payloads = @((Get-RequiredPropertyValue $manifest 'payloads' 'Signing manifest')) $uniquePayloadPaths = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($payload in $payloads) { $payloadPath = [string](Get-RequiredPropertyValue $payload 'path' 'Signing manifest payload') $payloadHash = [string](Get-RequiredPropertyValue $payload 'sha256' 'Signing manifest payload') $payloadSize = [long](Get-RequiredPropertyValue $payload 'size' 'Signing manifest payload') if ([string]::IsNullOrWhiteSpace($payloadPath) -or -not $uniquePayloadPaths.Add($payloadPath) -or $payloadHash -notmatch '^[0-9A-Fa-f]{64}$' -or $payloadSize -lt 0) { throw 'Signing manifest NSIS payload digest inventory is invalid.' } } if ($payloads.Count -lt 10 -or -not $uniquePayloadPaths.Contains('waggle.exe')) { throw 'Signing manifest NSIS payload inventory is invalid or incomplete.' } $expectedFixedPaths = @(Get-ExpectedNsisFixedPaths $Context $version) $expectedPackagedPaths = @(Get-ExpectedNsisPackagedPaths) $expectedFixedSet = [Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase ) foreach ($expectedPath in $expectedFixedPaths) { [void]$expectedFixedSet.Add($expectedPath) } $seenFixedSet = [Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase ) $seenIds = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) $seenOrders = [Collections.Generic.HashSet[int]]::new() $slots = @((Get-RequiredPropertyValue $manifest 'slots' 'Signing manifest')) if ($slots.Count -ne 14) { throw 'NSIS-only signing manifest must contain exactly 14 callback slots.' } $uninstallerSlots = 0 $installerSlots = 0 foreach ($slot in $slots) { $slotId = [string](Get-RequiredPropertyValue $slot 'id' 'Signing manifest slot') $slotKind = [string](Get-RequiredPropertyValue $slot 'kind' 'Signing manifest slot') $slotOrder = [int](Get-RequiredPropertyValue $slot 'order' 'Signing manifest slot') $maxUses = [int](Get-RequiredPropertyValue $slot 'maxUses' 'Signing manifest slot') if ($slotId -notmatch '^[a-z0-9][a-z0-9-]{2,63}$' -or -not $seenIds.Add($slotId) -or -not $seenOrders.Add($slotOrder) -or $slotOrder -lt 1 -or $slotOrder -gt $slots.Count -or $maxUses -ne 1) { throw 'Signing manifest callback slots must have unique ids, consecutive orders, and maxUses 1.' } switch ($slotKind) { 'fixed' { $slotPath = [string](Get-RequiredPropertyValue $slot 'path' 'Fixed signing slot') $slotHash = [string](Get-RequiredPropertyValue $slot 'preSignSha256' 'Fixed signing slot') $packagedPath = [string](Get-RequiredPropertyValue $slot 'packagedPath' 'Fixed signing slot') if ($slotHash -notmatch '^[0-9A-Fa-f]{64}$' -or -not $expectedFixedSet.Contains([IO.Path]::GetFullPath($slotPath)) -or -not $seenFixedSet.Add([IO.Path]::GetFullPath($slotPath)) -or $slotOrder -gt $expectedPackagedPaths.Count -or $packagedPath -cne $expectedPackagedPaths[$slotOrder - 1]) { throw 'Fixed signing slot is outside the exact NSIS-only callback manifest.' } } 'generated-nsis-uninstaller' { $uninstallerSlots++ $expectedEvidencePath = Join-Path $sessionDirectory 'signed-evidence\13-nsis-uninstaller.exe' if ([string](Get-RequiredPropertyValue $slot 'pathPattern' 'NSIS uninstaller slot') -cne $NsisUninstallerPattern -or $slotOrder -ne ($slots.Count - 1)) { throw 'NSIS uninstaller slot must be the one-time penultimate session lease.' } Assert-ExactCanonicalPathValue ` ([string](Get-RequiredPropertyValue $slot 'evidencePath' 'NSIS uninstaller slot')) ` $expectedEvidencePath 'NSIS uninstaller evidence path' } 'generated-nsis-installer' { $installerSlots++ $expectedInstaller = Join-Path ` $Context.ReleaseRoot ` "bundle\nsis\Waggle_${version}_x64-setup.exe" Assert-ExactCanonicalPathValue ` ([string](Get-RequiredPropertyValue $slot 'path' 'NSIS installer slot')) ` $expectedInstaller 'NSIS installer slot path' if ($slotOrder -ne $slots.Count) { throw 'NSIS installer slot must be the final callback lease.' } } default { throw 'Production signing manifest contains a non-NSIS callback slot.' } } } if ($seenFixedSet.Count -ne $expectedFixedSet.Count -or $uninstallerSlots -ne 1 -or $installerSlots -ne 1 -or [int](@($slots | Where-Object { $_.order -eq 1 })[0].order) -ne 1 -or -not [string]::Equals( [string](@($slots | Where-Object { $_.order -eq 1 })[0].path), [string]$expectedFixedPaths[0], [StringComparison]::OrdinalIgnoreCase )) { throw 'Signing manifest does not cover the exact ordered NSIS callback surface.' } $session = [pscustomobject]@{ Id = $sessionId Manifest = $manifest ManifestSha256 = $manifestSha256.ToUpperInvariant() ManifestPath = $manifestPath SessionDirectory = $sessionDirectory TempRoot = $tempRoot LedgerPath = $ledgerPath Context = $Context Locks = $locks } $ledger = Get-Content -Raw -LiteralPath $ledgerPath | ConvertFrom-Json -Depth 32 -DateKind String Assert-WaggleSigningLedgerState $session $ledger return $session } catch { foreach ($lock in $locks) { $lock.Dispose() } throw } } function Assert-WaggleSigningLedgerState { param( [Parameter(Mandatory = $true)] [object]$Session, [Parameter(Mandatory = $true)] [object]$Ledger ) if ([int](Get-RequiredPropertyValue $Ledger 'schemaVersion' 'Signing callback ledger') -ne 3 -or [string](Get-RequiredPropertyValue $Ledger 'sessionId' 'Signing callback ledger') -cne [string]$Session.Id -or -not [string]::Equals( [string](Get-RequiredPropertyValue $Ledger 'manifestSha256' 'Signing callback ledger'), [string]$Session.ManifestSha256, [StringComparison]::OrdinalIgnoreCase ) -or [string](Get-RequiredPropertyValue $Ledger 'state' 'Signing callback ledger') -notin @('open', 'failed', 'sealed')) { throw 'Signing callback ledger does not belong to the active manifest session.' } foreach ($field in @('terminalAtUtc', 'terminalReceiptSha256')) { if ($null -eq $Ledger.PSObject.Properties[$field]) { throw "Signing callback ledger is missing required property '$field'." } } $ledgerState = [string]$Ledger.state if ($ledgerState -ceq 'open') { if ($null -ne $Ledger.terminalAtUtc -or $null -ne $Ledger.terminalReceiptSha256) { throw 'An open signing callback ledger cannot contain terminal metadata.' } } else { $terminalAt = [DateTimeOffset]::MinValue if (-not [DateTimeOffset]::TryParseExact( [string]$Ledger.terminalAtUtc, 'O', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$terminalAt ) -or [string]$Ledger.terminalReceiptSha256 -notmatch '^[0-9A-Fa-f]{64}$') { throw 'Terminal signing callback ledger metadata is invalid.' } } $manifestSlots = @($Session.Manifest.slots) $entries = @((Get-RequiredPropertyValue $Ledger 'entries' 'Signing callback ledger')) if ($entries.Count -ne $manifestSlots.Count -or $entries.Count -ne 14) { throw 'Signing callback ledger must predeclare the exact 14-slot manifest roster.' } $reservedCount = 0 $failedCount = 0 for ($index = 0; $index -lt $manifestSlots.Count; $index++) { $slot = $manifestSlots[$index] $entry = $entries[$index] $slotId = [string](Get-RequiredPropertyValue $slot 'id' 'Signing manifest slot') $slotKind = [string](Get-RequiredPropertyValue $slot 'kind' 'Signing manifest slot') $slotOrder = [int](Get-RequiredPropertyValue $slot 'order' 'Signing manifest slot') if ([string](Get-RequiredPropertyValue $entry 'slotId' 'Signing callback ledger entry') -cne $slotId -or [string](Get-RequiredPropertyValue $entry 'kind' 'Signing callback ledger entry') -cne $slotKind -or [int](Get-RequiredPropertyValue $entry 'order' 'Signing callback ledger entry') -ne $slotOrder -or $slotOrder -ne ($index + 1)) { throw 'Signing callback ledger roster does not exactly match the ordered manifest slots.' } foreach ($field in @( 'reservationId', 'path', 'preSignSha256', 'reservedAtUtc', 'completedAtUtc', 'postSignSha256', 'signerSubject' )) { if ($null -eq $entry.PSObject.Properties[$field]) { throw "Signing callback ledger entry is missing required property '$field'." } } $status = [string](Get-RequiredPropertyValue $entry 'status' 'Signing callback ledger entry') $attempts = [int](Get-RequiredPropertyValue $entry 'attempts' 'Signing callback ledger entry') if ($status -ceq 'pending') { if ($attempts -ne 0 -or @( 'reservationId', 'path', 'preSignSha256', 'reservedAtUtc', 'completedAtUtc', 'postSignSha256', 'signerSubject' | Where-Object { $null -ne $entry.PSObject.Properties[$_].Value } ).Count -ne 0) { throw 'Pending signing callback ledger entries must be pristine and unused.' } continue } if ($status -notin @('reserved', 'completed', 'failed') -or $attempts -ne 1 -or [string]$entry.reservationId -notmatch '^[0-9a-f]{32}$' -or [string]$entry.preSignSha256 -notmatch '^[0-9A-Fa-f]{64}$' -or [string]::IsNullOrWhiteSpace([string]$entry.path)) { throw 'Consumed signing callback ledger entry has invalid reservation metadata.' } $entryPath = [IO.Path]::GetFullPath([string]$entry.path) if ($slotKind -ceq 'generated-nsis-uninstaller') { $relative = Get-ContainedRelativePath $entryPath $Session.TempRoot if ($null -eq $relative -or $relative -match '[\\/]' -or $relative -cnotmatch $NsisUninstallerPattern) { throw 'Signing callback ledger contains an invalid NSIS uninstaller path.' } } else { Assert-ExactCanonicalPathValue ` $entryPath ([string](Get-RequiredPropertyValue $slot 'path' 'Signing manifest slot')) ` 'Signing callback ledger artifact path' } if ($slotKind -ceq 'fixed' -and -not [string]::Equals( [string]$entry.preSignSha256, [string](Get-RequiredPropertyValue $slot 'preSignSha256' 'Fixed signing slot'), [StringComparison]::OrdinalIgnoreCase )) { throw 'Signing callback ledger fixed artifact hash does not match the manifest.' } $reservedAt = [DateTimeOffset]::MinValue if (-not [DateTimeOffset]::TryParseExact( [string]$entry.reservedAtUtc, 'O', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$reservedAt )) { throw 'Signing callback reservation timestamp is invalid.' } if ($status -ceq 'reserved') { if ($ledgerState -cne 'open') { throw 'Only an open signing callback ledger may contain a reservation.' } $reservedCount++ if ($reservedCount -ne 1 -or @( 'completedAtUtc', 'postSignSha256', 'signerSubject' | Where-Object { $null -ne $entry.PSObject.Properties[$_].Value } ).Count -ne 0) { throw 'Reserved signing callback ledger entry has invalid completion metadata.' } continue } if ($status -ceq 'failed') { if ($ledgerState -cne 'failed') { throw 'Only a failed signing callback ledger may contain a failed entry.' } $failedCount++ if ($failedCount -ne 1 -or @( 'completedAtUtc', 'postSignSha256', 'signerSubject' | Where-Object { $null -ne $entry.PSObject.Properties[$_].Value } ).Count -ne 0) { throw 'Failed signing callback ledger entry has invalid completion metadata.' } continue } $completedAt = [DateTimeOffset]::MinValue if (-not [DateTimeOffset]::TryParseExact( [string]$entry.completedAtUtc, 'O', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$completedAt ) -or $completedAt -lt $reservedAt -or [string]$entry.postSignSha256 -notmatch '^[0-9A-Fa-f]{64}$' -or [string]::IsNullOrWhiteSpace([string]$entry.signerSubject)) { throw 'Completed signing callback ledger entry has invalid completion metadata.' } } if ($ledgerState -ceq 'open' -and $failedCount -ne 0) { throw 'An open signing callback ledger cannot contain a failed entry.' } if ($ledgerState -ceq 'failed' -and $reservedCount -ne 0) { throw 'A failed signing callback ledger cannot retain a reservation.' } if ($ledgerState -ceq 'sealed' -and @($entries | Where-Object { [string]$_.status -cne 'completed' }).Count -ne 0) { throw 'A sealed signing callback ledger must contain only completed entries.' } if ($reservedCount -gt 1) { throw 'Signing callback ledger contains multiple reservations.' } if ($ledgerState -ceq 'open') { $incompletePhases = @($entries | Where-Object { [string]$_.status -cne 'completed' } | ForEach-Object { Get-WaggleSigningSlotPhase ([int]$_.order) }) $firstIncompletePhase = if ($incompletePhases.Count -eq 0) { 6 } else { ($incompletePhases | Measure-Object -Minimum).Minimum } if (@($entries | Where-Object { (Get-WaggleSigningSlotPhase ([int]$_.order)) -gt $firstIncompletePhase -and [string]$_.status -ne 'pending' }).Count -ne 0) { throw 'Signing callback ledger crossed an incomplete NSIS signing phase.' } } } function Write-WaggleLedgerAtomicNoLock { param( [Parameter(Mandatory = $true)] [object]$Session, [Parameter(Mandatory = $true)] [object]$Ledger ) $tempLedgerPath = $null $backupLedgerPath = $null try { $ledgerPath = Get-TrustedPath $Session.LedgerPath 'Signing callback ledger' Assert-ExactCanonicalPathValue ` $ledgerPath (Join-Path $Session.SessionDirectory 'callback-ledger.json') ` 'Signing callback ledger path' Assert-WaggleSigningLedgerState $Session $ledger $json = $ledger | ConvertTo-Json -Depth 32 -Compress $tempLedgerPath = Join-Path ` $Session.SessionDirectory ` "callback-ledger.$([Guid]::NewGuid().ToString('N')).tmp" $backupLedgerPath = Join-Path ` $Session.SessionDirectory ` "callback-ledger.$([Guid]::NewGuid().ToString('N')).bak" $bytes = [Text.UTF8Encoding]::new($false).GetBytes($json) $stream = [IO.FileStream]::new( $tempLedgerPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None, 4096, [IO.FileOptions]::WriteThrough ) try { $stream.Write($bytes, 0, $bytes.Length) $stream.Flush($true) } finally { $stream.Dispose() } [IO.File]::Replace($tempLedgerPath, $ledgerPath, $backupLedgerPath, $true) $tempLedgerPath = $null try { [IO.File]::Delete($backupLedgerPath) } catch { } $backupLedgerPath = $null } finally { if ($null -ne $tempLedgerPath -and (Test-Path -LiteralPath $tempLedgerPath -PathType Leaf)) { try { [IO.File]::Delete($tempLedgerPath) } catch { } } if ($null -ne $backupLedgerPath -and (Test-Path -LiteralPath $backupLedgerPath -PathType Leaf)) { try { [IO.File]::Delete($backupLedgerPath) } catch { } } } } function Invoke-WaggleLedgerMutation { param( [Parameter(Mandatory = $true)] [object]$Session, [Parameter(Mandatory = $true)] [scriptblock]$Mutation, [switch]$AllowTerminalReceipt ) $mutex = [Threading.Mutex]::new($false, "Local\WaggleSigning-$($Session.Id)") $hasMutex = $false try { try { $hasMutex = $mutex.WaitOne([TimeSpan]::FromSeconds(30)) } catch [Threading.AbandonedMutexException] { $hasMutex = $true } if (-not $hasMutex) { throw 'Timed out waiting for the signing callback ledger.' } $ledgerPath = Get-TrustedPath $Session.LedgerPath 'Signing callback ledger' try { $ledger = Get-Content -Raw -LiteralPath $ledgerPath | ConvertFrom-Json -Depth 32 -DateKind String } catch { throw 'Signing callback ledger is not valid JSON.' } Assert-WaggleSigningLedgerState $Session $ledger if ([string]$ledger.state -cne 'open') { throw 'Signing callback ledger is already terminal.' } if (-not $AllowTerminalReceipt -and @( @('terminal-intent.json', 'provenance-receipt.json', 'failure-receipt.json') | Where-Object { Test-Path -LiteralPath (Join-Path $Session.SessionDirectory $_) } ).Count -ne 0) { throw 'Signing callback ledger has a pending terminal receipt.' } $mutationResult = & $Mutation $ledger Write-WaggleLedgerAtomicNoLock $Session $ledger return $mutationResult } finally { if ($hasMutex) { $mutex.ReleaseMutex() } $mutex.Dispose() } } function Reserve-WaggleSigningCallback { param( [Parameter(Mandatory = $true)] [object]$Session, [Parameter(Mandatory = $true)] [string]$ArtifactPath ) $artifact = Get-TrustedPath $ArtifactPath 'Signing callback artifact' -AllowHardLink $matches = @($Session.Manifest.slots | Where-Object { if ([string]$_.kind -ceq 'generated-nsis-uninstaller') { $relative = Get-ContainedRelativePath $artifact $Session.TempRoot return $null -ne $relative -and $relative -notmatch '[\\/]' -and $relative -cmatch $NsisUninstallerPattern } $pathProperty = $_.PSObject.Properties['path'] return $null -ne $pathProperty -and [string]::Equals( [IO.Path]::GetFullPath([string]$pathProperty.Value), $artifact, [StringComparison]::OrdinalIgnoreCase ) }) if ($matches.Count -ne 1) { throw 'Signing callback artifact does not match exactly one manifest slot.' } $slot = $matches[0] $preSignHash = (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash if ([string]$slot.kind -ceq 'fixed' -and -not [string]::Equals( $preSignHash, [string]$slot.preSignSha256, [StringComparison]::OrdinalIgnoreCase )) { throw 'Signing callback artifact does not match the manifest pre-sign SHA-256 digest.' } if (@(@('terminal-intent.json', 'provenance-receipt.json', 'failure-receipt.json') | Where-Object { Test-Path -LiteralPath (Join-Path $Session.SessionDirectory $_) }).Count -ne 0) { throw 'Signing provenance receipt already exists.' } Invoke-WaggleLedgerMutation $Session { param($ledger) $entries = @($ledger.entries) $entry = $entries[[int]$slot.order - 1] if ([string]$entry.status -cne 'pending' -or [int]$entry.attempts -ne 0) { throw 'Signing callback slot was already consumed.' } $slotPhase = Get-WaggleSigningSlotPhase ([int]$slot.order) if (@($entries | Where-Object { [string]$_.status -ceq 'reserved' }).Count -ne 0 -or @($entries | Where-Object { (Get-WaggleSigningSlotPhase ([int]$_.order)) -lt $slotPhase -and [string]$_.status -cne 'completed' }).Count -ne 0) { throw 'Signing callback phase does not match the manifest ledger.' } $entry.status = 'reserved' $entry.attempts = 1 $entry.reservationId = [Guid]::NewGuid().ToString('N') $entry.path = $artifact $entry.preSignSha256 = $preSignHash $entry.reservedAtUtc = [DateTimeOffset]::UtcNow.ToString('O') return [pscustomobject][ordered]@{ reservationId = [string]$entry.reservationId slotId = [string]$entry.slotId order = [int]$entry.order kind = [string]$entry.kind path = [string]$entry.path preSignSha256 = [string]$entry.preSignSha256 } } } function Complete-WaggleSigningCallback { param( [Parameter(Mandatory = $true)] [object]$Session, [Parameter(Mandatory = $true)] [object]$Reservation, [Parameter(Mandatory = $true)] [string]$SignedArtifactPath ) $artifact = Get-TrustedPath $SignedArtifactPath 'Signed callback artifact' $postSignHash = (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash $signature = Get-AuthenticodeSignature -LiteralPath $artifact $signerSubject = if ($null -eq $signature.SignerCertificate) { $null } else { [string]$signature.SignerCertificate.Subject } Invoke-WaggleLedgerMutation $Session { param($ledger) $entries = @($ledger.entries) $matchingEntries = @($entries | Where-Object { [string]$_.slotId -ceq [string]$Reservation.slotId }) if ($matchingEntries.Count -ne 1 -or [string]$matchingEntries[0].status -cne 'reserved' -or [string]$matchingEntries[0].reservationId -cne [string]$Reservation.reservationId -or [int]$matchingEntries[0].order -ne [int]$Reservation.order -or [string]$matchingEntries[0].kind -cne [string]$Reservation.kind -or -not [string]::Equals( [string]$matchingEntries[0].path, [string]$Reservation.path, [StringComparison]::OrdinalIgnoreCase ) -or -not [string]::Equals( [IO.Path]::GetFullPath([string]$matchingEntries[0].path), $artifact, [StringComparison]::OrdinalIgnoreCase ) -or -not [string]::Equals( [string]$matchingEntries[0].preSignSha256, [string]$Reservation.preSignSha256, [StringComparison]::OrdinalIgnoreCase )) { throw 'Signing callback reservation is missing or already completed.' } $matchingEntries[0].status = 'completed' $matchingEntries[0].completedAtUtc = [DateTimeOffset]::UtcNow.ToString('O') $matchingEntries[0].postSignSha256 = $postSignHash $matchingEntries[0].signerSubject = $signerSubject return $matchingEntries[0] } } function Write-WaggleDurableJsonNew { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [object]$Value ) $bytes = [Text.UTF8Encoding]::new($false).GetBytes( ($Value | ConvertTo-Json -Depth 32 -Compress) ) $stream = [IO.FileStream]::new( $Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None, 4096, [IO.FileOptions]::WriteThrough ) try { $stream.Write($bytes, 0, $bytes.Length) $stream.Flush($true) } finally { $stream.Dispose() } } function Assert-WaggleTerminalReceipt { param( [Parameter(Mandatory = $true)] [object]$Session, [Parameter(Mandatory = $true)] [ValidateSet('failed', 'sealed')] [string]$State, [Parameter(Mandatory = $true)] [object]$Receipt ) if ([int](Get-RequiredPropertyValue $Receipt 'schemaVersion' 'Signing terminal receipt') -ne 1 -or [string](Get-RequiredPropertyValue $Receipt 'status' 'Signing terminal receipt') -cne $State -or [string](Get-RequiredPropertyValue $Receipt 'sessionId' 'Signing terminal receipt') -cne [string]$Session.Id -or -not [string]::Equals( [string](Get-RequiredPropertyValue $Receipt 'manifestSha256' 'Signing terminal receipt'), [string]$Session.ManifestSha256, [StringComparison]::OrdinalIgnoreCase )) { throw 'Signing terminal receipt does not belong to the active session.' } $terminalAtUtc = [string](Get-RequiredPropertyValue ` $Receipt 'terminalAtUtc' 'Signing terminal receipt') $terminalAt = [DateTimeOffset]::MinValue if (-not [DateTimeOffset]::TryParseExact( $terminalAtUtc, 'O', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$terminalAt )) { throw 'Signing terminal receipt has an invalid terminal timestamp.' } $sourceRevision = [string](Get-RequiredPropertyValue ` $Receipt 'sourceRevision' 'Signing terminal receipt') if ($sourceRevision -cne [string]$Session.Context.SourceRevision) { throw 'Signing terminal receipt does not match the active source revision.' } if ($State -ceq 'failed') { foreach ($name in @('failureCode', 'failureMessage', 'rollbackOutcome')) { if ([string]::IsNullOrWhiteSpace([string](Get-RequiredPropertyValue ` $Receipt $name 'Signing failure receipt'))) { throw 'Signing failure receipt has incomplete failure provenance.' } } if ($null -eq $Receipt.PSObject.Properties['failedSlotId']) { throw 'Signing failure receipt is missing its failed-slot provenance.' } } else { foreach ($name in @( 'manifestPath', 'callbackLedgerPath', 'installerPath', 'installerSha256', 'signerSubject', 'payloadManifestSha256', 'artifactBindings' )) { [void](Get-RequiredPropertyValue $Receipt $name 'Signing provenance receipt') } Assert-ExactCanonicalPathValue ` ([string]$Receipt.manifestPath) ([string]$Session.ManifestPath) ` 'Signing provenance manifest path' Assert-ExactCanonicalPathValue ` ([string]$Receipt.callbackLedgerPath) ([string]$Session.LedgerPath) ` 'Signing provenance callback ledger path' $installerSlots = @($Session.Manifest.slots | Where-Object { [string]$_.kind -ceq 'generated-nsis-installer' }) if ($installerSlots.Count -ne 1) { throw 'Signing provenance session lacks one exact installer slot.' } Assert-ExactCanonicalPathValue ` ([string]$Receipt.installerPath) ([string]$installerSlots[0].path) ` 'Signing provenance installer path' if ([string]$Receipt.installerSha256 -notmatch '^[0-9A-Fa-f]{64}$' -or [string]$Receipt.payloadManifestSha256 -notmatch '^[0-9A-Fa-f]{64}$' -or [string]::IsNullOrWhiteSpace([string]$Receipt.signerSubject) -or @($Receipt.artifactBindings).Count -lt 2) { throw 'Signing provenance receipt has invalid artifact provenance.' } } return $terminalAtUtc } function Get-WaggleTerminalIntentPath { param([Parameter(Mandatory = $true)] [object]$Session) return Join-Path $Session.SessionDirectory 'terminal-intent.json' } function Assert-WaggleTerminalIntent { param( [Parameter(Mandatory = $true)] [object]$Session, [Parameter(Mandatory = $true)] [ValidateSet('failed', 'sealed')] [string]$State, [Parameter(Mandatory = $true)] [object]$Intent ) if ([int](Get-RequiredPropertyValue $Intent 'schemaVersion' 'Signing terminal intent') -ne 1 -or [string](Get-RequiredPropertyValue $Intent 'state' 'Signing terminal intent') -cne $State -or [string](Get-RequiredPropertyValue $Intent 'sessionId' 'Signing terminal intent') -cne [string]$Session.Id -or -not [string]::Equals( [string](Get-RequiredPropertyValue $Intent 'manifestSha256' 'Signing terminal intent'), [string]$Session.ManifestSha256, [StringComparison]::OrdinalIgnoreCase ) -or [string](Get-RequiredPropertyValue ` $Intent 'receiptSha256' 'Signing terminal intent') -notmatch '^[0-9A-Fa-f]{64}$') { throw 'Signing terminal intent does not belong to the active session.' } $terminalAtUtc = [string](Get-RequiredPropertyValue ` $Intent 'terminalAtUtc' 'Signing terminal intent') $terminalAt = [DateTimeOffset]::MinValue if (-not [DateTimeOffset]::TryParseExact( $terminalAtUtc, 'O', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$terminalAt )) { throw 'Signing terminal intent has an invalid terminal timestamp.' } return $terminalAtUtc } function Set-WaggleSigningTerminalState { param( [Parameter(Mandatory = $true)] [object]$Session, [Parameter(Mandatory = $true)] [ValidateSet('failed', 'sealed')] [string]$State, [Parameter(Mandatory = $true)] [object]$Receipt, [Parameter(Mandatory = $true)] [scriptblock]$LedgerMutation ) $mutex = [Threading.Mutex]::new($false, "Local\WaggleSigning-$($Session.Id)") $hasMutex = $false $temporaryReceipt = $null $temporaryIntent = $null $receiptLock = $null $intentLock = $null try { try { $hasMutex = $mutex.WaitOne([TimeSpan]::FromSeconds(30)) } catch [Threading.AbandonedMutexException] { $hasMutex = $true } if (-not $hasMutex) { throw 'Timed out waiting to terminalize the signing session.' } $fileName = if ($State -ceq 'sealed') { 'provenance-receipt.json' } else { 'failure-receipt.json' } $receiptPath = Join-Path $Session.SessionDirectory $fileName $oppositeFileName = if ($State -ceq 'sealed') { 'failure-receipt.json' } else { 'provenance-receipt.json' } $oppositePath = Join-Path $Session.SessionDirectory $oppositeFileName if (Test-Path -LiteralPath $oppositePath) { throw 'Signing session already contains the opposite terminal receipt.' } $intentPath = Get-WaggleTerminalIntentPath $Session $ledgerPath = Get-TrustedPath $Session.LedgerPath 'Signing callback ledger' try { $currentLedger = Get-Content -Raw -LiteralPath $ledgerPath | ConvertFrom-Json -Depth 32 -DateKind String } catch { throw 'Signing callback ledger is not valid JSON.' } Assert-WaggleSigningLedgerState $Session $currentLedger if ([string]$currentLedger.state -notin @('open', $State)) { throw 'Signing session is already in a different terminal state.' } if ([string]$currentLedger.state -ceq 'open') { & $LedgerMutation $currentLedger } if (Test-Path -LiteralPath $receiptPath) { $trustedReceiptPath = Get-TrustedPath $receiptPath 'Signing terminal receipt' $receiptLock = Open-ReadLock $trustedReceiptPath try { $persistedReceipt = Get-Content -Raw -LiteralPath $trustedReceiptPath | ConvertFrom-Json -Depth 32 -DateKind String } catch { throw 'Signing terminal receipt is not valid JSON.' } $terminalAtUtc = Assert-WaggleTerminalReceipt ` $Session $State $persistedReceipt [void](Assert-WaggleTerminalReceipt $Session $State $Receipt) $candidateReceipt = $Receipt | ConvertTo-Json -Depth 32 | ConvertFrom-Json -Depth 32 -DateKind String $candidateReceipt.terminalAtUtc = $terminalAtUtc if (($candidateReceipt | ConvertTo-Json -Depth 32 -Compress) -cne ($persistedReceipt | ConvertTo-Json -Depth 32 -Compress)) { throw 'Signing terminal receipt does not match the current terminalization candidate.' } $receiptHash = (Get-FileHash -LiteralPath $trustedReceiptPath -Algorithm SHA256).Hash if (-not (Test-Path -LiteralPath $intentPath -PathType Leaf)) { throw 'Signing terminal receipt exists without its durable terminal intent.' } $trustedIntentPath = Get-TrustedPath $intentPath 'Signing terminal intent' $intentLock = Open-ReadLock $trustedIntentPath try { $intent = Get-Content -Raw -LiteralPath $trustedIntentPath | ConvertFrom-Json -Depth 16 -DateKind String } catch { throw 'Signing terminal intent is not valid JSON.' } $intentTerminalAtUtc = Assert-WaggleTerminalIntent $Session $State $intent if ($intentTerminalAtUtc -cne $terminalAtUtc -or -not [string]::Equals( [string]$intent.receiptSha256, $receiptHash, [StringComparison]::OrdinalIgnoreCase )) { throw 'Signing terminal receipt does not match its durable terminal intent.' } } else { $terminalAtUtc = Assert-WaggleTerminalReceipt $Session $State $Receipt $candidateReceipt = $Receipt | ConvertTo-Json -Depth 32 | ConvertFrom-Json -Depth 32 -DateKind String if (Test-Path -LiteralPath $intentPath) { $trustedIntentPath = Get-TrustedPath $intentPath 'Signing terminal intent' $intentLock = Open-ReadLock $trustedIntentPath try { $intent = Get-Content -Raw -LiteralPath $trustedIntentPath | ConvertFrom-Json -Depth 16 -DateKind String } catch { throw 'Signing terminal intent is not valid JSON.' } $terminalAtUtc = Assert-WaggleTerminalIntent $Session $State $intent $candidateReceipt.terminalAtUtc = $terminalAtUtc } $temporaryReceipt = Join-Path ` $Session.SessionDirectory "$fileName.$([Guid]::NewGuid().ToString('N')).tmp" Write-WaggleDurableJsonNew $temporaryReceipt $candidateReceipt $trustedTemporaryReceipt = Get-TrustedPath ` $temporaryReceipt 'Staged signing terminal receipt' $persistedReceipt = Get-Content -Raw -LiteralPath $trustedTemporaryReceipt | ConvertFrom-Json -Depth 32 -DateKind String $terminalAtUtc = Assert-WaggleTerminalReceipt ` $Session $State $persistedReceipt $receiptHash = (Get-FileHash -LiteralPath $trustedTemporaryReceipt -Algorithm SHA256).Hash if ($null -ne $intentLock) { if (-not [string]::Equals( [string]$intent.receiptSha256, $receiptHash, [StringComparison]::OrdinalIgnoreCase )) { throw 'Signing terminalization candidate does not match its durable terminal intent.' } } else { $intent = [pscustomobject][ordered]@{ schemaVersion = 1 state = $State sessionId = $Session.Id manifestSha256 = $Session.ManifestSha256 terminalAtUtc = $terminalAtUtc receiptSha256 = $receiptHash } $temporaryIntent = Join-Path ` $Session.SessionDirectory "terminal-intent.$([Guid]::NewGuid().ToString('N')).tmp" Write-WaggleDurableJsonNew $temporaryIntent $intent [void](Assert-WaggleTerminalIntent $Session $State ( Get-Content -Raw -LiteralPath $temporaryIntent | ConvertFrom-Json -Depth 16 -DateKind String )) [IO.File]::Move($temporaryIntent, $intentPath) $temporaryIntent = $null $trustedIntentPath = Get-TrustedPath $intentPath 'Signing terminal intent' $intentLock = Open-ReadLock $trustedIntentPath try { $persistedIntent = Get-Content -Raw -LiteralPath $trustedIntentPath | ConvertFrom-Json -Depth 16 -DateKind String } catch { throw 'Signing terminal intent is not valid JSON.' } $persistedIntentTerminalAtUtc = Assert-WaggleTerminalIntent ` $Session $State $persistedIntent if ($persistedIntentTerminalAtUtc -cne $terminalAtUtc -or -not [string]::Equals( [string]$persistedIntent.receiptSha256, $receiptHash, [StringComparison]::OrdinalIgnoreCase )) { throw 'Published signing terminal intent does not match its staged receipt.' } } } if ([string]$currentLedger.state -ceq $State) { if (-not [string]::Equals( [string]$currentLedger.terminalReceiptSha256, $receiptHash, [StringComparison]::OrdinalIgnoreCase ) -or [string]$currentLedger.terminalAtUtc -cne $terminalAtUtc) { throw 'Terminal ledger does not match its durable receipt.' } return $receiptPath } $currentLedger.state = $State $currentLedger.terminalAtUtc = $terminalAtUtc $currentLedger.terminalReceiptSha256 = $receiptHash Assert-WaggleSigningLedgerState $Session $currentLedger if ($null -ne $temporaryReceipt) { [IO.File]::Move($temporaryReceipt, $receiptPath) $temporaryReceipt = $null $trustedReceiptPath = Get-TrustedPath $receiptPath 'Signing terminal receipt' $receiptLock = Open-ReadLock $trustedReceiptPath if (-not [string]::Equals( (Get-FileHash -LiteralPath $trustedReceiptPath -Algorithm SHA256).Hash, $receiptHash, [StringComparison]::OrdinalIgnoreCase )) { throw 'Published signing terminal receipt does not match its durable terminal intent.' } } Write-WaggleLedgerAtomicNoLock $Session $currentLedger return Get-TrustedPath $receiptPath 'Signing terminal receipt' } finally { if ($null -ne $temporaryReceipt -and (Test-Path -LiteralPath $temporaryReceipt -PathType Leaf)) { try { [IO.File]::Delete($temporaryReceipt) } catch { } } if ($null -ne $temporaryIntent -and (Test-Path -LiteralPath $temporaryIntent -PathType Leaf)) { try { [IO.File]::Delete($temporaryIntent) } catch { } } if ($null -ne $receiptLock) { $receiptLock.Dispose() } if ($null -ne $intentLock) { $intentLock.Dispose() } if ($hasMutex) { $mutex.ReleaseMutex() } $mutex.Dispose() } } function Publish-WaggleTerminalReceipt { param( [Parameter(Mandatory = $true)] [object]$Session, [Parameter(Mandatory = $true)] [ValidateSet('failed', 'sealed')] [string]$State, [Parameter(Mandatory = $true)] [object]$Receipt, [Parameter(Mandatory = $true)] [scriptblock]$LedgerMutation ) return Set-WaggleSigningTerminalState ` -Session $Session -State $State -Receipt $Receipt ` -LedgerMutation $LedgerMutation } function Fail-WaggleSigningSession { param( [Parameter(Mandatory = $true)] [object]$Session, [AllowNull()] [object]$Reservation, [Parameter(Mandatory = $true)] [string]$FailureCode, [Parameter(Mandatory = $true)] [string]$FailureMessage, [string]$RollbackOutcome = 'not-required' ) $ledger = Get-Content -Raw -LiteralPath $Session.LedgerPath | ConvertFrom-Json -Depth 32 -DateKind String Assert-WaggleSigningLedgerState $Session $ledger if ([string]$ledger.state -ceq 'failed') { $persistedReceiptPath = Join-Path $Session.SessionDirectory 'failure-receipt.json' try { $persistedReceipt = Get-Content -Raw -LiteralPath $persistedReceiptPath | ConvertFrom-Json -Depth 32 -DateKind String } catch { throw 'Signing failure receipt is not valid JSON.' } return Publish-WaggleTerminalReceipt $Session 'failed' $persistedReceipt { param($_) } } if ([string]$ledger.state -cne 'open') { throw 'A sealed signing session cannot be marked failed.' } $reserved = @($ledger.entries | Where-Object { [string]$_.status -ceq 'reserved' }) if ($reserved.Count -gt 1) { throw 'Signing ledger contains multiple reservations.' } $failedSlot = $null if ($null -ne $Reservation) { $reserved = @($reserved | Where-Object { [string]$_.reservationId -ceq [string]$Reservation.reservationId }) if ($reserved.Count -ne 1) { throw 'Signing failure does not match the active reservation.' } $failedSlot = [string]$reserved[0].slotId } elseif ($reserved.Count -eq 1) { $failedSlot = [string]$reserved[0].slotId } $terminalAt = [DateTimeOffset]::UtcNow.ToString('O') $receipt = [pscustomobject][ordered]@{ schemaVersion = 1 status = 'failed' sessionId = $Session.Id sourceRevision = $Session.Context.SourceRevision manifestPath = $Session.ManifestPath manifestSha256 = $Session.ManifestSha256 failureCode = $FailureCode failureMessage = $FailureMessage failedSlotId = $failedSlot rollbackOutcome = $RollbackOutcome terminalAtUtc = $terminalAt } return Publish-WaggleTerminalReceipt $Session 'failed' $receipt { param($activeLedger) $activeReserved = @($activeLedger.entries | Where-Object { [string]$_.status -ceq 'reserved' }) if ($null -ne $failedSlot) { $activeReserved = @($activeReserved | Where-Object { [string]$_.slotId -ceq $failedSlot }) if ($activeReserved.Count -ne 1) { throw 'Signing failure reservation changed before terminalization.' } $activeReserved[0].status = 'failed' } elseif ($activeReserved.Count -ne 0) { throw 'Signing failure receipt omitted an active reservation.' } } } function Expand-PinnedArtifactSigningPackage { param( [Parameter(Mandatory = $true)] [string]$PackagePath, [Parameter(Mandatory = $true)] [string]$StagingRoot ) $package = Get-TrustedPath $PackagePath 'Artifact Signing package' $packageLock = Open-ReadLock $package try { if ((Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash -cne $ArtifactSigningPackageSha256) { throw 'Artifact Signing package does not match the pinned SHA-256 digest.' } $stagedPackage = Join-Path $StagingRoot 'Microsoft.ArtifactSigning.Client.1.0.128.nupkg' [IO.File]::Copy($package, $stagedPackage, $false) if ((Get-FileHash -LiteralPath $stagedPackage -Algorithm SHA256).Hash -cne $ArtifactSigningPackageSha256) { throw 'Staged Artifact Signing package differs from the pinned package.' } } finally { $packageLock.Dispose() } $toolRoot = New-PrivateDirectory (Join-Path $StagingRoot 'artifact-signing-client') Add-Type -AssemblyName System.IO.Compression.FileSystem [IO.Compression.ZipFile]::ExtractToDirectory($stagedPackage, $toolRoot) $x64Root = Get-TrustedPath (Join-Path $toolRoot 'bin\x64') 'Artifact Signing x64 root' 'Container' $toolFiles = @(Get-ChildItem -LiteralPath $x64Root -Force) if ($toolFiles.Count -ne 41 -or @($toolFiles | Where-Object { -not $_.PSIsContainer }).Count -ne 41) { throw 'Artifact Signing x64 toolset does not match the pinned 41-file closure.' } $manifestLines = @( $toolFiles | Sort-Object Name | ForEach-Object { "{0} {1}" -f (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash, $_.Name } ) $manifestBytes = [Text.Encoding]::UTF8.GetBytes(($manifestLines -join "`n") + "`n") $manifestHash = [Convert]::ToHexString( [Security.Cryptography.SHA256]::HashData($manifestBytes) ) if ($manifestHash -cne $ArtifactSigningX64ManifestSha256) { throw 'Artifact Signing x64 dependency manifest does not match the pinned digest.' } $dlib = Get-TrustedPath ` (Join-Path $x64Root 'Azure.CodeSigning.Dlib.dll') ` 'Azure Artifact Signing dlib' if ([string](Get-Item -LiteralPath $dlib).VersionInfo.FileVersion -cne $ArtifactSigningClientVersion) { throw "Azure Artifact Signing dlib must be version $ArtifactSigningClientVersion x64." } Assert-MicrosoftAuthenticodeFile $dlib 'Azure Artifact Signing dlib' $ArtifactSigningDlibSha256 $locks = [Collections.Generic.List[IDisposable]]::new() try { foreach ($toolFile in $toolFiles) { $trustedToolFile = Get-TrustedPath $toolFile.FullName 'Artifact Signing dependency' $locks.Add((Open-ReadLock $trustedToolFile)) } } catch { foreach ($lock in $locks) { $lock.Dispose() } throw } return [pscustomobject]@{ Dlib = $dlib Locks = $locks } } function Assert-DotNet8X64Runtime { $dotnet = Get-TrustedPath 'C:\Program Files\dotnet\dotnet.exe' '.NET host' -AllowHardLink Assert-MicrosoftAuthenticodeFile $dotnet '.NET host' $null $DotNetPublisher $dotnetLock = Open-ReadLock $dotnet try { $runtimes = @(& $dotnet --list-runtimes) if ($LASTEXITCODE -ne 0 -or -not ($runtimes -match '^Microsoft\.NETCore\.App 8\.\d+\.\d+ \[')) { throw 'Artifact Signing requires an installed x64 .NET 8 runtime.' } } finally { $dotnetLock.Dispose() } } function Get-SystemFsutilPath { return Get-TrustedPath ` 'C:\Windows\System32\fsutil.exe' 'fsutil' -AllowHardLink } function Get-HardLinkPaths { param([Parameter(Mandatory = $true)] [string]$Path) $fsutil = Get-SystemFsutilPath $fsutilLock = Open-ReadLock $fsutil try { Assert-MicrosoftAuthenticodeFile ` $fsutil 'fsutil' $null $MicrosoftWindowsPublisher -AllowCatalog $rawLinks = @(& $fsutil hardlink list $Path) if ($LASTEXITCODE -ne 0 -or $rawLinks.Count -lt 2) { throw 'Could not enumerate the complete hard-link set for the signing artifact.' } } finally { $fsutilLock.Dispose() } $volumeRoot = [IO.Path]::GetPathRoot($Path) $links = foreach ($rawLink in $rawLinks) { $volumeRelative = [string]$rawLink if ($volumeRelative -notmatch '^\\[^\\]') { throw 'fsutil returned an invalid hard-link path.' } $absolutePath = $volumeRoot.TrimEnd('\') + $volumeRelative Get-TrustedPath $absolutePath 'Hard-link sibling' -AllowHardLink } return @($links | Sort-Object -Unique) } function Assert-ApprovedHardLinkTopology { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$ReleaseRoot, [switch]$AllowDetachedMain ) $expectedMain = [IO.Path]::GetFullPath((Join-Path $ReleaseRoot 'waggle.exe')) $expectedDependency = [IO.Path]::GetFullPath((Join-Path $ReleaseRoot 'deps\waggle.exe')) $isMain = [string]::Equals($Path, $expectedMain, [StringComparison]::OrdinalIgnoreCase) $isDependency = [string]::Equals( $Path, $expectedDependency, [StringComparison]::OrdinalIgnoreCase ) $item = Get-Item -LiteralPath $Path -Force $linkTypeProperty = $item.PSObject.Properties['LinkType'] $linkType = if ($null -eq $linkTypeProperty) { '' } else { [string]$linkTypeProperty.Value } if ([string]::IsNullOrEmpty($linkType)) { if ($isMain -and -not $AllowDetachedMain) { throw 'Cargo main executable must have the exact release\waggle.exe <-> release\deps\waggle.exe hard-link topology.' } return } if ($linkType -cne 'HardLink') { throw 'Only the exact Cargo main-executable hard-link topology is allowed.' } if (-not $isMain -and -not $isDependency) { throw 'Only the exact Cargo executable pair may be hard-linked.' } $actualLinks = @(Get-HardLinkPaths $Path) $actualSet = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($actualLink in $actualLinks) { [void]$actualSet.Add($actualLink) } if ($actualSet.Count -ne 2 -or -not $actualSet.Contains($expectedMain) -or -not $actualSet.Contains($expectedDependency)) { throw 'Cargo main executable has an unexpected hard-link sibling.' } } function Restore-WaggleReplacedArtifact { param( [Parameter(Mandatory = $true)] [string]$ArtifactPath, [Parameter(Mandatory = $true)] [string]$BackupPath, [Parameter(Mandatory = $true)] [string]$ExpectedOriginalSha256, [Parameter(Mandatory = $true)] [string[]]$ExpectedHardLinkPaths ) if ($ExpectedOriginalSha256 -notmatch '^[0-9A-Fa-f]{64}$') { throw 'Rollback expected-original SHA-256 is invalid.' } $backup = Get-TrustedPath $BackupPath 'Rollback backup' -AllowHardLink if ((Get-FileHash -LiteralPath $backup -Algorithm SHA256).Hash -cne $ExpectedOriginalSha256.ToUpperInvariant()) { throw 'Rollback backup SHA-256 does not match expected original digest.' } $artifact = Get-TrustedPath $ArtifactPath 'Rollback artifact' -AllowHardLink $expectedSet = [Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase ) foreach ($expectedPath in @($ExpectedHardLinkPaths)) { if ([string]::IsNullOrWhiteSpace($expectedPath)) { throw 'Rollback hard-link inventory contains an empty path.' } [void]$expectedSet.Add([IO.Path]::GetFullPath($expectedPath)) } $isCargoPair = $expectedSet.Count -eq 2 $releaseRoot = $null if ($isCargoPair) { $releaseRoot = Split-Path $artifact -Parent $expectedMain = [IO.Path]::GetFullPath((Join-Path $releaseRoot 'waggle.exe')) $expectedDependency = [IO.Path]::GetFullPath((Join-Path $releaseRoot 'deps\waggle.exe')) if (-not [string]::Equals( $artifact, $expectedMain, [StringComparison]::OrdinalIgnoreCase ) -or -not $expectedSet.Contains($expectedMain) -or -not $expectedSet.Contains($expectedDependency)) { throw 'Rollback hard-link inventory does not match the exact Cargo executable pair.' } $dependency = Get-TrustedPath ` $expectedDependency 'Rollback Cargo dependency' -AllowHardLink if ((Get-FileHash -LiteralPath $dependency -Algorithm SHA256).Hash -cne $ExpectedOriginalSha256.ToUpperInvariant()) { throw 'Rollback Cargo dependency no longer matches the expected original digest.' } $backupLinks = @(Get-HardLinkPaths $backup) $backupSet = [Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase ) foreach ($backupLink in $backupLinks) { [void]$backupSet.Add($backupLink) } if ($backupSet.Count -ne 2 -or -not $backupSet.Contains($backup) -or -not $backupSet.Contains($expectedDependency)) { throw 'Rollback backup has unexpected hard-link topology.' } } elseif ($expectedSet.Count -ne 1 -or -not $expectedSet.Contains($artifact)) { throw 'Rollback regular-file inventory must contain only the exact artifact path.' } else { $backupItem = Get-Item -LiteralPath $backup -Force $backupLinkType = $backupItem.PSObject.Properties['LinkType'] if ($null -ne $backupLinkType -and -not [string]::IsNullOrEmpty([string]$backupLinkType.Value)) { throw 'Rollback regular-file backup unexpectedly has linked-file topology.' } } $failedReplacement = Join-Path ` (Split-Path $backup -Parent) ` "failed-replacement-$([Guid]::NewGuid().ToString('N')).bin" [IO.File]::Replace($backup, $artifact, $failedReplacement, $true) try { if ((Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash -cne $ExpectedOriginalSha256.ToUpperInvariant()) { throw 'Rollback did not restore the exact original artifact bytes.' } if ($isCargoPair) { Assert-ApprovedHardLinkTopology $artifact $releaseRoot } else { $restoredItem = Get-Item -LiteralPath $artifact -Force $restoredLinkType = $restoredItem.PSObject.Properties['LinkType'] if ($null -ne $restoredLinkType -and -not [string]::IsNullOrEmpty([string]$restoredLinkType.Value)) { throw 'Rollback restored regular artifact with unexpected linked-file topology.' } } } finally { if (Test-Path -LiteralPath $failedReplacement -PathType Leaf) { [IO.File]::Delete($failedReplacement) } } } function Get-ApprovedArtifact { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [object]$SigningSession, [switch]$AllowDetachedMain ) $context = $SigningSession.Context $tauriRoot = $context.TauriRoot $resourcesRoot = $context.ResourcesRoot $releaseRoot = $context.ReleaseRoot $artifact = Get-TrustedPath $Path 'Signing artifact' -AllowHardLink $tempRoot = $SigningSession.TempRoot $version = [string](Get-Content -Raw -LiteralPath $context.ConfigPath | ConvertFrom-Json).version if ($version -notmatch '^\d+\.\d+\.\d+$') { throw 'Tauri version is not strict semantic version.' } $kind = Get-ArtifactPolicyKind $artifact $releaseRoot $resourcesRoot $tempRoot $version Assert-ApprovedHardLinkTopology $artifact $releaseRoot -AllowDetachedMain:$AllowDetachedMain if ($kind -eq 'MSI') { Assert-MsiFile $artifact } else { Assert-PeFile $artifact } return $artifact } function Assert-WaggleSignedArtifact { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$SignTool ) & $SignTool verify /pa /all /v $Path if ($LASTEXITCODE -ne 0) { throw "Signed artifact verification failed with SignTool exit code $LASTEXITCODE." } $signature = Get-AuthenticodeSignature -LiteralPath $Path if ($signature.Status -ne [Management.Automation.SignatureStatus]::Valid -or [string]$signature.SignatureType -cne 'Authenticode' -or $null -eq $signature.SignerCertificate -or $null -eq $signature.TimeStamperCertificate) { throw 'Signed artifact lacks a valid, timestamped Authenticode signature.' } if (-not [string]::Equals( [string]$signature.SignerCertificate.Subject, $ApprovedPublisher, [StringComparison]::Ordinal )) { throw 'Signed artifact publisher does not match the repository-approved identity.' } $hasCodeSigningEku = @( $signature.SignerCertificate.Extensions | Where-Object { $_ -is [Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension] } | ForEach-Object { $_.EnhancedKeyUsages } | Where-Object { $_.Value -eq $CodeSigningOid } ).Count -gt 0 if (-not $hasCodeSigningEku) { throw 'Signed artifact certificate lacks the Code Signing EKU.' } } function Write-WaggleJsonNoBom { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [object]$Value ) [IO.File]::WriteAllText( $Path, ($Value | ConvertTo-Json -Depth 32), [Text.UTF8Encoding]::new($false) ) } function Write-WaggleSigningOverride { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$WrapperPath, [AllowNull()] [string]$ResourcesRoot = $null, [switch]$EnableSigning ) $windows = [ordered]@{} if ($EnableSigning) { $windows.signCommand = [ordered]@{ cmd = $SystemPowerShellPath args = @( '-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $WrapperPath, '-ArtifactPath', '%1' ) } } $bundle = [ordered]@{ active = $true targets = @('nsis') windows = $windows } if (-not [string]::IsNullOrWhiteSpace($ResourcesRoot)) { $trustedResourcesRoot = Get-TrustedPath ` $ResourcesRoot 'Signing override resource root' 'Container' $resourceMap = [ordered]@{} $resourceMap[$trustedResourcesRoot] = 'resources' $bundle.resources = $resourceMap } Write-WaggleJsonNoBom $Path ([ordered]@{ build = [ordered]@{ beforeBuildCommand = ''; beforeBundleCommand = '' } bundle = $bundle }) return Get-TrustedPath $Path 'Tauri signing override config' } function Get-NsisPayloadInventory { param( [Parameter(Mandatory = $true)] [string]$InstallerPath, [Parameter(Mandatory = $true)] [string]$InventoryTool ) $installer = Get-TrustedPath $InstallerPath 'NSIS installer inventory artifact' -AllowHardLink $output = @(& $InventoryTool l -slt -- $installer) if ($LASTEXITCODE -ne 0) { throw "NSIS payload inventory failed with 7-Zip exit code $LASTEXITCODE." } $afterDelimiter = $false $paths = [Collections.Generic.List[string]]::new() $uniquePaths = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($line in $output) { if ([string]$line -ceq '----------') { $afterDelimiter = $true continue } if ($afterDelimiter -and [string]$line -cmatch '^Path = (.+)$' -and $Matches[1] -cne '[0]') { $path = [string]$Matches[1] if ($path -match '[\x00-\x1F\x7F]' -or [IO.Path]::IsPathRooted($path) -or $path -match '(^|[\\/])\.\.?(?:[\\/]|$)' -or $path -match ':' -or -not $uniquePaths.Add($path)) { throw 'NSIS payload inventory contains an unsafe, duplicate, or case-colliding path.' } $paths.Add($path) } } $paths.Sort([StringComparer]::Ordinal) $inventory = @($paths) if ($inventory.Count -lt 10 -or $inventory -notcontains 'waggle.exe') { throw 'NSIS payload inventory is incomplete or missing waggle.exe.' } return $inventory } function Expand-NsisPayloadManifest { param( [Parameter(Mandatory = $true)] [string]$InstallerPath, [Parameter(Mandatory = $true)] [string]$InventoryTool, [Parameter(Mandatory = $true)] [string]$ExtractionRoot ) $installer = Get-TrustedPath $InstallerPath 'NSIS payload artifact' -AllowHardLink $expectedPaths = @(Get-NsisPayloadInventory $installer $InventoryTool) $root = New-PrivateDirectory $ExtractionRoot if (@(Get-ChildItem -LiteralPath $root -Force).Count -ne 0) { throw 'NSIS payload extraction root must be empty.' } & $InventoryTool x -y "-o$root" -- $installer | Out-Null if ($LASTEXITCODE -ne 0) { throw "NSIS payload extraction failed with 7-Zip exit code $LASTEXITCODE." } $entries = [Collections.Generic.List[object]]::new() $actualPaths = [Collections.Generic.List[string]]::new() foreach ($file in @(Get-ChildItem -LiteralPath $root -Recurse -File -Force)) { $trustedFile = Get-TrustedPath $file.FullName 'Extracted NSIS payload' $relative = (Get-ContainedRelativePath $trustedFile $root).Replace('/', '\') $actualPaths.Add($relative) $entries.Add([pscustomobject][ordered]@{ path = $relative sha256 = (Get-FileHash -LiteralPath $trustedFile -Algorithm SHA256).Hash size = [long]$file.Length extractedPath = $trustedFile }) } $actualPaths.Sort([StringComparer]::Ordinal) if ($actualPaths.Count -ne $expectedPaths.Count -or [string]::Join("`n", $actualPaths) -cne [string]::Join("`n", $expectedPaths)) { throw 'Extracted NSIS payload roster differs from the exact archive inventory.' } return @($entries | Sort-Object { [string]$_.path }) } function Get-WagglePayloadManifestSha256 { param([Parameter(Mandatory = $true)] [object[]]$Payloads) $canonical = @($Payloads | Sort-Object { [string]$_.path } | ForEach-Object { [ordered]@{ path = [string]$_.path; sha256 = [string]$_.sha256; size = [long]$_.size } }) | ConvertTo-Json -Depth 8 -Compress return [Convert]::ToHexString( [Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($canonical)) ) } function Assert-WaggleUnsignedPayloadResourceProjection { param( [Parameter(Mandatory = $true)] [object[]]$Payloads, [Parameter(Mandatory = $true)] [object]$ResourcesInventory ) $payloadResources = @( @($Payloads) | Where-Object { ([string]$_.path).StartsWith('resources\', [StringComparison]::Ordinal) } | ForEach-Object { [pscustomobject][ordered]@{ path = ([string]$_.path).Substring('resources\'.Length) size = [long]$_.size sha256 = [string]$_.sha256 } } ) $expectedResources = @((Get-RequiredPropertyValue ` $ResourcesInventory 'entries' 'Hosted build resourcesInventory')) Assert-WaggleCanonicalInventoryEntries ` $payloadResources 'Unsigned NSIS payload resource projection' Assert-WaggleCanonicalInventoryEntries ` $expectedResources 'Hosted build resourcesInventory' if ($payloadResources.Count -ne $expectedResources.Count) { throw 'Unsigned NSIS payload resource projection does not match build receipt.' } for ($index = 0; $index -lt $payloadResources.Count; $index++) { if ([string]$payloadResources[$index].path -cne [string]$expectedResources[$index].path -or [long]$payloadResources[$index].size -ne [long]$expectedResources[$index].size -or -not [string]::Equals( [string]$payloadResources[$index].sha256, [string]$expectedResources[$index].sha256, [StringComparison]::OrdinalIgnoreCase )) { throw 'Unsigned NSIS payload resource projection does not match build receipt.' } } } function New-WagglePendingLedger { param( [Parameter(Mandatory = $true)] [string]$SessionId, [Parameter(Mandatory = $true)] [string]$ManifestSha256, [Parameter(Mandatory = $true)] [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 Resolve-WagglePreflightFixedPath { param( [Parameter(Mandatory = $true)] [string]$SigningPath, [Parameter(Mandatory = $true)] [string]$SigningTargetRoot, [Parameter(Mandatory = $true)] [string]$PreflightTargetRoot ) $relativePath = Get-ContainedRelativePath ` ([IO.Path]::GetFullPath($SigningPath)) ` ([IO.Path]::GetFullPath($SigningTargetRoot)) if ($null -eq $relativePath) { throw 'Preflight signing path escaped the signing target root.' } return Get-TrustedPath ` (Join-Path $PreflightTargetRoot $relativePath) ` 'Preflight signing artifact' -AllowHardLink } function New-WaggleSigningManifest { param( [Parameter(Mandatory = $true)] [object]$Context, [Parameter(Mandatory = $true)] [object[]]$Payloads, [Parameter(Mandatory = $true)] [string]$PreflightTargetRoot, [AllowNull()] [object]$BuildReceipt = $null ) $sessionId = [Environment]::GetEnvironmentVariable('WAGGLE_SIGNING_SESSION_ID') $sessionDirectory = Assert-PrivateDirectoryAcl ` (Join-Path (Join-Path $Context.TauriRoot 'target\.signing-sessions') "run-$sessionId") ` 'Signing session directory' $manifestPath = Join-Path $sessionDirectory 'manifest.json' $ledgerPath = Join-Path $sessionDirectory 'callback-ledger.json' if (Test-Path -LiteralPath $manifestPath -or Test-Path -LiteralPath $ledgerPath) { throw 'Signing session manifest or ledger already exists.' } $version = [string](Get-Content -Raw -LiteralPath $Context.ConfigPath | ConvertFrom-Json).version $fixedPaths = @(Get-ExpectedNsisFixedPaths $Context $version) $packagedPaths = @(Get-ExpectedNsisPackagedPaths) $trustedPreflightTargetRoot = Get-TrustedPath ` $PreflightTargetRoot 'Preflight signing target root' 'Container' $preflightReleaseRoot = Get-TrustedPath ` (Join-Path $trustedPreflightTargetRoot 'x86_64-pc-windows-msvc\release') ` 'Preflight signing release root' 'Container' $slots = [Collections.Generic.List[object]]::new() for ($index = 0; $index -lt $fixedPaths.Count; $index++) { $fixedPath = [IO.Path]::GetFullPath($fixedPaths[$index]) $preflightPath = Resolve-WagglePreflightFixedPath ` $fixedPath $Context.TargetRoot $trustedPreflightTargetRoot Assert-PeFile $preflightPath Assert-ApprovedHardLinkTopology ` $preflightPath $preflightReleaseRoot if ((Get-AuthenticodeSignature -LiteralPath $preflightPath).Status -ne [Management.Automation.SignatureStatus]::NotSigned) { throw 'Unsigned NSIS preflight contains an already signed or invalid callback artifact.' } $slots.Add([pscustomobject][ordered]@{ id = 'fixed-{0:d2}' -f ($index + 1) order = $index + 1 kind = 'fixed' maxUses = 1 path = $fixedPath packagedPath = $packagedPaths[$index] preSignSha256 = if ($index -eq 0) { Get-NsisPatchedMainSha256 $preflightPath } else { (Get-FileHash -LiteralPath $preflightPath -Algorithm SHA256).Hash } }) } $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' }) $installerPath = Join-Path ` $Context.ReleaseRoot "bundle\nsis\Waggle_${version}_x64-setup.exe" $slots.Add([pscustomobject][ordered]@{ id = 'nsis-installer' order = 14 kind = 'generated-nsis-installer' maxUses = 1 path = $installerPath }) $toolchain = [ordered]@{ wrapperPath = $Context.WrapperPath wrapperSha256 = (Get-FileHash -LiteralPath $Context.WrapperPath -Algorithm SHA256).Hash tauriConfigPath = $Context.ConfigPath tauriConfigSha256 = (Get-FileHash -LiteralPath $Context.ConfigPath -Algorithm SHA256).Hash tauriOverrideConfigPath = $Context.OverrideConfigPath tauriOverrideConfigSha256 = (Get-FileHash -LiteralPath $Context.OverrideConfigPath -Algorithm SHA256).Hash 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 nsisRoot = $Context.NsisRoot nsisClosureSha256 = $Context.NsisClosureSha256 makensisPath = $Context.MakensisPath makensisSha256 = $Context.MakensisSha256 gitPath = $Context.GitPath gitSha256 = $Context.GitSha256 gitRuntimePath = $Context.GitRuntimePath gitRuntimeSha256 = $Context.GitRuntimeSha256 nodePath = $Context.NodePath nodeSha256 = $Context.NodeSha256 sevenZipPath = $Context.SevenZipPath sevenZipSha256 = $Context.SevenZipSha256 signToolPath = $Context.SignToolPath signToolSha256 = $Context.SignToolSha256 artifactSigningPackagePath = $Context.ArtifactSigningPackagePath artifactSigningPackageSha256 = $Context.ArtifactSigningPackageSha256 artifactSigningX64ManifestSha256 = $Context.ArtifactSigningX64ManifestSha256 } if ($null -ne $Context.PortableToolchainRoot) { $toolchain.portableToolchainRoot = $Context.PortableToolchainRoot $toolchain.sevenZipDllPath = $Context.SevenZipDllPath $toolchain.sevenZipDllSha256 = $Context.SevenZipDllSha256 $toolchain.portableToolchainReceiptPath = $Context.PortableToolchainReceiptPath $toolchain.portableToolchainReceiptSha256 = $Context.PortableToolchainReceiptSha256 $toolchain.portableToolchainInventorySha256 = $PortableToolchainInventorySha256 $toolchain.portableToolchainFileCount = $PortableToolchainFileCount } $now = [DateTimeOffset]::UtcNow $manifest = [pscustomobject][ordered]@{ schemaVersion = 1 mode = 'nsis' sessionId = $sessionId sourceRevision = $Context.SourceRevision buildReceipt = $BuildReceipt createdAtUtc = $now.ToString('O') expiresAtUtc = $now.AddHours(4).ToString('O') repoRoot = $Context.RepoRoot tauriRoot = $Context.TauriRoot targetRoot = $Context.TargetRoot releaseRoot = $Context.ReleaseRoot resourcesRoot = $Context.ResourcesRoot tempRoot = [Environment]::GetEnvironmentVariable('WAGGLE_NSIS_SIGNING_TEMP_ROOT') ledgerPath = $ledgerPath appVersion = $version payloads = @($Payloads | Sort-Object { [string]$_.path } | ForEach-Object { [pscustomobject][ordered]@{ path = [string]$_.path sha256 = [string]$_.sha256 size = [long]$_.size } }) toolchain = $toolchain slots = @($slots) } Write-WaggleJsonNoBom $manifestPath $manifest $manifestSha256 = (Get-FileHash -LiteralPath $manifestPath -Algorithm SHA256).Hash Write-WaggleJsonNoBom ` $ledgerPath (New-WagglePendingLedger $sessionId $manifestSha256 @($slots)) [Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_PATH', $manifestPath) [Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_SHA256', $manifestSha256) return Get-WaggleSigningSession $Context } function Get-CompletedWaggleSigningLedger { param([Parameter(Mandatory = $true)] [object]$Session) $ledger = Get-Content -Raw -LiteralPath $Session.LedgerPath | ConvertFrom-Json -Depth 32 -DateKind String Assert-WaggleSigningLedgerState $Session $ledger if ([string]$ledger.state -cne 'open' -or @($ledger.entries | Where-Object { [string]$_.status -cne 'completed' }).Count -ne 0) { throw 'Signing callback ledger is incomplete after the NSIS package.' } return $ledger } function Assert-WaggleFinalPayloadBindings { param( [Parameter(Mandatory = $true)] [object]$Session, [Parameter(Mandatory = $true)] [object]$Ledger, [Parameter(Mandatory = $true)] [object[]]$ActualPayloads, [Parameter(Mandatory = $true)] [scriptblock]$SignatureVerifier ) $expectedPayloads = @($Session.Manifest.payloads | Sort-Object { [string]$_.path }) $actualPayloadsSorted = @($ActualPayloads | Sort-Object { [string]$_.path }) if ($expectedPayloads.Count -ne $actualPayloadsSorted.Count -or [string]::Join("`n", @($expectedPayloads | ForEach-Object { [string]$_.path })) -cne [string]::Join("`n", @($actualPayloadsSorted | ForEach-Object { [string]$_.path }))) { throw 'Final signed NSIS payload inventory differs from the unsigned preflight inventory.' } $fixedSlots = @($Session.Manifest.slots | Where-Object { [string]$_.kind -ceq 'fixed' }) $bindings = [Collections.Generic.List[object]]::new() for ($index = 0; $index -lt $expectedPayloads.Count; $index++) { $expected = $expectedPayloads[$index] $actual = $actualPayloadsSorted[$index] $slot = @($fixedSlots | Where-Object { [string]$_.packagedPath -ceq [string]$expected.path }) $expectedHash = [string]$expected.sha256 $slotOrder = $null if ($slot.Count -eq 1) { $slotOrder = [int]$slot[0].order $expectedHash = [string]$Ledger.entries[$slotOrder - 1].postSignSha256 } elseif ($slot.Count -gt 1) { throw 'Multiple signing slots map to one packaged NSIS payload.' } if (-not [string]::Equals( [string]$actual.sha256, $expectedHash, [StringComparison]::OrdinalIgnoreCase )) { throw "Final NSIS payload bytes differ for '$([string]$expected.path)'." } if ($null -ne $slotOrder) { & $SignatureVerifier ([string]$actual.extractedPath) $Session.Context.SignToolPath $bindings.Add([pscustomobject][ordered]@{ order = $slotOrder kind = 'packaged-signed-payload' packagedPath = [string]$expected.path sha256 = $expectedHash }) } } return @($bindings | Sort-Object order) } function Assert-WaggleSigningPackageComplete { param( [Parameter(Mandatory = $true)] [object]$Session, [AllowNull()] [object[]]$ActualPayloads, [AllowNull()] [scriptblock]$SignatureVerifier ) $ledger = Get-CompletedWaggleSigningLedger $Session if ($null -eq $SignatureVerifier) { $SignatureVerifier = { param($Path, $SignTool) Assert-WaggleSignedArtifact $Path $SignTool } } $installerEntry = @($ledger.entries | Where-Object { [int]$_.order -eq 14 }) if ($installerEntry.Count -ne 1) { throw 'Signing callback ledger lacks the final NSIS installer entry.' } $installer = Get-TrustedPath ([string]$installerEntry[0].path) 'Final NSIS installer' if (-not [string]::Equals( (Get-FileHash -LiteralPath $installer -Algorithm SHA256).Hash, [string]$installerEntry[0].postSignSha256, [StringComparison]::OrdinalIgnoreCase )) { throw 'Final NSIS installer hash does not match the completed callback ledger.' } $installerLock = Open-ReadLock $installer $uninstallerLock = $null $extractionRoot = $null $validationSucceeded = $false try { & $SignatureVerifier $installer $Session.Context.SignToolPath if ($null -eq $ActualPayloads) { $extractionRoot = Join-Path $Session.SessionDirectory 'final-payload-extraction' $ActualPayloads = @(Expand-NsisPayloadManifest ` $installer $Session.Context.SevenZipPath $extractionRoot) } $artifactBindings = [Collections.Generic.List[object]]::new() foreach ($binding in @(Assert-WaggleFinalPayloadBindings ` $Session $ledger $ActualPayloads $SignatureVerifier)) { $artifactBindings.Add($binding) } $uninstallerSlot = @($Session.Manifest.slots | Where-Object { [string]$_.kind -ceq 'generated-nsis-uninstaller' }) if ($uninstallerSlot.Count -ne 1) { throw 'Signing manifest lacks the generated NSIS uninstaller evidence slot.' } $uninstallerEvidence = Get-TrustedPath ` ([string]$uninstallerSlot[0].evidencePath) 'Signed NSIS uninstaller evidence' $uninstallerLock = Open-ReadLock $uninstallerEvidence $uninstallerEntry = $ledger.entries[[int]$uninstallerSlot[0].order - 1] if (-not [string]::Equals( (Get-FileHash -LiteralPath $uninstallerEvidence -Algorithm SHA256).Hash, [string]$uninstallerEntry.postSignSha256, [StringComparison]::OrdinalIgnoreCase )) { throw 'Signed NSIS uninstaller evidence differs from the callback ledger.' } & $SignatureVerifier $uninstallerEvidence $Session.Context.SignToolPath $artifactBindings.Add([pscustomobject][ordered]@{ order = [int]$uninstallerEntry.order kind = 'generated-nsis-uninstaller-evidence' evidencePath = $uninstallerEvidence sha256 = [string]$uninstallerEntry.postSignSha256 }) $artifactBindings.Add([pscustomobject][ordered]@{ order = [int]$installerEntry[0].order kind = 'generated-nsis-installer' path = $installer sha256 = [string]$installerEntry[0].postSignSha256 }) $validationSucceeded = $true } finally { if ($null -ne $extractionRoot -and (Test-Path -LiteralPath $extractionRoot)) { Remove-Item -LiteralPath $extractionRoot -Recurse -Force -ErrorAction SilentlyContinue } if (-not $validationSucceeded) { if ($null -ne $uninstallerLock) { $uninstallerLock.Dispose() } $installerLock.Dispose() } } try { if (-not [string]::Equals( (Get-FileHash -LiteralPath $installer -Algorithm SHA256).Hash, [string]$installerEntry[0].postSignSha256, [StringComparison]::OrdinalIgnoreCase )) { throw 'Final NSIS installer changed before provenance publication.' } $terminalAt = [DateTimeOffset]::UtcNow.ToString('O') $receipt = [pscustomobject][ordered]@{ schemaVersion = 1 status = 'sealed' sessionId = $Session.Id sourceRevision = $Session.Context.SourceRevision manifestPath = $Session.ManifestPath manifestSha256 = $Session.ManifestSha256 callbackLedgerPath = $Session.LedgerPath installerPath = $installer installerSha256 = [string]$installerEntry[0].postSignSha256 signerSubject = [string]$installerEntry[0].signerSubject payloadManifestSha256 = Get-WagglePayloadManifestSha256 $ActualPayloads artifactBindings = @($artifactBindings | Sort-Object order) terminalAtUtc = $terminalAt } $receiptPath = Publish-WaggleTerminalReceipt $Session 'sealed' $receipt { param($activeLedger) if (@($activeLedger.entries | Where-Object { [string]$_.status -cne 'completed' }).Count -ne 0) { throw 'Signing callback ledger changed before final sealing.' } } return [pscustomobject]@{ InstallerPath = $installer ReceiptPath = $receiptPath } } finally { if ($null -ne $uninstallerLock) { $uninstallerLock.Dispose() } $installerLock.Dispose() } } function Install-WaggleArtifactSigningPackage { param( [Parameter(Mandatory = $true)] [string]$TauriRoot, [string]$SourcePath = '' ) $destinationDirectoryPath = Join-Path $TauriRoot 'target\.artifact-signing-tools' $destinationPath = Join-Path ` $destinationDirectoryPath 'Microsoft.ArtifactSigning.Client.1.0.128.nupkg' if (Test-Path -LiteralPath $destinationPath -PathType Leaf) { $destination = Get-TrustedPath $destinationPath 'Artifact Signing package' if ((Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash -cne $ArtifactSigningPackageSha256) { throw 'Existing production Artifact Signing package has the wrong SHA-256 digest.' } return $destination } if ([string]::IsNullOrWhiteSpace($SourcePath)) { throw 'Production Artifact Signing package is not provisioned by the protected hosted signing workflow.' } $source = Get-TrustedPath $SourcePath 'Artifact Signing package source' $sourceLock = Open-ReadLock $source $temporaryPath = $null try { if ((Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash -cne $ArtifactSigningPackageSha256) { throw 'Artifact Signing package source does not match the pinned SHA-256 digest.' } $destinationDirectory = New-PrivateDirectory $destinationDirectoryPath $temporaryPath = Join-Path ` $destinationDirectory "artifact-signing.$([Guid]::NewGuid().ToString('N')).tmp" [IO.File]::Copy($source, $temporaryPath, $false) if ((Get-FileHash -LiteralPath $temporaryPath -Algorithm SHA256).Hash -cne $ArtifactSigningPackageSha256) { throw 'Provisioned Artifact Signing package copy failed digest verification.' } [IO.File]::Move($temporaryPath, $destinationPath) $temporaryPath = $null return Get-TrustedPath $destinationPath 'Artifact Signing package' } finally { $sourceLock.Dispose() if ($null -ne $temporaryPath -and (Test-Path -LiteralPath $temporaryPath -PathType Leaf)) { try { [IO.File]::Delete($temporaryPath) } catch { } } } } function Open-WagglePackageToolchainLocks { param([Parameter(Mandatory = $true)] [object]$Context) $bindings = @( @($Context.WrapperPath, (Get-FileHash -LiteralPath $Context.WrapperPath -Algorithm SHA256).Hash, 'Signing wrapper'), @($Context.NodePath, $Context.NodeSha256, 'Node.js runtime'), @($Context.SevenZipPath, $Context.SevenZipSha256, '7-Zip'), @($Context.SevenZipDllPath, $Context.SevenZipDllSha256, '7-Zip runtime library'), @($Context.TauriCliPath, $Context.TauriCliSha256, 'Tauri CLI'), @($Context.TauriCliPackagePath, $Context.TauriCliPackageSha256, 'Tauri CLI package'), @($Context.TauriCliMainPath, $Context.TauriCliMainSha256, 'Tauri CLI main module'), @($Context.TauriCliIndexPath, $Context.TauriCliIndexSha256, 'Tauri CLI native loader'), @($Context.TauriNativePackagePath, $Context.TauriNativePackageSha256, 'Tauri native package'), @($Context.TauriNativeBinaryPath, $Context.TauriNativeBinarySha256, 'Tauri native binary'), @($Context.MakensisPath, $Context.MakensisSha256, 'makensis'), @($Context.GitPath, $Context.GitSha256, 'Git executable'), @($Context.GitRuntimePath, $Context.GitRuntimeSha256, 'Git runtime'), @($Context.SignToolPath, $Context.SignToolSha256, 'SignTool'), @($Context.ArtifactSigningPackagePath, $Context.ArtifactSigningPackageSha256, 'Artifact Signing package') ) $locks = [Collections.Generic.List[IDisposable]]::new() try { foreach ($portableLock in @($Context.PortableToolchainLocks)) { $locks.Add($portableLock) } $Context.PortableToolchainLocks.Clear() $trackedBundleInputs = @( 'app/src-tauri/tauri.conf.json', 'app/src-tauri/Cargo.toml', 'app/src-tauri/Cargo.lock', 'app/src-tauri/build.rs', 'app/src-tauri/nsis/installer.nsi', 'app/src-tauri/icons/32x32.png', 'app/src-tauri/icons/128x128.png', 'app/src-tauri/icons/128x128@2x.png', 'app/src-tauri/icons/icon.icns', 'app/src-tauri/icons/icon.ico', 'app/src-tauri/icons/icon.png' ) foreach ($relativePath in $trackedBundleInputs) { $trackedPath = Get-TrustedPath ` (Join-Path $Context.RepoRoot $relativePath) ` 'Tracked Tauri packaging input' -AllowHardLink $locks.Add((Open-ReadLock $trackedPath)) } $nsisInventory = New-WagglePrebuiltInventory -Root $Context.NsisRoot if (@($nsisInventory.entries).Count -ne 442 -or -not [string]::Equals( [string]$nsisInventory.sha256, [string]$Context.NsisClosureSha256, [StringComparison]::OrdinalIgnoreCase )) { throw 'NSIS compiler closure does not match the pinned 442-file inventory.' } $nsisLease = Open-WaggleValidatedPrebuiltTree ` $Context.NsisRoot $nsisInventory 'NSIS compiler closure' foreach ($nsisLock in $nsisLease.Locks) { $locks.Add($nsisLock) } $nsisLease.Locks.Clear() foreach ($binding in $bindings) { $path = Get-TrustedPath ([string]$binding[0]) ([string]$binding[2]) -AllowHardLink $lock = Open-ReadLock $path if ((Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash -cne [string]$binding[1]) { $lock.Dispose() throw "$($binding[2]) does not match the pinned production-build digest." } $locks.Add($lock) } $lockedRevision = [string](& $Context.GitPath -C $Context.RepoRoot rev-parse --verify HEAD) $lockedStatus = @(& $Context.GitPath -C $Context.RepoRoot status --porcelain=v1 --untracked-files=all) if ($LASTEXITCODE -ne 0 -or $lockedRevision -cne [string]$Context.SourceRevision -or $lockedStatus.Count -ne 0) { throw 'Exact repository revision changed before packaging inputs were locked.' } Assert-WaggleHostedSigningBoundary $lockedRevision ([string]( Get-Content -Raw -LiteralPath $Context.ConfigPath | ConvertFrom-Json ).version) return $locks } catch { foreach ($lock in $locks) { $lock.Dispose() } throw } } function Invoke-WagglePinnedNodeScript { param( [Parameter(Mandatory = $true)] [object]$Context, [Parameter(Mandatory = $true)] [string]$ScriptPath, [Parameter(Mandatory = $true)] [string]$WorkingDirectory, [string[]]$Arguments = @(), [string]$Label = 'Node build step' ) Push-Location $WorkingDirectory try { & $Context.NodePath $ScriptPath @Arguments if ($LASTEXITCODE -ne 0) { throw "$Label failed with exit code $LASTEXITCODE." } } finally { Pop-Location } } function Assert-WaggleHostedResourceInventoryProjection { param( [Parameter(Mandatory = $true)] [object]$TargetInventory, [Parameter(Mandatory = $true)] [object]$ResourcesInventory ) $projection = @( @((Get-RequiredPropertyValue ` $TargetInventory 'entries' 'Hosted build targetInventory')) | Where-Object { ([string]$_.path).StartsWith('resources\', [StringComparison]::Ordinal) } | ForEach-Object { [pscustomobject][ordered]@{ path = ([string]$_.path).Substring('resources\'.Length) size = [long]$_.size sha256 = [string]$_.sha256 } } ) $resourceEntries = @((Get-RequiredPropertyValue ` $ResourcesInventory 'entries' 'Hosted build resourcesInventory')) Assert-WaggleCanonicalInventoryEntries ` $projection 'Hosted build target resource projection' Assert-WaggleCanonicalInventoryEntries ` $resourceEntries 'Hosted build resourcesInventory' if ($projection.Count -ne $resourceEntries.Count) { throw 'Hosted build target resource projection does not match resourcesInventory.' } for ($index = 0; $index -lt $projection.Count; $index++) { if ([string]$projection[$index].path -cne [string]$resourceEntries[$index].path -or [long]$projection[$index].size -ne [long]$resourceEntries[$index].size -or -not [string]::Equals( [string]$projection[$index].sha256, [string]$resourceEntries[$index].sha256, [StringComparison]::OrdinalIgnoreCase )) { throw 'Hosted build target resource projection does not match resourcesInventory.' } } if (-not [string]::Equals( (Get-WaggleInventorySha256 $projection), [string](Get-RequiredPropertyValue ` $ResourcesInventory 'sha256' 'Hosted build resourcesInventory'), [StringComparison]::OrdinalIgnoreCase )) { throw 'Hosted build target resource projection does not match resourcesInventory.' } } function Get-WaggleHostedBuildReceipt { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$ExpectedSha256, [Parameter(Mandatory = $true)] [string]$ExpectedSourceRevision ) if ($ExpectedSha256 -notmatch '^[0-9A-Fa-f]{64}$') { throw 'Build receipt SHA-256 must be one exact digest.' } $receiptPath = Get-TrustedPath $Path 'Hosted build receipt' $lock = Open-ReadLock $receiptPath try { if (-not [string]::Equals( (Get-FileHash -LiteralPath $receiptPath -Algorithm SHA256).Hash, $ExpectedSha256, [StringComparison]::OrdinalIgnoreCase )) { throw 'Hosted build receipt does not match its SHA-256 handoff.' } try { $receipt = Get-Content -Raw -LiteralPath $receiptPath | ConvertFrom-Json -Depth 32 -DateKind String } catch { throw 'Hosted build receipt is not valid JSON.' } if ([int](Get-RequiredPropertyValue $receipt 'schemaVersion' 'Hosted build receipt') -ne 1 -or [string](Get-RequiredPropertyValue $receipt 'repository' 'Hosted build receipt') -cne 'marolinik/waggle-os' -or [string](Get-RequiredPropertyValue $receipt 'sourceRevision' 'Hosted build receipt') -cne $ExpectedSourceRevision -or [string](Get-RequiredPropertyValue $receipt 'targetTriple' 'Hosted build receipt') -cne 'x86_64-pc-windows-msvc') { throw 'Hosted build receipt does not bind the approved repository, revision, and target.' } foreach ($name in @('targetInventory', 'resourcesInventory')) { $inventory = Get-RequiredPropertyValue $receipt $name 'Hosted build receipt' $entries = @((Get-RequiredPropertyValue $inventory 'entries' "Hosted build $name")) Assert-WaggleCanonicalInventoryEntries $entries "Hosted build $name" if (-not [string]::Equals( (Get-WaggleInventorySha256 $entries), [string](Get-RequiredPropertyValue $inventory 'sha256' "Hosted build $name"), [StringComparison]::OrdinalIgnoreCase )) { throw "Hosted build $name aggregate digest is invalid." } } Assert-WaggleHostedResourceInventoryProjection ` $receipt.targetInventory $receipt.resourcesInventory $checker = Get-RequiredPropertyValue $receipt 'checker' 'Hosted build receipt' if ([int](Get-RequiredPropertyValue $checker 'exitCode' 'Hosted build checker') -ne 0 -or [string](Get-RequiredPropertyValue $checker 'sha256' 'Hosted build checker') -notmatch '^[0-9A-Fa-f]{64}$') { throw 'Hosted build sidecar/resource checker did not succeed with a bound digest.' } return [pscustomobject]@{ Receipt = $receipt; Path = $receiptPath; Lock = $lock } } catch { $lock.Dispose() throw } } function Assert-WaggleHostedSigningBoundary { param( [Parameter(Mandatory = $true)] [string]$ExpectedRevision, [Parameter(Mandatory = $true)] [string]$ExpectedVersion ) if ($ExpectedRevision -notmatch '^[0-9a-f]{40}$' -or $ExpectedVersion -notmatch '^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$') { throw 'Hosted Package mode requires exact revision and app-version inputs.' } $expectedTag = "v$ExpectedVersion" $expectedRef = "refs/tags/$expectedTag" $expectedWorkflowRef = "marolinik/waggle-os/.github/workflows/release.yml@$expectedRef" $expected = [ordered]@{ GITHUB_ACTIONS = 'true' GITHUB_EVENT_NAME = 'push' RUNNER_ENVIRONMENT = 'github-hosted' GITHUB_REPOSITORY = 'marolinik/waggle-os' GITHUB_SHA = $ExpectedRevision GITHUB_REF = $expectedRef GITHUB_REF_TYPE = 'tag' GITHUB_REF_NAME = $expectedTag GITHUB_WORKFLOW_REF = $expectedWorkflowRef GITHUB_WORKFLOW_SHA = $ExpectedRevision } foreach ($entry in $expected.GetEnumerator()) { if ([Environment]::GetEnvironmentVariable([string]$entry.Key) -cne [string]$entry.Value) { throw "Hosted Package mode requires exact $($entry.Key) boundary evidence." } } } function Invoke-WaggleSigningPackage { if (-not $IsWindows -or -not [Environment]::Is64BitProcess -or $PSVersionTable.PSEdition -cne 'Core' -or $PSVersionTable.PSVersion -lt [Version]'7.5.0') { throw 'Production signing Package mode requires approved 64-bit PowerShell 7.5+ on Windows.' } $actualHost = Get-TrustedPath (Get-Process -Id $PID).Path 'Current PowerShell 7 host' -AllowHardLink Assert-ApprovedPowerShell7Path $actualHost $scriptPath = Get-TrustedPath $PSCommandPath 'Signing wrapper' $appRoot = Split-Path (Split-Path $scriptPath -Parent) -Parent $repoRoot = Get-TrustedPath (Split-Path $appRoot -Parent) 'Repository root' 'Container' $tauriRoot = Get-TrustedPath (Join-Path $appRoot 'src-tauri') 'Tauri root' 'Container' if (@( $UnsignedInputRoot, $SigningInputRoot, $BuildReceiptPath, $BuildReceiptSha256, $PortableToolchainRoot, $PortableNodePath, $PortableGitPath, $PortableSevenZipPath, $PortableToolchainReceiptPath, $PortableToolchainReceiptSha256 | Where-Object { [string]::IsNullOrWhiteSpace([string]$_) } ).Count -ne 0) { throw 'Hosted Package mode requires both prebuilt roots, the exact build receipt, and the complete portable toolchain handoff.' } $unsignedSourceRoot = Get-TrustedPath ` $UnsignedInputRoot 'Unsigned prebuilt input root' 'Container' $signingSourceRoot = Get-TrustedPath ` $SigningInputRoot 'Signing prebuilt input root' 'Container' $portableToolchain = Get-WagglePortableToolchain ` $PortableToolchainRoot $PortableNodePath $PortableGitPath ` $PortableSevenZipPath $PortableToolchainReceiptPath ` $PortableToolchainReceiptSha256 ` -DisallowedRoots @( $repoRoot, $unsignedSourceRoot, $signingSourceRoot, $BuildReceiptPath, $ArtifactSigningPackageSource ) $portableBootstrapLocks = $portableToolchain.Locks try { $gitLock = Open-ReadLock $portableToolchain.GitPath try { $sourceRevision = [string](& $portableToolchain.GitPath ` -C $repoRoot rev-parse --verify HEAD) if ($LASTEXITCODE -ne 0 -or $sourceRevision -notmatch '^[0-9a-f]{40}$') { throw 'Could not resolve the exact repository source revision.' } } finally { $gitLock.Dispose() } $appVersion = [string]( Get-Content -Raw -LiteralPath (Join-Path $tauriRoot 'tauri.conf.json') | ConvertFrom-Json ).version Assert-WaggleHostedSigningBoundary $sourceRevision $appVersion foreach ($pair in @( @($unsignedSourceRoot, $signingSourceRoot), @($unsignedSourceRoot, $BuildReceiptPath), @($signingSourceRoot, $BuildReceiptPath), @($unsignedSourceRoot, $tauriRoot), @($signingSourceRoot, $tauriRoot) )) { if (Test-WagglePathsOverlap ([string]$pair[0]) ([string]$pair[1])) { throw 'Hosted Package input, receipt, repository, and output roots must be distinct.' } } $receiptLease = Get-WaggleHostedBuildReceipt ` $BuildReceiptPath $BuildReceiptSha256 $sourceRevision $receipt = $receiptLease.Receipt } catch { foreach ($lock in $portableBootstrapLocks) { $lock.Dispose() } throw } $sessionId = [Guid]::NewGuid().ToString('N') $sessionDirectory = New-PrivateDirectory ` (Join-Path $tauriRoot "target\.signing-sessions\run-$sessionId") $tempRoot = New-PrivateDirectory ` (Join-Path $tauriRoot "target\.signing-temp\run-$sessionId") $overridePath = Join-Path $sessionDirectory 'tauri.signing-override.json' $environmentNames = @( 'WAGGLE_SIGNING_SESSION_ID', 'WAGGLE_SIGNING_MANIFEST_PATH', 'WAGGLE_SIGNING_MANIFEST_SHA256', 'CARGO_TARGET_DIR', 'TEMP', 'TMP', 'WAGGLE_NSIS_SIGNING_TEMP_ROOT', 'WAGGLE_SIGNING_RESOURCES_ROOT', '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', 'NODE_OPTIONS', 'NODE_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) } $session = $null $packageLocks = $null $unsignedSourceLease = $null $signingSourceLease = $null $unsignedWorkLease = $null $signingWorkLease = $null $unsignedWorkRoot = $null $signingWorkRoot = $null $preflightEvidenceRoot = $null $unsignedExtractionRoot = $null $unsignedOverrideLock = $null $packageSucceeded = $false try { [Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_SESSION_ID', $sessionId) [Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_PATH', $null) [Environment]::SetEnvironmentVariable('WAGGLE_SIGNING_MANIFEST_SHA256', $null) [Environment]::SetEnvironmentVariable( 'WAGGLE_SIGNING_PORTABLE_TOOLCHAIN_ROOT', $portableToolchain.PortableToolchainRoot ) [Environment]::SetEnvironmentVariable( 'WAGGLE_SIGNING_PORTABLE_NODE_PATH', $portableToolchain.NodePath ) [Environment]::SetEnvironmentVariable( 'WAGGLE_SIGNING_PORTABLE_GIT_PATH', $portableToolchain.GitPath ) [Environment]::SetEnvironmentVariable( 'WAGGLE_SIGNING_PORTABLE_SEVEN_ZIP_PATH', $portableToolchain.SevenZipPath ) [Environment]::SetEnvironmentVariable( 'WAGGLE_SIGNING_PORTABLE_RECEIPT_PATH', $portableToolchain.ReceiptPath ) [Environment]::SetEnvironmentVariable( 'WAGGLE_SIGNING_PORTABLE_RECEIPT_SHA256', $portableToolchain.ReceiptSha256 ) [Environment]::SetEnvironmentVariable('TEMP', $tempRoot) [Environment]::SetEnvironmentVariable('TMP', $tempRoot) [Environment]::SetEnvironmentVariable('WAGGLE_NSIS_SIGNING_TEMP_ROOT', $tempRoot) [Environment]::SetEnvironmentVariable('NODE_OPTIONS', $null) [Environment]::SetEnvironmentVariable('NODE_PATH', $null) [Environment]::SetEnvironmentVariable('NAPI_RS_NATIVE_LIBRARY_PATH', $null) [Environment]::SetEnvironmentVariable('NAPI_RS_FORCE_WASI', $null) [Environment]::SetEnvironmentVariable('npm_config_node_options', $null) [Environment]::SetEnvironmentVariable('TARGET_ARCH', 'x64') $cargoReleaseRelative = 'x86_64-pc-windows-msvc\release' $sourceDisallowedRoots = @( $sessionDirectory, $tempRoot, $tauriRoot, $BuildReceiptPath ) $unsignedSourceLease = Open-WaggleValidatedPrebuiltTree ` $unsignedSourceRoot $receipt.targetInventory ` 'Unsigned receipt-bound prebuilt input tree' ` ($sourceDisallowedRoots + @($signingSourceRoot)) ` -CargoReleaseRelativePath $cargoReleaseRelative $signingSourceLease = Open-WaggleValidatedPrebuiltTree ` $signingSourceRoot $receipt.targetInventory ` 'Signing receipt-bound prebuilt input tree' ` ($sourceDisallowedRoots + @($unsignedSourceRoot)) ` -CargoReleaseRelativePath $cargoReleaseRelative $workRoot = Join-Path $sessionDirectory 'work' $unsignedWorkRoot = Join-Path $workRoot 'unsigned' $signingWorkRoot = Join-Path $workRoot 'signing' [void](Assert-WaggleHostedDiskCapacity $sessionDirectory $receipt.targetInventory) New-WagglePrebuiltWorkCopy ` $unsignedSourceRoot $unsignedWorkRoot $receipt.targetInventory ` 'Unsigned hosted prebuilt input' $cargoReleaseRelative | Out-Null $unsignedReleaseRoot = Join-Path $unsignedWorkRoot $cargoReleaseRelative $unsignedResourcesRoot = Join-Path $unsignedWorkRoot 'resources' $unsignedMutablePaths = @( (Join-Path $unsignedReleaseRoot 'waggle.exe'), (Join-Path $unsignedReleaseRoot 'deps\waggle.exe') ) $unsignedRegeneratedRoots = @( (Join-Path $unsignedReleaseRoot 'nsis'), (Join-Path $unsignedReleaseRoot 'bundle\nsis') ) $unsignedWorkLease = Open-WaggleValidatedPrebuiltTree ` $unsignedWorkRoot $receipt.targetInventory 'Unsigned private work tree' ` @($unsignedSourceRoot, $signingSourceRoot, $signingWorkRoot, $tempRoot) ` -MutablePaths $unsignedMutablePaths ` -RegeneratedRoots $unsignedRegeneratedRoots ` -CargoReleaseRelativePath $cargoReleaseRelative Clear-WaggleRegeneratedRoots $unsignedWorkLease Write-WaggleSigningOverride ` $overridePath $scriptPath $unsignedResourcesRoot | Out-Null Assert-WaggleSigningOverrideContract ` $overridePath $scriptPath $unsignedResourcesRoot -Unsigned Install-WaggleArtifactSigningPackage ` $tauriRoot $ArtifactSigningPackageSource | Out-Null [Environment]::SetEnvironmentVariable('CARGO_TARGET_DIR', $unsignedWorkRoot) [Environment]::SetEnvironmentVariable( 'WAGGLE_SIGNING_RESOURCES_ROOT', $unsignedResourcesRoot ) $context = Get-WaggleSigningContext -AllowPortableBeforeManifest $packageLocks = Open-WagglePackageToolchainLocks $context $unsignedOverrideLock = Open-ReadLock $overridePath try { Assert-WaggleSigningOverrideContract ` $overridePath $scriptPath $unsignedResourcesRoot -Unsigned Invoke-WagglePinnedNodeScript ` $context $context.TauriCliPath $context.AppRoot @( 'bundle', '--target', 'x86_64-pc-windows-msvc', '--no-sign', '--bundles', 'nsis', '--config', $overridePath, '--ci' ) 'Unsigned immutable NSIS preflight package' } finally { $unsignedOverrideLock.Dispose() $unsignedOverrideLock = $null } $version = [string](Get-Content -Raw -LiteralPath $context.ConfigPath | ConvertFrom-Json).version $installerPath = Join-Path ` $context.ReleaseRoot "bundle\nsis\Waggle_${version}_x64-setup.exe" $unsignedExtractionRoot = Join-Path $sessionDirectory 'unsigned-payload-extraction' $payloads = @(Expand-NsisPayloadManifest ` $installerPath $context.SevenZipPath $unsignedExtractionRoot) Assert-WaggleUnsignedPayloadResourceProjection ` $payloads $receipt.resourcesInventory $preflightEvidenceRoot = New-WagglePreflightEvidenceCopy ` $context (Join-Path $sessionDirectory 'preflight-evidence') Remove-Item -LiteralPath $unsignedExtractionRoot -Recurse -Force $unsignedExtractionRoot = $null Remove-Item -LiteralPath $installerPath -Force foreach ($lock in $unsignedWorkLease.Locks) { $lock.Dispose() } $unsignedWorkLease = $null Remove-Item -LiteralPath $unsignedWorkRoot -Recurse -Force $unsignedWorkRoot = $null [void](Assert-WaggleHostedDiskCapacity $sessionDirectory $receipt.targetInventory) New-WagglePrebuiltWorkCopy ` $signingSourceRoot $signingWorkRoot $receipt.targetInventory ` 'Signing hosted prebuilt input' $cargoReleaseRelative | Out-Null $signingReleaseRoot = Join-Path $signingWorkRoot $cargoReleaseRelative $signingResourcesRoot = Join-Path $signingWorkRoot 'resources' $signingMutablePaths = @( Get-ExpectedNsisFixedPaths ` ([pscustomobject]@{ ReleaseRoot = $signingReleaseRoot ResourcesRoot = $signingResourcesRoot }) ` '0.0.0' ) + @((Join-Path $signingReleaseRoot 'deps\waggle.exe')) $signingRegeneratedRoots = @( (Join-Path $signingReleaseRoot 'nsis'), (Join-Path $signingReleaseRoot 'bundle\nsis') ) $signingWorkLease = Open-WaggleValidatedPrebuiltTree ` $signingWorkRoot $receipt.targetInventory 'Signing private work tree' ` @($unsignedSourceRoot, $signingSourceRoot, $tempRoot) ` -MutablePaths $signingMutablePaths ` -RegeneratedRoots $signingRegeneratedRoots ` -CargoReleaseRelativePath $cargoReleaseRelative Clear-WaggleRegeneratedRoots $signingWorkLease Write-WaggleSigningOverride ` $overridePath $scriptPath $signingResourcesRoot -EnableSigning | Out-Null Assert-WaggleSigningOverrideContract ` $overridePath $scriptPath $signingResourcesRoot [Environment]::SetEnvironmentVariable('CARGO_TARGET_DIR', $signingWorkRoot) [Environment]::SetEnvironmentVariable( 'WAGGLE_SIGNING_RESOURCES_ROOT', $signingResourcesRoot ) $context = Get-WaggleSigningContext -AllowPortableBeforeManifest $session = New-WaggleSigningManifest ` $context $payloads $preflightEvidenceRoot $receipt Remove-Item -LiteralPath $preflightEvidenceRoot -Recurse -Force $preflightEvidenceRoot = $null Assert-DotNet8X64Runtime Invoke-WagglePinnedNodeScript ` $context $context.TauriCliPath $context.AppRoot @( 'bundle', '--target', 'x86_64-pc-windows-msvc', '--bundles', 'nsis', '--config', $overridePath, '--ci' ) 'Signed NSIS bundle' $result = Assert-WaggleSigningPackageComplete $session $packageSucceeded = $true Write-Host "Public Artifact Signing package sealed: $($result.InstallerPath)" Write-Host "Provenance receipt: $($result.ReceiptPath)" } catch { $packageFailure = $_ if ($null -ne $session) { try { Fail-WaggleSigningSession ` $session $null 'package_or_finalization_failure' ` $packageFailure.Exception.Message | Out-Null } catch { throw "Signing package failed: $($packageFailure.Exception.Message) Terminalization also failed: $($_.Exception.Message)" } } throw $packageFailure } finally { if ($null -ne $unsignedExtractionRoot -and (Test-Path -LiteralPath $unsignedExtractionRoot)) { Remove-Item -LiteralPath $unsignedExtractionRoot -Recurse -Force -ErrorAction SilentlyContinue } if ($null -ne $preflightEvidenceRoot -and (Test-Path -LiteralPath $preflightEvidenceRoot)) { Remove-Item -LiteralPath $preflightEvidenceRoot -Recurse -Force -ErrorAction SilentlyContinue } if ($null -ne $unsignedOverrideLock) { $unsignedOverrideLock.Dispose() } if ($null -ne $session) { foreach ($sessionLock in $session.Locks) { $sessionLock.Dispose() } } if ($null -ne $packageLocks) { foreach ($lock in $packageLocks) { $lock.Dispose() } } foreach ($lock in $portableBootstrapLocks) { $lock.Dispose() } foreach ($lease in @( $unsignedWorkLease, $signingWorkLease, $unsignedSourceLease, $signingSourceLease )) { if ($null -ne $lease) { foreach ($lock in $lease.Locks) { $lock.Dispose() } } } if ($null -ne $unsignedWorkRoot -and (Test-Path -LiteralPath $unsignedWorkRoot)) { Remove-Item -LiteralPath $unsignedWorkRoot -Recurse -Force -ErrorAction SilentlyContinue } if (-not $packageSucceeded -and $null -ne $signingWorkRoot -and (Test-Path -LiteralPath $signingWorkRoot)) { Remove-Item -LiteralPath $signingWorkRoot -Recurse -Force -ErrorAction SilentlyContinue } $receiptLease.Lock.Dispose() foreach ($name in $environmentNames) { [Environment]::SetEnvironmentVariable($name, $savedEnvironment[$name]) } } } function Invoke-WaggleArtifactSigning { param([Parameter(Mandatory = $true)] [string]$Path) if (-not $IsWindows -or -not [Environment]::Is64BitProcess -or $PSVersionTable.PSEdition -cne 'Core' -or $PSVersionTable.PSVersion -lt [Version]'7.5.0') { throw 'Azure Artifact Signing requires approved 64-bit PowerShell 7.5+ on Windows.' } $actualHost = Get-TrustedPath (Get-Process -Id $PID).Path 'Current PowerShell 7 host' -AllowHardLink Assert-ApprovedPowerShell7Path $actualHost $context = Get-WaggleSigningContext $session = Get-WaggleSigningSession $context $artifact = $null $reservation = $null $stagingRoot = $null $signToolLock = $null $toolsetLocks = $null $signedArtifactLock = $null $replaced = $false $expectedRollbackPaths = $null try { $artifact = Get-ApprovedArtifact $Path $session $reservation = Reserve-WaggleSigningCallback $session $artifact $sourceHash = [string]$reservation.preSignSha256 try { Assert-DotNet8X64Runtime $signTool = $context.SignToolPath $packagePath = $context.ArtifactSigningPackagePath $stagingParent = New-PrivateDirectory ` (Join-Path $context.TauriRoot 'target\.signing-staging') $stagingRoot = New-PrivateDirectory ` (Join-Path $stagingParent ([Guid]::NewGuid().ToString('N'))) $stagedArtifact = Join-Path $stagingRoot ([IO.Path]::GetFileName($artifact)) $backupPath = Join-Path $stagingRoot 'original.backup' $metadataPath = Join-Path $stagingRoot 'artifact-signing.json' $metadata = [ordered]@{ Endpoint = $ArtifactSigningEndpoint CodeSigningAccountName = $ArtifactSigningAccount CertificateProfileName = $ArtifactSigningProfile } [IO.File]::Copy($artifact, $stagedArtifact, $false) $stagedArtifact = Get-TrustedPath $stagedArtifact 'Staged signing artifact' Assert-PeFile $stagedArtifact if ((Get-FileHash -LiteralPath $stagedArtifact -Algorithm SHA256).Hash -cne $sourceHash) { throw 'Staged artifact bytes differ from the validated source artifact.' } [IO.File]::WriteAllText( $metadataPath, ($metadata | ConvertTo-Json -Compress), [Text.UTF8Encoding]::new($false) ) $signToolLock = Open-ReadLock $signTool Assert-MicrosoftAuthenticodeFile $signTool 'SignTool' $SignToolSha256 $toolset = Expand-PinnedArtifactSigningPackage $packagePath $stagingRoot $dlib = $toolset.Dlib $toolsetLocks = $toolset.Locks & $signTool sign ` /v /debug ` /fd SHA256 ` /tr 'http://timestamp.acs.microsoft.com' ` /td SHA256 ` /dlib $dlib ` /dmdf $metadataPath ` $stagedArtifact if ($LASTEXITCODE -ne 0) { throw "Azure Artifact Signing failed with SignTool exit code $LASTEXITCODE." } Assert-WaggleSignedArtifact $stagedArtifact $signTool $artifact = Get-ApprovedArtifact $artifact $session if ((Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash -cne $sourceHash) { throw 'Source artifact changed while its validated staging copy was being signed.' } $sourceItem = Get-Item -LiteralPath $artifact -Force $sourceLinkTypeProperty = $sourceItem.PSObject.Properties['LinkType'] $sourceLinkType = if ($null -eq $sourceLinkTypeProperty) { '' } else { [string]$sourceLinkTypeProperty.Value } $expectedRollbackPaths = if ($sourceLinkType -ceq 'HardLink') { @(Get-HardLinkPaths $artifact) } else { @($artifact) } [IO.File]::Replace($stagedArtifact, $artifact, $backupPath, $true) $replaced = $true $artifact = Get-ApprovedArtifact $artifact $session -AllowDetachedMain $signedArtifactLock = Open-ReadLock $artifact Assert-WaggleSignedArtifact $artifact $signTool if ([string]$reservation.kind -ceq 'generated-nsis-uninstaller') { $uninstallerSlot = $session.Manifest.slots[[int]$reservation.order - 1] $evidencePath = [string]$uninstallerSlot.evidencePath $evidenceDirectory = New-PrivateDirectory (Split-Path $evidencePath -Parent) Assert-ExactCanonicalPathValue ` $evidenceDirectory (Join-Path $session.SessionDirectory 'signed-evidence') ` 'Signed evidence directory' if (Test-Path -LiteralPath $evidencePath) { throw 'Signed NSIS uninstaller evidence was already published.' } $temporaryEvidence = Join-Path ` $evidenceDirectory "uninstaller.$([Guid]::NewGuid().ToString('N')).tmp" [IO.File]::Copy($artifact, $temporaryEvidence, $false) if ((Get-FileHash -LiteralPath $temporaryEvidence -Algorithm SHA256).Hash -cne (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash) { throw 'Signed NSIS uninstaller evidence copy failed digest verification.' } [IO.File]::Move($temporaryEvidence, $evidencePath) } Complete-WaggleSigningCallback $session $reservation $artifact | Out-Null $replaced = $false Remove-Item -LiteralPath $backupPath -Force -ErrorAction SilentlyContinue } catch { $signingFailure = $_ $rollbackOutcome = 'not-required' if ($null -ne $signedArtifactLock) { $signedArtifactLock.Dispose() $signedArtifactLock = $null } if ($replaced) { if (-not (Test-Path -LiteralPath $backupPath -PathType Leaf)) { $rollbackOutcome = 'rollback-failed: Rollback backup is missing.' } else { try { Restore-WaggleReplacedArtifact ` -ArtifactPath $artifact -BackupPath $backupPath ` -ExpectedOriginalSha256 $sourceHash ` -ExpectedHardLinkPaths $expectedRollbackPaths $rollbackOutcome = 'restored-original' } catch { $rollbackOutcome = "rollback-failed: $($_.Exception.Message)" } } } try { Fail-WaggleSigningSession ` $session $reservation 'callback_failure' ` $signingFailure.Exception.Message $rollbackOutcome | Out-Null } catch { throw "Signing callback failed: $($signingFailure.Exception.Message) Terminalization also failed: $($_.Exception.Message)" } throw $signingFailure } finally { if ($null -ne $toolsetLocks) { foreach ($toolsetLock in $toolsetLocks) { $toolsetLock.Dispose() } } if ($null -ne $signedArtifactLock) { $signedArtifactLock.Dispose() } if ($null -ne $signToolLock) { $signToolLock.Dispose() } if ($null -ne $stagingRoot) { Remove-Item -LiteralPath $stagingRoot -Recurse -Force -ErrorAction SilentlyContinue } } } finally { foreach ($sessionLock in $session.Locks) { $sessionLock.Dispose() } } Write-Host "Azure Artifact Signing verified: $artifact" } if ($MyInvocation.InvocationName -ne '.') { if ($TrustedPowerShellHost) { if ($Mode -ceq 'Package') { Invoke-WaggleSigningPackage } else { if ([string]::IsNullOrWhiteSpace($ArtifactPath)) { throw 'Signing callback requires an artifact path.' } Invoke-WaggleArtifactSigning -Path $ArtifactPath } } else { Invoke-TrustedPowerShellRelaunch ` -LaunchMode $Mode -Path $ArtifactPath ` -PackageSource $ArtifactSigningPackageSource ` -ToolchainRoot $PortableToolchainRoot ` -Node $PortableNodePath -Git $PortableGitPath -SevenZip $PortableSevenZipPath ` -ToolchainReceipt $PortableToolchainReceiptPath ` -ToolchainReceiptSha256 $PortableToolchainReceiptSha256 } }