Add Authenticode signing support for PowerShell modules#92
Add Authenticode signing support for PowerShell modules#92HeyItsGilbert wants to merge 2 commits intomainfrom
Conversation
Introduces a flexible, opt-in code-signing pipeline covering:
- Authenticode signing of module files (*.psd1, *.psm1, *.ps1)
- Windows catalog (.cat) file creation via New-FileCatalog
- Authenticode signing of the catalog file
**New public functions:**
- Get-PSBuildCertificate – resolves a code-signing cert from five
interchangeable sources: Auto, Store, Thumbprint, EnvVar (Base64 PFX
in a CI/CD secret), or PfxFile. Auto mode checks the env var first
and falls back to the certificate store, enabling the same psakeFile
to work on developer machines and in GitHub Actions / Azure DevOps.
- Invoke-PSBuildModuleSigning – signs files matching configurable glob
patterns under the module output directory using Set-AuthenticodeSignature.
- New-PSBuildFileCatalog – wraps New-FileCatalog to create a
SHA2 (version 2) or SHA1 (version 1) catalog file.
**New psake/Invoke-Build tasks:**
- SignModule – signs module files; skips gracefully when disabled or
on non-Windows platforms.
- BuildCatalog – creates the .cat file; independent enable flag.
- SignCatalog – signs the catalog file.
- Sign – meta task (depends on SignCatalog).
All tasks respect overridable $PSB*Dependency variables so consumers
can splice them into any position in their build graph, e.g.:
$PSBPublishDependency = @('Sign')
**New $PSBPreference.Sign properties:**
Enabled, CertificateSource, CertStoreLocation, Thumbprint,
CertificateEnvVar, CertificatePasswordEnvVar, PfxFilePath,
PfxFilePassword, Certificate (direct X509Certificate2 object),
TimestampServer, HashAlgorithm, FilesToSign,
Catalog.{Enabled, Version, FileName}
All features are disabled by default (Sign.Enabled = $false).
https://claude.ai/code/session_01Bt5Xb9HLoSppQ22PQUTyGP
There was a problem hiding this comment.
Pull request overview
Adds Authenticode signing support to the PowerShellBuild pipeline by introducing certificate resolution + module/catalog signing functions and wiring them into both psake and Invoke-Build task graphs.
Changes:
- Adds new public functions for certificate resolution, module file signing, and Windows catalog generation.
- Extends build configuration and localized messages to support signing options.
- Introduces new build tasks (SignModule/BuildCatalog/SignCatalog/Sign) in both psake and Invoke-Build task files.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| PowerShellBuild/psakeFile.ps1 | Adds signing/catalog tasks and dependency defaults to the psake build pipeline. |
| PowerShellBuild/IB.tasks.ps1 | Adds equivalent signing/catalog tasks for Invoke-Build. |
| PowerShellBuild/build.properties.ps1 | Introduces Sign configuration (cert sources, timestamp, algorithms, catalog settings). |
| PowerShellBuild/en-US/Messages.psd1 | Adds localized strings for certificate resolution, signing, and catalog creation. |
| PowerShellBuild/Public/Get-PSBuildCertificate.ps1 | New public function to resolve certificates from Store/Thumbprint/EnvVar/PfxFile. |
| PowerShellBuild/Public/Invoke-PSBuildModuleSigning.ps1 | New public function to sign module files using Set-AuthenticodeSignature. |
| PowerShellBuild/Public/New-PSBuildFileCatalog.ps1 | New public function to generate a Windows catalog file via New-FileCatalog. |
| PowerShellBuild/PowerShellBuild.psd1 | Exports the newly added public functions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Path = $ModulePath | ||
| CatalogFilePath = $CatalogFilePath | ||
| CatalogVersion = $CatalogVersion | ||
| Verbose = $VerbosePreference |
There was a problem hiding this comment.
New-FileCatalog already honors common -Verbose behavior via $VerbosePreference. Passing Verbose = $VerbosePreference to a switch parameter can coerce non-empty strings (e.g., 'SilentlyContinue') to $true, unintentionally enabling verbose output. Consider removing the Verbose entry or setting it to a proper boolean based on whether verbose was requested.
| Verbose = $VerbosePreference |
| return $false | ||
| } | ||
| if (-not (Get-Command -Name 'Set-AuthenticodeSignature' -ErrorAction Ignore)) { | ||
| Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.' |
There was a problem hiding this comment.
This warning text is in the SignCatalog task precondition but says "Module signing". Consider changing it to "Catalog signing" (or generally "Authenticode signing") so the warning matches the failing task.
| Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.' | |
| Write-Warning 'Set-AuthenticodeSignature is not available. Catalog signing requires Windows.' |
| Task SignModule -Depends $PSBSignModuleDependency -PreCondition $signModulePreReqs { | ||
| $certParams = @{ | ||
| CertificateSource = $PSBPreference.Sign.CertificateSource | ||
| CertStoreLocation = $PSBPreference.Sign.CertStoreLocation | ||
| CertificateEnvVar = $PSBPreference.Sign.CertificateEnvVar | ||
| CertificatePasswordEnvVar = $PSBPreference.Sign.CertificatePasswordEnvVar | ||
| } | ||
| if ($PSBPreference.Sign.Thumbprint) { | ||
| $certParams.Thumbprint = $PSBPreference.Sign.Thumbprint | ||
| } | ||
| if ($PSBPreference.Sign.PfxFilePath) { | ||
| $certParams.PfxFilePath = $PSBPreference.Sign.PfxFilePath | ||
| } | ||
| if ($PSBPreference.Sign.PfxFilePassword) { | ||
| $certParams.PfxFilePassword = $PSBPreference.Sign.PfxFilePassword | ||
| } | ||
|
|
||
| $certificate = if ($PSBPreference.Sign.Certificate) { | ||
| $PSBPreference.Sign.Certificate | ||
| } else { | ||
| Get-PSBuildCertificate @certParams | ||
| } | ||
|
|
There was a problem hiding this comment.
The certificate-resolution block is duplicated across SignModule and SignCatalog tasks (and also repeated in IB.tasks.ps1). This increases the risk of future drift (e.g., adding a new cert source/parameter in one place but not the other). Consider factoring this into a shared helper (e.g., Resolve-PSBuildSigningCertificate) used by both tasks.
| Task SignModule -Depends $PSBSignModuleDependency -PreCondition $signModulePreReqs { | |
| $certParams = @{ | |
| CertificateSource = $PSBPreference.Sign.CertificateSource | |
| CertStoreLocation = $PSBPreference.Sign.CertStoreLocation | |
| CertificateEnvVar = $PSBPreference.Sign.CertificateEnvVar | |
| CertificatePasswordEnvVar = $PSBPreference.Sign.CertificatePasswordEnvVar | |
| } | |
| if ($PSBPreference.Sign.Thumbprint) { | |
| $certParams.Thumbprint = $PSBPreference.Sign.Thumbprint | |
| } | |
| if ($PSBPreference.Sign.PfxFilePath) { | |
| $certParams.PfxFilePath = $PSBPreference.Sign.PfxFilePath | |
| } | |
| if ($PSBPreference.Sign.PfxFilePassword) { | |
| $certParams.PfxFilePassword = $PSBPreference.Sign.PfxFilePassword | |
| } | |
| $certificate = if ($PSBPreference.Sign.Certificate) { | |
| $PSBPreference.Sign.Certificate | |
| } else { | |
| Get-PSBuildCertificate @certParams | |
| } | |
| function Resolve-PSBuildSigningCertificate { | |
| param( | |
| [Parameter(Mandatory = $true)] | |
| $SignPreference | |
| ) | |
| $certParams = @{ | |
| CertificateSource = $SignPreference.CertificateSource | |
| CertStoreLocation = $SignPreference.CertStoreLocation | |
| CertificateEnvVar = $SignPreference.CertificateEnvVar | |
| CertificatePasswordEnvVar = $SignPreference.CertificatePasswordEnvVar | |
| } | |
| if ($SignPreference.Thumbprint) { | |
| $certParams.Thumbprint = $SignPreference.Thumbprint | |
| } | |
| if ($SignPreference.PfxFilePath) { | |
| $certParams.PfxFilePath = $SignPreference.PfxFilePath | |
| } | |
| if ($SignPreference.PfxFilePassword) { | |
| $certParams.PfxFilePassword = $SignPreference.PfxFilePassword | |
| } | |
| if ($SignPreference.Certificate) { | |
| return $SignPreference.Certificate | |
| } | |
| return Get-PSBuildCertificate @certParams | |
| } | |
| Task SignModule -Depends $PSBSignModuleDependency -PreCondition $signModulePreReqs { | |
| $certificate = Resolve-PSBuildSigningCertificate -SignPreference $PSBPreference.Sign |
| $result = $false | ||
| } | ||
| if (-not (Get-Command -Name 'Set-AuthenticodeSignature' -ErrorAction Ignore)) { | ||
| Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.' |
There was a problem hiding this comment.
This warning text is in the SignCatalog precondition but says "Module signing". Consider changing it to "Catalog signing" (or generally "Authenticode signing") to match the task being evaluated and reduce confusion when troubleshooting.
| Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.' | |
| Write-Warning 'Set-AuthenticodeSignature is not available. Catalog signing requires Windows.' |
| } | ||
|
|
||
| # Synopsis: Signs module files and catalog (meta task) | ||
| task Sign SignCatalog |
There was a problem hiding this comment.
In Invoke-Build, the meta task Sign only depends on SignCatalog. When Sign.Catalog.Enabled is false, SignCatalog will be skipped by its -If condition and the dependency chain (BuildCatalog/SignModule) won’t run, so Invoke-Build Sign effectively does nothing even if module signing is enabled. Consider making Sign depend on both SignModule and SignCatalog (with SignCatalog remaining conditional) so module signing still runs when catalog signing is disabled.
| task Sign SignCatalog | |
| task Sign SignModule, SignCatalog |
| $cert = Get-ChildItem -Path $CertStoreLocation | | ||
| Where-Object { $_.Thumbprint -eq $Thumbprint -and $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) } | |
There was a problem hiding this comment.
In the Thumbprint source, certificate lookup isn’t restricted to code-signing certificates (unlike the Store source which uses -CodeSigningCert). This can select a certificate that Set-AuthenticodeSignature rejects. Consider applying the same code-signing/EKU filter (e.g., -CodeSigningCert or an EKU check) and validating that -Thumbprint is non-empty when CertificateSource=Thumbprint so failures are deterministic and actionable.
| $cert = Get-ChildItem -Path $CertStoreLocation | | |
| Where-Object { $_.Thumbprint -eq $Thumbprint -and $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) } | | |
| if ([string]::IsNullOrWhiteSpace($Thumbprint)) { | |
| throw "CertificateSource 'Thumbprint' requires a non-empty Thumbprint value." | |
| } | |
| # Normalize thumbprint input by removing whitespace for robust matching | |
| $normalizedThumbprint = ($Thumbprint -replace '\s', '') | |
| $cert = Get-ChildItem -Path $CertStoreLocation -CodeSigningCert | | |
| Where-Object { | |
| ($_.Thumbprint -replace '\s', '') -ieq $normalizedThumbprint -and | |
| $_.HasPrivateKey -and | |
| $_.NotAfter -gt (Get-Date) | |
| } | |
| [ValidateSet('SHA256', 'SHA384', 'SHA512', 'SHA1')] | ||
| [string]$HashAlgorithm = 'SHA256', |
There was a problem hiding this comment.
The HashAlgorithm parameter currently allows SHA1 as a valid option for Authenticode signatures via ValidateSet('SHA256', 'SHA384', 'SHA512', 'SHA1'). SHA1 is a broken hash function for collision resistance; allowing new code to be signed with SHA1 exposes signatures to practical collision attacks and weakens the overall trust of the signed modules. To reduce this risk, remove SHA1 from the allowed HashAlgorithm values (or gate it behind an explicit legacy/compatibility flag) so only SHA-256 or stronger algorithms can be used.
| [ValidateRange(1, 2)] | ||
| [int]$CatalogVersion = 2 |
There was a problem hiding this comment.
The CatalogVersion parameter accepts a value of 1, which corresponds to SHA1-based catalogs (1 = SHA1), enabling creation of new catalog files using the deprecated SHA1 hash algorithm. SHA1 has known collision attacks, so producing new SHA1 catalogs weakens the integrity guarantees of your module distribution and can be targeted by attackers with sufficient resources. To harden catalog integrity, restrict CatalogVersion to the SHA2-based option only (version 2), or make SHA1 usage opt-in via a clearly marked legacy/compatibility setting that is disabled by default.
* Introduced tests for `Get-PSBuildCertificate`, `Invoke-PSBuildModuleSigning`, and `New-PSBuildFileCatalog`. * Validated various certificate sourcing methods including `Auto`, `Store`, `Thumbprint`, `EnvVar`, and `PfxFile`. * Ensured proper parameter validation and help documentation for each function. * Established integration tests to verify the workflow of signing modules and creating catalogs.
Summary
This PR adds comprehensive Authenticode code-signing capabilities to PowerShellBuild, enabling modules to be signed with digital certificates from multiple sources. It includes three new public functions and corresponding build tasks for signing module files and creating/signing Windows catalog files.
Key Changes
New Function:
Get-PSBuildCertificate- Resolves code-signing X509Certificate2 objects from five different sources:New Function:
Invoke-PSBuildModuleSigning- Signs PowerShell module files (*.psd1, *.psm1, *.ps1) with Authenticode signatures, supporting configurable timestamp servers and hash algorithms (SHA256, SHA384, SHA512, SHA1)New Function:
New-PSBuildFileCatalog- Creates Windows catalog (.cat) files that record cryptographic hashes of module contents for tamper detectionNew Build Tasks - Added to both psakeFile.ps1 and IB.tasks.ps1:
SignModule- Signs module files with AuthenticodeBuildCatalog- Creates a Windows catalog fileSignCatalog- Signs the catalog fileSign- Meta-task that orchestrates the full signing pipelineConfiguration - Extended
build.properties.ps1with comprehensiveSignconfiguration section supporting:Localization - Added localized messages for certificate resolution, file signing, and catalog creation
Implementation Details
https://claude.ai/code/session_01Bt5Xb9HLoSppQ22PQUTyGP