Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
b7e5e57
feat(powershell): align cmdlet output with built-in Authenticode cont…
mamoreau-devolutions May 23, 2026
4b58209
feat(trust): automatic AuthRoot CAB download and trust-at-boundary ch…
mamoreau-devolutions May 23, 2026
e6927ce
feat(powershell): add pcert:\ portable certificate store provider
mamoreau-devolutions May 23, 2026
67bf4b5
feat(powershell): add Test-PsignModule cmdlet for execution policy va…
mamoreau-devolutions May 23, 2026
4c52be4
feat(powershell): add Protect-PsignModule and Unprotect-PsignSignatur…
mamoreau-devolutions May 23, 2026
2f2b352
feat(powershell): add format views, argument completers, and help topic
mamoreau-devolutions May 23, 2026
0505a40
docs(powershell): add module README with usage examples
mamoreau-devolutions May 23, 2026
33e88c1
feat(powershell): rename cmdlets to Get/Set-PsignSignature with backw…
mamoreau-devolutions May 23, 2026
7466e38
feat(powershell): add content signing tests and TimestampHashAlgorith…
mamoreau-devolutions May 23, 2026
211e0e0
feat(powershell): add IncludeChain completer and expand help examples
mamoreau-devolutions May 23, 2026
536edc0
fix(tests): skip build when native DLL is locked by current session
mamoreau-devolutions May 23, 2026
2b73058
fix(powershell): address quality audit findings
mamoreau-devolutions May 23, 2026
43f0bed
test: expand Pester coverage to 110 tests across all cmdlets
mamoreau-devolutions May 23, 2026
546624e
fix(tests): make PsignModule.Cmdlets.Tests cross-platform compatible
mamoreau-devolutions May 24, 2026
b795f12
chore: bump version to 0.4.0
mamoreau-devolutions May 24, 2026
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
18 changes: 9 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ repository = "https://github.com/Devolutions/psign"

[package]
name = "psign"
version = "0.3.0"
version = "0.4.0"
edition = "2024"
description = "Rust port of the Windows SDK signtool.exe (Authenticode sign/verify/timestamp) with portable digest helpers."
license.workspace = true
Expand Down
194 changes: 194 additions & 0 deletions PowerShell/Devolutions.Psign/Devolutions.Psign.Format.ps1xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<?xml version="1.0" encoding="utf-8"?>
<Configuration>
<ViewDefinitions>

<!-- Test-PsignModule result: summary list -->
<View>
<Name>PsignModuleValidationResult</Name>
<ViewSelectedBy>
<TypeName>Devolutions.Psign.PowerShell.Models.PsignModuleValidationResult</TypeName>
</ViewSelectedBy>
<ListControl>
<ListEntries>
<ListEntry>
<ListItems>
<ListItem><PropertyName>ModuleName</PropertyName></ListItem>
<ListItem><PropertyName>ModulePath</PropertyName></ListItem>
<ListItem><PropertyName>Policy</PropertyName></ListItem>
<ListItem><PropertyName>Valid</PropertyName></ListItem>
<ListItem><PropertyName>Summary</PropertyName></ListItem>
<ListItem><Label>Passed</Label><PropertyName>PassedCount</PropertyName></ListItem>
<ListItem><Label>Failed</Label><PropertyName>FailedCount</PropertyName></ListItem>
<ListItem><Label>Skipped</Label><PropertyName>SkippedCount</PropertyName></ListItem>
</ListItems>
</ListEntry>
</ListEntries>
</ListControl>
</View>

<!-- Test-PsignModule per-file result table -->
<View>
<Name>PsignModuleFileResult</Name>
<ViewSelectedBy>
<TypeName>Devolutions.Psign.PowerShell.Models.PsignModuleFileResult</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader><Label>File</Label><Width>40</Width></TableColumnHeader>
<TableColumnHeader><Label>Role</Label><Width>18</Width></TableColumnHeader>
<TableColumnHeader><Label>Required</Label><Width>8</Width></TableColumnHeader>
<TableColumnHeader><Label>Status</Label><Width>12</Width></TableColumnHeader>
<TableColumnHeader><Label>Pass</Label><Width>5</Width></TableColumnHeader>
<TableColumnHeader><Label>Signer</Label><Width>30</Width></TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem><PropertyName>RelativePath</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Role</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>RequiredByPolicy</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Status</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Passes</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>SignerSubject</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>

<!-- Protect-PsignModule result -->
<View>
<Name>PsignModuleSigningResult</Name>
<ViewSelectedBy>
<TypeName>Devolutions.Psign.PowerShell.Cmdlets.PsignModuleSigningResult</TypeName>
</ViewSelectedBy>
<ListControl>
<ListEntries>
<ListEntry>
<ListItems>
<ListItem><PropertyName>ModuleName</PropertyName></ListItem>
<ListItem><PropertyName>ModulePath</PropertyName></ListItem>
<ListItem><Label>Total Files</Label><PropertyName>TotalFiles</PropertyName></ListItem>
<ListItem><PropertyName>Succeeded</PropertyName></ListItem>
<ListItem><PropertyName>Failed</PropertyName></ListItem>
</ListItems>
</ListEntry>
</ListEntries>
</ListControl>
</View>

