Skip to content

Commit a8a877d

Browse files
Add Authenticode signing support for PowerShell modules (#92)
## 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9e8a743 commit a8a877d

12 files changed

+1196
-40
lines changed

PowerShellBuild/IB.tasks.ps1

Lines changed: 152 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,26 @@ Set-Variable -Name PSBPreference -Option ReadOnly -Scope Script -Value (. ([IO.P
33
$__DefaultBuildDependencies = $PSBPreference.Build.Dependencies
44

55
# Synopsis: Initialize build environment variables
6-
task Init {
6+
Task Init {
77
Initialize-PSBuild -UseBuildHelpers -BuildEnvironment $PSBPreference
88
}
99

1010
# Synopsis: Clears module output directory
11-
task Clean Init, {
11+
Task Clean Init, {
1212
Clear-PSBuildOutputFolder -Path $PSBPreference.Build.ModuleOutDir
1313
}
1414

1515
# Synopsis: Builds module based on source directory
16-
task StageFiles Clean, {
16+
Task StageFiles Clean, {
1717
$buildParams = @{
18-
Path = $PSBPreference.General.SrcRootDir
19-
ModuleName = $PSBPreference.General.ModuleName
20-
DestinationPath = $PSBPreference.Build.ModuleOutDir
21-
Exclude = $PSBPreference.Build.Exclude
22-
Compile = $PSBPreference.Build.CompileModule
23-
CompileDirectories = $PSBPreference.Build.CompileDirectories
24-
CopyDirectories = $PSBPreference.Build.CopyDirectories
25-
Culture = $PSBPreference.Help.DefaultLocale
18+
Path = $PSBPreference.General.SrcRootDir
19+
ModuleName = $PSBPreference.General.ModuleName
20+
DestinationPath = $PSBPreference.Build.ModuleOutDir
21+
Exclude = $PSBPreference.Build.Exclude
22+
Compile = $PSBPreference.Build.CompileModule
23+
CompileDirectories = $PSBPreference.Build.CompileDirectories
24+
CopyDirectories = $PSBPreference.Build.CopyDirectories
25+
Culture = $PSBPreference.Help.DefaultLocale
2626
}
2727

2828
if ($PSBPreference.Help.ConvertReadMeToAboutHelp) {
@@ -59,7 +59,7 @@ $analyzePreReqs = {
5959
}
6060

6161
# Synopsis: Execute PSScriptAnalyzer tests
62-
task Analyze -If (. $analyzePreReqs) Build,{
62+
Task Analyze -If (. $analyzePreReqs) Build, {
6363
$analyzeParams = @{
6464
Path = $PSBPreference.Build.ModuleOutDir
6565
SeverityThreshold = $PSBPreference.Test.ScriptAnalysis.FailBuildOnSeverityLevel
@@ -86,7 +86,7 @@ $pesterPreReqs = {
8686
}
8787

8888
# Synopsis: Execute Pester tests
89-
task Pester -If (. $pesterPreReqs) Build,{
89+
Task Pester -If (. $pesterPreReqs) Build, {
9090
$pesterParams = @{
9191
Path = $PSBPreference.Test.RootDir
9292
ModuleName = $PSBPreference.General.ModuleName
@@ -117,7 +117,7 @@ $genMarkdownPreReqs = {
117117
}
118118

119119
# Synopsis: Generates PlatyPS markdown files from module help
120-
task GenerateMarkdown -if (. $genMarkdownPreReqs) StageFiles,{
120+
Task GenerateMarkdown -if (. $genMarkdownPreReqs) StageFiles, {
121121
$buildMDParams = @{
122122
ModulePath = $PSBPreference.Build.ModuleOutDir
123123
ModuleName = $PSBPreference.General.ModuleName
@@ -141,7 +141,7 @@ $genHelpFilesPreReqs = {
141141
}
142142

143143
# Synopsis: Generates MAML-based help from PlatyPS markdown files
144-
task GenerateMAML -if (. $genHelpFilesPreReqs) GenerateMarkdown, {
144+
Task GenerateMAML -if (. $genHelpFilesPreReqs) GenerateMarkdown, {
145145
Build-PSBuildMAMLHelp -Path $PSBPreference.Docs.RootDir -DestinationPath $PSBPreference.Build.ModuleOutDir
146146
}
147147

@@ -155,7 +155,7 @@ $genUpdatableHelpPreReqs = {
155155
}
156156

157157
# Synopsis: Create updatable help .cab file based on PlatyPS markdown help
158-
task GenerateUpdatableHelp -if (. $genUpdatableHelpPreReqs) BuildHelp, {
158+
Task GenerateUpdatableHelp -if (. $genUpdatableHelpPreReqs) BuildHelp, {
159159
Build-PSBuildUpdatableHelp -DocsPath $PSBPreference.Docs.RootDir -OutputPath $PSBPreference.Help.UpdatableHelpOutDir
160160
}
161161

@@ -184,17 +184,149 @@ Task Publish Test, {
184184
#region Summary Tasks
185185

186186
# Synopsis: Builds help documentation
187-
task BuildHelp GenerateMarkdown,GenerateMAML
187+
Task BuildHelp GenerateMarkdown, GenerateMAML
188188

189189
Task Build {
190190
if ([String]$PSBPreference.Build.Dependencies -ne [String]$__DefaultBuildDependencies) {
191191
throw [NotSupportedException]'You cannot use $PSBPreference.Build.Dependencies with Invoke-Build. Please instead redefine the build task or your default task to include your dependencies. Example: Task . Dependency1,Dependency2,Build,Test or Task Build Dependency1,Dependency2,StageFiles'
192192
}
193-
},StageFiles,BuildHelp
193+
}, StageFiles, BuildHelp
194194

195195
# Synopsis: Execute Pester and ScriptAnalyzer tests
196-
task Test Analyze,Pester
196+
Task Test Analyze, Pester
197197

198-
task . Build,Test
198+
Task . Build, Test
199+
200+
# Synopsis: Signs module files (*.psd1, *.psm1, *.ps1) with an Authenticode signature
201+
Task SignModule -If {
202+
if (-not $PSBPreference.Sign.Enabled) {
203+
Write-Warning 'Module signing is not enabled.'
204+
return $false
205+
}
206+
if (-not (Get-Command -Name 'Set-AuthenticodeSignature' -ErrorAction Ignore)) {
207+
Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.'
208+
return $false
209+
}
210+
$true
211+
} Build, {
212+
$certParams = @{
213+
CertificateSource = $PSBPreference.Sign.CertificateSource
214+
CertStoreLocation = $PSBPreference.Sign.CertStoreLocation
215+
CertificateEnvVar = $PSBPreference.Sign.CertificateEnvVar
216+
CertificatePasswordEnvVar = $PSBPreference.Sign.CertificatePasswordEnvVar
217+
}
218+
if ($PSBPreference.Sign.Thumbprint) {
219+
$certParams.Thumbprint = $PSBPreference.Sign.Thumbprint
220+
}
221+
if ($PSBPreference.Sign.PfxFilePath) {
222+
$certParams.PfxFilePath = $PSBPreference.Sign.PfxFilePath
223+
}
224+
if ($PSBPreference.Sign.PfxFilePassword) {
225+
$certParams.PfxFilePassword = $PSBPreference.Sign.PfxFilePassword
226+
}
227+
228+
$certificate = if ($PSBPreference.Sign.Certificate) {
229+
$PSBPreference.Sign.Certificate
230+
} else {
231+
Get-PSBuildCertificate @certParams
232+
}
233+
234+
if ($null -eq $certificate) {
235+
throw $LocalizedData.NoCertificateFound
236+
}
237+
238+
$signingParams = @{
239+
Path = $PSBPreference.Build.ModuleOutDir
240+
Certificate = $certificate
241+
TimestampServer = $PSBPreference.Sign.TimestampServer
242+
HashAlgorithm = $PSBPreference.Sign.HashAlgorithm
243+
Include = $PSBPreference.Sign.FilesToSign
244+
}
245+
Invoke-PSBuildModuleSigning @signingParams
246+
}
247+
248+
# Synopsis: Creates a Windows catalog (.cat) file for the built module
249+
Task BuildCatalog -If {
250+
if (-not ($PSBPreference.Sign.Enabled -and $PSBPreference.Sign.Catalog.Enabled)) {
251+
Write-Warning 'Catalog generation is not enabled.'
252+
return $false
253+
}
254+
if (-not (Get-Command -Name 'New-FileCatalog' -ErrorAction Ignore)) {
255+
Write-Warning 'New-FileCatalog is not available. Catalog generation requires Windows.'
256+
return $false
257+
}
258+
$true
259+
} SignModule, {
260+
$catalogFileName = if ($PSBPreference.Sign.Catalog.FileName) {
261+
$PSBPreference.Sign.Catalog.FileName
262+
} else {
263+
"$($PSBPreference.General.ModuleName).cat"
264+
}
265+
$catalogFilePath = Join-Path -Path $PSBPreference.Build.ModuleOutDir -ChildPath $catalogFileName
266+
267+
$catalogParams = @{
268+
ModulePath = $PSBPreference.Build.ModuleOutDir
269+
CatalogFilePath = $catalogFilePath
270+
CatalogVersion = $PSBPreference.Sign.Catalog.Version
271+
}
272+
New-PSBuildFileCatalog @catalogParams
273+
}
274+
275+
# Synopsis: Signs the module catalog (.cat) file with an Authenticode signature
276+
Task SignCatalog -If {
277+
if (-not ($PSBPreference.Sign.Enabled -and $PSBPreference.Sign.Catalog.Enabled)) {
278+
Write-Warning 'Catalog signing is not enabled.'
279+
return $false
280+
}
281+
if (-not (Get-Command -Name 'Set-AuthenticodeSignature' -ErrorAction Ignore)) {
282+
Write-Warning 'Set-AuthenticodeSignature is not available. Catalog signing requires Windows.'
283+
return $false
284+
}
285+
$true
286+
} BuildCatalog, {
287+
$certParams = @{
288+
CertificateSource = $PSBPreference.Sign.CertificateSource
289+
CertStoreLocation = $PSBPreference.Sign.CertStoreLocation
290+
CertificateEnvVar = $PSBPreference.Sign.CertificateEnvVar
291+
CertificatePasswordEnvVar = $PSBPreference.Sign.CertificatePasswordEnvVar
292+
}
293+
if ($PSBPreference.Sign.Thumbprint) {
294+
$certParams.Thumbprint = $PSBPreference.Sign.Thumbprint
295+
}
296+
if ($PSBPreference.Sign.PfxFilePath) {
297+
$certParams.PfxFilePath = $PSBPreference.Sign.PfxFilePath
298+
}
299+
if ($PSBPreference.Sign.PfxFilePassword) {
300+
$certParams.PfxFilePassword = $PSBPreference.Sign.PfxFilePassword
301+
}
302+
303+
$certificate = if ($PSBPreference.Sign.Certificate) {
304+
$PSBPreference.Sign.Certificate
305+
} else {
306+
Get-PSBuildCertificate @certParams
307+
}
308+
309+
if ($null -eq $certificate) {
310+
throw $LocalizedData.NoCertificateFound
311+
}
312+
313+
$catalogFileName = if ($PSBPreference.Sign.Catalog.FileName) {
314+
$PSBPreference.Sign.Catalog.FileName
315+
} else {
316+
"$($PSBPreference.General.ModuleName).cat"
317+
}
318+
319+
$signingParams = @{
320+
Path = $PSBPreference.Build.ModuleOutDir
321+
Certificate = $certificate
322+
TimestampServer = $PSBPreference.Sign.TimestampServer
323+
HashAlgorithm = $PSBPreference.Sign.HashAlgorithm
324+
Include = @($catalogFileName)
325+
}
326+
Invoke-PSBuildModuleSigning @signingParams
327+
}
328+
329+
# Synopsis: Signs module files and catalog (meta task)
330+
Task Sign SignModule, SignCatalog
199331

200332
#endregion Summary Tasks

PowerShellBuild/PowerShellBuild.psd1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
'Build-PSBuildModule'
2020
'Build-PSBuildUpdatableHelp'
2121
'Clear-PSBuildOutputFolder'
22+
'Get-PSBuildCertificate'
2223
'Initialize-PSBuild'
24+
'Invoke-PSBuildModuleSigning'
25+
'New-PSBuildFileCatalog'
2326
'Publish-PSBuildModule'
2427
'Test-PSBuildPester'
2528
'Test-PSBuildScriptAnalysis'

0 commit comments

Comments
 (0)