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

4705 lines
195 KiB
PowerShell

[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
}
}