Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion PowerShell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ Import-Module ./PowerShell/Devolutions.Psign/Devolutions.Psign.psd1
| `Set-PsignSignature` | Sign files using local keys, PFX, cert store, Azure KV, or Trusted Signing |
| `Test-PsignModule` | Validate a module against AllSigned/RemoteSigned execution policy |
| `Protect-PsignModule` | Batch-sign all policy-checked files in a module |
| `Unprotect-PsignSignature` | Strip signature blocks from script files |
| `Unprotect-PsignSignature` | Strip script signature blocks and clear PE signatures |

`Set-PsignSignature -AppendSignature` appends PE Authenticode signatures; without it, PE signing replaces existing signatures. Signature inspection exposes decoded CMS details through the `SignedCms` property when PKCS#7 bytes are available.

## Quick Start

Expand Down
91 changes: 91 additions & 0 deletions PowerShell/tests/PsignSignature.NativeFeatures.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
Set-StrictMode -Version Latest

function script:Ensure-PsignModuleForNativeFeatureTests {
Remove-Module Devolutions.Psign -Force -ErrorAction SilentlyContinue
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$modulePath = Join-Path (Join-Path $repoRoot 'PowerShell\Devolutions.Psign') 'Devolutions.Psign.psd1'
Import-Module $modulePath -Force
}

Describe 'Psign native signature features' {
BeforeAll {
Ensure-PsignModuleForNativeFeatureTests
$script:TempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Force -Path $script:TempRoot | Out-Null
$script:RepoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
}

AfterAll {
if ($script:TempRoot) {
Remove-Item -LiteralPath $script:TempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}

It 'does not export compatibility command aliases' {
foreach ($name in @('Get-OpenAuthenticodeSignature', 'Set-OpenAuthenticodeSignature', 'Add-OpenAuthenticodeSignature', 'Clear-OpenAuthenticodeSignature')) {
Get-Command $name -Module Devolutions.Psign -ErrorAction SilentlyContinue | Should -BeNullOrEmpty
}
}

It 'clears psc1 XML signature blocks through Unprotect-PsignSignature' {
$path = Join-Path $script:TempRoot 'console.psc1'
@'
<PSConsoleFile>
</PSConsoleFile>
<!-- SIG # Begin signature block -->
<!-- fake -->
<!-- SIG # End signature block -->
'@ | Set-Content -LiteralPath $path -Encoding UTF8

Unprotect-PsignSignature -LiteralPath $path

(Get-Content -LiteralPath $path -Raw) | Should -Not -Match 'SIG # Begin signature block'
}

It 'clears PE signatures without a provider override' {
$source = Join-Path $script:RepoRoot 'tests\fixtures\pe-authenticode-upstream\tiny32.signed.efi'
$path = Join-Path $script:TempRoot 'tiny32.clear.efi'
Copy-Item -LiteralPath $source -Destination $path

$before = Get-PsignSignature -LiteralPath $path -SkipTrust
$before.Status | Should -Be ([System.Management.Automation.SignatureStatus]::Valid)
$before.SignedCms | Should -Not -BeNullOrEmpty
$before.Certificate | Should -Not -BeNullOrEmpty

Unprotect-PsignSignature -LiteralPath $path

$after = Get-PsignSignature -LiteralPath $path -SkipTrust
$after.Status | Should -Be ([System.Management.Automation.SignatureStatus]::NotSigned)
}

It 'appends PE signatures through Set-PsignSignature -AppendSignature' {
$source = Join-Path $script:RepoRoot 'tests\fixtures\pe-authenticode-upstream\tiny32.signed.efi'
$pfxPath = Join-Path $script:RepoRoot 'tests\fixtures\devolutions-authenticode\authenticode-test-cert.pfx'
$path = Join-Path $script:TempRoot 'tiny32.append.efi'
Copy-Item -LiteralPath $source -Destination $path

$before = Get-PsignSignature -LiteralPath $path -SkipTrust
$before.SignatureCount | Should -Be 1

Set-PsignSignature -LiteralPath $path -PfxPath $pfxPath -Password (ConvertTo-SecureString 'CodeSign123!' -AsPlainText -Force) -AppendSignature | Out-Null

$after = Get-PsignSignature -LiteralPath $path -SkipTrust
$after.SignatureCount | Should -Be 2
}

It 'replaces PE signatures by default through Set-PsignSignature' {
$source = Join-Path $script:RepoRoot 'tests\fixtures\pe-authenticode-upstream\tiny32.signed.efi'
$pfxPath = Join-Path $script:RepoRoot 'tests\fixtures\devolutions-authenticode\authenticode-test-cert.pfx'
$path = Join-Path $script:TempRoot 'tiny32.replace.efi'
Copy-Item -LiteralPath $source -Destination $path

$before = Get-PsignSignature -LiteralPath $path -SkipTrust
$before.SignatureCount | Should -Be 1

Set-PsignSignature -LiteralPath $path -PfxPath $pfxPath -Password (ConvertTo-SecureString 'CodeSign123!' -AsPlainText -Force) | Out-Null

$after = Get-PsignSignature -LiteralPath $path -SkipTrust
$after.SignatureCount | Should -Be 1
$after.SignedCms | Should -Not -BeNullOrEmpty
}
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ cargo build -p psign --bin psign-tool --locked
# psign-tool portable rdp --cert cert.der --key key.pk8 file.rdp
# Portable PE signing with a local RSA key:
# psign-tool portable sign-pe --cert cert.der --key key.pk8 --output signed.exe unsigned.exe
# Existing PE signatures are replaced by default; add --append-signature to match signtool /as.
# Portable trust verification with explicit anchors:
# psign-tool portable trust-verify-pe signed.exe --anchor-dir anchors
# Portable custom ZIP Authenticode verification:
Expand Down
19 changes: 17 additions & 2 deletions crates/psign-digest-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1988,7 +1988,7 @@ enum Command {
#[arg(long, value_name = "PATH")]
output: PathBuf,
},
/// Sign an unsigned PE image with portable Authenticode CMS + `WIN_CERTIFICATE` embedding.
/// Sign a PE image with portable Authenticode CMS + `WIN_CERTIFICATE` embedding.
///
/// This is the first production-oriented portable Authenticode signing path. It supports local RSA
/// PKCS#1 v1.5 keys or Azure Key Vault RSA signing and SHA-2 digests; timestamp embedding and
Expand All @@ -2015,6 +2015,9 @@ enum Command {
/// RFC3161 timestamp digest algorithm.
#[arg(long = "timestamp-digest", visible_alias = "td", value_enum)]
timestamp_digest: Option<HashAlg>,
/// Append a signature instead of replacing existing embedded Authenticode signatures.
#[arg(long = "append-signature", visible_alias = "as")]
append_signature: bool,
/// Azure Key Vault URL for remote RSA signing.
#[arg(long = "azure-key-vault-url", visible_alias = "kvu")]
azure_key_vault_url: Option<String>,
Expand Down Expand Up @@ -4294,6 +4297,7 @@ where
digest,
timestamp_url,
timestamp_digest,
append_signature,
azure_key_vault_url,
azure_key_vault_certificate,
azure_key_vault_certificate_version,
Expand All @@ -4306,7 +4310,18 @@ where
artifact_signing,
output,
} => {
let pe = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
let mut pe =
std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
if !append_signature {
pe = pe_embed::pe_remove_authenticode_certificates(pe)
.with_context(|| {
format!(
"remove existing PE Authenticode signatures from {}",
path.display()
)
})?
.0;
}
let has_local = cert.is_some() || key.is_some();
let has_kv = azure_key_vault_url
.as_deref()
Expand Down
Loading