top of page

Start Signing Your Scripts

Sep 11
3 min read

Most PowerShell scripts running in enterprise environments today are unsigned. People think code-signing is hard. It really isn't.

This small blogpost is a small hack you can apply to make it even easier!


Disclaimer: This blogpost assumes you have a code-signing certificate already. This is most likely an internal one from a PKI environment. We will do a follow-up post explaining how this can also work with alternative code-signing solutions out there.


You have to author your scripts somewhere. If you are already doing that in VS Code, great, read on... and if you're not, this might be a reason to start doing it from VS Code. Every script you author has to be saved. and in VS Code there is a way to make signing part of the save action itself, so there's no additional step to take.


The extension


Save and Run is a small VS Code extension that runs a configured command whenever a file matching a pattern gets saved. It passes the command straight into the VS Code terminal, so you keep output and colors instead of a silent background process. You tell it which files to match with a regex, and what to run when they hit disk.

Youcan leverage this functionality to sign your file once you hit the save button.


Wiring it up for code signing


Here's a script that installs the extension and configures it to sign PowerShell and VBScript files on save, using the code signing certificate with the longest longevity that is currently in your personal certificate store:


cmd.exe /c "code.cmd --install-extension wk-j.save-and-run 2> NUL"
$settingsPath = "$env:APPDATA\Code\User\settings.json"
$data = Get-Content -Raw -Path $settingsPath -ErrorAction SilentlyContinue | ConvertFrom-Json

$list = New-Object System.Collections.ArrayList
$list.Add(@{"match"=".ps[dm]{0,1}1$|.vbs$";"cmd"="Set-AuthenticodeSignature -Certificate `$(gci Cert:\CurrentUser\My -CodeSigningCert | Sort-Object -Property NotAfter -Descending | Select-Object -First 1) -TimestampServer http://timestamp.digicert.com -FilePath '`${file}'";"useShortcut"="false";"silent"="false"})
$list.Add(@{"match"=".ps[dm]{0,1}1$|.vbs$";"cmd"="Set-AuthenticodeSignature -Certificate `$(gci Cert:\CurrentUser\My -CodeSigningCert | Sort-Object -Property NotAfter -Descending | Select-Object -First 1) -TimestampServer http://timestamp.digicert.com -FilePath '`${file}'";"useShortcut"="true";"silent"="false"})

$commands = @{"commands"=$list}

if ($data) { $data | Add-Member -MemberType NoteProperty -Name "saveAndRun" -Value $commands -Force }
$data | ConvertTo-Json -Depth 5 | Out-File $settingsPath -Encoding utf8

The actual magic in the script above looks like this :


"saveAndRun": {

        "commands": [

          { "match": ".*",

            "cmd": "echo 'I run for all files. I am configured in user settings.json. Auto-codesigning is configured here as well, which is triggered on saving script files or pressing ctrl+shift+r.'",

            "useShortcut": false,

            "silent": false },

          { "match": ".ps[dm]{0,1}1$|.vbs$",

            "cmd": "set-authenticodesignature -Certificate  $(gci Cert:\\CurrentUser\\My -CodeSigningCert | Sort-Object -Property NotAfter -Descending | Select-Object -First 1) -timestampserver http://timestamp.digicert.com -filepath '${file}'",

            "useShortcut": false,

            "silent": false },

          { "match": ".ps[dm]{0,1}1$|.vbs$",

            "cmd": "set-authenticodesignature -Certificate  $(gci Cert:\\CurrentUser\\My -CodeSigningCert | Sort-Object -Property NotAfter -Descending | Select-Object -First 1) -timestampserver http://timestamp.digicert.com -filepath '${file}'",

            "useShortcut": true,

            "silent": false }

        ] }



A few things worth calling out about what's actually happening here.


  • The regex matches .ps1, .psd1, .psm1, and .vbs files.

  • The certificate lookup pulls every code signing cert out of Cert:\CurrentUser\My, sorts by expiry, and picks the one furthest from expiring, so you don't have to hardcode a thumbprint that breaks the moment you renew.


In addtion, you'll notice we include a call to a timestamp server and it matters more than people think: it means the signature stays valid even after the certificate itself expires, preventing unnecessary failures upon certificate expiration.


Once this is in place, saving the file is the signing action. There's no separate step anymore to take and thus no excuse anymore to not have your scripts signed.


The caveats


This only works if you already have a valid code signing certificate sitting in your personal cert store. This can come from your internal PKI solution, a cloud-pki infra or a code-signing certificate you have purchased from a public vendor.


If you want to start playing/testing with this feature and you have no certificate available ? It's trivial to create a self-signed certificate using powershell. run the code below with admin credentials and you should be good to go :


New-SelfSignedCertificate -CertStoreLocation cert:\CurrentUser\my -Type CodeSigningCert -Subject 'My first signing cert'

Why bother with code-signing if AppControl for Business is not in scope (yet)


Even without ACfB in the picture, code-signing is still valuable. You could already raise your security bar this way with enforcing a powershell AllSigned execution policy.

In addition, doing this exercise will help you take back control over your powershell scripts. Do you really know what scripts are running in your environment now ? Enforcing code-signing is a good way to track them down and re-evaluate them.

 
 
 

Comments


Please reach out to request a demo, create an account, or get assistance

Thanks for submitting! We'll get back to you shortly.

© 2025 by App Control

bottom of page