Skip to content

feat(powershell): comprehensive portable Authenticode cmdlet suite#12

Merged
Marc-André Moreau (mamoreau-devolutions) merged 15 commits into
masterfrom
mamoreau-devolutions/authenticode-cmdlets
May 24, 2026
Merged

feat(powershell): comprehensive portable Authenticode cmdlet suite#12
Marc-André Moreau (mamoreau-devolutions) merged 15 commits into
masterfrom
mamoreau-devolutions/authenticode-cmdlets

Conversation

@mamoreau-devolutions
Copy link
Copy Markdown
Contributor

Summary

A complete cross-platform PowerShell module (Devolutions.Psign) for Authenticode signing, verification, and module compliance — no Windows APIs required.

New cmdlets

Cmdlet Purpose
Get-PsignSignature Verify Authenticode signatures on scripts, XML, and raw content
Set-PsignSignature Sign files with local certs, PFX, portable cert store, Azure Key Vault, or Trusted Signing
Unprotect-PsignSignature Strip signatures from scripts and XML files
Test-PsignModule Validate a PowerShell module against AllSigned/RemoteSigned policy
Protect-PsignModule Batch-sign all policy-checked files in a module

Backward-compatible aliases Get-PortableSignature / Set-PortableSignature are preserved.

Infrastructure

  • pcert:\ provider — cross-platform certificate store (~/.psign/certstore/) mimicking the Windows Cert:\ layout. Supports Get-ChildItem, Get-Item, New-Item, Remove-Item.
  • Automatic AuthRoot trust — downloads and caches the Microsoft AuthRoot CAB under ~/.psign/ for portable chain building (opt-out via $env:PSIGN_NO_AUTO_TRUST=1).
  • Format views & argument completers — table/list formatting for PortableSignature, tab-completion for Thumbprint, StoreName, HashAlgorithm, IncludeChain, TimestampHashAlgorithm, RevocationMode, Policy.

Test coverage

110 Pester tests passing in a single cross-platform session:

  • Signing material validation (error paths, EKU/KeyUsage checks, ECDSA detection)
  • Wildcard expansion, pipeline input, -Content mode, -SkipTrust
  • Signature removal (scripts + XML), -WhatIf, encoding preservation
  • Module policy validation (AllSigned, RemoteSigned, tamper detection)
  • Batch module signing with round-trip verification

Quality fixes

  • X509Certificate2 disposal in EndProcessing() to prevent leaks
  • ECDSA key detection with clear "not supported" error message
  • Cached SignerCertificate/TimeStamperCertificate properties (decode once)
  • DLL lock avoidance in legacy smoke test

…ract

- Change PortableSignature.Status to SignatureStatus enum type
- Change TrustStatus to SignatureStatus? enum type
- Add computed SignatureType property (System.Management.Automation.SignatureType)
- Add SubjectAlternativeName extraction from signer certificate SAN extension
- Add PortableStatus/PortableTrustStatus string accessors for backward compat
- Add LP alias to LiteralPath on both Get/Set cmdlets
- Add ValueFromPipeline/ValueFromPipelineByPropertyName to content params
- Add ValidateNotNullOrEmpty to Content parameter
- Add certificate code-signing suitability preflight to Set-PortableSignature
  (validates EKU 1.3.6.1.5.5.7.3.3 and KeyUsage DigitalSignature)
- Add Pester 5 metadata compatibility tests
- Add migration guide to docs/portable-powershell-module.md

Scripts using $sig.Status -eq 'Valid' or enum comparisons continue to
work because PowerShell coerces between strings and SignatureStatus enum.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ain building

Add automatic trust verification for Get-PortableSignature on Linux/cross-platform:

- AuthRootCache.cs: downloads and caches Microsoft AuthRoot CAB at
  ~/.psign/authroot/authrootstl.cab with configurable max age (30 days)
- Auto-trust in Get-PortableSignature: when no explicit trust params are
  provided, automatically uses the cached AuthRoot CAB for verification
- -SkipTrust switch and PSIGN_NO_AUTO_TRUST env var for opt-out
- Fix CTL parsing: handle naked CTL children in econtent (real-world
  authrootstl.cab uses back-to-back ASN.1 elements without outer SEQUENCE)
- Trust-at-boundary chain building: issuer_chain_excluding_leaf(_online)
  now accepts Option<&AnchorStore> and terminates when a cert's thumbprint
  matches the anchor store (avoids needing root cert DER when only
  thumbprints are available from the CTL)
