mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 01:59:34 +02:00
## Summary of the Pull Request Adds a `PowerRename.UITests.Next` suite powered by winappcli and automates all 18 scenarios from #40663. The suite covers PowerRename settings, search and replace behavior, regular expressions, formatting and filtering options, file-list interactions, and both classic and Windows 11 context-menu workflows. The PR also stabilizes shared `UITestAutomation.Next` runner lifetimes and settings restoration, adds automation IDs for the original and renamed counters, and prepares unsigned CI builds for PowerRename shell testing. CI now signs the sparse context-menu MSIX and the runner/Settings IPC companions with a disposable machine-trusted test identity. ## PR Checklist - [x] Closes: #40663 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [x] **New binaries:** Not applicable; no new shipped product binaries are added - [x] JSON for signing: Not applicable - [x] WXS for installer: Not applicable - [x] YML for CI pipeline: The new UI-test project is discovered through the existing `*UITest*.csproj` pipeline flow and is registered in `PowerToys.slnx` - [x] YML for signed pipeline: Not applicable - [x] **Documentation updated:** Not applicable; there are no user-facing behavior or documentation changes ## Detailed Description of the Pull Request / Additional comments ### PowerRename UI tests - Adds a 25-case `PowerRename.UITests.Next` executable covering all 18 checklist items from #40663. - Exercises classic context-menu registration on Windows 10 and Windows 11. - Exercises the signed Windows 11 tier-1 context menu, including icon visibility and real invocation with an Explorer selection. - Covers search/replace preview and application, text formatting, file/folder/subfolder inclusion, filename/extension scope, enumeration, case sensitivity, match-all behavior, regular expressions, file timestamps, Boost syntax, MRU autocomplete, persisted values, and file-list selection/filtering. - Preserves the existing legacy tests. ### Test reliability and automation hooks - Reuses one runner/Settings lifetime across the complete PowerRename suite to avoid repeated cold launches on constrained agents. - Retains and restores global settings for the full class lifetime and verifies that both Settings and the runner remain healthy. - Propagates scope and PowerRename cleanup failures instead of silently leaking process or profile state. - Adds `OriginalCount` and `RenamedCount` automation IDs. These are automation-only metadata and do not change the visible UI. - Uses stable preview samples, exact count targeting, authoritative Explorer selection, readable classic-menu inventories, and live UIA visibility for popup items. ### Unsigned CI build support - Extends the existing sparse-package test signer with required Authenticode companion files. - PowerRename jobs sign `PowerToys.exe` and `PowerToys.Settings.exe` with the same disposable machine-trusted test identity used for sparse MSIX packages. - This preserves Release IPC authentication while allowing Settings module-toggle commands to work on unsigned PR builds. - Windows 11 and ARM64 PowerRename jobs require a validly signed `PowerRenameContextMenuPackage.msix` before tests start. ## Validation Steps Performed ### Builds and discovery - `PowerRename.UITests.Next` Debug x64 build: passed - `PowerRename.UITests.Next` Debug ARM64 cross-build: passed - `PowerRenameUI` Release x64 build: passed - `UITestAutomation.Next.UnitTests` Debug x64 build: passed - `UITestAutomation.Next.UnitTests`: 16/16 passed - Microsoft.Testing.Platform discovery: 25 unique PowerRename test cases ### Local VM matrix All runs used the complete unfiltered `TestCategory=PowerRename` suite and restored the standard user's settings file byte-for-byte. | Guest | Profile | Result | |---|---|---:| | Windows 10 x64 | Default, 4 vCPU / 8 GB | 25/25 | | Windows 10 x64 | Constrained, 1 vCPU / 4 GB | 25/25 | | Windows 11 x64 | Default, 4 vCPU / 8 GB | 25/25 | | Windows 11 x64 | Constrained, 1 vCPU / 4 GB | 25/25 | ### Azure DevOps UI Test Automation - Final build: [155646235 / 20260824.1](https://dev.azure.com/microsoft/Dart/_build/results?buildId=155646235) - Source revision: `4496683104aea622b30179909bd6e95a17d5500f` - ARM64: 25/25 passed - Windows 10 x64: 25/25 passed - Windows 11 x64: 25/25 passed - Total: 75/75 passed, with zero failed, skipped, not-executed, or unanalyzed results - ARM64 and x64 Release product builds succeeded and published their normal artifacts - Signing steps verified the PowerRename sparse MSIX where applicable and both IPC companion executables on every PowerRename test job <img width="372" height="314" alt="image" src="https://github.com/user-attachments/assets/bc303673-0325-4f85-8d70-ef93110baf5b" />
485 lines
19 KiB
PowerShell
485 lines
19 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Self-sign PowerToys sparse MSIX shell-extension packages with a machine-trusted TEST certificate so
|
|
they register on unsigned CI builds, letting UI tests exercise the real end-user workflow (the modern
|
|
Windows 11 context menu) instead of signing-free fallbacks.
|
|
|
|
.DESCRIPTION
|
|
CI PR-validation builds are produced with codeSign:false, so every sparse-MSIX shell extension
|
|
(ImageResizer / PowerRename / FileLocksmith / NewPlus context menus, the CmdPal PowerToysSparse
|
|
package) ships UNSIGNED. PowerToys registers these at module-enable time via
|
|
PackageManager.AddPackageByUriAsync, which requires a signature that chains to a trusted root, so on
|
|
CI they fail with 0x800B0100 (TRUST_E_NOSIGNATURE) and the modern context menu never appears.
|
|
|
|
Run this on the test agent AFTER the build is downloaded/installed and BEFORE PowerToys enables the
|
|
module. For every package it:
|
|
1. reads the manifest Publisher subject,
|
|
2. ensures a self-signed code-signing certificate with that exact subject exists,
|
|
3. force-trusts that certificate (LocalMachine + CurrentUser Root and TrustedPeople), and
|
|
4. signs the package with signtool.
|
|
|
|
This asserts NO security -- it is a test-only trust anchor for validating normal app usage. It only
|
|
signs packages that are not already validly signed (unless -Force), so real framework packages
|
|
(VCLibs, WindowsAppSDK) are left untouched.
|
|
|
|
.PARAMETER PackageRoot
|
|
One or more folders to search recursively for sparse packages. Missing folders are skipped, so you
|
|
can pass both the run-in-place build tree and the installed location:
|
|
-PackageRoot "$(Pipeline.Workspace)\build-x64-Release", "$env:ProgramFiles\PowerToys"
|
|
|
|
.PARAMETER Include
|
|
Filename patterns to sign. Defaults to *.msix and *.appx.
|
|
|
|
.PARAMETER RequiredPackage
|
|
Filename patterns that must be found and end with a Valid signature. Missing, unsigned, or untrusted
|
|
matches make the script fail after attempting all packages.
|
|
|
|
.PARAMETER RequiredAuthenticodeFile
|
|
Filename patterns for unpackaged companion binaries that must be found and signed with the same
|
|
machine-trusted TEST identity. This is used for authenticated PowerToys IPC on unsigned CI builds.
|
|
|
|
.PARAMETER AuthenticodePublisher
|
|
Certificate subject used to sign RequiredAuthenticodeFile matches. The default matches the
|
|
Microsoft publisher identity required by PowerToys' Release IPC caller authentication.
|
|
|
|
.PARAMETER CertificateMarkerPath
|
|
Optional durable text file that receives each test certificate thumbprint used by this invocation.
|
|
CI uses the marker before signing and from an <c>always()</c> cleanup step to remove the trust anchor
|
|
and private key, including after an interrupted prior job.
|
|
|
|
.PARAMETER Force
|
|
Re-sign even packages that already carry a valid signature.
|
|
|
|
.PARAMETER SkipLocalTrust
|
|
Sign without importing the test certificate into this machine's trust stores. Use when signing on a
|
|
build host and registering the package somewhere else (for example packaging a UI-test payload on the
|
|
host and running it in a VM), so the build machine never gains a test trust anchor.
|
|
|
|
.PARAMETER ExportCertificatePath
|
|
Write the public certificate to this path so the machine that registers the package can trust it.
|
|
|
|
.EXAMPLE
|
|
.\signSparsePackages.ps1 -PackageRoot "$env:ProgramFiles\PowerToys" `
|
|
-RequiredPackage ImageResizerContextMenuPackage.msix
|
|
|
|
.EXAMPLE
|
|
# Local sideloading into a UI-test VM runtime:
|
|
.\signSparsePackages.ps1 -PackageRoot "C:\PowerToysUiTestRun\PowerToys"
|
|
|
|
.EXAMPLE
|
|
# Sign a payload on the host, trust it only inside the VM that will register it:
|
|
.\signSparsePackages.ps1 -PackageRoot "X:\payload\product" -SkipLocalTrust `
|
|
-ExportCertificatePath "X:\payload\pt-test-signer.cer"
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]]$PackageRoot,
|
|
|
|
[Parameter()]
|
|
[string[]]$Include = @('*.msix', '*.appx'),
|
|
|
|
[Parameter()]
|
|
[string[]]$RequiredPackage = @(),
|
|
|
|
[Parameter()]
|
|
[string[]]$RequiredAuthenticodeFile = @(),
|
|
|
|
[Parameter()]
|
|
[string]$AuthenticodePublisher = 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US',
|
|
|
|
[Parameter()]
|
|
[string]$CertificateMarkerPath,
|
|
|
|
[switch]$Force,
|
|
|
|
[switch]$SkipLocalTrust,
|
|
|
|
[Parameter()]
|
|
[string]$ExportCertificatePath
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
$testCertificateFriendlyName = 'PowerToys UI Test Signing'
|
|
|
|
function Select-SignToolByArch {
|
|
param([string[]]$Paths)
|
|
|
|
$paths = @($Paths | Where-Object { $_ } | Select-Object -Unique)
|
|
if (-not $paths) { return $null }
|
|
$archPref = @($env:PROCESSOR_ARCHITECTURE, 'x64', 'x86', 'arm64') |
|
|
ForEach-Object { $_.ToLower() } | Select-Object -Unique
|
|
foreach ($arch in $archPref) {
|
|
$match = $paths | Where-Object { $_ -match "\\$arch\\" } | Select-Object -First 1
|
|
if ($match) { return $match }
|
|
}
|
|
return $paths[0]
|
|
}
|
|
|
|
# Locate signtool.exe on the agent: PATH, then any Windows Kits install (all versions/layouts,
|
|
# including the App Certification Kit). The pinned, signature-verified NuGet fallback is separate.
|
|
function Find-SignTool {
|
|
$cmd = Get-Command signtool.exe -ErrorAction SilentlyContinue
|
|
if ($cmd) { return $cmd.Source }
|
|
|
|
$found = @()
|
|
$kitRoots = @(
|
|
"${env:ProgramFiles(x86)}\Windows Kits",
|
|
"$env:ProgramFiles\Windows Kits",
|
|
"$env:ProgramW6432\Windows Kits"
|
|
) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
|
|
foreach ($root in $kitRoots) {
|
|
# Scope to bin\ and the App Certification Kit so the huge Include\ / Lib\ trees are skipped.
|
|
$scopes = Get-ChildItem -Path $root -Directory -Recurse -Depth 1 -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Name -eq 'bin' -or $_.Name -eq 'App Certification Kit' } |
|
|
Select-Object -ExpandProperty FullName
|
|
foreach ($scope in $scopes) {
|
|
$found += Get-ChildItem -Path $scope -Recurse -Filter 'signtool.exe' -File -ErrorAction SilentlyContinue |
|
|
Select-Object -ExpandProperty FullName
|
|
}
|
|
}
|
|
|
|
return Select-SignToolByArch -Paths $found
|
|
}
|
|
|
|
# Last resort when the agent has no Windows SDK: fetch signtool from the public
|
|
# Microsoft.Windows.SDK.BuildTools NuGet package. Best-effort.
|
|
function Get-SignToolFromNuget {
|
|
$nupkg = $null
|
|
$archive = $null
|
|
try {
|
|
[xml]$centralPackages = Get-Content (Join-Path $PSScriptRoot '..\Directory.Packages.props') -Raw
|
|
$versionNode = $centralPackages.SelectSingleNode(
|
|
"/Project/ItemGroup/PackageVersion[@Include='Microsoft.Windows.SDK.BuildTools']")
|
|
$version = if ($versionNode) { $versionNode.GetAttribute('Version') } else { $null }
|
|
if ($version -notmatch '^\d+\.\d+\.\d+\.\d+$') {
|
|
throw 'Microsoft.Windows.SDK.BuildTools must have a concrete four-part version in Directory.Packages.props.'
|
|
}
|
|
|
|
$dest = Join-Path $env:TEMP "pt-sdk-buildtools-$version"
|
|
$nupkg = Join-Path $env:TEMP "sdk-buildtools-$version.nupkg"
|
|
$archive = Join-Path $env:TEMP "sdk-buildtools-$version.zip"
|
|
$packageFileName = "microsoft.windows.sdk.buildtools.$version.nupkg"
|
|
$cachedPackage = @($env:NUGET_PACKAGES, (Join-Path $env:USERPROFILE '.nuget\packages')) |
|
|
Where-Object { $_ } |
|
|
ForEach-Object { Join-Path $_ "microsoft.windows.sdk.buildtools\$version\$packageFileName" } |
|
|
Where-Object { Test-Path -LiteralPath $_ } |
|
|
Select-Object -First 1
|
|
if ($cachedPackage) {
|
|
Write-Host "signtool not found on the agent; verifying cached Windows SDK BuildTools $version."
|
|
Copy-Item -LiteralPath $cachedPackage -Destination $nupkg -Force
|
|
}
|
|
else {
|
|
Write-Host "signtool not found on the agent; fetching Windows SDK BuildTools $version from NuGet."
|
|
Invoke-WebRequest "https://api.nuget.org/v3-flatcontainer/microsoft.windows.sdk.buildtools/$version/$packageFileName" -OutFile $nupkg -UseBasicParsing
|
|
}
|
|
|
|
$verificationOutput = @(& dotnet nuget verify $nupkg --all 2>&1)
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "NuGet signature verification failed for Microsoft.Windows.SDK.BuildTools ${version}: $($verificationOutput -join [Environment]::NewLine)"
|
|
}
|
|
$verificationOutput | ForEach-Object { Write-Host $_ }
|
|
|
|
Copy-Item $nupkg $archive -Force
|
|
Remove-Item $dest -Recurse -Force -ErrorAction SilentlyContinue
|
|
Expand-Archive -Path $archive -DestinationPath $dest -Force
|
|
$paths = Get-ChildItem -Path $dest -Recurse -Filter 'signtool.exe' -File -ErrorAction SilentlyContinue |
|
|
Select-Object -ExpandProperty FullName
|
|
return Select-SignToolByArch -Paths $paths
|
|
}
|
|
catch {
|
|
Write-Warning "Could not obtain signtool from NuGet: $($_.Exception.Message)"
|
|
return $null
|
|
}
|
|
finally {
|
|
@($nupkg, $archive) | Where-Object { $_ } | ForEach-Object {
|
|
Remove-Item -LiteralPath $_ -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-VerifiedSignTool {
|
|
$path = Find-SignTool
|
|
if (-not $path) { $path = Get-SignToolFromNuget }
|
|
if (-not $path) {
|
|
throw 'signtool.exe not found and could not be fetched from NuGet. Install the Windows SDK.'
|
|
}
|
|
|
|
$signature = Get-AuthenticodeSignature -FilePath $path
|
|
if ($signature.Status -ne 'Valid' -or
|
|
-not $signature.SignerCertificate -or
|
|
$signature.SignerCertificate.Subject -notmatch '(^|,\s*)O=Microsoft Corporation(,|$)') {
|
|
throw "signtool.exe is not validly signed by Microsoft: $path"
|
|
}
|
|
|
|
return $path
|
|
}
|
|
|
|
# Read <Identity Publisher="..."> straight out of the .msix/.appx (a zip) without extracting it.
|
|
function Get-PackagePublisher {
|
|
param([string]$PackagePath)
|
|
|
|
$zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath)
|
|
try {
|
|
$entry = $zip.GetEntry('AppxManifest.xml')
|
|
if (-not $entry) { return $null }
|
|
$reader = New-Object System.IO.StreamReader($entry.Open())
|
|
try { $xml = [xml]$reader.ReadToEnd() } finally { $reader.Dispose() }
|
|
return $xml.Package.Identity.Publisher
|
|
}
|
|
finally {
|
|
$zip.Dispose()
|
|
}
|
|
}
|
|
|
|
function Import-CertTrust {
|
|
param(
|
|
[string]$CerPath,
|
|
[string]$Thumbprint,
|
|
[string]$StorePath,
|
|
[switch]$Optional
|
|
)
|
|
|
|
if (Get-ChildItem $StorePath -ErrorAction SilentlyContinue | Where-Object { $_.Thumbprint -eq $Thumbprint }) {
|
|
return $true
|
|
}
|
|
try {
|
|
Import-Certificate -FilePath $CerPath -CertStoreLocation $StorePath -ErrorAction Stop | Out-Null
|
|
return $true
|
|
}
|
|
catch {
|
|
if ($Optional) {
|
|
Write-Warning "Could not import test cert into $StorePath (admin may be required): $($_.Exception.Message)"
|
|
return $false
|
|
}
|
|
throw
|
|
}
|
|
}
|
|
|
|
$certCache = @{}
|
|
function Get-TrustedSigningCert {
|
|
param([string]$Subject)
|
|
|
|
if ($certCache.ContainsKey($Subject)) { return $certCache[$Subject] }
|
|
|
|
$cert = Get-ChildItem Cert:\CurrentUser\My |
|
|
Where-Object {
|
|
$_.Subject -eq $Subject -and
|
|
$_.HasPrivateKey -and
|
|
$_.FriendlyName -eq $testCertificateFriendlyName
|
|
} |
|
|
Sort-Object NotAfter -Descending | Select-Object -First 1
|
|
|
|
if (-not $cert) {
|
|
Write-Host "Creating self-signed test certificate for: $Subject"
|
|
$cert = New-SelfSignedCertificate -Subject $Subject `
|
|
-CertStoreLocation Cert:\CurrentUser\My `
|
|
-FriendlyName $testCertificateFriendlyName `
|
|
-KeyAlgorithm RSA -KeyLength 2048 `
|
|
-Type CodeSigningCert -HashAlgorithm SHA256 `
|
|
-NotAfter (Get-Date).AddYears(1)
|
|
}
|
|
|
|
if ($CertificateMarkerPath) {
|
|
$markerParent = Split-Path $CertificateMarkerPath -Parent
|
|
if ($markerParent -and -not (Test-Path $markerParent)) {
|
|
New-Item $markerParent -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
$recordedThumbprints = if (Test-Path $CertificateMarkerPath) {
|
|
@(Get-Content $CertificateMarkerPath | Where-Object { $_ })
|
|
} else {
|
|
@()
|
|
}
|
|
if ($recordedThumbprints -notcontains $cert.Thumbprint) {
|
|
Add-Content -Path $CertificateMarkerPath -Value $cert.Thumbprint -Encoding ascii
|
|
}
|
|
}
|
|
|
|
# Force-trust so AddPackageByUriAsync accepts the signature. A self-signed cert is its own root,
|
|
# so it must live in a Root store (chain) and TrustedPeople (AppX sideload allow-list). Use the
|
|
# LocalMachine stores: they import silently and the elevated CI test agent can write them.
|
|
# CurrentUser\Root is deliberately NOT used -- importing into the user Root store raises a CryptoAPI
|
|
# consent dialog that fails non-interactively ("UI is not allowed in this operation"), even elevated.
|
|
$cerPath = Join-Path $env:TEMP ("pt-test-signer-{0}.cer" -f $cert.Thumbprint)
|
|
Export-Certificate -Cert $cert -FilePath $cerPath -Force | Out-Null
|
|
|
|
if ($ExportCertificatePath) {
|
|
$exportParent = Split-Path $ExportCertificatePath -Parent
|
|
if ($exportParent -and -not (Test-Path $exportParent)) {
|
|
New-Item $exportParent -ItemType Directory -Force | Out-Null
|
|
}
|
|
Copy-Item $cerPath $ExportCertificatePath -Force
|
|
Write-Host "Exported public certificate to: $ExportCertificatePath"
|
|
}
|
|
|
|
if ($SkipLocalTrust) {
|
|
Write-Host "Skipping local trust for '$Subject'; trust the exported certificate where the package is registered."
|
|
$certCache[$Subject] = $cert
|
|
return $cert
|
|
}
|
|
|
|
$rootTrusted = Import-CertTrust -CerPath $cerPath -Thumbprint $cert.Thumbprint -StorePath 'Cert:\LocalMachine\Root' -Optional
|
|
Import-CertTrust -CerPath $cerPath -Thumbprint $cert.Thumbprint -StorePath 'Cert:\LocalMachine\TrustedPeople' -Optional | Out-Null
|
|
Import-CertTrust -CerPath $cerPath -Thumbprint $cert.Thumbprint -StorePath 'Cert:\CurrentUser\TrustedPeople' -Optional | Out-Null
|
|
if (-not $rootTrusted) {
|
|
Write-Warning "Could not establish machine root trust for '$Subject' (run elevated). Signed packages may not register."
|
|
}
|
|
|
|
$certCache[$Subject] = $cert
|
|
return $cert
|
|
}
|
|
|
|
$packages = @()
|
|
foreach ($root in $PackageRoot) {
|
|
if (-not (Test-Path $root)) {
|
|
Write-Host "Skipping missing package root: $root"
|
|
continue
|
|
}
|
|
$packages += Get-ChildItem -Path $root -Recurse -File -Include $Include -ErrorAction SilentlyContinue
|
|
}
|
|
$packages = $packages | Sort-Object FullName -Unique
|
|
|
|
$requiredAuthenticodeFiles = @()
|
|
foreach ($pattern in ($RequiredAuthenticodeFile | Where-Object { $_ } | Select-Object -Unique)) {
|
|
$matches = @()
|
|
foreach ($root in $PackageRoot) {
|
|
if (Test-Path $root) {
|
|
$matches += Get-ChildItem -Path $root -Recurse -File -Filter $pattern -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
$matches = @($matches | Sort-Object FullName -Unique)
|
|
if ($matches.Count -eq 0) {
|
|
throw "Required Authenticode file '$pattern' was not found under: $($PackageRoot -join ', ')."
|
|
}
|
|
|
|
$requiredAuthenticodeFiles += $matches
|
|
}
|
|
$requiredAuthenticodeFiles = @($requiredAuthenticodeFiles | Sort-Object FullName -Unique)
|
|
|
|
if (-not $packages -and $RequiredPackage.Count -gt 0) {
|
|
throw "No packages found under '$($PackageRoot -join ', ')' while requiring: $($RequiredPackage -join ', ')."
|
|
}
|
|
|
|
if (-not $packages -and $requiredAuthenticodeFiles.Count -eq 0) {
|
|
Write-Host "No packages or required Authenticode files found under: $($PackageRoot -join ', ')"
|
|
return
|
|
}
|
|
|
|
$requiredPackages = @()
|
|
foreach ($pattern in ($RequiredPackage | Where-Object { $_ } | Select-Object -Unique)) {
|
|
$matches = @($packages | Where-Object { $_.Name -like $pattern })
|
|
if ($matches.Count -eq 0) {
|
|
throw "Required sparse package '$pattern' was not found under: $($PackageRoot -join ', ')."
|
|
}
|
|
|
|
$requiredPackages += $matches
|
|
}
|
|
$requiredPackages = @($requiredPackages | Sort-Object FullName -Unique)
|
|
|
|
$signed = 0
|
|
$signtool = $null
|
|
foreach ($pkg in $packages) {
|
|
if (-not $Force) {
|
|
$existing = Get-AuthenticodeSignature -FilePath $pkg.FullName
|
|
if ($existing.Status -eq 'Valid') {
|
|
Write-Host "Already validly signed, skipping: $($pkg.Name)"
|
|
continue
|
|
}
|
|
}
|
|
|
|
$publisher = $null
|
|
try { $publisher = Get-PackagePublisher -PackagePath $pkg.FullName } catch { }
|
|
if (-not $publisher) {
|
|
Write-Host "No manifest publisher, skipping: $($pkg.Name)"
|
|
continue
|
|
}
|
|
|
|
if (-not $signtool) {
|
|
$signtool = Get-VerifiedSignTool
|
|
Write-Host "Using signtool: $signtool"
|
|
}
|
|
|
|
$cert = Get-TrustedSigningCert -Subject $publisher
|
|
Write-Host "Signing $($pkg.Name) (Publisher: $publisher)"
|
|
& $signtool sign /fd SHA256 /sha1 $cert.Thumbprint $pkg.FullName
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Warning "signtool failed for $($pkg.Name) (exit $LASTEXITCODE)"
|
|
continue
|
|
}
|
|
|
|
$verify = Get-AuthenticodeSignature -FilePath $pkg.FullName
|
|
if ($verify.Status -eq 'Valid') {
|
|
$signed++
|
|
}
|
|
else {
|
|
Write-Warning "Signature not Valid after signing $($pkg.Name): $($verify.Status)"
|
|
}
|
|
}
|
|
|
|
if ($requiredPackages.Count -gt 0) {
|
|
$invalidRequiredPackages = @($requiredPackages | Where-Object {
|
|
(Get-AuthenticodeSignature -FilePath $_.FullName).Status -ne 'Valid'
|
|
})
|
|
if ($invalidRequiredPackages.Count -gt 0) {
|
|
throw "Required sparse package(s) are not validly signed and trusted: $($invalidRequiredPackages.FullName -join ', ')."
|
|
}
|
|
|
|
Write-Host "Verified required sparse package(s): $($requiredPackages.FullName -join ', ')"
|
|
}
|
|
|
|
if ($requiredAuthenticodeFiles.Count -gt 0) {
|
|
if (-not $signtool) {
|
|
$signtool = Get-VerifiedSignTool
|
|
Write-Host "Using signtool: $signtool"
|
|
}
|
|
|
|
$filesToSign = @($requiredAuthenticodeFiles | Where-Object {
|
|
$existing = Get-AuthenticodeSignature -FilePath $_.FullName
|
|
$Force -or $existing.Status -ne 'Valid'
|
|
})
|
|
$testSignedPaths = @{}
|
|
$cert = if ($filesToSign.Count -gt 0) {
|
|
Get-TrustedSigningCert -Subject $AuthenticodePublisher
|
|
} else {
|
|
$null
|
|
}
|
|
|
|
foreach ($file in $filesToSign) {
|
|
|
|
Write-Host "Signing companion binary: $($file.FullName)"
|
|
& $signtool sign /fd SHA256 /sha1 $cert.Thumbprint $file.FullName
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "signtool failed for required Authenticode file '$($file.FullName)' (exit $LASTEXITCODE)."
|
|
}
|
|
|
|
$testSignedPaths[$file.FullName] = $true
|
|
}
|
|
|
|
$invalidAuthenticodeFiles = @($requiredAuthenticodeFiles | Where-Object {
|
|
$signature = Get-AuthenticodeSignature -FilePath $_.FullName
|
|
$signerName = if ($signature.SignerCertificate) {
|
|
$signature.SignerCertificate.GetNameInfo([Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $false)
|
|
} else {
|
|
$null
|
|
}
|
|
if ($testSignedPaths.ContainsKey($_.FullName)) {
|
|
-not $signature.SignerCertificate -or
|
|
$signature.SignerCertificate.Thumbprint -ne $cert.Thumbprint -or
|
|
(-not $SkipLocalTrust -and $signature.Status -ne 'Valid')
|
|
} else {
|
|
-not $signature.SignerCertificate -or
|
|
$signerName -ne 'Microsoft Corporation' -or
|
|
$signature.Status -ne 'Valid'
|
|
}
|
|
})
|
|
if ($invalidAuthenticodeFiles.Count -gt 0) {
|
|
throw "Required Authenticode file(s) are not signed with the trusted test identity: $($invalidAuthenticodeFiles.FullName -join ', ')."
|
|
}
|
|
|
|
Write-Host "Verified required Authenticode file(s): $($requiredAuthenticodeFiles.FullName -join ', ')"
|
|
}
|
|
|
|
Write-Host "Signed $signed package(s) with a trusted test certificate."
|