docs/Sign-Module.ps1
|
<#
.SYNOPSIS Signs all SPClean module files with an Authenticode certificate. .DESCRIPTION Applies Set-AuthenticodeSignature to every .ps1 and .psd1 in the module tree. Requires a code-signing certificate. Options: 1. Commercial EV cert from DigiCert/Sectigo/GlobalSign (recommended for distribution) 2. Self-signed cert created by this script (trusted only on this machine) Run this after every code change before publishing. .PARAMETER CertThumbprint Thumbprint of an existing cert in Cert:\CurrentUser\My. If omitted, the script offers to create a self-signed cert. .PARAMETER CreateSelfSigned Creates a self-signed code-signing cert (Cert:\CurrentUser\My) and uses it. Useful for dev/test; NOT trusted by other machines unless the cert is exported and added to their Trusted Publishers store. #> [CmdletBinding(SupportsShouldProcess)] param( [string] $CertThumbprint, [switch] $CreateSelfSigned ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $moduleRoot = Split-Path $PSScriptRoot -Parent # ─── Resolve / create certificate ──────────────────────────────────────────── if ($CreateSelfSigned) { Write-Host "Creating self-signed code-signing certificate..." $cert = New-SelfSignedCertificate -Subject 'CN=SPClean Code Signing' ` -CertStoreLocation 'Cert:\CurrentUser\My' ` -KeyUsage DigitalSignature ` -Type CodeSigningCert ` -NotAfter (Get-Date).AddYears(3) Write-Host " Created: $($cert.Thumbprint) — $($cert.Subject)" } elseif ($CertThumbprint) { $cert = Get-Item "Cert:\CurrentUser\My\$CertThumbprint" -ErrorAction Stop Write-Host "Using cert: $($cert.Subject) [$CertThumbprint]" } else { # List available code-signing certs and prompt $available = Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.EnhancedKeyUsageList.ObjectId -contains '1.3.6.1.5.5.7.3.3' } if ($available.Count -eq 0) { Write-Host "No code-signing certificate found in Cert:\CurrentUser\My." Write-Host "Re-run with -CreateSelfSigned to create one, or supply -CertThumbprint." exit 1 } Write-Host "Available code-signing certificates:" $available | ForEach-Object { Write-Host " $($_.Thumbprint) $($_.Subject)" } Write-Error "Specify -CertThumbprint <value> from the list above." } # ─── Collect files to sign ──────────────────────────────────────────────────── $files = Get-ChildItem -Path $moduleRoot -Recurse -Include '*.ps1','*.psd1' | Where-Object { $_.FullName -notmatch '\\Tests\\' -and $_.FullName -notmatch '\\docs\\' } Write-Host "" Write-Host "Files to sign ($($files.Count)):" $files | ForEach-Object { Write-Host " $($_.FullName -replace [regex]::Escape($moduleRoot), '.')" } Write-Host "" # ─── Sign ───────────────────────────────────────────────────────────────────── $signed = 0 $failed = 0 foreach ($f in $files) { if (-not $PSCmdlet.ShouldProcess($f.Name, 'Set-AuthenticodeSignature')) { continue } try { $result = Set-AuthenticodeSignature -FilePath $f.FullName -Certificate $cert ` -TimestampServer 'http://timestamp.digicert.com' -ErrorAction Stop if ($result.Status -eq 'Valid') { Write-Host " Signed: $($f.Name)" -ForegroundColor Green $signed++ } else { Write-Warning " Unexpected status '$($result.Status)' for $($f.Name)" $failed++ } } catch { Write-Warning " Failed to sign $($f.Name): $_" $failed++ } } Write-Host "" Write-Host "=== Signing complete: $signed signed, $failed failed ===" # ─── Verify ─────────────────────────────────────────────────────────────────── if ($signed -gt 0) { Write-Host "" Write-Host "Verifying signatures..." $files | ForEach-Object { $sig = Get-AuthenticodeSignature -FilePath $_.FullName $color = if ($sig.Status -eq 'Valid') { 'Green' } else { 'Red' } Write-Host " $($sig.Status.ToString().PadRight(12)) $($_.Name)" -ForegroundColor $color } } |