<!-- Protect-PsignModule per-file sign result -->
<View>
<Name>PsignModuleFileSignResult</Name>
<ViewSelectedBy>
<TypeName>Devolutions.Psign.PowerShell.Cmdlets.PsignModuleFileSignResult</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader><Label>File</Label><Width>40</Width></TableColumnHeader>
<TableColumnHeader><Label>Role</Label><Width>18</Width></TableColumnHeader>
<TableColumnHeader><Label>Status</Label><Width>12</Width></TableColumnHeader>
<TableColumnHeader><Label>OK</Label><Width>5</Width></TableColumnHeader>
<TableColumnHeader><Label>Error</Label><Width>40</Width></TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem><PropertyName>RelativePath</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Role</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Status</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Success</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>ErrorMessage</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>

<!-- Unprotect-PsignSignature result -->
<View>
<Name>PsignUnprotectResult</Name>
<ViewSelectedBy>
<TypeName>Devolutions.Psign.PowerShell.Cmdlets.PsignUnprotectResult</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader><Label>Path</Label><Width>50</Width></TableColumnHeader>
<TableColumnHeader><Label>Removed</Label><Width>8</Width></TableColumnHeader>
<TableColumnHeader><Label>Bytes</Label><Width>8</Width></TableColumnHeader>
<TableColumnHeader><Label>Message</Label><Width>30</Width></TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem><PropertyName>Path</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>SignatureRemoved</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>BytesRemoved</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Message</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>

<!-- Get-PsignSignature / Set-PsignSignature table -->
<View>
<Name>PortableSignature_Table</Name>
<ViewSelectedBy>
<TypeName>Devolutions.Psign.PowerShell.Models.PortableSignature</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader><Label>Path</Label><Width>40</Width></TableColumnHeader>
<TableColumnHeader><Label>Status</Label><Width>14</Width></TableColumnHeader>
<TableColumnHeader><Label>Type</Label><Width>14</Width></TableColumnHeader>
<TableColumnHeader><Label>Signer</Label><Width>40</Width></TableColumnHeader>
<TableColumnHeader><Label>Timestamp</Label><Width>20</Width></TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem><ScriptBlock>if ($_.SourcePathOrExtension) { $_.SourcePathOrExtension } else { Split-Path $_.Path -Leaf }</ScriptBlock></TableColumnItem>
<TableColumnItem><PropertyName>Status</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>SignatureType</PropertyName></TableColumnItem>
<TableColumnItem><ScriptBlock>if ($_.SignerCertificate) { $_.SignerCertificate.Subject -replace '^CN=','' -replace ',.*$','' } else { '' }</ScriptBlock></TableColumnItem>
<TableColumnItem><ScriptBlock>if ($_.TimestampSigningTime) { $_.TimestampSigningTime.ToString('yyyy-MM-dd HH:mm') } else { '' }</ScriptBlock></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>

<!-- Get-PsignSignature / Set-PsignSignature detailed list -->
<View>
<Name>PortableSignature_List</Name>
<ViewSelectedBy>
<TypeName>Devolutions.Psign.PowerShell.Models.PortableSignature</TypeName>
</ViewSelectedBy>
<ListControl>
<ListEntries>
<ListEntry>
<ListItems>
<ListItem><PropertyName>Path</PropertyName></ListItem>
<ListItem><PropertyName>Status</PropertyName></ListItem>
<ListItem><PropertyName>StatusMessage</PropertyName></ListItem>
<ListItem><PropertyName>SignatureType</PropertyName></ListItem>
<ListItem><Label>Format</Label><PropertyName>Format</PropertyName></ListItem>
<ListItem><Label>SignerCertificate</Label><ScriptBlock>if ($_.SignerCertificate) { "[Subject]`n $($_.SignerCertificate.Subject)`n[Issuer]`n $($_.SignerCertificate.Issuer)`n[Thumbprint]`n $($_.SignerCertificate.Thumbprint)`n[Not Before]`n $($_.SignerCertificate.NotBefore)`n[Not After]`n $($_.SignerCertificate.NotAfter)" }</ScriptBlock></ListItem>
<ListItem><Label>TimeStamperCertificate</Label><ScriptBlock>if ($_.TimeStamperCertificate) { "[Subject]`n $($_.TimeStamperCertificate.Subject)" }</ScriptBlock></ListItem>
<ListItem><PropertyName>TimestampSigningTime</PropertyName></ListItem>
<ListItem><PropertyName>DigestAlgorithm</PropertyName></ListItem>
<ListItem><PropertyName>SignatureCount</PropertyName></ListItem>
<ListItem><PropertyName>EmbeddedCertificateCount</PropertyName></ListItem>
<ListItem><Label>TrustStatus</Label><PropertyName>PortableTrustStatus</PropertyName></ListItem>
<ListItem><PropertyName>IsOSBinary</PropertyName></ListItem>
<ListItem><PropertyName>SubjectAlternativeName</PropertyName></ListItem>
<ListItem><Label>Diagnostics</Label><ScriptBlock>if ($_.PortableDiagnostics.Count -gt 0) { $_.PortableDiagnostics -join "`n" }</ScriptBlock></ListItem>
</ListItems>
</ListEntry>
</ListEntries>
</ListControl>
</View>

