Skip to content

Commit 36db55f

Browse files
committed
feat(signing): ✨ Add certificate validation options for code signing
- Introduced `-SkipValidation` parameter to bypass validation checks for certificates from `EnvVar` or `PfxFile` sources. - Enhanced error handling for missing or invalid certificates. - Updated localization strings for better clarity on certificate validation messages.
1 parent 8934b70 commit 36db55f

File tree

5 files changed

+89
-34
lines changed

5 files changed

+89
-34
lines changed

PowerShellBuild/IB.tasks.ps1

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
Remove-Variable -Name PSBPreference -Scope Script -Force -ErrorAction Ignore
22
Set-Variable -Name PSBPreference -Option ReadOnly -Scope Script -Value (. ([IO.Path]::Combine($PSScriptRoot, 'build.properties.ps1')))
3-
$__DefaultBuildDependencies = $PSBPreference.Build.Dependencies
3+
$__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,21 +184,21 @@ 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
199199

200200
# Synopsis: Signs module files (*.psd1, *.psm1, *.ps1) with an Authenticode signature
201-
task SignModule -If {
201+
Task SignModule -If {
202202
if (-not $PSBPreference.Sign.Enabled) {
203203
Write-Warning 'Module signing is not enabled.'
204204
return $false
@@ -246,7 +246,7 @@ task SignModule -If {
246246
}
247247

248248
# Synopsis: Creates a Windows catalog (.cat) file for the built module
249-
task BuildCatalog -If {
249+
Task BuildCatalog -If {
250250
if (-not ($PSBPreference.Sign.Enabled -and $PSBPreference.Sign.Catalog.Enabled)) {
251251
Write-Warning 'Catalog generation is not enabled.'
252252
return $false
@@ -273,13 +273,13 @@ task BuildCatalog -If {
273273
}
274274

275275
# Synopsis: Signs the module catalog (.cat) file with an Authenticode signature
276-
task SignCatalog -If {
276+
Task SignCatalog -If {
277277
if (-not ($PSBPreference.Sign.Enabled -and $PSBPreference.Sign.Catalog.Enabled)) {
278278
Write-Warning 'Catalog signing is not enabled.'
279279
return $false
280280
}
281281
if (-not (Get-Command -Name 'Set-AuthenticodeSignature' -ErrorAction Ignore)) {
282-
Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.'
282+
Write-Warning 'Set-AuthenticodeSignature is not available. Catalog signing requires Windows.'
283283
return $false
284284
}
285285
$true
@@ -327,6 +327,6 @@ task SignCatalog -If {
327327
}
328328

329329
# Synopsis: Signs module files and catalog (meta task)
330-
task Sign SignCatalog
330+
Task Sign SignModule, SignCatalog
331331

332332
#endregion Summary Tasks

PowerShellBuild/Public/Get-PSBuildCertificate.ps1

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ function Get-PSBuildCertificate {
5050
File system path to a PFX/P12 certificate file. Required when CertificateSource is PfxFile.
5151
.PARAMETER PfxFilePassword
5252
Password for the PFX file as a SecureString. Used by PfxFile source.
53+
.PARAMETER SkipValidation
54+
Skip validation checks (private key presence, expiration, Code Signing EKU) for certificates
55+
loaded from EnvVar or PfxFile sources. Use with caution; invalid certificates will fail during
56+
actual signing operations with less descriptive errors.
5357
.OUTPUTS
5458
System.Security.Cryptography.X509Certificates.X509Certificate2
5559
Returns the resolved certificate, or $null if none was found (Store/Thumbprint sources).
@@ -79,6 +83,11 @@ function Get-PSBuildCertificate {
7983
#>
8084
[CmdletBinding()]
8185
[OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
86+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
87+
'PSAvoidUsingPlainTextForPassword',
88+
'CertificatePasswordEnvVar',
89+
Justification = 'This is not a password in plain text. It is the name of an environment variable that contains the password, which is a common pattern for CI/CD pipelines and secrets management.'
90+
)]
8291
param(
8392
[ValidateSet('Auto', 'Store', 'Thumbprint', 'EnvVar', 'PfxFile')]
8493
[string]$CertificateSource = 'Auto',
@@ -93,7 +102,9 @@ function Get-PSBuildCertificate {
93102

94103
[string]$PfxFilePath,
95104

96-
[securestring]$PfxFilePassword
105+
[securestring]$PfxFilePassword,
106+
107+
[switch]$SkipValidation
97108
)
98109

99110
# Resolve 'Auto' to the actual source based on environment variable presence
@@ -104,7 +115,7 @@ function Get-PSBuildCertificate {
104115
} else {
105116
'Store'
106117
}
107-
Write-Verbose "CertificateSource is 'Auto'. Resolved to '$resolvedSource'."
118+
Write-Verbose ($LocalizedData.CertificateSourceAutoResolved -f $resolvedSource)
108119
}
109120

110121
$cert = $null
@@ -119,18 +130,37 @@ function Get-PSBuildCertificate {
119130
}
120131
}
121132
'Thumbprint' {
122-
$cert = Get-ChildItem -Path $CertStoreLocation |
123-
Where-Object { $_.Thumbprint -eq $Thumbprint -and $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) } |
133+
if ([string]::IsNullOrWhiteSpace($Thumbprint)) {
134+
throw "CertificateSource 'Thumbprint' requires a non-empty Thumbprint value."
135+
}
136+
137+
# Normalize thumbprint input by removing whitespace for robust matching
138+
$normalizedThumbprint = ($Thumbprint -replace '\s', '')
139+
140+
$cert = Get-ChildItem -Path $CertStoreLocation -CodeSigningCert |
141+
Where-Object {
142+
($_.Thumbprint -replace '\s', '') -ieq $normalizedThumbprint -and
143+
$_.HasPrivateKey -and
144+
$_.NotAfter -gt (Get-Date)
145+
} |
124146
Select-Object -First 1
125147
if ($cert) {
126148
Write-Verbose ($LocalizedData.CertificateResolvedFromThumbprint -f $Thumbprint, $cert.Subject)
127149
}
128150
}
129151
'EnvVar' {
130152
$b64Value = [System.Environment]::GetEnvironmentVariable($CertificateEnvVar)
131-
$buffer = [System.Convert]::FromBase64String($b64Value)
153+
if ([string]::IsNullOrWhiteSpace($b64Value)) {
154+
throw "Environment variable '$CertificateEnvVar' is not set or is empty. When using CertificateSource='EnvVar', you must provide a Base64-encoded PFX in this variable."
155+
}
156+
157+
try {
158+
$buffer = [System.Convert]::FromBase64String($b64Value)
159+
} catch [System.FormatException] {
160+
throw "Environment variable '$CertificateEnvVar' does not contain a valid Base64-encoded PFX value."
161+
}
132162
$password = [System.Environment]::GetEnvironmentVariable($CertificatePasswordEnvVar)
133-
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($buffer, $password)
163+
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($buffer, $password)
134164
Write-Verbose ($LocalizedData.CertificateResolvedFromEnvVar -f $CertificateEnvVar)
135165
}
136166
'PfxFile' {
@@ -139,5 +169,27 @@ function Get-PSBuildCertificate {
139169
}
140170
}
141171

172+
# Validate certificates loaded from EnvVar or PfxFile sources unless -SkipValidation is specified
173+
if ($cert -and -not $SkipValidation -and ($resolvedSource -eq 'EnvVar' -or $resolvedSource -eq 'PfxFile')) {
174+
# Check for private key
175+
if (-not $cert.HasPrivateKey) {
176+
throw ($LocalizedData.CertificateMissingPrivateKey -f $cert.Subject)
177+
}
178+
179+
# Check expiration
180+
if ($cert.NotAfter -le (Get-Date)) {
181+
throw ($LocalizedData.CertificateExpired -f $cert.NotAfter, $cert.Subject)
182+
}
183+
184+
# Check for Code Signing EKU (1.3.6.1.5.5.7.3.3)
185+
$codeSigningOid = '1.3.6.1.5.5.7.3.3'
186+
$hasCodeSigningEku = $cert.EnhancedKeyUsageList | Where-Object { $_.ObjectId -eq $codeSigningOid }
187+
if (-not $hasCodeSigningEku) {
188+
throw ($LocalizedData.CertificateMissingCodeSigningEku -f $cert.Subject)
189+
}
190+
191+
Write-Verbose "Certificate validation passed: HasPrivateKey=$($cert.HasPrivateKey), NotAfter=$($cert.NotAfter), CodeSigningEKU=Present"
192+
}
193+
142194
$cert
143195
}

PowerShellBuild/Public/New-PSBuildFileCatalog.ps1

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ function New-PSBuildFileCatalog {
6565
Path = $ModulePath
6666
CatalogFilePath = $CatalogFilePath
6767
CatalogVersion = $CatalogVersion
68-
Verbose = $VerbosePreference
6968
}
7069

7170
Microsoft.PowerShell.Security\New-FileCatalog @catalogParams

PowerShellBuild/en-US/Messages.psd1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,8 @@ CertificateResolvedFromPfxFile=Resolved code signing certificate from PFX file [
3131
SigningModuleFiles=Signing [{0}] file(s) matching [{1}] in [{2}]...
3232
CreatingFileCatalog=Creating file catalog [{0}] (version {1})...
3333
FileCatalogCreated=File catalog created: [{0}]
34+
CertificateSourceAutoResolved=CertificateSource is 'Auto'. Resolved to '{0}'.
35+
CertificateMissingPrivateKey=The resolved certificate does not have an accessible private key. Code signing requires a certificate with a private key. Subject=[{0}]
36+
CertificateExpired=The resolved certificate has expired (NotAfter: {0}). Code signing requires a valid, unexpired certificate. Subject=[{1}]
37+
CertificateMissingCodeSigningEku=The resolved certificate does not have the Code Signing Enhanced Key Usage (EKU: 1.3.6.1.5.5.7.3.3). Subject=[{0}]
3438
'@

PowerShellBuild/psakeFile.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ $signCatalogPreReqs = {
312312
$result = $false
313313
}
314314
if (-not (Get-Command -Name 'Set-AuthenticodeSignature' -ErrorAction Ignore)) {
315-
Write-Warning 'Set-AuthenticodeSignature is not available. Module signing requires Windows.'
315+
Write-Warning 'Set-AuthenticodeSignature is not available. Catalog signing requires Windows.'
316316
$result = $false
317317
}
318318
$result

0 commit comments

Comments
 (0)