top of page
Writer's pictureTenaka

PowerShell Code Signing with a Self-Signed Certificate



Hey PowerShell enthusiasts! Ever wondered how to beef up your script security? Not every system gets the luxury of a Certificate Authority (CA)? Imagine your scheduled management scripts getting messed around by that one admin who loves tinker or worse, some bad actors. Today, let's tackle that risk head-on! We're diving into the world of self-signed certificates and code signing to keep your scripts safe and sound.


Creating self-signed certificates for PowerShell script validation involves generating digital certificates locally and without relying on a Certificate Authority (CA). Using PowerShell's New-SelfSignedCertificate cmdlet, parameters like Subject and KeyUsage are specified. This process allows script integrity through code signing.

 

Once created, the certificate can be used to digitally sign scripts with the `Set-AuthenticodeSignature` cmdlet, providing a level of assurance about the script's legitimacy and origin.

 

Self-signed certificates may lack third-party validation, they boost script security by mitigating the risks of unauthorized changes. Still, be cautious; mishandling self-signed certificates could introduce vulnerabilities. Properly document and securely distribute certificates to maintain signed PowerShell script integrity in controlled environments.


This guide is geared towards Active Directory Domains lacking a CA and DevOps keen on signing their PowerShell scripts. Don't worry; we're all about good practices here! To get started, make sure you have an offline Windows Server for crafting your Self-Signed certificate, a Windows 11 client (not extensively tested, but should work), and a separate client for testing the signed scripts with Admin access for tweaking Group Policy and importing certificates into the local machine store.


Less chat more script.....


Certificate Server

Here are the key snippets from the script – the ones that matter. The script is downloadable from Github.



Declare working directories, either create the directories or allow the script to, not forgetting to add scripts that need signing to "C:\_PSScripts\".

$certExport = "C:\_Certs\"

$ScriptRepo = "C:\_PSScripts\"


Set parameters.

$params = @{

Subject = 'Self Signed PS Code Signing'

DnsName = 'Self@Tenaka.net'

FriendlyName = 'Self Signed PS Code Signing'

NotAfter = (Get-Date).AddYears(5)

Type = 'CodeSigning'

CertStoreLocation = 'cert:\CurrentUser\My'

KeyUsage = 'DigitalSignature'

KeyAlgorithm = 'RSA'

KeyLength = 2048

HashAlgorithm = 'sha256'

}


Create a new self-signed certificate based on the above parameters and send the details to 'newCodeSigningCert' variable for reference later.

New-SelfSignedCertificate @params -OutVariable newCodeSigningCert


Export the public key to the file system.

Export-Certificate -Cert "cert:\CurrentUser\My\$($newCodeSigningCert.Thumbprint)" -FilePath "$($certExport)\CodeSigning.cer"


Re-import certificate into Trusted Root otherwise it's not possible to validate any signed scripts.

Import-Certificate -FilePath "$($certExport)\CodeSigning.cer" -Cert Cert:\LocalMachine\root

 

Sign all scripts in C:\_PSScripts using a Foreach loop

$gtPSscripts = Get-ChildItem -Path $ScriptRepo -filter *.ps1 -Recurse -Force

foreach ($PSscriptItem in $gtPSscripts)

{Set-AuthenticodeSignature $PSscriptItem.fullname -Certificate (Get-ChildItem "cert:\CurrentUser\My\$($newCodeSigningCert.Thumbprint)" -CodeSigningCert)}


And there you have it! Snag those signed scripts and the exported certificate (.cer), then copy them over to the test client. Easy peasy!


Check out any of the signed scripts, and you'll spot a signature block appended to the script.


# SIG # Begin signature block

# MIIFrQYJKoZIhvcNAQcCoIIFnjCCBZoCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB

# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR

# vhJhRK4rqe9AhAcGnbPDQg37+EgaN93UzTn2YIOVmbFrQcOwQfDJEzzVOrkLKJdX

