Script signing is the process of adding a digital signature to a PowerShell script. The signature proves two things: who wrote the script, and that the script has not been changed since it was signed. Organizations use signed scripts to prevent unauthorized or tampered code from running on their systems.
Why Sign Scripts?
Without Signing: With Signing:
+------------------+ +------------------+
| Script: Delete | | Script: Delete |
| all log files | ← Anyone can | all log files |
| | modify and | |
| No signature | run this | Signed by: Alice |
+------------------+ | Timestamp: valid |
| Untampered: YES |
+------------------+
Only runs if policy
trusts Alice's cert
Execution Policy and Signing
Execution Policy controls which scripts PowerShell allows to run. Signing works hand-in-hand with the
AllSigned and
RemoteSigned policies.
| Policy | Signed Required? | Who Needs Signing? |
|---|
Restricted | N/A | No scripts run at all |
AllSigned | Yes | Every script must be signed by a trusted publisher |
RemoteSigned | Partly | Only scripts downloaded from the internet need signing |
Unrestricted | No | All scripts run (warns for internet scripts) |
Bypass | No | Everything runs, no warnings (use only in automation) |
What Is a Code Signing Certificate?
A code signing certificate is a digital identity card for a developer or organization. It comes from:
+------------------------------+
| Certificate Authority (CA) |
| (The trusted issuer) |
+------------------------------+
|
| Issues certificate to
v
+------------------------------+
| Developer / Organization |
| Certificate contains: |
| - Owner name |
| - Public key |
| - Expiry date |
| - CA's digital signature |
+------------------------------+
|
| Used to sign
v
+------------------------------+
| PowerShell Script (.ps1) |
| Contains: |
| - Script code |
| - Digital signature block |
+------------------------------+
Getting a Code Signing Certificate
Option 1: Create a Self-Signed Certificate (Testing Only)
# Create a self-signed certificate for testing
$cert = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject "CN=MyTestCert" `
-CertStoreLocation "Cert:\CurrentUser\My"
Write-Output $cert.Thumbprint
Option 2: Use a Certificate from Enterprise CA
In enterprise environments, the IT team issues code signing certificates through Active Directory Certificate Services (AD CS). The certificate appears in your personal certificate store automatically.
Option 3: Buy from a Public CA
Companies like DigiCert, Sectigo, or GlobalSign sell publicly trusted code signing certificates. These work anywhere without requiring recipients to manually trust your certificate.
Signing a Script
# Step 1: Get your signing certificate
$cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1
# Step 2: Sign the script
Set-AuthenticodeSignature -FilePath "C:\Scripts\Deploy.ps1" -Certificate $cert
# Step 3: Verify the signature was applied
Get-AuthenticodeSignature -FilePath "C:\Scripts\Deploy.ps1"
Output of Get-AuthenticodeSignature
Directory: C:\Scripts
SignerCertificate Status Path
----------------- ------ ----
3C8D8B26E1A2F45D9E3B... Valid Deploy.ps1
What a Signed Script Looks Like
PowerShell appends the signature block at the bottom of the script file. The script code above stays exactly as written.
# Your script content here
Write-Output "Deploying application..."
Copy-Item "C:\Build\app.exe" "C:\Deploy\"
# SIG # Begin signature block
# MIIFxAYJKoZIhvcNAQcCoIIFtTCCBbECAQExDzAN
# BglghkgBZQMEAgEFADCBgwYJKoZIhvcNAQcBoHYE
# ... (many more lines of encoded signature)
# SIG # End signature block
The signature block is unreadable to humans — it is a Base64-encoded certificate. PowerShell reads and verifies it automatically when the script runs.
Trusting a Self-Signed Certificate
A self-signed certificate is not trusted by default. To use it under the
AllSigned policy, you must add it to the trusted stores.
# Get the self-signed cert
$cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1
# Add it to the Trusted Root store (makes it a trusted CA)
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root","LocalMachine")
$store.Open("ReadWrite")
$store.Add($cert)
$store.Close()
# Add it to the Trusted Publishers store (trusts this publisher's scripts)
$store2 = New-Object System.Security.Cryptography.X509Certificates.X509Store("TrustedPublisher","LocalMachine")
$store2.Open("ReadWrite")
$store2.Add($cert)
$store2.Close()
Checking a Script's Signature Status
# Check a single script
$result = Get-AuthenticodeSignature "C:\Scripts\Deploy.ps1"
Write-Output $result.Status # Valid, NotSigned, HashMismatch, etc.
Write-Output $result.SignerCertificate.Subject
# Check multiple scripts
Get-ChildItem "C:\Scripts\*.ps1" | Get-AuthenticodeSignature |
Select-Object Path, Status |
Format-Table -AutoSize
Signature Status Values
| Status | Meaning |
|---|
Valid | Signature is good — script is trusted and untampered |
NotSigned | No signature present |
HashMismatch | Script was changed after signing — DO NOT run |
UnknownError | Certificate problem or corrupt signature |
NotTrusted | Signer's certificate is not in the trusted store |
What Happens When a Signed Script Is Modified
Original script (signed): Script after someone edits it:
+------------------------+ +------------------------+
| Write-Output "Hello" | | Write-Output "Hello" |
| | → | Remove-Item C:\Data\* | ← Added!
| # SIG # Begin... | | |
| (valid signature) | | # SIG # Begin... |
+------------------------+ | (INVALID — hash broken)|
+------------------------+
PowerShell: Status = Valid PowerShell: Status = HashMismatch
Script BLOCKED
Signing with a Timestamp
A timestamp proves the script was signed while the certificate was still valid — even if the certificate expires later, the script remains valid.
$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1
Set-AuthenticodeSignature `
-FilePath "C:\Scripts\Deploy.ps1" `
-Certificate $cert `
-TimestampServer "http://timestamp.digicert.com"
Without a timestamp, the signature becomes invalid the day the certificate expires, and all signed scripts stop running.
Setting Execution Policy to Enforce Signing
# Require all scripts to be signed (recommended for production)
Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope LocalMachine
# Require only internet-downloaded scripts to be signed
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Summary
Script signing adds a tamper-proof identity stamp to PowerShell scripts. A valid signature confirms the author's identity and proves the script has not been altered. Use
Set-AuthenticodeSignature to sign scripts with a code signing certificate. Pair signing with the
AllSigned execution policy in production environments to block unsigned or modified scripts. Always include a timestamp when signing to keep scripts valid beyond the certificate's expiry date.