- Auto-enable AIA when authroot_cab is specified: intermediate certs
  typically have AIA extensions pointing to the root CA URL, enabling
  the chain builder to fetch missing root certs on demand
- Test isolation: PSIGN_NO_AUTO_TRUST=1 in existing test suites to
  prevent auto-trust from interfering with self-signed cert tests

End-to-end verified: Get-PortableSignature on pwsh.exe now returns
Status=Valid, TrustStatus=Valid with automatic AuthRoot trust.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement a NavigationCmdletProvider that exposes psign's file-based
certificate store (~/.psign/cert-store/) as a pcert:\ PowerShell drive,
mirroring the Windows cert:\ provider hierarchy.

Features:
- Auto-registers pcert: drive on module import
- Full navigation: cd, Get-ChildItem at root/scope/store levels
- CRUD: New-Item (import cert), Get-Item, Remove-Item, Copy-Item
- Custom drives via New-PSDrive -Root for alternate store locations
- Accepts X509Certificate2, byte[] (DER), string (PEM/path) for import
- Returns X509Certificate2 objects (same as Windows cert:\ provider)
- HasPrivateKey reflects .key file existence

Refactored shared cert store helpers from SetPortableSignatureCommand
into Provider/CertStorePathHelper.cs for reuse.

21 Pester tests covering drive registration, navigation, CRUD,
custom drives, and error handling all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lidation

Implements a cmdlet that validates whether a PowerShell module would load
cleanly under AllSigned or RemoteSigned execution policies. This enables
portable, cross-platform module signature compliance checking.

The cmdlet:
- Parses the .psd1 manifest to discover RootModule, ScriptsToProcess,
  NestedModules, TypesToProcess, and FormatsToProcess
- Accurately models PowerShell engine behavior: .psd1 is never checked,
  .dll binary modules bypass execution policy, only .ps1/.psm1/.ps1xml/
  .cdxml are validated
- Reports per-file pass/fail with role, status, and failure reason
- Supports -RequireTrustedPublisher to also verify the signer's leaf
  cert is in pcert:\*\Trust (TrustedPublisher equivalent)
- Supports -IncludeUnreferenced for belt-and-suspenders validation of
  all signable files in the module directory

Usage:
  Test-PsignModule -Path ./MyModule -Policy AllSigned
  Test-PsignModule ./MyModule -RequireTrustedPublisher

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e cmdlets

New cmdlets:
- Protect-PsignModule: signs all policy-checked files in a PowerShell
  module in one shot (manifest-aware, skips binary .dll modules,
  supports all signing backends: local cert, PFX, thumbprint,
  Azure Key Vault, Trusted Signing)
- Unprotect-PsignSignature: strips Authenticode signature blocks from
  script files (.ps1, .psm1, .psd1, .ps1xml, .cdxml) with encoding
  preservation

Also adds:
- Devolutions.Psign.Format.ps1xml for default formatted output of
  Test-PsignModule, Protect-PsignModule, and Unprotect-PsignSignature
- CertStorePathHelper.LoadCertificateAndKey for thumbprint-based
  signing material resolution
- 13 new Pester tests covering all three new cmdlets (47 total pass)

Module now exports 5 cmdlets:
  Get-PortableSignature, Set-PortableSignature,
  Test-PsignModule, Protect-PsignModule, Unprotect-PsignSignature

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- PortableSignature table/list format views for Get/Set-PortableSignature
  output (shows path, status, type, signer, timestamp at a glance)
- Argument completers in .psm1 for tab-completion of:
  - Thumbprint (enumerates pcert store, shows cert subjects)
  - StoreName, HashAlgorithm, RevocationMode, Policy
- about_Devolutions.Psign help topic covering all cmdlets, trust model,
  signing sources, certificate store, and examples
- HelpMessage attributes on all mandatory parameters

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Covers all 5 cmdlets, certificate store, trust model, signing sources,
pipeline integration patterns, and module compliance workflow.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ard-compat aliases

Rename the core cmdlets from Get-PortableSignature / Set-PortableSignature to
Get-PsignSignature / Set-PsignSignature for consistent project branding.

- Rename C# files and classes (GetPsignSignatureCommand, SetPsignSignatureCommand)
- Update [Cmdlet] noun to 'PsignSignature'
- Add [Alias("Get-PortableSignature")] and [Alias("Set-PortableSignature")] for backward compat
- Update CmdletsToExport and AliasesToExport in manifest
- Register argument completers for both canonical and alias names
- Update all tests, docs, and READMEs to use new canonical names
- Add alias verification test in compatibility suite

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…m completer

