How To: Self Signed PowerShell Scripts

Posted in Guide on Apr 15, 2019 by Zach Schneider

I've grown so accustomed to to using Linux shells, that using the Windows Command Prompt has become bothersome. Fortunately, PowerShell comes closer to the *nix shell experience than the Command Prompt ever will.

PowerShell scripts are much more powerful than DOS-era batch scripts, but running your own scripts (unsigned, that is), requires you to fully disable security measures that protect your computer from running potentially dangerous code. To avoid this, one can sign their own scripts. There are companies out there that you can pay a regular fee to, in return for a certificate that allows you to sign code. This is great for people who distribute their code publicly, however, a self-signed CA certificate will work just fine for local code.

There are two methods for generating a self-signed code signing certificate. The Windows 10 method is the recommended one, although, there is the makecert.exe method for Windows 7.

Windows 10 Method

Create a Personal Certificate

Windows 10 PowerShell has a built-in cmdlet to create self-signed certificates. This is simpler than the deprecated makecert.exe method. Open PowerShell as Administrator (Win+X, A), and run the command:

PS > $codeCertSubject="CN=My Code Signing Certificate"
PS > New-SelfSignedCertificate `
-CertStoreLocation cert:\CurrentUser\My `
-Subject $codeCertSubject `
-KeyAlgorithm RSA `
-KeyLength 2048 `
-HashAlgorithm sha256 `
-Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" `
-KeyExportPolicy Exportable `
-KeyUsage DigitalSignature `
-Type CodeSigningCert 

This will create a code signing CA certificate, and a personal certificate. Feel free to replace My Code Singing Certificate with a name you like (include CN= at the beginning). This name will be saved in a variable, and will be used in the rest of the guide automatically.

Add the Root to the Trusted Root Certification Authorities Store

This will move the root certificate out of the Intermediate Certification Authorities store, and into the Trusted Root Certification Authorities store. Windows will not trust the personal certificate without doing this first.

PS > Move-Item -Path Cert:\CurrentUser\CA\$(Get-ChildItem -Path Cert:\CurrentUser\CA `
| where { $_.subject -eq $codeCertSubject } | select -ExpandProperty Thumbprint) `
-Destination Cert:\CurrentUser\Root 

Windows will ask you to confirm that you want to add the certificate. Once you confirm, skip down to Confirm Availability of Code Signing Certificates

Deprecated Method (using makecert.exe)

Install the Windows SDK

In order to use makecert.exe, we must first install the Windows SDK. For Windows 7, download it here. For Windows 10, download it here.

After installing the Windows SDK, add the SDK directory to the PATH Environment variable: Control Panel > System and Security > System > Advanced system settings > Environment Variables. The process differs slightly between Windows 7 & 10:

For Windows 7: Click PATH under User variables, then click Edit..., than add ;C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1\bin.

For Windows 10: Click Path under User variables, then click Edit..., then New, and type in C:\Program Files (x86)\Windows Kits\10\bin\x64.

Create the Certificate Authority

Open PowerShell as Administrator, cd to a directory where you'll store your CA keys, and run:

makecert -n "CN=My Code Signing Certificate Root" -a sha256 -eku 1.3.6.1.5.5.7.3.3 -r -sv root.pvk root.cer -ss Root -sr localMachine 

This will create the CA certificate, and add it to the certificate to the Trusted Root Certification Authorities store.

Create a Personal Certificate

In order to sign scripts, you will need a personal certificate. We can sign a certificate using the CA we just created.

makecert -pe -n "CN=$env:UserName" -ss MY -a sha256 -eku 1.3.6.1.5.5.7.3.3 -iv root.pvk -ic root.cer 

Confirm Availability of Code Signing Certificates

Now that we have created a personal self-signed code signing certificate using one of the methods above, we will confirm that it is available in the Personal certificate store.

PS > Get-ChildItem cert:\CurrentUser\My -codesigning 

This should return something like to following, if there are code signing certificates available:

PSParentPath: Microsoft.PowerShell.Security\Certificate::CurrentUser\My

Thumbprint                               Subject
----------                               ------- 
4451E1B12E9E3EB00D9EF46ABBB616010E39DF1A CN=My Code Signing Certificate 

If this is blank, or you do not see the certificate you just created, you will be unable to sign scripts.

Allow Execution of Signed Scripts

By default, Windows disables execution of all PowerShell scripts. This command will set the execution policy to allow scripts that have been signed by a trusted publisher:

PS > Set-ExecutionPolicy AllSigned 

Confirm that you want to change the execution policy.

Sign a PowerShell Script

Now that the certificate is generated, and trusted, all that is left is to actually sign code. To test this, create a script that will list some IP configuration details of the computer.

PS > Get-WMIobject win32_networkadapterconfiguration | where {$_.IPEnabled -eq "True"} | Select-Object pscomputername,ipaddress,defaultipgateway,ipsubnet,dnsserversearchorder | format-Table -Auto 

Try running the script, and you'll get an error:

PS > .\test.ps1 .\test.ps1 : File .\test.ps1 cannot be loaded. The file .\test.ps1 is not digitally signed. You cannot run this script on the current system. For more information about running scripts and setting execution policy, see about_Execution_Policies at https:/go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:1
+ .\test.ps1
+ ~~~~~~~~~~
+ CategoryInfo : SecurityError: (:) [], PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess 

Now, sign the script:

PS > Set-AuthenticodeSignature .\test.ps1 `
@(Get-ChildItem -Path Cert:\CurrentUser\My `
| where { $_.subject -eq $codeCertSubject })[0] 

You'll get the following output if the script was signed successfully:

SignerCertificate                        Status Path
-----------------                        ------ ----
4451E1B12E9E3EB00D9EF46ABBB616010E39DF1A Valid  test.ps1

Attempt to run the script again, and Windows will ask you if you want to run the script from an untrusted publisher. Choose "Always run" to allow scripts signed by your certificate, and you should see your IP configuration.

Success!