Skip to content

PowerShell Code Signing

How to use PowerShell code signing with Authenticode to verify script integrity and authenticity, enforce execution policies like AllSigned, and protect against tampering.

Code signing is a cryptographic mechanism that applies a digital signature to PowerShell scripts (.ps1), module files (.psm1), and manifests (.psd1) using Microsoft Authenticode technology. Signing scripts establishes authenticity (verifying the identity of the publisher) and integrity (guaranteeing that the code has not been altered or tampered with since the signature was applied). Combined with the AllSigned or RemoteSigned execution policy, signing is the mechanism that lets trusted scripts run while blocking unsigned or tampered ones.

Why Sign PowerShell Scripts

Signing a script provides two guarantees:

  • Integrity i.e. the script's content hasn't changed since it was signed. Any modification, even a single character, invalidates the signature.
  • Authenticity i.e. the signature is tied to a certificate, verifying who signed the script.

In enterprise environments governed by security baselines and application control policies, executing unsigned scripts poses serious risks. Code signing solves several key operational and security challenges:

  • Tamper Evidence: digital signatures embed a cryptographic hash of the script body. If an attacker, user, or transmission error alters a single byte or space in the file, the hash check fails and invalidates the signature.
  • Publisher Identity: scripts signed by an internal Public Key Infrastructure (PKI) or public Certificate Authority (CA) prove that the code originated from an authorized internal team.
  • Execution Policy Compliance: an execution policy set to AllSigned blocks every script unless it carries a valid signature issued by a certificate trusted in the host machine's Trusted Publishers store.

When Signing Matters

Scenario Why Signing Helps
Distributing scripts across an organization Ensures scripts weren't tampered with in transit
Enforcing AllSigned execution policy Required for any script to run, including your own
Publishing a module publicly Builds trust with users installing your code
Compliance/audit requirements Provides a verifiable chain of authorship

Creating a Code Signing Certificate with New-SelfSignedCertificate

In production, certificates are typically distributed by an enterprise Certificate Authority (Active Directory Certificate Services) or a commercial CA. For lab environments or internal development, you can generate a self-signed code signing certificate using New-SelfSignedCertificate.

