# Windows uses the executable's integrity label when creating its process. A Low # label inherited from a sandboxed workspace prevents NSIS from writing to TEMP, # before .onInit or any installer hook can run. [CmdletBinding()] param( [string]$OutputDir, [switch]$CheckProcessOnly, [switch]$CheckOnly ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest # whoami's group names are localized; the mandatory-label SID is stable. $groups = (& whoami.exe /groups) -join "`n" if ($LASTEXITCODE -ne 0 -or $groups -notmatch 'S-1-16-(\d+)') { throw 'Cannot determine the Windows process integrity level.' } if ([int]$Matches[1] -lt 8192) { throw 'Windows packaging requires Medium integrity or higher. Start the terminal / CI runner outside the Low integrity sandbox. Redirecting TEMP or changing directory write permissions does not fix executable integrity labels.' } if ($CheckProcessOnly) { return } if (-not ('RmicBundleIntegrity' -as [type])) { $compilerOptions = @{} if ($PSVersionTable.PSEdition -eq 'Desktop') { # Framework CodeDOM otherwise tries to label its private temp directory # High when an admin token has been capped at Medium by an exe label. $parameters = New-Object System.CodeDom.Compiler.CompilerParameters $parameters.ReferencedAssemblies.Add('System.dll') | Out-Null $parameters.TempFiles = New-Object System.CodeDom.Compiler.TempFileCollection([IO.Path]::GetTempPath(), $false) $compilerOptions.CompilerParameters = $parameters } Add-Type @compilerOptions -TypeDefinition @' using System; using System.ComponentModel; using System.Runtime.InteropServices; public static class RmicBundleIntegrity { [DllImport("advapi32.dll", CharSet = CharSet.Unicode)] private static extern uint GetNamedSecurityInfoW(string name, int type, uint info, out IntPtr owner, out IntPtr group, out IntPtr dacl, out IntPtr sacl, out IntPtr sd); [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool ConvertSecurityDescriptorToStringSecurityDescriptorW( IntPtr sd, uint revision, uint info, out IntPtr text, out uint length); [DllImport("kernel32.dll")] private static extern IntPtr LocalFree(IntPtr memory); public static string ReadLabel(string path) { // LABEL_SECURITY_INFORMATION reads only the label, without requiring the // SeSecurityPrivilege needed to read an entire SACL (Get-Acl -Audit). IntPtr owner, group, dacl, sacl, sd, text = IntPtr.Zero; uint error = GetNamedSecurityInfoW(path, 1, 0x10, out owner, out group, out dacl, out sacl, out sd); if (error != 0) throw new Win32Exception((int)error, path); try { uint length; if (!ConvertSecurityDescriptorToStringSecurityDescriptorW(sd, 1, 0x10, out text, out length)) throw new Win32Exception(Marshal.GetLastWin32Error()); return Marshal.PtrToStringUni(text); } finally { if (text != IntPtr.Zero) LocalFree(text); LocalFree(sd); } } } '@ } function Assert-MediumIntegrity([string]$Path) { $label = [RmicBundleIntegrity]::ReadLabel($Path) # An absent label is Medium. Windows may render known SIDs as SDDL aliases. if ($label -match ';(LW|UN|S-1-16-(\d+))\)') { if ($Matches[1] -in @('LW', 'UN') -or [int]$Matches[2] -lt 8192) { throw "Low / Untrusted integrity label on $Path. Run prepare-windows-bundle.ps1 from a normal Windows terminal before distributing this artifact." } } } $hostOutputDir = $null if (-not $OutputDir) { # Ask Cargo so CARGO_TARGET_DIR and Cargo's target-dir setting are respected. $manifest = Join-Path $PSScriptRoot '../src-tauri/Cargo.toml' $metadata = & cargo +stable metadata --no-deps --format-version 1 --manifest-path $manifest if ($LASTEXITCODE -ne 0) { throw 'Cannot resolve the Cargo output directory.' } $target = $env:TAURI_ENV_TARGET_TRIPLE if (-not $target) { $target = 'x86_64-pc-windows-msvc' } $profile = if ($env:TAURI_ENV_DEBUG -eq 'true') { 'debug' } else { 'release' } $targetDir = ($metadata | ConvertFrom-Json).target_directory $OutputDir = Join-Path $targetDir "$target/$profile" # Cross-target Cargo builds also run host build scripts from target/. # Old Low executables there can fail before the bundling hook is reached. $hostOutputDir = Join-Path $targetDir $profile } $OutputDir = [IO.Path]::GetFullPath($OutputDir) # Limit changes to build outputs. Do not relabel the workspace, TEMP, user data, # or DACLs, and do not request elevation for the installer. $directories = @( $OutputDir (Join-Path $OutputDir 'bundle') (Join-Path $OutputDir 'bundle/nsis') (Join-Path $OutputDir 'nsis') ) if ($hostOutputDir) { $directories = @($hostOutputDir) + $directories } if (Test-Path -LiteralPath (Join-Path $OutputDir 'nsis')) { $directories += @(Get-ChildItem -LiteralPath (Join-Path $OutputDir 'nsis') -Directory -Recurse | Select-Object -ExpandProperty FullName) } foreach ($directory in $directories) { if (-not $CheckOnly) { New-Item -ItemType Directory -Force -Path $directory | Out-Null & icacls.exe $directory /setintegritylevel '(OI)(CI)M' | Out-Null if ($LASTEXITCODE -ne 0) { throw "Cannot set Medium integrity on $directory" } } Assert-MediumIntegrity $directory } $executables = @(Get-ChildItem -LiteralPath $OutputDir -Filter '*.exe' -File) foreach ($directory in @('nsis', 'bundle/nsis')) { $executables += @(Get-ChildItem -LiteralPath (Join-Path $OutputDir $directory) -Filter '*.exe' -File -Recurse) } if ($hostOutputDir -and (Test-Path -LiteralPath (Join-Path $hostOutputDir 'build'))) { $executables += @(Get-ChildItem -LiteralPath (Join-Path $hostOutputDir 'build') -Filter '*.exe' -File -Recurse) } foreach ($executable in $executables) { if (-not $CheckOnly) { # Also replace explicit Low labels left by an earlier Low integrity build. & icacls.exe $executable.FullName /setintegritylevel M | Out-Null if ($LASTEXITCODE -ne 0) { throw "Cannot set Medium integrity on $($executable.FullName)" } } Assert-MediumIntegrity $executable.FullName } if ($CheckOnly -and -not (Get-ChildItem -LiteralPath (Join-Path $OutputDir 'bundle/nsis') -Filter '*.exe' -File)) { throw "No NSIS installer found in $OutputDir/bundle/nsis." } Write-Host "Windows bundle integrity verified: $OutputDir ($($executables.Count) executables)."