# yjdMD070/gJajAELBJDoxsY=

# SIG # End signature block


Test the Signed Scripts on a Client

Let's assume the freshly signed scripts and certificate file reside in the same directories. Now open PowerShell with admin rights and execute the following commands.


Declare the working directories.

$certExport = "C:\_Certs\"

$ScriptRepo = "C:\_PSScripts\"


Import the certificate into the Trusted Root LocalMachine Certificate store.

Import-Certificate -FilePath "$($certExport)\CodeSigning.cer" -Cert Cert:\LocalMachine\root

To prevent the following prompt:

Do you want to run software from this untrusted publisher?

File C:\_PSScripts\gwmi-signed.ps1 is published by CN=Self Signed PS Code Signing and is not trusted on your system. Only

run scripts from trusted publishers.

[V] Never run [D] Do not run [R] Run once [A] Always run [?] Help (default is "D"): A


Import the certificate into the Trusted Publishers LocalMachine Certificate store to prevent any prompts when executing the scripts.

Import-Certificate -FilePath "$($certExport)\CodeSigning.cer" -Cert Cert:\LocalMachine\AuthRoot


Launch Group Policy Editor or gpedit.msc.


Browse to Computer Configuration, Administrative Templates, Windows Components, Windows PowerShell


Enable 'Turn on Script Execution', select 'Allow Only Signed Scritps' in the drop-down and click OK.


Run 'gpupdate /force' to apply the settings.


If your scripts have a digital signature using your own certificate, they'll run smoothly in PowerShell. But the ones that aren't signed won't work.


Perfect Script Security... mostly.

Scripts that are signed and then updated without re-signing won't run either and you'll receive the error below.


 .\gwmi-signed.ps1

.\gwmi-signed.ps1 : File C:\_PSScripts\gwmi-signed.ps1 cannot be loaded. The file C:\Certs\gwmi-signed.ps1 is not digitally signed. You cannot run this script on the current system. For more information about running scripts and setting execution policy.


Bypassing the Execution Policy from PowerShell isn't possible.

Set-ExecutionPolicy -ExecutionPolicy Bypass

Execution Policy Change

The execution policy helps protect you from scripts that you do not trust. Changing the execution policy might expose you to the security risks

[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "N"): y

Set-ExecutionPolicy : Windows PowerShell updated your execution policy successfully, but the setting is overridden by a policy defined at a more specific scope. Due to the override, your shell will retain its current effective


ReadMe: PowerShell_ISE doesn't impose any limitations or restrictions. Unlike other environments, it doesn't enforce the Execution Policy, allowing the execution of any script, whether signed or not.


Keep it Secret, Keep it Safe

A PFX certificate, also called PKCS#12 or P12, is a file format used for keeping and moving cryptographic stuff like private keys and their matching public key certificates. It provides a secure way to store and share these sensitive elements.

A PFX file typically includes:

  • Private Key

  • Public Key Certificate

  • Certificate Chain

  • Password Protection


Once you use the New-SelfSignedCertificate command, the resulting certificate comes with both the public and private keys and can be exported as a PFX file containing the private key – basically, the whole shebang. That's why it's crucial to keep the signing server offline and well-guarded. It's also a good idea to back up the certificate, just for safety or to migrate to another host.


The following commands will do just that


Create a secure string password.

$CertPassword = ConvertTo-SecureString -String "ChangeME1234" -Force -AsPlainText


Export the private key as a pfx and password protect.

Export-PfxCertificate -Cert "cert:\CurrentUser\My\$($newCodeSigningCert.Thumbprint)" -FilePath "$($certExport)\selfsigncert.pfx" -Password $CertPassword


Happy scripting!

Remember, signing your PowerShell scripts with a self-signed certificate adds an extra layer of security to your code. Stay vigilant, keep those scripts locked and loaded with your personalized signature, and code on with confidence!


Thanks for your time, really appreciate it! Take care and goodbye!




216 views0 comments
bottom of page