generate-signing-cert.ps1powershell
$cert = New-SelfSignedCertificate -Type CodeSigningCert `
    -Subject "CN=PowerShellInternalSigner" `
    -CertStoreLocation "Cert:\CurrentUser\My" `
    -NotAfter (Get-Date).AddYears(2)

[PSCustomObject]@{
    Subject    = $cert.Subject
    Thumbprint = $cert.Thumbprint
    Expires    = $cert.NotAfter
}

where:

  • New-SelfSignedCertificate is a cmdlet that creates and enrolls a local X.509 certificate.
  • -Type CodeSigningCert pre-configures the Enhanced Key Usage (EKU) extension for Code Signing (OID 1.3.6.1.5.5.7.3.3).
  • -Subject is the distinguished name identifying the publisher (e.g., CN=PowerShellInternalSigner).
  • -CertStoreLocation specifies where the private and public keys are saved (Cert:\CurrentUser\My).
  • -NotAfter sets the expiration window (defaults to one year; configured above for two years).
Consolepowershell
./generate-signing-cert.ps1
Outputplaintext
Subject                     Thumbprint                               Expires
-------                     ----------                               -------
CN=PowerShellInternalSigner 4A7B1C8D9E0F2A3B4C5D6E7F8A9B0C1D2E3F4A5B 3/31/2028 12:00:00 PM

Trusting a Self-Signed Certificate Locally

A self-signed certificate is not trusted by Windows by default. For the system to recognize it as valid, you must export its public certificate (.cer) and install it into both the Trusted Root Certification Authorities and Trusted Publishers stores.

Consolepowershell
$rootStore = [System.Security.Cryptography.X509Certificates.X509Store]::new("Root", "CurrentUser")
$rootStore.Open("ReadWrite")
$rootStore.Add($cert)
$rootStore.Close()

$publisherStore = [System.Security.Cryptography.X509Certificates.X509Store]::new("TrustedPublisher", "CurrentUser")
$publisherStore.Open("ReadWrite")
$publisherStore.Add($cert)
$publisherStore.Close()

Signing a Script with Set-AuthenticodeSignature

The Set-AuthenticodeSignature cmdlet computes a cryptographic hash of the script, encrypts the hash with the certificate's private key, and appends a formatted Authenticode signature block to the end of the file.

sign-script-demo.ps1powershell
$scriptPath = ".\Deploy.ps1"
Set-Content -Path $scriptPath -Value "Write-Output 'Executing production deployment...'"

$signerCert = Get-ChildItem -Path "Cert:\CurrentUser\My" -CodeSigningCert |
    Where-Object -Property Subject -Match "PowerShellInternalSigner" |
    Select-Object -First 1

Set-AuthenticodeSignature -FilePath $scriptPath `
    -Certificate $signerCert `
    -TimestampServer "http://tsa.open-tsa.eu"

where:

  • Set-AuthenticodeSignature signs the target file using the designated certificate.
  • -FilePath is the path to the script or module file to sign.
  • -Certificate is the System.Security.Cryptography.X509Certificates.X509Certificate2 object with an accessible private key.
  • -TimestampServer is a public RFC 3161 timestamping authority URL. Timestamping guarantees that the signature remains legally valid even after the signing certificate expires, provided the certificate was active at the time of signing.
Consolepowershell
./sign-script-demo.ps1
Outputplaintext
SignerCertificate                         Status Path
-----------------                         ------ ----
4A7B1C8D9E0F2A3B4C5D6E7F8A9B0C1D2E3F4A5B  Valid  Deploy.ps1
Deploy.ps1 (appended signature)powershell
Write-Output 'Executing production deployment...'

Verifying a Signature with Get-AuthenticodeSignature

The Get-AuthenticodeSignature cmdlet inspects an on-disk script, extracts the signature block, validates the integrity hash against the file contents, and checks the certificate trust chain.

verify-signature-demo.ps1powershell
$sig = Get-AuthenticodeSignature -FilePath ".\Deploy.ps1"

[PSCustomObject]@{
    FilePath    = $sig.Path
    Status      = $sig.Status
    StatusMsg   = $sig.StatusMessage
    SignerCN    = $sig.SignerCertificate.Subject
    HasTimestamp= ($null -ne $sig.TimeStamperCertificate)
}

where:

  • Get-AuthenticodeSignature analyzes the file and returns a Signature object.
  • $sig.Status returns an enumerated status string (see table below).
  • $sig.TimeStamperCertificate verifies whether an external timestamp authority validated the signing time.
Consolepowershell
./verify-signature-demo.ps1
Outputplaintext
FilePath     : C:\Scripts\Deploy.ps1
Status       : Valid
StatusMsg    : Signature verified.
SignerCN     : CN=PowerShellInternalSigner
HasTimestamp : True

Understanding Status Values

Status Meaning
Valid Signature is cryptographically sound and the certificate is trusted.
NotSigned The script has no signature at all.
HashMismatch The script was modified after signing, so the integrity check failed.
NotTrusted The signature is valid, but the certificate isn't trusted on this machine.
UnknownError Signature validation failed for another reason (e.g., expired certificate without a timestamp).

Practical Usage: Signing and Verifying a Deployment Script

A common workflow combines signing during release with automatic verification before execution, ensuring only untampered, trusted scripts run in production.

sign-and-verify.ps1powershell
param(
    [Parameter(Mandatory)]
    [string]$ScriptPath
)

$cert = Get-ChildItem -Path "Cert:\CurrentUser\My" -CodeSigningCert | Select-Object -First 1

if (-not $cert) {
    throw "No code signing certificate found in the current user's certificate store."
}

Set-AuthenticodeSignature -FilePath $ScriptPath -Certificate $cert -TimestampServer "http://tsa.open-tsa.eu"

$signature = Get-AuthenticodeSignature -FilePath $ScriptPath

if ($signature.Status -eq "Valid") {
    Write-Output "$ScriptPath signed successfully and verified as Valid."
} else {
    Write-Output "Signing completed, but verification returned: $($signature.Status)"
}

where:

  • Get-ChildItem -CodeSigningCert finds the first available code-signing certificate in the personal store.
  • The script signs the file and immediately re-verifies it, confirming the signature was applied correctly.
Consolepowershell
./sign-and-verify.ps1 -ScriptPath "C:\Scripts\deploy.ps1"
Outputplaintext
deploy.ps1 signed successfully and verified as Valid.

Practical Administration: Automated Script Tamper and Integrity Audit

In regulated environments where execution policies are set to AllSigned, administrators must audit operational directories for unsigned scripts, broken signatures, or tampered code before triggering automated execution pipelines.

audit-script-signatures.ps1powershell
param (
    [string]$Directory = "C:\ProductionScripts"
)

$files = Get-ChildItem -Path $Directory -Filter "*.ps1" -Recurse

$auditResults = foreach ($file in $files) {
    $sig = Get-AuthenticodeSignature -FilePath $file.FullName
    
    $compliance = switch ($sig.Status) {
        "Valid"        { "Compliant" }
        "NotSigned"    { "BLOCKED: Unsigned File" }
        "HashMismatch" { "DANGER: File Tampered / Corrupted" }
        Default        { "WARNING: Untrusted ($($sig.Status))" }
    }

    [PSCustomObject]@{
        FileName   = $file.Name
        Compliance = $compliance
        Status     = $sig.Status
        Signer     = if ($sig.SignerCertificate) { $sig.SignerCertificate.Subject } else { "None" }
    }
}

$auditResults | Format-Table -AutoSize

where:

  • Get-ChildItem -Filter "*.ps1" recursively collects all candidate PowerShell scripts.
  • switch ($sig.Status) evaluates signature states to assign operational compliance labels.
  • HashMismatch explicitly flags scripts whose contents have changed since they were signed.
Consolepowershell
./audit-script-signatures.ps1 -Directory "C:\ProductionScripts"
Outputplaintext
FileName         Compliance                        Status       Signer
--------         ----------                        ------       ------
Deploy.ps1       Compliant                         Valid        CN=PowerShellInternalSigner
HealthCheck.ps1  DANGER: File Tampered / Corrupted HashMismatch CN=PowerShellInternalSigner
AdHocPatch.ps1   BLOCKED: Unsigned File            NotSigned    None
Navigation