In short
To sign a PowerShell script, load your code signing certificate from the Windows certificate store and run Set-AuthenticodeSignature -FilePath .\Deploy.ps1 -Certificate $cert -HashAlgorithm SHA256 -TimestampServer http://time.certum.pl. Pass -HashAlgorithm SHA256 explicitly — Windows PowerShell 5.1 still defaults to SHA-1 — and keep the timestamp URL on http://, not https://. Check the result with Get-AuthenticodeSignature.
PowerShell is the rare platform where script signing is built into the language itself — no SDK download, one cmdlet. What trips people up is everything around that cmdlet: which certificate PowerShell will accept, a hash-algorithm default that quietly produces SHA-1 signatures on Windows PowerShell 5.1, execution policies that decide whether the signature is ever checked, and a trusted-publisher prompt that surprises whoever runs the script next. This guide covers the whole path: what signing does to the file, when Windows demands it, the exact command, and the errors you'll actually meet.
What signing a PowerShell script does
Signing a PowerShell script appends an Authenticode signature to the end of the file as a comment block. Set-AuthenticodeSignature hashes the script's content, signs that hash with your certificate's private key, and writes the result between # SIG # Begin signature block and # SIG # End signature block markers. PowerShell then re-checks that block whenever the execution policy calls for it, proving who signed the script and that nothing changed since.
This is the same Authenticode mechanism that signs Windows executables — the SignTool guide covers that side — but with one visible difference: in a script the signature sits in plain text at the bottom of the file, where you can scroll down and see it. Deleting those comment lines simply makes the script unsigned again. And because the signature is a hash of everything above it, any edit — a changed variable, a new comment, even saving with a different text encoding — invalidates it until you re-sign.
Execution policies: when a signature is required
PowerShell's execution policy decides whether a signature is ever checked. Restricted (the Windows client default) runs no scripts at all, RemoteSigned (the Windows Server default) demands a signature only on scripts downloaded from the internet, and AllSigned demands a valid, trusted signature on every script. Signing matters most in environments that enforce AllSigned or feed scripts to machines through download-marked channels.
"Downloaded" has a precise meaning here: Windows attaches a Mark of the Web to files that arrive via browsers, email clients, and messaging apps, and RemoteSigned keys off that mark. Unblock-File removes it, which is why an unsigned script that "won't run" starts working after unblocking — the policy no longer counts it as remote. A script you write locally never carries the mark, so RemoteSigned runs it unsigned.
One thing the execution policy is not is a security boundary — Microsoft says this plainly in its documentation. A determined user can bypass it in several documented ways. Its real job, and the reason signing still matters, is preventing the unintentional running of scripts: the pasted-from-a-forum one-liner, the tampered deployment script, the file someone edited without telling anyone. Signing plus AllSigned turns "did anyone change this?" into a check the machine does for you.
What you need before you sign
One thing, really: a certificate with the code signing purpose, visible to Windows with its private key. Where it comes from depends on who has to trust the script — your own machines can trust a self-signed certificate you create in one command, your company's machines can trust one from your internal CA, and everyone else's machines need one from a publicly trusted CA.
- Testing on your own machine:
New-SelfSignedCertificate -Type CodeSigningCertcreates a working signing certificate in seconds. The signature only verifies on machines where you've manually installed that certificate as trusted — fine for a lab, useless for distribution. - Inside one organization: a code signing certificate from your Active Directory Certificate Services CA (or similar internal CA) verifies on every domain machine automatically, since they already trust the internal root. This is the standard setup for IT teams enforcing AllSigned.
- Scripts that leave your organization: you need a certificate from a publicly trusted CA, issued to a validated identity. My-SSL carries Certum standard and EV code signing certificates that work with Set-AuthenticodeSignature the same way they work with SignTool; the validation paperwork is covered in the required documents guide.
Publicly trusted keys live on hardware now
Under CA/Browser Forum rules in force since June 1, 2023, publicly trusted code signing certificates ship with the private key on a certified USB token, smart card, or HSM — not as a .pfx file. For PowerShell signing that changes less than you'd think: once the token's middleware is installed, the certificate appears in the Windows certificate store, Get-ChildItem -CodeSigningCert finds it, and signing prompts for the token PIN instead of a password.
Sign the script with Set-AuthenticodeSignature
Two lines do the job: fetch the certificate from your Personal store with Get-ChildItem -CodeSigningCert, then pass it to Set-AuthenticodeSignature along with the file, -HashAlgorithm SHA256, and a timestamp server. The cmdlet returns a signature object whose Status should read Valid.
# Find your code signing certificate (must have a private key)
$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert
# Sign the script
Set-AuthenticodeSignature -FilePath .\Deploy.ps1 -Certificate $cert `
-HashAlgorithm SHA256 -TimestampServer http://time.certum.pl
# Expected output:
# SignerCertificate Status Path
# ----------------- ------ ----
# A1B2C3D4E5F6... Valid Deploy.ps1If $cert comes back empty, the store has no certificate with both the code signing purpose and a private key — jump to the troubleshooting section. If several certificates match, index into the list ($cert[0]) or filter by subject with Where-Object.
The parameter that deserves the most attention is -HashAlgorithm SHA256. Windows PowerShell 5.1 — still the default shell on every Windows install — defaults to SHA-1, an algorithm deprecated for signatures years ago. PowerShell 7.3 finally flipped the default to SHA256, but the flag costs nothing and saves you from ever wondering which shell signed a file. Treat it as a fixed part of the command, the same way /fd SHA256 is fixed in SignTool.
Timestamp the signature
The -TimestampServer parameter has a free timestamp authority certify when the script was signed. Without it, every signature you've made stops verifying the day your certificate expires — and code signing certificates issued today live one to three years, while deployment scripts routinely outlive that. With it, the signature stays valid indefinitely, because the timestamp proves it was applied while the certificate was still good. Our code signing timestamps explainer covers the mechanism in depth, including the cases where a timestamp can't rescue a signature.
Public servers that answer Windows signing tools include http://time.certum.pl (Certum) and http://timestamp.digicert.com (DigiCert). Note the http:// — as of PowerShell 7.6, the cmdlet cannot timestamp over https. PowerShell 7.3 started accepting https URLs syntactically, but the underlying Windows API doesn't support them: the file gets signed without a timestamp and the command returns an error. Plain http is correct and safe here — the timestamp reply is itself signed by the timestamp authority, so the transport doesn't need to be.
Verify the signature
Run Get-AuthenticodeSignature against the file and read the Status property. Valid means the hash matches and the certificate chains to a trusted root on this machine; HashMismatch means the file changed after signing; UnknownError almost always means the chain ends in a root this machine doesn't trust; NotSigned speaks for itself.
Get-AuthenticodeSignature .\Deploy.ps1 |
Format-List Status, StatusMessage, SignerCertificate, TimeStamperCertificate
# Status : Valid
# StatusMessage : Signature verified.
# SignerCertificate : [Subject] CN=Your Company Name, ...
# TimeStamperCertificate : [Subject] CN=Certum Timestamp Authority, ...Check TimeStamperCertificate specifically: if it's blank, the script signed fine but without a timestamp, and the signature will die with the certificate. Because Valid depends on the verifying machine's root store, the real test for anything you distribute is verifying on a second, clean machine — the one place where "works on my machine" is a certificate-store statement, not a joke.
Common errors and fixes
Nearly every failed attempt lands on one of five problems: the execution policy blocking scripts before signing is even relevant, an empty certificate variable, an untrusted root, a stale hash after an edit, or the trusted-publisher prompt. Match your symptom below.
"…cannot be loaded because running scripts is disabled on this system"
Not a signing error — the execution policy is Restricted, the Windows client default, which runs no scripts at all. Set-ExecutionPolicy RemoteSigned -Scope CurrentUser is the usual fix for a workstation. If the setting won't stick, a Group Policy is overriding it — check with Get-ExecutionPolicy -List.
Get-ChildItem -CodeSigningCert returns nothing
The Personal store has no certificate that combines the code signing purpose with an accessible private key. Common causes: the certificate was imported without its key, it lives in the machine store while you're searching the user store (try Cert:\LocalMachine\My), an SSL certificate is being pressed into service (wrong key usage — it won't appear), or the USB token is unplugged / its middleware isn't installed, so Windows can't see the key at all.
Status: UnknownError — "…terminated in a root certificate which is not trusted"
The signature itself is fine; the verifying machine doesn't trust the certificate's root. With a self-signed certificate that's expected — install it into Trusted Root Certification Authorities (and Trusted Publishers, to silence the AllSigned prompt) on each test machine. If you're seeing this beyond a lab, it's the signal you've outgrown self-signed and need a CA-issued certificate.
Status: HashMismatch
The file changed after signing. Any edit does it, and so does re-saving with a different encoding — some editors convert line endings or add a byte-order mark on save, which changes the bytes without changing what you see. Re-sign after every change; if scripts change often, put the signing command in your build or release pipeline so it can't be forgotten.
"Do you want to run software from this untrusted publisher?"
AllSigned verified your signature and is now asking about the publisher — the certificate isn't in that machine's Trusted Publishers store yet. Users can choose "Always run" to add it themselves; IT teams push the publisher certificate to Trusted Publishers via Group Policy so the prompt never appears. The decision tree above shows where this check sits.
The bottom line
Fetch the certificate with Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert, sign with Set-AuthenticodeSignature … -HashAlgorithm SHA256 -TimestampServer http://time.certum.pl, and confirm with Get-AuthenticodeSignature. The two habits that prevent future pain: always pass SHA256 explicitly, and never sign without the timestamp. If the scripts travel beyond machines you control, a Certum code signing certificate gives them an identity every Windows machine already trusts — and it signs your EXEs and installers with the same key.