- Add PsignSignature.ContentSigning.Tests.ps1 with 8 tests covering:
  - PowerShell script content sign + verify round-trip
  - ps1xml and psm1 content signing
  - Unsigned content detection (NotSigned)
  - Tampered content detection (HashMismatch)
  - Signing via -Thumbprint from portable cert store
  - Integration with pcert: provider
- Add TimestampHashAlgorithm argument completer (Sha1, Sha256, Sha384, Sha512)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add IncludeChain argument completer (Signer, NotRoot, All)
- Add content signing and pcert: integration examples to about help topic
- Update TimestampHashAlgorithm and IncludeChain in TAB COMPLETION docs
- Fix stale Get-PortableSignature reference in trust model section

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The legacy smoke test tried to rebuild and copy psign-core.dll even when the
module was already loaded by earlier Pester test files in the same process.
Now detects an already-loaded module and skips the build, avoiding the
IOException from overwriting a locked DLL.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix X509Certificate2 leak: add EndProcessing to dispose pfxCertificate
  and storeCertificate after pipeline processing completes
- Improve ECDSA error: detect ECDSA keys and report a clear unsupported
  message instead of the generic 'requires RSA private key'
- Remove dead code: pointless null assignment in LoadPfxCertificate
- Cache computed properties: PortableSignature.SignerCertificate and
  TimeStamperCertificate now decode only once per access
- Fix CertStorePathHelper: try ECDSA key import when RSA fails, so
  pcert: provider can associate ECDSA private keys
- Expand package.ps1 smoke test to validate all 5 exported cmdlets

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 5 new test files covering:
- Set-PsignSignature: signing material validation (16 tests)
  - Error paths: no material, incomplete key pair, non-CodeSigning EKU,
    missing DigitalSignature KeyUsage, multiple sources, ECDSA
  - PFX signing with correct/wrong passwords
  - OutputPath (single/multiple inputs)
  - Force on read-only files
  - HashAlgorithm variants (Sha384, Sha512)
  - IncludeChain modes (Signer, All)

- Get-PsignSignature: expanded verification (11 tests)
  - Wildcard path expansion
  - Pipeline input (string paths, FileInfo objects)
  - Error handling for non-existent files
  - Content mode edge cases (bare ext, dotted, full filename)
  - TrustedCertificatePath trust evaluation
  - SkipTrust digest-only verification

- Unprotect-PsignSignature: signature removal (10 tests)
  - Script formats (.ps1, .psm1, .psd1)
  - XML formats (.ps1xml, .cdxml)
  - WhatIf support
  - Wildcard path processing
  - Error handling and encoding preservation (UTF-8 BOM)

- Test-PsignModule: policy validation (8 tests)
  - AllSigned and RemoteSigned policies
  - Error handling (non-existent path, no manifest fallback)
  - Tamper detection (HashMismatch)
  - IncludeUnreferenced switch

- Protect-PsignModule: batch module signing (8 tests)
  - CertificatePath/PrivateKeyPath and PfxPath signing
  - Error handling (non-existent path, no signing material)
  - IncludeUnreferenced switch
  - Result properties verification
  - Round-trip with Test-PsignModule

All 110 tests pass in a single session (cross-platform, no
Windows-only APIs like New-SelfSignedCertificate).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add -KeyExportPolicy Exportable to New-SelfSignedCertificate so the
  private key can be exported via ExportPkcs8PrivateKey() in CI
- Add BeforeDiscovery/Skip gate so all Describe blocks are skipped on
  non-Windows platforms (the file depends on New-SelfSignedCertificate
  and Cert:\ provider which are Windows-only)
- Cross-platform equivalents already exist in the expanded test files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mamoreau-devolutions Marc-André Moreau (mamoreau-devolutions) force-pushed the mamoreau-devolutions/authenticode-cmdlets branch from fc47ba2 to 546624e Compare May 24, 2026 03:02
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mamoreau-devolutions Marc-André Moreau (mamoreau-devolutions) merged commit aa36d81 into master May 24, 2026
36 checks passed
@mamoreau-devolutions Marc-André Moreau (mamoreau-devolutions) deleted the mamoreau-devolutions/authenticode-cmdlets branch May 24, 2026 10:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant