Skip to content

Instantly share code, notes, and snippets.

@jborean93
Last active April 15, 2024 23:53
Show Gist options
  • Save jborean93/f9029a6561916e368bd23fc47757b4c8 to your computer and use it in GitHub Desktop.
Save jborean93/f9029a6561916e368bd23fc47757b4c8 to your computer and use it in GitHub Desktop.
Behaviour of signed PowerShell scripts

PowerShell Code Signing

This is to try and document the behaviour around PowerShell code signing.

Setup

The following code can be used to set up this scenario. This must be run as an administrator in Windows PowerShell.

Note: PowerShell uses implicit remoting for the New-SelfSignedCertificate which breaks the constains serialization. You must run this on Windows PowerShell.

$testPrefix = 'SelfSignedTest'
$certPassword = ConvertTo-SecureString -String 'SecurePassword123' -Force -AsPlainText

$enhancedKeyUsage = [Security.Cryptography.OidCollection]::new()
$null = $enhancedKeyUsage.Add('1.3.6.1.5.5.7.3.3')  # Code Signing

$caParams = @{
    Extension = @(
        [Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($true, $false, 0, $true),
        [Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new('KeyCertSign', $false),
        [Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension ]::new($enhancedKeyUsage, $false)
    )
    CertStoreLocation = 'Cert:\CurrentUser\My'
    NotAfter = (Get-Date).AddYears(2)
    Type = 'Custom'
}
$caRoot = New-SelfSignedCertificate @caParams -Subject "CN=$testPrefix-Root"
$caIntermediate = New-SelfSignedCertificate @caParams -Subject "CN=$testPrefix-Intermediate" -Signer $caRoot

$certParams = @{
    CertStoreLocation = 'Cert:\CurrentUser\My'
    KeyUsage = 'DigitalSignature'
    TextExtension = @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")
    Type = 'Custom'
}
$certSigned = New-SelfSignedCertificate @certParams -Subject "CN=$testPrefix-Signed" -Signer $caIntermediate
$certSelfSigned = New-SelfSignedCertificate @certParams -Subject "CN=$testPrefix-SelfSigned"

$null = $caRoot | Export-PfxCertificate -Password $certPassword -FilePath 'root.pfx'
$caRoot | Remove-Item

$null = $caIntermediate | Export-PfxCertificate -Password $certPassword -FilePath 'intermediate.pfx'
$caIntermediate | Remove-Item

$null = $certSigned | Export-PfxCertificate -Password $certPassword -FilePath 'signed.pfx'
$certSigned | Remove-Item

$null = $certSelfSigned | Export-PfxCertificate -Password $certPassword -FilePath 'self_signed.pfx'
$certSelfSigned | Remove-Item

$scriptContent = 'echo "hello world"'
Set-Content -Path ps_ca_signed.ps1 -Value $scriptContent
Set-Content -Path ps_self_signed.ps1 -Value $scriptContent
Set-Content -Path ps_unsigned.ps1 -Value $scriptContent

$null = Set-AuthenticodeSignature -Certificate $certSigned -FilePath ps_ca_signed.ps1
$null = Set-AuthenticodeSignature -Certificate $certSelfSigned -FilePath ps_self_signed.ps1

Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope Process -Force

# Make sure the policy is in place (this should fail)
.\ps_unsigned.ps1

Scenarios

In the same PowerShell session as above you can run the following scenarios

Scenario Trusted Roots Trusted Publishers Works
Root in Trusted Root CA root - Yes (prompt to trust¹)
Root in Trusted Publisher - root No (untrusted chain)
Root in Trusted Root and Publisher root root Yes (prompt to trust¹)
Intermediate in Trusted Root CA intermediate - No (untrusted chain)
Intermediate in Trusted Publisher - intermediate No (untrusted chain)
Intermediate in Trusted Root and Publisher intermediate intermediate No (untrusted chain)
Cert in Trusted Root CA cert - No (untrusted chain)
Cert in Trusted Publisher - cert No (untrusted chain)
Cert in Trusted Publisher with Trusted Root root cert Yes
Cert in Trusted Publisher with Trusted Intermediate intermediate cert No (untrusted chain)
Self Signed Untrusted - - No (untrusted chain)
Self Signed in Trusted Root CA self_signed - Yes (prompt to trust¹)
Self Signed in Trusted Publisher - self_signed No (untrusted chain)
Self Signed in Trusted Root and Publisher self_signed self_signed Yes

¹ A will place the cert into Cert:\CurrentUser\TrustedPublisher if A is selected in the prompt.

If running these scenarios in a new session make sure you set the execution policy and define $certPassword again.

Root in Trusted Root CA

$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\Root -FilePath root.pfx
try {
    .\ps_ca_signed.ps1
}
finally {
    $certPath | Remove-Item
}
Do you want to run software from this untrusted publisher?
File C:\temp\cert\ps_ca_signed.ps1 is published by CN=SelfSignedTest-Signed 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"):

hello world

Root in Trusted Publisher

$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\TrustedPublisher -FilePath root.pfx
try {
    .\ps_ca_signed.ps1
}
finally {
    $certPath | Remove-Item
}
.\ps_ca_signed.ps1 : File C:\temp\cert\ps_ca_signed.ps1 cannot be loaded. A certificate chain processed, but
terminated in a root certificate which is not trusted by the trust provider.
At line:2 char:5
+     .\ps_ca_signed.ps1
+     ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

Root in Trusted Root and Publisher

$rootPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\Root -FilePath root.pfx
$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\TrustedPublisher -FilePath root.pfx
try {
    .\ps_ca_signed.ps1
}
finally {
    $rootPath | Remove-Item
    $certPath | Remove-Item
}
Do you want to run software from this untrusted publisher?
File C:\temp\cert\ps_ca_signed.ps1 is published by CN=SelfSignedTest-Signed 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"):

hello world

Intermediate in Trusted Root CA

$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\Root -FilePath intermediate.pfx
try {
    .\ps_ca_signed.ps1
}
finally {
    $certPath | Remove-Item
}
.\ps_ca_signed.ps1 : File C:\temp\cert\ps_ca_signed.ps1 cannot be loaded. A certificate chain processed, but
terminated in a root certificate which is not trusted by the trust provider.
At line:2 char:5
+     .\ps_ca_signed.ps1
+     ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

Intermediate in Trusted Publisher

$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\TrustedPublisher -FilePath intermediate.pfx
try {
    .\ps_ca_signed.ps1
}
finally {
    $certPath | Remove-Item
}
.\ps_ca_signed.ps1 : File C:\temp\cert\ps_ca_signed.ps1 cannot be loaded. A certificate chain processed, but
terminated in a root certificate which is not trusted by the trust provider.
At line:2 char:5
+     .\ps_ca_signed.ps1
+     ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

Intermediate in Trusted Root and Publisher

$rootPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\Root -FilePath intermediate.pfx
$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\TrustedPublisher -FilePath intermediate.pfx
try {
    .\ps_ca_signed.ps1
}
finally {
    $rootPath | Remove-Item
    $certPath | Remove-Item
}
.\ps_ca_signed.ps1 : File C:\temp\cert\ps_ca_signed.ps1 cannot be loaded. A certificate chain processed, but
terminated in a root certificate which is not trusted by the trust provider.
At line:2 char:5
+     .\ps_ca_signed.ps1
+     ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

Cert in Trusted Root CA

$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\Root -FilePath signed.pfx
try {
    .\ps_ca_signed.ps1
}
finally {
    $certPath | Remove-Item
}
.\ps_ca_signed.ps1 : File C:\temp\cert\ps_ca_signed.ps1 cannot be loaded. A certificate chain processed, but
terminated in a root certificate which is not trusted by the trust provider.
At line:2 char:5
+     .\ps_ca_signed.ps1
+     ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

Cert in Trusted Publisher

$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\TrustedPublisher -FilePath signed.pfx
try {
    .\ps_ca_signed.ps1
}
finally {
    $certPath | Remove-Item
}
.\ps_ca_signed.ps1 : File C:\temp\cert\ps_ca_signed.ps1 cannot be loaded. A certificate chain processed, but
terminated in a root certificate which is not trusted by the trust provider.
At line:2 char:5
+     .\ps_ca_signed.ps1
+     ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

Cert in Trusted Publisher with Trusted Root

$rootPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\Root -FilePath root.pfx
$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\TrustedPublisher -FilePath signed.pfx
try {
    .\ps_ca_signed.ps1
}
finally {
    $rootPath | Remove-Item
    $certPath | Remove-Item
}
hello world

Cert in Trusted Publisher with Trusted Intermediate

$rootPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\Root -FilePath intermediate.pfx
$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\TrustedPublisher -FilePath signed.pfx
try {
    .\ps_ca_signed.ps1
}
finally {
    $rootPath | Remove-Item
    $certPath | Remove-Item
}
.\ps_ca_signed.ps1 : File C:\temp\cert\ps_ca_signed.ps1 cannot be loaded. A certificate chain processed, but
terminated in a root certificate which is not trusted by the trust provider.
At line:2 char:5
+     .\ps_ca_signed.ps1
+     ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

Self Signed Untrusted

.\ps_self_signed.ps1
.\ps_self_signed.ps1 : File C:\temp\cert\ps_self_signed.ps1 cannot be loaded. A certificate chain processed, but
terminated in a root certificate which is not trusted by the trust provider.
At line:1 char:1
+ .\ps_self_signed.ps1
+ ~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

Self Signed in Trusted Root CA

$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\Root -FilePath self_signed.pfx
try {
    .\ps_self_signed.ps1
}
finally {
    $certPath | Remove-Item
}
Do you want to run software from this untrusted publisher?
File C:\temp\cert\ps_self_signed.ps1 is published by CN=SelfSignedTest-SelfSigned 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"):

hello world

Self Signed in Trusted Publisher

$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\TrustedPublisher -FilePath self_signed.pfx
try {
    .\ps_self_signed.ps1
}
finally {
    $certPath | Remove-Item
}
.\ps_self_signed.ps1 : File C:\temp\cert\ps_self_signed.ps1 cannot be loaded. A certificate chain processed, but
terminated in a root certificate which is not trusted by the trust provider.
At line:2 char:5
+     .\ps_self_signed.ps1
+     ~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

Self Signed in Trusted Root and Publisher

$rootPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\Root -FilePath self_signed.pfx
$certPath = Import-PfxCertificate -Password $certPassword -CertStoreLocation Cert:\LocalMachine\TrustedPublisher -FilePath self_signed.pfx
try {
    .\ps_self_signed.ps1
}
finally {
    $rootPath | Remove-Item
    $certPath | Remove-Item
}
hello world

Summary

In the end to to trust a signed certificate you need to do the following

  • The root cert in the chain must be in the Trusted Root CA store
    • This must be the root cert in the chain, you cannot just trust any intermediate certs
    • Any intermediate certs must be in the Intermediate CA store but that seems to be down automatically when you import the root
  • The final cert (the one that signed the script) must be in the Trusted Publishers store

If the signing cert is not in the Trusted Publishers store then PowerShell will prompt with the following:

Do you want to run software from this untrusted publisher?
File C:\temp\cert\ps_ca_signed.ps1 is published by CN=SelfSignedTest-Signed 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"):

Here is what each of the options do

  • [V] Never run
    • Does not run the script
    • Places the signing cert into Cert:\CurrentUser\Disallowed (Untrusted Certificates\Certificates)
    • Due to the cert being in the Disallowed store subsequence runs will fail without any prompt
    • Errors with the below
.\ps_ca_signed.ps1 : File C:\temp\cert\ps_ca_signed.ps1 cannot be loaded because its operation is blocked by software
restriction policies, such as those created by using Group Policy.
At line:1 char:1
+ .\ps_ca_signed.ps1
+ ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess
  • [D] Do not run
    • Like V but does not place the cert into the Disallowed store
    • Subsequence runs will continue to prompt for desired action
    • Errors with the below
.\ps_ca_signed.ps1 : File C:\temp\cert\ps_ca_signed.ps1 cannot be loaded because you opted not to run this software
now.
At line:1 char:1
+ .\ps_ca_signed.ps1
+ ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess
  • [R] Run once

    • Runs the script
    • Subsequent runs will continue to prompt for desired action
  • [A] Always run

    • Runs the script
    • Places the cert into Cert:\CurrentUser\TrustedPublisher
    • Subsequence runs will run automatically as the cert is now trusted

PSGet Code Signing

This is to try and document the behaviour around PowerShellGet/PSResourceGet code signing publisher behaviour.

Setup

The following code can be used to set up this scenario. This must be run as an administrator in Windows PowerShell.

Note: PowerShell uses implicit remoting for the New-SelfSignedCertificate which breaks the constains serialization. You must run this on Windows PowerShell.

$testPrefix = 'SignedTest'
$certPassword = ConvertTo-SecureString -String 'SecurePassword123' -Force -AsPlainText

$enhancedKeyUsage = [Security.Cryptography.OidCollection]::new()
$null = $enhancedKeyUsage.Add('1.3.6.1.5.5.7.3.3')  # Code Signing

$caParams = @{
    Extension = @(
        [Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($true, $false, 0, $true),
        [Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new('KeyCertSign', $false),
        [Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension ]::new($enhancedKeyUsage, $false)
    )
    CertStoreLocation = 'Cert:\CurrentUser\My'
    NotAfter = (Get-Date).AddYears(2)
    Type = 'Custom'
}
$caRoot1 = New-SelfSignedCertificate @caParams -Subject "CN=$testPrefix-Root1"
$caRoot2 = New-SelfSignedCertificate @caParams -Subject "CN=$testPrefix-Root2"
$caIntermediate1Root1 = New-SelfSignedCertificate @caParams -Subject "CN=$testPrefix-Root1-Intermediate1" -Signer $caRoot1
$caIntermediate2Root1 = New-SelfSignedCertificate @caParams -Subject "CN=$testPrefix-Root1-Intermediate2" -Signer $caRoot1
$caIntermediate1Root2 = New-SelfSignedCertificate @caParams -Subject "CN=$testPrefix-Root2-Intermediate1" -Signer $caRoot2

$certParams = @{
    CertStoreLocation = 'Cert:\CurrentUser\My'
    KeyUsage = 'DigitalSignature'
    TextExtension = @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")
    Type = 'Custom'
}
$certSigned1Intermediate1Root1 = New-SelfSignedCertificate @certParams -Subject "CN=$testPrefix-Root1-Intermediate1-Signed" -Signer $caIntermediate1Root1
$certSigned2Intermediate1Root1 = New-SelfSignedCertificate @certParams -Subject "CN=$testPrefix-Root1-Intermediate1-Signed" -Signer $caIntermediate1Root1
$certSigned3Intermediate1Root1 = New-SelfSignedCertificate @certParams -Subject "CN=$testPrefix-Other-Root1-Intermediate1-Signed" -Signer $caIntermediate1Root1
$certSignedIntermediate2Root1 = New-SelfSignedCertificate @certParams -Subject "CN=$testPrefix-Root1-Intermediate2-Signed" -Signer $caIntermediate2Root1
$certSignedIntermediate1Root2 = New-SelfSignedCertificate @certParams -Subject "CN=$testPrefix-Root2-Intermediate1-Signed" -Signer $caIntermediate1Root2

$null = $caRoot1 | Export-PfxCertificate -Password $certPassword -FilePath 'root1.pfx'
$caRoot1 | Remove-Item

$null = $caRoot2 | Export-PfxCertificate -Password $certPassword -FilePath 'root2.pfx'
$caRoot2 | Remove-Item

$null = $caIntermediate1Root1 | Export-PfxCertificate -Password $certPassword -FilePath 'intermediate1-root1.pfx'
$caIntermediate1Root1 | Remove-Item

$null = $caIntermediate1Root2 | Export-PfxCertificate -Password $certPassword -FilePath 'intermediate1-root2.pfx'
$caIntermediate1Root2 | Remove-Item

$null = $caIntermediate2Root1 | Export-PfxCertificate -Password $certPassword -FilePath 'intermediate2-root1.pfx'
$caIntermediate2Root1 | Remove-Item

$null = $certSigned1Intermediate1Root1 | Export-PfxCertificate -Password $certPassword -FilePath 'signed1-intermediate1-root1.pfx'
$certSigned1Intermediate1Root1 | Remove-Item

$null = $certSigned2Intermediate1Root1 | Export-PfxCertificate -Password $certPassword -FilePath 'signed2-intermediate1-root1.pfx'
$certSigned2Intermediate1Root1 | Remove-Item

$null = $certSigned3Intermediate1Root1 | Export-PfxCertificate -Password $certPassword -FilePath 'signed3-intermediate1-root1.pfx'
$certSigned3Intermediate1Root1 | Remove-Item

$null = $certSignedIntermediate2Root1 | Export-PfxCertificate -Password $certPassword -FilePath 'signed-intermediate2-root1.pfx'
$certSignedIntermediate2Root1 | Remove-Item

$null = $certSignedIntermediate1Root2 | Export-PfxCertificate -Password $certPassword -FilePath 'signed-intermediate1-root2.pfx'
$certSignedIntermediate1Root2 | Remove-Item

$rootStore = Get-Item Cert:\LocalMachine\Root
try {
    $rootStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
    $rootStore.Add($caRoot1)
    $rootStore.Add($caRoot2)
}
finally {
    $rootStore.Dispose()
}

$repoParams = @{
    Name = 'SignatureTest'
    SourceLocation = Join-Path $pwd 'Source'
    PublishLocation = Join-Path $pwd 'Publish'
    InstallationPolicy = 'Trusted'
}

if (Test-Path -LiteralPath $repoParams.SourceLocation) {
    Remove-Item -LiteralPath $repoParams.SourceLocation -Recurse -Force
}
New-Item -Path $repoParams.SourceLocation -ItemType Directory | Out-Null

if (Test-Path -LiteralPath $repoParams.PublishLocation) {
    Remove-Item -LiteralPath $repoParams.PublishLocation -Recurse -Force
}
New-Item -Path $repoParams.PublishLocation -ItemType Directory | Out-Null

if (Get-PSRepository -Name $repoParams.Name -ErrorAction SilentlyContinue) {
    Unregister-PSRepository -Name $repoParams.Name
}
Register-PSRepository @repoParams

if (Get-PSResourceRepository -name $repoParams.Name -ErrorAction SilentlyContinue) {
    Unregister-PSResourceRepository -Name $repoParams.Name
}
Register-PSResourceRepository -Name $repoParams.Name -Trusted -Uri $repoParams.SourceLocation

$moduleGuid = [Guid]::NewGuid()
$moduleName = 'SigningTest'
$moduleTemp = Join-Path $pwd $moduleName

@(
    [PSCustomObject]@{
        Scenario = 'Unsigned'
        Certificate = $null
    }
    [PSCustomObject]@{
        Scenario = 'Cert1Intermediate1Root1'
        Certificate = $certSigned1Intermediate1Root1
    }
    [PSCustomObject]@{
        Scenario = 'Cert2Intermediate1Root1'
        Certificate = $certSigned2Intermediate1Root1
    }
    [PSCustomObject]@{
        Scenario = 'Cert3Intermediate1Root1'
        Certificate = $certSigned3Intermediate1Root1
    }
    [PSCustomObject]@{
        Scenario = 'Intermediate2Root1'
        Certificate = $certSignedIntermediate2Root1
    }
    [PSCustomObject]@{
        Scenario = 'Intermediate1Root2'
        Certificate = $certSignedIntermediate1Root2
    }
) | ForEach-Object {
    $info = $_

    '1.0.0', '1.1.0' | ForEach-Object {
        if (Test-Path -LiteralPath $moduleTemp) {
            Remove-Item -LiteralPath $moduleTemp -Recurse -Force
        }
        New-Item -Path $moduleTemp -ItemType Directory | Out-Null

        $moduleManifest = @{
            Path = Join-Path $moduleTemp "$($moduleName).psd1"
            Guid = $moduleGuid
            Author = 'Author'
            Description = $info.Scenario
            RootModule = "$($moduleName).psm1"
            ModuleVersion = $_
            PowerShellVersion = '5.1'
            FunctionsToExport = 'Test-Signing'
        }
        New-ModuleManifest @moduleManifest
        Set-Content -LiteralPath (Join-Path $moduleTemp "$($moduleName).psm1") -Value @"
Function Test-Signing {
    'Scenario: $($info.Scenario), Version: $_'
}

Export-ModuleMember -FunctionsToExport Test-Signing
"@

        if ($info.Certificate) {
            "psd1", "psm1" | ForEach-Object {
                $path = Join-Path $moduleTemp "$moduleName.$_"

                $sigParams = @{
                    Certificate = $info.Certificate
                    FilePath = $path
                    HashAlgorithm = 'SHA256'
                    TimestampServer = 'http://timestamp.digicert.com'
                }
                $null = Set-AuthenticodeSignature @sigParams
            }
        }

        Publish-Module -Path $moduleTemp -Repository $repoParams.Name
        $nupkgPath = Join-Path $repoParams.PublishLocation "$moduleName.$_.nupkg"
        $destPath = Join-Path $pwd "$($info.Scenario)-$_.nupkg"
        Move-Item -LiteralPath $nupkgPath -Destination $destPath -Force
    }
}

Remove-Item -LiteralPath $moduleTemp -Force -Recurse

Scenarios

In the same PowerShell session as above you can run the following scenarios

Scenario Behaviour
Same Certificate Installs no prompt
Same Intermediary Installs no prompt
Same Intermediary Different Subject 2.2.5
Same Root Fails, requires -SkipPublisherCheck
Different Root Fails, requires -SkipPublisherCheck

Same Certificate

Copy-Item -Path Cert1Intermediate1Root1-1.0.0.nupkg -Destination Source/SigningTest.1.0.0.nupkg
Copy-Item -Path Cert1Intermediate1Root1-1.1.0.nupkg -Destination Source/SigningTest.1.1.0.nupkg

Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.0.0 -Scope CurrentUser
Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.1.0 -Scope CurrentUser

# Cleanup
$pwshFolder = if ($PSEdition -eq 'Core') { 'PowerShell' } else { 'WindowsPowerShell' }
$modulePath = [System.IO.Path]::Combine(([System.Environment]::GetFolderPath('MyDocuments')), $pwshFolder, 'Modules', 'SigningTest')
Remove-Item -Path Publish/*.nupkg -Force
Remove-Item -LiteralPath $modulePath -Force -Recurse

Same Intermediary

Copy-Item -Path Cert1Intermediate1Root1-1.0.0.nupkg -Destination Source/SigningTest.1.0.0.nupkg
Copy-Item -Path Cert2Intermediate1Root1-1.1.0.nupkg -Destination Source/SigningTest.1.1.0.nupkg

Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.0.0 -Scope CurrentUser
Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.1.0 -Scope CurrentUser

# Cleanup
$pwshFolder = if ($PSEdition -eq 'Core') { 'PowerShell' } else { 'WindowsPowerShell' }
$modulePath = [System.IO.Path]::Combine(([System.Environment]::GetFolderPath('MyDocuments')), $pwshFolder, 'Modules', 'SigningTest')
Remove-Item -Path Publish/*.nupkg -Force
Remove-Item -LiteralPath $modulePath -Force -Recurse

Same Intermediary different subject

Copy-Item -Path Cert1Intermediate1Root1-1.0.0.nupkg -Destination Source/SigningTest.1.0.0.nupkg
Copy-Item -Path Cert3Intermediate1Root1-1.1.0.nupkg -Destination Source/SigningTest.1.1.0.nupkg

Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.0.0 -Scope CurrentUser
Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.1.0 -Scope CurrentUser
Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.1.0 -Scope CurrentUser -SkipPublisherCheck

# Cleanup
$pwshFolder = if ($PSEdition -eq 'Core') { 'PowerShell' } else { 'WindowsPowerShell' }
$modulePath = [System.IO.Path]::Combine(([System.Environment]::GetFolderPath('MyDocuments')), $pwshFolder, 'Modules', 'SigningTest')
Remove-Item -Path Publish/*.nupkg -Force
Remove-Item -LiteralPath $modulePath -Force -Recurse

PowerShellGet 2.2.5

PackageManagement\Install-Package : Authenticode issuer 'CN=SignedTest-Other-Root1-Intermediate1-Signed' of the
new module 'SigningTest' with version '1.1.0' from root certificate authority 'CN=SignedTest-Root1' is not
matching with the authenticode issuer 'CN=SignedTest-Root1-Intermediate1-Signed' of the previously-installed
module 'SigningTest' with version '1.0.0' from root certificate authority 'CN=SignedTest-Root1'. If you still want
to install or update, use -SkipPublisherCheck parameter.

Same Root

Copy-Item -Path Intermediate1Root1-1.0.0.nupkg -Destination Source/SigningTest.1.0.0.nupkg
Copy-Item -Path Intermediate2Root1-1.1.0.nupkg -Destination Source/SigningTest.1.1.0.nupkg

Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.0.0 -Scope CurrentUser
Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.1.0 -Scope CurrentUser
Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.1.0 -Scope CurrentUser -SkipPublisherCheck

# Cleanup
$pwshFolder = if ($PSEdition -eq 'Core') { 'PowerShell' } else { 'WindowsPowerShell' }
$modulePath = [System.IO.Path]::Combine(([System.Environment]::GetFolderPath('MyDocuments')), $pwshFolder, 'Modules', 'SigningTest')
Remove-Item -Path Publish/*.nupkg -Force
Remove-Item -LiteralPath $modulePath -Force -Recurse

PowerShellGet 2.2.5

PackageManagement\Install-Package : Authenticode issuer 'CN=SignedTest-Root1-Intermediate2-Signed' of the new
module 'SigningTest' with version '1.1.0' from root certificate authority 'CN=SignedTest-Root1' is not matching
with the authenticode issuer 'CN=SignedTest-Root1-Intermediate1-Signed' of the previously-installed module
'SigningTest' with version '1.0.0' from root certificate authority 'CN=SignedTest-Root1'. If you still want to
install or update, use -SkipPublisherCheck parameter.

Different Root

Copy-Item -Path Intermediate1Root1-1.0.0.nupkg -Destination Source/SigningTest.1.0.0.nupkg
Copy-Item -Path Intermediate1Root2-1.1.0.nupkg -Destination Source/SigningTest.1.1.0.nupkg

Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.0.0 -Scope CurrentUser
Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.1.0 -Scope CurrentUser
Install-Module -Name SigningTest -Repository SignatureTest -RequiredVersion 1.1.0 -Scope CurrentUser -SkipPublisherCheck

# Cleanup
$pwshFolder = if ($PSEdition -eq 'Core') { 'PowerShell' } else { 'WindowsPowerShell' }
$modulePath = [System.IO.Path]::Combine(([System.Environment]::GetFolderPath('MyDocuments')), $pwshFolder, 'Modules', 'SigningTest')
Remove-Item -Path Publish/*.nupkg -Force
Remove-Item -LiteralPath $modulePath -Force -Recurse

PowerShellGet 2.2.5

PackageManagement\Install-Package : Authenticode issuer 'CN=SignedTest-Root2-Intermediate1-Signed' of the new
module 'SigningTest' with version '1.1.0' from root certificate authority 'CN=SignedTest-Root2' is not matching
with the authenticode issuer 'CN=SignedTest-Root1-Intermediate1-Signed' of the previously-installed module
'SigningTest' with version '1.0.0' from root certificate authority 'CN=SignedTest-Root1'. If you still want to
install or update, use -SkipPublisherCheck parameter.
@JCervero
Copy link

JCervero commented Nov 3, 2022

Came across this while trying to re-work our code signing processes and I'm wondering if what I want to do is even possible given your testing outcomes. We have an internal CA and the root and intermediate are published to all domain computers. I want our admins and programmers to be able to request their own code signing certificates and then I want code signed with those certs to run (first time, without prompt) on all endpoints without having to publish each cert to the Trusted Publishers store.

You data would seem to indicate that's not possible. Regardless of trusting the chain the signing cert itself either must be published or a user must accept the cert on the first run. Am I missing anything or misinterpreting anything?

@jborean93
Copy link
Author

That's correct, you can ensure the cert is trusted from a CA perspective which ensures it doesn't fail outright and will prompt to trust the publisher. Unfortunately you still need to trust the individual publisher (each cert you issue) on the host that will use it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment