-
-
Notifications
You must be signed in to change notification settings - Fork 26
Add Authenticode signing support for PowerShell modules #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
8bfbcbe
feat: Add Authenticode signing and catalog support (closes #90)
claude 8934b70
feat(tests): ✨ Add comprehensive tests for code signing functions
HeyItsGilbert 36db55f
feat(signing): ✨ Add certificate validation options for code signing
HeyItsGilbert 4c77115
feat(certificate): ✨ Add support for non-Windows platform checks in `…
HeyItsGilbert 49409cd
test(signing): 🧪 Remove redundant tests for `Get-PSBuildCertificate`
HeyItsGilbert 6b14de1
test(signing): 🧪 Skip tests for Store mode on non-Windows platforms
HeyItsGilbert d8f3479
test: ✏️ Add unit tests for code signing functions
HeyItsGilbert 1d4359b
feat(signing): ✨ Add verbose logging for certificate resolution and b…
HeyItsGilbert 0637a34
refactor(tests): 🔧 Remove redundant test for default Auto mode in `Ge…
HeyItsGilbert 8c9a6e0
test(signing): 🧪 Add verbose output checks for Auto and Store modes i…
HeyItsGilbert File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| function Get-PSBuildCertificate { | ||
| <# | ||
| .SYNOPSIS | ||
| Resolves a code-signing X509Certificate2 from one of several common sources. | ||
| .DESCRIPTION | ||
| Resolves a code-signing certificate suitable for use with Set-AuthenticodeSignature. | ||
| Supports five certificate sources to accommodate local development, CI/CD pipelines, | ||
| and custom signing infrastructure: | ||
|
|
||
| Auto - Checks the CertificateEnvVar environment variable first. If it is | ||
| populated, uses EnvVar mode; otherwise falls back to Store mode. | ||
| This is the recommended default for projects that run both locally | ||
| and in automated pipelines. | ||
|
|
||
| Store - Selects the first valid, unexpired code-signing certificate that has | ||
| a private key from the Windows certificate store at CertStoreLocation. | ||
| Suitable for developer workstations where a certificate is installed. | ||
|
|
||
| Thumbprint - Like Store, but matches a specific certificate by its thumbprint. | ||
| Recommended when multiple code-signing certificates are installed and | ||
|
HeyItsGilbert marked this conversation as resolved.
|
||
| you need a deterministic selection. | ||
|
|
||
| EnvVar - Decodes a Base64-encoded PFX from an environment variable and | ||
| optionally decrypts it with a password from a second variable. | ||
| The most common approach for GitHub Actions, Azure DevOps Pipelines, | ||
| and GitLab CI where secrets are stored as masked variables. | ||
|
|
||
| PfxFile - Loads a PFX/P12 file from disk with an optional SecureString password. | ||
| Useful for local scripts, containers, and environments where a | ||
| certificate file is mounted or distributed via a secrets manager. | ||
|
|
||
| Note: Authenticode signing is a Windows-only capability. This function will fail | ||
| on non-Windows platforms when using Store or Thumbprint sources. | ||
| .PARAMETER CertificateSource | ||
| The source from which to resolve the code-signing certificate. | ||
| Valid values: Auto, Store, Thumbprint, EnvVar, PfxFile. Default: Auto. | ||
| .PARAMETER CertStoreLocation | ||
| Windows certificate store path to search when CertificateSource is Store or Thumbprint. | ||
| Default: Cert:\CurrentUser\My. | ||
| .PARAMETER Thumbprint | ||
| The exact certificate thumbprint to look up. Required when CertificateSource is Thumbprint. | ||
| .PARAMETER CertificateEnvVar | ||
| Name of the environment variable holding the Base64-encoded PFX certificate. | ||
| Used by the EnvVar source and by Auto as the presence-detection key. | ||
| Default: SIGNCERTIFICATE. | ||
| .PARAMETER CertificatePasswordEnvVar | ||
| Name of the environment variable holding the PFX password. Used by EnvVar source. | ||
| Default: CERTIFICATEPASSWORD. | ||
| .PARAMETER PfxFilePath | ||
| File system path to a PFX/P12 certificate file. Required when CertificateSource is PfxFile. | ||
| .PARAMETER PfxFilePassword | ||
| Password for the PFX file as a SecureString. Used by PfxFile source. | ||
| .OUTPUTS | ||
| System.Security.Cryptography.X509Certificates.X509Certificate2 | ||
| Returns the resolved certificate, or $null if none was found (Store/Thumbprint sources). | ||
| .EXAMPLE | ||
| PS> $cert = Get-PSBuildCertificate | ||
|
|
||
| Resolve automatically: use the SIGNCERTIFICATE env var when present, otherwise search | ||
| the current user's certificate store. | ||
| .EXAMPLE | ||
| PS> $cert = Get-PSBuildCertificate -CertificateSource Store | ||
|
|
||
| Explicitly load the first valid code-signing certificate from the current user's store. | ||
| .EXAMPLE | ||
| PS> $cert = Get-PSBuildCertificate -CertificateSource Thumbprint -Thumbprint 'AB12CD34EF56...' | ||
|
|
||
| Load a specific certificate from the certificate store by its thumbprint. | ||
| .EXAMPLE | ||
| PS> $cert = Get-PSBuildCertificate -CertificateSource EnvVar ` | ||
| -CertificateEnvVar 'MY_PFX' -CertificatePasswordEnvVar 'MY_PFX_PASS' | ||
|
|
||
| Decode a PFX certificate stored in a CI/CD secret environment variable. | ||
| .EXAMPLE | ||
| PS> $pass = Read-Host -Prompt 'Certificate password' -AsSecureString | ||
| PS> $cert = Get-PSBuildCertificate -CertificateSource PfxFile -PfxFilePath './codesign.pfx' -PfxFilePassword $pass | ||
|
|
||
| Load a code-signing certificate from a PFX file on disk. | ||
| #> | ||
| [CmdletBinding()] | ||
| [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])] | ||
| param( | ||
| [ValidateSet('Auto', 'Store', 'Thumbprint', 'EnvVar', 'PfxFile')] | ||
| [string]$CertificateSource = 'Auto', | ||
|
|
||
| [string]$CertStoreLocation = 'Cert:\CurrentUser\My', | ||
|
|
||
| [string]$Thumbprint, | ||
|
|
||
| [string]$CertificateEnvVar = 'SIGNCERTIFICATE', | ||
|
|
||
| [string]$CertificatePasswordEnvVar = 'CERTIFICATEPASSWORD', | ||
|
|
||
| [string]$PfxFilePath, | ||
|
|
||
| [securestring]$PfxFilePassword | ||
| ) | ||
|
|
||
| # Resolve 'Auto' to the actual source based on environment variable presence | ||
| $resolvedSource = $CertificateSource | ||
| if ($resolvedSource -eq 'Auto') { | ||
| $resolvedSource = if (-not [string]::IsNullOrEmpty([System.Environment]::GetEnvironmentVariable($CertificateEnvVar))) { | ||
| 'EnvVar' | ||
| } else { | ||
| 'Store' | ||
| } | ||
| Write-Verbose "CertificateSource is 'Auto'. Resolved to '$resolvedSource'." | ||
|
HeyItsGilbert marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| $cert = $null | ||
|
|
||
| switch ($resolvedSource) { | ||
| 'Store' { | ||
| $cert = Get-ChildItem -Path $CertStoreLocation -CodeSigningCert | | ||
| Where-Object { $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) } | | ||
| Select-Object -First 1 | ||
| if ($cert) { | ||
| Write-Verbose ($LocalizedData.CertificateResolvedFromStore -f $CertStoreLocation, $cert.Subject) | ||
| } | ||
| } | ||
| 'Thumbprint' { | ||
| $cert = Get-ChildItem -Path $CertStoreLocation | | ||
| Where-Object { $_.Thumbprint -eq $Thumbprint -and $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) } | | ||
|
HeyItsGilbert marked this conversation as resolved.
Outdated
|
||
| Select-Object -First 1 | ||
| if ($cert) { | ||
| Write-Verbose ($LocalizedData.CertificateResolvedFromThumbprint -f $Thumbprint, $cert.Subject) | ||
| } | ||
| } | ||
| 'EnvVar' { | ||
| $b64Value = [System.Environment]::GetEnvironmentVariable($CertificateEnvVar) | ||
| $buffer = [System.Convert]::FromBase64String($b64Value) | ||
|
HeyItsGilbert marked this conversation as resolved.
Outdated
|
||
| $password = [System.Environment]::GetEnvironmentVariable($CertificatePasswordEnvVar) | ||
| $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($buffer, $password) | ||
| Write-Verbose ($LocalizedData.CertificateResolvedFromEnvVar -f $CertificateEnvVar) | ||
| } | ||
| 'PfxFile' { | ||
| $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($PfxFilePath, $PfxFilePassword) | ||
| Write-Verbose ($LocalizedData.CertificateResolvedFromPfxFile -f $PfxFilePath) | ||
| } | ||
|
HeyItsGilbert marked this conversation as resolved.
|
||
| } | ||
|
|
||
| $cert | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| function Invoke-PSBuildModuleSigning { | ||
| <# | ||
| .SYNOPSIS | ||
| Signs PowerShell module files with an Authenticode signature. | ||
| .DESCRIPTION | ||
| Signs all files matching the Include patterns found under Path using | ||
| Set-AuthenticodeSignature. Typically called after the module is staged to the output | ||
| directory and before the catalog file is created, so that all signed source files are | ||
| captured in the catalog hash. | ||
|
|
||
| Authenticode signing is Windows-only. This function will fail on Linux or macOS. | ||
|
|
||
| Use Get-PSBuildCertificate to resolve the certificate from any of the supported sources | ||
| (certificate store, PFX file, Base64 environment variable, thumbprint, etc.) before | ||
| calling this function. | ||
| .PARAMETER Path | ||
| The directory to search recursively for files to sign. Typically the module output | ||
| directory (PSBPreference.Build.ModuleOutDir). | ||
| .PARAMETER Certificate | ||
| The X509Certificate2 code-signing certificate to sign files with. Must have a private | ||
| key and an Extended Key Usage (EKU) of Code Signing (1.3.6.1.5.5.7.3.3). | ||
| .PARAMETER TimestampServer | ||
| RFC 3161 timestamp server URI to embed in the Authenticode signature, allowing the | ||
| signature to remain valid after the certificate expires. Default: http://timestamp.digicert.com. | ||
|
|
||
| Other common timestamp servers: | ||
| http://timestamp.sectigo.com | ||
| http://timestamp.comodoca.com | ||
| http://tsa.starfieldtech.com | ||
| http://timestamp.globalsign.com/scripts/timstamp.dll | ||
|
HeyItsGilbert marked this conversation as resolved.
|
||
| .PARAMETER HashAlgorithm | ||
| Hash algorithm for the Authenticode signature. | ||
| Valid values: SHA256 (default), SHA384, SHA512, SHA1. | ||
| SHA1 is deprecated; prefer SHA256 or higher. | ||
| .PARAMETER Include | ||
| Glob patterns of file names to sign. Searched recursively under Path. | ||
| Default: *.psd1, *.psm1, *.ps1. | ||
| .OUTPUTS | ||
| System.Management.Automation.Signature | ||
| Returns the Signature objects from Set-AuthenticodeSignature for each signed file. | ||
| .EXAMPLE | ||
| PS> $cert = Get-PSBuildCertificate | ||
| PS> Invoke-PSBuildModuleSigning -Path .\Output\MyModule\1.0.0 -Certificate $cert | ||
|
|
||
| Sign all .psd1, .psm1, and .ps1 files in the module output directory using a | ||
| certificate resolved automatically from the environment or certificate store. | ||
| .EXAMPLE | ||
| PS> $cert = Get-PSBuildCertificate -CertificateSource Thumbprint -Thumbprint 'AB12CD...' | ||
| PS> Invoke-PSBuildModuleSigning -Path .\Output\MyModule\1.0.0 -Certificate $cert ` | ||
| -TimestampServer 'http://timestamp.sectigo.com' -Include '*.psd1','*.psm1' | ||
|
|
||
| Sign only the manifest and root module using a specific certificate and a custom | ||
| timestamp server. | ||
| #> | ||
| [CmdletBinding()] | ||
| [OutputType([System.Management.Automation.Signature])] | ||
| param( | ||
| [parameter(Mandatory)] | ||
| [ValidateScript({ | ||
| if (-not (Test-Path -Path $_ -PathType Container)) { | ||
| throw ($LocalizedData.PathArgumentMustBeAFolder) | ||
| } | ||
| $true | ||
| })] | ||
| [string]$Path, | ||
|
|
||
| [parameter(Mandatory)] | ||
| [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, | ||
|
|
||
| [string]$TimestampServer = 'http://timestamp.digicert.com', | ||
|
|
||
| [ValidateSet('SHA256', 'SHA384', 'SHA512', 'SHA1')] | ||
| [string]$HashAlgorithm = 'SHA256', | ||
|
HeyItsGilbert marked this conversation as resolved.
HeyItsGilbert marked this conversation as resolved.
|
||
|
|
||
| [string[]]$Include = @('*.psd1', '*.psm1', '*.ps1') | ||
| ) | ||
|
|
||
| $files = Get-ChildItem -Path $Path -Recurse -Include $Include | ||
|
HeyItsGilbert marked this conversation as resolved.
|
||
| Write-Verbose ($LocalizedData.SigningModuleFiles -f $files.Count, ($Include -join ', '), $Path) | ||
|
|
||
| $sigParams = @{ | ||
| Certificate = $Certificate | ||
| TimestampServer = $TimestampServer | ||
| HashAlgorithm = $HashAlgorithm | ||
| } | ||
|
|
||
| $files | Set-AuthenticodeSignature @sigParams | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.