</ViewDefinitions>
</Configuration>
16 changes: 13 additions & 3 deletions PowerShell/Devolutions.Psign/Devolutions.Psign.psd1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@{
RootModule = 'Devolutions.Psign.psm1'
ModuleVersion = '0.3.0'
ModuleVersion = '0.4.0'
GUID = 'e6e50e4b-bf25-4ed6-a343-49f904e79f8f'
Author = 'Devolutions'
CompanyName = 'Devolutions'
Expand All @@ -9,9 +9,19 @@
CompatiblePSEditions = @('Core')
PowerShellVersion = '7.4'
NestedModules = @('lib/net8.0/Devolutions.Psign.PowerShell.dll')
CmdletsToExport = @('Get-PortableSignature', 'Set-PortableSignature')
FormatsToProcess = @('Devolutions.Psign.Format.ps1xml')
CmdletsToExport = @(
'Get-PsignSignature',
'Set-PsignSignature',
'Test-PsignModule',
'Protect-PsignModule',
'Unprotect-PsignSignature'
)
FunctionsToExport = @()
AliasesToExport = @()
AliasesToExport = @(
'Get-PortableSignature',
'Set-PortableSignature'
)
PrivateData = @{
PSData = @{
Tags = @('Authenticode', 'CodeSigning', 'Portable', 'psign')
Expand Down
70 changes: 70 additions & 0 deletions PowerShell/Devolutions.Psign/Devolutions.Psign.psm1
Original file line number Diff line number Diff line change
@@ -1 +1,71 @@
# Binary cmdlets are loaded through the module manifest's NestedModules entry.

# Argument completers for common parameters

Register-ArgumentCompleter -CommandName Get-PsignSignature, Get-PortableSignature, Set-PsignSignature, Set-PortableSignature, Protect-PsignModule -ParameterName Thumbprint -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
$baseDir = if ($fakeBoundParameters.ContainsKey('CertStoreDirectory')) {
$fakeBoundParameters['CertStoreDirectory']
} elseif ($env:PSIGN_CERT_STORE) {
$env:PSIGN_CERT_STORE
} else {
Join-Path ([Environment]::GetFolderPath('UserProfile')) '.psign' 'cert-store'
}
$scope = if ($fakeBoundParameters.ContainsKey('MachineStore') -and $fakeBoundParameters['MachineStore']) { 'LocalMachine' } else { 'CurrentUser' }
$store = if ($fakeBoundParameters.ContainsKey('StoreName')) { $fakeBoundParameters['StoreName'] } else { 'MY' }
$storeDir = Join-Path $baseDir $scope $store
if (Test-Path $storeDir) {
Get-ChildItem -Path $storeDir -Filter '*.der' | ForEach-Object {
$thumb = $_.BaseName
if ($thumb -like "$wordToComplete*") {
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($_.FullName)
$subject = $cert.Subject -replace '^CN=','' -replace ',.*$',''
$cert.Dispose()
[System.Management.Automation.CompletionResult]::new(
$thumb, "$thumb ($subject)", 'ParameterValue', $subject)
}
}
}
}

Register-ArgumentCompleter -CommandName Set-PsignSignature, Set-PortableSignature, Protect-PsignModule -ParameterName StoreName -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
@('MY', 'Root', 'CA', 'Trust', 'Disallowed') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}

Register-ArgumentCompleter -CommandName Set-PsignSignature, Set-PortableSignature, Protect-PsignModule -ParameterName HashAlgorithm -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
@('Sha256', 'Sha384', 'Sha512') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}

Register-ArgumentCompleter -CommandName Set-PsignSignature, Set-PortableSignature, Protect-PsignModule -ParameterName IncludeChain -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
@('Signer', 'NotRoot', 'All') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}

Register-ArgumentCompleter -CommandName Set-PsignSignature, Set-PortableSignature, Protect-PsignModule -ParameterName TimestampHashAlgorithm -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
@('Sha1', 'Sha256', 'Sha384', 'Sha512') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}

Register-ArgumentCompleter -CommandName Get-PsignSignature, Get-PortableSignature -ParameterName RevocationMode -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
@('Off', 'BestEffort', 'Require') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}

Register-ArgumentCompleter -CommandName Test-PsignModule -ParameterName Policy -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
@('AllSigned', 'RemoteSigned') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}
Loading