PowerShell Script Signing

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.
PolicySigned Required?Who Needs Signing?
RestrictedN/ANo scripts run at all
AllSignedYesEvery script must be signed by a trusted publisher
RemoteSignedPartlyOnly scripts downloaded from the internet need signing
UnrestrictedNoAll scripts run (warns for internet scripts)
BypassNoEverything 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

StatusMeaning
ValidSignature is good — script is trusted and untampered
NotSignedNo signature present
HashMismatchScript was changed after signing — DO NOT run
UnknownErrorCertificate problem or corrupt signature
NotTrustedSigner'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.

Leave a Comment

Your email address will not be published. Required fields are marked *