Skip to content

Add Authenticode signing support for PowerShell modules#92

Open
HeyItsGilbert wants to merge 2 commits intomainfrom
claude/add-module-signing-XKMbq
Open

Add Authenticode signing support for PowerShell modules#92
HeyItsGilbert wants to merge 2 commits intomainfrom
claude/add-module-signing-XKMbq

Conversation

@HeyItsGilbert
Copy link
Member

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:

    • Auto (environment variable or certificate store, configurable)
    • Windows certificate store (with optional thumbprint filtering)
    • Base64-encoded PFX from environment variables (CI/CD pipelines)
    • PFX files on disk with optional password protection
    • Pre-resolved certificate objects (for custom providers like Azure Key Vault)
  • 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 detection

  • New Build Tasks - Added to both psakeFile.ps1 and IB.tasks.ps1:

    • SignModule - Signs module files with Authenticode
    • BuildCatalog - Creates a Windows catalog file
    • SignCatalog - Signs the catalog file
    • Sign - Meta-task that orchestrates the full signing pipeline
  • Configuration - Extended build.properties.ps1 with comprehensive Sign configuration section supporting:

    • Certificate source selection and parameters
    • Timestamp server configuration
    • Hash algorithm selection
    • File inclusion patterns
    • Catalog generation settings (version, filename)
  • Localization - Added localized messages for certificate resolution, file signing, and catalog creation

Implementation Details

  • All signing operations include platform checks (Windows-only) with appropriate warnings
  • Pre-condition checks ensure signing is only attempted when enabled and dependencies are available
  • Certificate resolution supports both explicit configuration and environment-based auto-detection
  • Task dependencies ensure proper execution order: Build → SignModule → BuildCatalog → SignCatalog
  • Verbose logging throughout for troubleshooting certificate resolution and signing operations

https://claude.ai/code/session_01Bt5Xb9HLoSppQ22PQUTyGP

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
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
Verbose = $VerbosePreference

Copilot uses AI. Check for mistakes.
return $false
}
if (-not (Get-Command -Name 'Set-AuthenticodeSignature' -ErrorAction Ignore)) {
Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.'
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.'
Write-Warning 'Set-AuthenticodeSignature is not available. Catalog signing requires Windows.'

Copilot uses AI. Check for mistakes.
Comment on lines +245 to +267
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
}

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
$result = $false
}
if (-not (Get-Command -Name 'Set-AuthenticodeSignature' -ErrorAction Ignore)) {
Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.'
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.'
Write-Warning 'Set-AuthenticodeSignature is not available. Catalog signing requires Windows.'

Copilot uses AI. Check for mistakes.
}

# Synopsis: Signs module files and catalog (meta task)
task Sign SignCatalog
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
task Sign SignCatalog
task Sign SignModule, SignCatalog

Copilot uses AI. Check for mistakes.
Comment on lines +122 to +123
$cert = Get-ChildItem -Path $CertStoreLocation |
Where-Object { $_.Thumbprint -eq $Thumbprint -and $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) } |
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
$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)
} |

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +73
[ValidateSet('SHA256', 'SHA384', 'SHA512', 'SHA1')]
[string]$HashAlgorithm = 'SHA256',
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +59
[ValidateRange(1, 2)]
[int]$CatalogVersion = 2
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments