From 1237c6a55fa22b9d2004dcdc1c7ecc7eff45805b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Sat, 30 May 2026 12:19:38 -0400 Subject: [PATCH] Expand feature matrix and add P7X tooling Add standalone PKCS#7/P7X portable inspection helpers, update the feature matrix and gap analysis, and guard portable CLI matrix drift with a regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/psign-digest-cli/src/main.rs | 32 ++++++++++ crates/psign-sip-digest/src/pkcs7_wire.rs | 10 ++- docs/gap-analysis-signing-platforms.md | 39 +++++++++++- docs/psign-cli-matrix.json | 35 +++++++++++ docs/psign-cli-matrix.md | 28 ++++++++- docs/roadmap-authenticode-linux.md | 2 +- docs/rust-sip-gaps.md | 2 +- tests/cli_matrix_docs.rs | 76 +++++++++++++++++++++++ tests/cli_pe_digest.rs | 60 ++++++++++++++++++ 9 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 tests/cli_matrix_docs.rs diff --git a/crates/psign-digest-cli/src/main.rs b/crates/psign-digest-cli/src/main.rs index 35fb9d2..138c9d6 100644 --- a/crates/psign-digest-cli/src/main.rs +++ b/crates/psign-digest-cli/src/main.rs @@ -2370,6 +2370,17 @@ enum Command { #[arg(long, value_enum, default_value_t = InspectInputKind::Pe)] input: InspectInputKind, }, + /// Inspect standalone PKCS#7 / P7X bytes as JSON. + /// + /// Accepts PKCS#7 `ContentInfo`, bare `SignedData`, or AppX `AppxSignature.p7x` + /// files with a `PKCX` wrapper. + InspectPkcs7 { path: PathBuf }, + /// Strip an AppX `AppxSignature.p7x` `PKCX` wrapper and write the inner PKCS#7 DER. + ExtractPkcxPkcs7 { + path: PathBuf, + #[arg(long, value_name = "PATH")] + output: Option, + }, /// Validate JSON metadata shape for Microsoft Artifact Signing (`Endpoint`, `CodeSigningAccountName`, `CertificateProfileName`; optional `ExcludeCredentials` string array). No network / no signing. /// /// Reads **`--path`** or stdin when omitted (use `-` for stdin explicitly). @@ -5153,6 +5164,27 @@ where }; println!("{json}"); } + Command::InspectPkcs7 { path } => { + let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?; + let json = serde_json::to_string_pretty(&inspect_authenticode_pkcs7_der(&bytes)?)?; + println!("{json}"); + } + Command::ExtractPkcxPkcs7 { path, output } => { + let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?; + let inner = pkcs7_wire::strip_pkcx_p7x_wrapper(&bytes).ok_or_else(|| { + anyhow!( + "extract-pkcx-pkcs7 {}: missing PKCX wrapper", + path.display() + ) + })?; + match output.as_ref() { + Some(p) => std::fs::write(p, inner) + .with_context(|| format!("write PKCS#7 to {}", p.display()))?, + None => std::io::stdout() + .write_all(inner) + .context("write PKCS#7 to stdout")?, + } + } Command::ArtifactSigningMetadataCheck { path } => { run_artifact_signing_metadata_check(path)?; } diff --git a/crates/psign-sip-digest/src/pkcs7_wire.rs b/crates/psign-sip-digest/src/pkcs7_wire.rs index bdf0f9c..c91921f 100644 --- a/crates/psign-sip-digest/src/pkcs7_wire.rs +++ b/crates/psign-sip-digest/src/pkcs7_wire.rs @@ -83,8 +83,16 @@ pub fn pkcs7_outer_sequence_prefix(data: &[u8]) -> Option<&[u8]> { data.get(..n) } -/// Normalize detached PKCS#7 blobs: bare `SignedData` sequences are wrapped as PKCS#7 `ContentInfo`. +/// Strip the Windows AppX/AppInstaller **PKCX** wrapper used by standalone +/// `AppxSignature.p7x` files, returning the inner PKCS#7 DER. +pub fn strip_pkcx_p7x_wrapper(data: &[u8]) -> Option<&[u8]> { + data.strip_prefix(b"PKCX") +} + +/// Normalize detached PKCS#7 blobs: PKCX-wrapped AppX `AppxSignature.p7x` files +/// are unwrapped, and bare `SignedData` sequences are wrapped as PKCS#7 `ContentInfo`. pub fn normalize_pkcs7_der_for_authenticode(sig_blob: &[u8]) -> Cow<'_, [u8]> { + let sig_blob = strip_pkcx_p7x_wrapper(sig_blob).unwrap_or(sig_blob); let Some(inner) = tlv_outer_sequence_payload(sig_blob) else { return Cow::Borrowed(sig_blob); }; diff --git a/docs/gap-analysis-signing-platforms.md b/docs/gap-analysis-signing-platforms.md index 9d03e6e..76fef19 100644 --- a/docs/gap-analysis-signing-platforms.md +++ b/docs/gap-analysis-signing-platforms.md @@ -45,7 +45,7 @@ This inventory starts from the in-tree supported formats, then expands to inbox | **Cleartext AppX/MSIX** (`.appx`, `.msix`, `.appxbundle`, `.msixbundle`, `.appxupload`, `.msixupload`) | Sign/verify with AppX client data and dlib bridge. | Remaining native parity failures can occur around `SignerSignEx3` AppX glue, publisher binding, sealing, and package constraints. | `verify-msix` digest consistency; `msix-manifest-info` / `msix-set-publisher`; native-shaped portable Artifact Signing final signing for flat `.appx` / `.msix` packages with `AppxSignature.p7x` / `PKCX` embedding and optional RFC3161 timestamping; guarded `psign-tool code` prepare execution signs nested PE/package entries, updates `AppxManifest.xml` Publisher from `--publisher-name`, regenerates `AppxBlockMap.xml`, propagates publisher updates into nested packages inside upload/bundle containers, and rejects already-final-signed `AppxSignature.p7x` packages before final AppX SIP signing. | Bundle/upload final signing, encrypted packages, manifest publisher-vs-signer policy, and full AppX package policy remain pending. | | **Encrypted AppX/MSIX** (`.eappx`, `.emsix`, `.eappxbundle`, `.emsixbundle`) | Delegates to OS `EappxSip*` / `EappxBundleSip*`. | No in-tree understanding beyond OS delegation and parity fixtures. | Explicitly rejected by `verify-msix`, MSIX metadata helpers, and `psign-tool code` with Windows AppxSip OS-delegation diagnostics. | Encrypted package crypto/header handling is absent; ZIP-only digest logic is insufficient. | | **AppX extension SIP chain** | Delegates to installed `ExtensionsSip*` providers. | No bundled/provider-specific parity coverage; behavior depends on optional third-party SIP DLLs. | Not implemented. | No extension-provider discovery, DLL contract, or portable provider model. | -| **Standalone P7X / PKCX** (`.p7x`) | OS `P7xSip*` can participate when registered; real package signatures are produced as `AppxSignature.p7x` inside signed AppX/MSIX packages. | Direct standalone `.p7x` signing is rejected by current SignTool; first-class commands for extracting/interpreting PKCX remain absent. | Raw PKCS#7 inspection/trust primitives may apply after extraction. | No dedicated PKCX/P7X container command or portable PKCX header handling. | +| **Standalone P7X / PKCX** (`.p7x`) | OS `P7xSip*` can participate when registered; real package signatures are produced as `AppxSignature.p7x` inside signed AppX/MSIX packages. | Direct standalone `.p7x` signing is rejected by current SignTool. | `inspect-pkcs7` accepts raw PKCS#7, bare `SignedData`, and PKCX-wrapped `AppxSignature.p7x`; `extract-pkcx-pkcs7` strips the PKCX wrapper; detached trust remains available through `trust-verify-detached` when caller supplies the detached content. | No standalone `.p7x` signing/export flow mapped to native `/p7*` switches. | | **PowerShell-class scripts** (`.ps1`, `.psm1`, `.psd1`, `.ps1xml`, `.psc1`, `.cdxml`, `.mof`) | Sign/verify through `pwrshsip.dll`; parity fixtures cover `.ps1`, `.psm1`, `.psd1`. | Need parity fixtures and format detection for `.ps1xml`, `.psc1`, `.cdxml`, `.mof`. | `verify-script` digest consistency for PowerShell-style markers. | No signing/embed; digest remains heuristic for every malformed block and encoding edge case. | | **WSH scripts** (`.js`, `.jse`, `.vbs`, `.vbe`, `.wsf`) | Sign/verify through `wshext.dll`; parity fixtures cover `.js`, `.vbs`, `.wsf`. | Need `.jse` and `.vbe` parity coverage. | `verify-script` digest consistency for WSH markers. | No signing/embed; native COM text conversion and unusual encodings may diverge. | | **Office / VBA macro projects** | Delegates to installed `mso.dll` / `VBE7.DLL` SIP when present. | No direct Office/VBA CLI affordance or parity fixture set; depends on installed Office components. | Not implemented. | No VBA project graph hashing; likely needs VBE7/Office FFI or permanent OS delegation. | @@ -72,7 +72,7 @@ The committed corpus already includes generated unsigned and signed vectors for | **MST transforms** (`.mst`) | Unsigned generated transform exists; signed native output is retained in skipped corpus rows because `/pa` verification rejects it. | A verifiable signed `.mst` fixture if native Windows Installer policy supports one, or deeper tests around the documented reject. | | **Encrypted AppX/MSIX** (`.eappx`, `.eappxbundle`, `.emsix`, `.emsixbundle`) | Unsigned/placeholder negative files exist. | Real signed encrypted package fixtures, if the project decides to test OS-only Windows delegation. | | **WSH component scripts** (`.wsc`) | Unsigned probe files exist and native SignTool rejection is recorded; `.jse` / `.vbe` have signed generated probes. | Signed `.wsc` fixture if a supported provider/tooling path is identified. | -| **Standalone P7X / PKCX** (`.p7x`) | Unsigned direct-signing probe exists and native SignTool rejection is recorded; a real `AppxSignature.p7x` is extracted from a signed MSIX fixture. | First-class PKCX/P7X parsing/verification behavior remains an implementation gap. | +| **Standalone P7X / PKCX** (`.p7x`) | Unsigned direct-signing probe exists and native SignTool rejection is recorded; a real `AppxSignature.p7x` is extracted from a signed MSIX fixture. | PKCX extraction and standalone PKCS#7/P7X inspection are covered by portable CLI tests; native-shaped standalone `.p7x` signing/export remains uncovered. | | **App Installer descriptors** (`.appinstaller`) | Unsigned descriptor exists and native direct-signing rejection is recorded; a real SignTool `/p7` companion signature is generated for detached verification coverage. | Companion PKCS#7 generation and policy checks remain implementation gaps. | | **Optional-provider / XML signing surfaces** (`.application`, `.manifest`, `.vsto`, `.deploy`) | Unsigned probe files exist and native SignTool rejection/provider-unavailable outcomes are recorded. | Signed ClickOnce/VSTO-style fixtures and tool-specific signing metadata. | | **Office macro containers** (`.docm`, `.xlsm`, `.pptm`, `.xlam`) | Unsigned probe files exist. | Signed Office/VBA macro-project fixtures generated with installed Office/VBE SIP, plus verification expectations. | @@ -96,6 +96,39 @@ The committed corpus already includes generated unsigned and signed vectors for --- +## Top 3 gaps worth filling next + +These are the highest-leverage gaps after comparing the native switch matrix, portable lifecycle coverage, package-orchestration work, and fixture corpus. The stable IDs are mirrored in `psign-cli-matrix.json` as `top_gap_ids`; implementation details should remain here to avoid overloading the CLI switch matrix with roadmap prose. + +| Priority | Gap id | Current state | Fill plan | +|----------|--------|---------------|-----------| +| 1 | `portable-msix-bundle-upload-final-signing` | Flat cleartext `.appx` / `.msix` portable Artifact Signing exists; `psign-tool code` can prepare nested bundle/upload contents and regenerate manifests/block maps, but final bundle/upload signing and encrypted packages remain outside the portable embedder. | Reuse the flat MSIX signer and publisher/block-map preparation, add bundle/upload traversal that signs nested packages before the outer container, define explicit rejection for encrypted packages, and add fixtures for unsigned bundle/upload -> signed verify/tamper cases. | +| 2 | `catalog-driver-package-authoring` | Portable `sign-catalog` can author generic CTL catalogs and `verify-catalog-member` can check explicit file membership, while Windows mode can sign/verify existing catalogs and mutate catalog databases. | Extend catalog authoring toward MakeCat/Inf2Cat/New-FileCatalog-compatible member metadata, add driver/INF policy diagnostics separately from generic catalogs, and grow the corpus with psign-authored plus native-authored driver-package catalogs. | +| 3 | `wdac-ci-policy-signing` | Detached PKCS#7 and catalog primitives exist, but WDAC / Code Integrity policy signing is documented only as adjacent backlog. | Define policy-file detection and expected signature container shape, route signing through existing detached PKCS#7/catalog CMS helpers, then add verification diagnostics that distinguish CMS validity from Windows deployment/CI policy acceptance. | + +These three outrank smaller parity refinements such as stricter timestamp routing or OCSP/CRL policy hardening because they unlock whole user workflows rather than polishing already-available verification paths. + +## Additional gap candidates worth filling + +The next tier is still worth tracking because each item either unlocks a recognizable signing workflow, removes a common Windows-only dependency, or closes an ambiguity that would otherwise make portable verification hard to trust. They are not mirrored in `psign-cli-matrix.json` yet because they are roadmap candidates rather than stable top priorities. + +| Rank | Gap id | Why it is worth filling | First useful slice | +|------|--------|-------------------------|--------------------| +| 4 | `standalone-pkcx-p7x-tooling` | Native SignTool users encounter standalone PKCS#7 / PKCX artifacts through `/p7`, `/p7ce`, `/p7co`, `/p7u`, and catalog-style detached workflows. The first standalone slice is implemented: `inspect-pkcs7` and `extract-pkcx-pkcs7` make raw PKCS#7 and AppX `PKCX` files first-class portable inputs. | Grow into explicit detached verify ergonomics and signing/export flows that map cleanly to the native `/p7*` switches. | +| 5 | `office-vba-signing-verification` | Office/VBA macro signing is a real Authenticode SIP surface that remains a practical Windows-only island; even diagnostic coverage would help teams inventory and migrate signed macro assets. | Start with Windows-mode detection, verify, and remove diagnostics around the `mso.dll` SIP, then document portable rejection and fixture requirements before attempting portable embed support. | +| 6 | `split-digest-signing-pipeline` | `/dg`, `/ds`, `/di`, and `/dxml` workflows are important for HSM, air-gapped, and service-mediated signing where the machine that hashes is not the machine that applies the signature. | Normalize one portable digest -> external signature -> ingest path for PE first, then reuse it for CAB/MSI/catalog as their portable embedders mature. | +| 7 | `portable-trust-policy-hardening` | Current portable trust is intentionally anchor-directory based; production verification often also needs disallowed CTLs/STLs, EKU/application-policy rules, revocation depth, OCSP/CRL edge cases, and TrustedPublisher semantics. | Add policy switches and diagnostics without pretending to be the OS trust store, prioritizing disallowed roots/intermediates and AuthRoot-derived pin/rule fixtures. | +| 8 | `clickonce-mage-compatible-signing` | ClickOnce deployments are still common in enterprise Windows estates and have their own manifest canonicalization, timestamping, and policy expectations beyond generic XML/package handling. | Implement detect/inspect/verify parity first, then add Mage-compatible signing and timestamping only after native-vs-portable fixture parity is clear. | +| 9 | `nuget-repository-signature-policy` | NuGet packages have author signatures, repository signatures, countersignatures, timestamp policy, and package-source trust decisions that go beyond the existing package digest primitives. | Extend inspection to distinguish author vs repository signatures and add policy diagnostics before attempting repository-signature authoring. | +| 10 | `vsix-timestamping-and-policy` | VSIX signing support without timestamp/policy parity leaves long-lived extension distribution workflows incomplete. | Add timestamp mutation and verify diagnostics for existing signed VSIX packages, with tamper/expiration fixtures. | +| 11 | `appinstaller-policy-verify` | App Installer files are small but security-sensitive orchestration manifests; portable hashing alone does not answer whether an update/feed policy is safe or acceptable. | Add policy-focused inspection and verify diagnostics for signed `.appinstaller` manifests, separate from MSIX package signing. | +| 12 | `encrypted-msix-delegation-fixtures` | Encrypted MSIX/AppX packages should probably remain a Windows/decryption-bound path, but explicit fixture-backed detection prevents confusing portable failures and regression drift. | Add corpus coverage and diagnostics that distinguish encrypted-package rejection from malformed-package failures, then route Windows-mode operations to the OS where possible. | +| 13 | `cab-replacement-multivolume-mutation` | CAB support covers important single-volume signing paths, but setup media and legacy installers can require replacement/multivolume behavior and removal parity. | Add negative/diagnostic coverage for unsupported CAB layouts first, then implement replacement and remove flows for the safest subset. | +| 14 | `msi-policy-expansion` | MSI/MSP signatures have policy branches such as `MsiDigitalSignatureEx` and installer-specific verification behavior that are distinct from generic PKCS#7 validity. | Extend MSI inspection to report signature-table variants and policy-relevant metadata before adding stricter trust decisions. | +| 15 | `extension-sip-provider-model` | Windows can delegate arbitrary subject formats to installed SIP providers, while portable mode only supports built-in Rust format handlers. | Define a narrow provider interface for digest/inspect/verify experiments, but keep signing gated until deterministic fixtures and security boundaries are understood. | + +--- + ## Native Windows SDK `signtool.exe` **Strengths:** Full Authenticode lifecycle — **sign**, **verify** (many policies), **timestamp**, **remove**, **catalog** ops, **sealing** / AppX constraints, response files, broad switch surface ([`psign-cli-matrix.json`](psign-cli-matrix.json)). @@ -163,7 +196,7 @@ Portable support is intentionally split by lifecycle stage. This keeps Linux/mac | Lifecycle stage | `psign-tool --mode portable ...` | `psign-tool portable ...` | Support level | |-----------------|-----------------------------------|----------------------------|---------------| | Digest computation | Routed through `verify` only when it can infer a supported subject format | `pe-digest`, `cab-digest`, and format-specific `verify-*` commands | Supported for PE/WinMD, CAB, MSI/MSP, WIM/ESD, cleartext MSIX/AppX, catalogs, and scripts | -| PKCS#7 inspection / extraction | `inspect-signature` routes to `inspect-authenticode` | `inspect-authenticode`, `extract-pe-pkcs7`, `extract-cab-pkcs7`, `extract-msi-pkcs7`, `list-pe-pkcs7` | Supported diagnostics; no trust decision by itself | +| PKCS#7 inspection / extraction | `inspect-signature` routes to `inspect-authenticode` | `inspect-authenticode`, `inspect-pkcs7`, `extract-pkcx-pkcs7`, `extract-pe-pkcs7`, `extract-cab-pkcs7`, `extract-msi-pkcs7`, `list-pe-pkcs7` | Supported diagnostics; no trust decision by itself | | Explicit-anchor trust verification | `verify` routes only when portable trust inputs are present and the inferred format has a trust command | `trust-verify-pe`, `trust-verify-cab`, `trust-verify-msi`, `trust-verify-esd`, `trust-verify-catalog`, `trust-verify-detached` | Supported with explicit anchors and bounded online AIA/OCSP/CRL; not OS store policy | | Remote hash/signing | PE Key Vault signing through top-level `sign`; other remote helpers are not routed | `sign-pe --azure-key-vault-*`, `artifact-signing-submit`, `azure-key-vault-sign-digest`, signer prehash commands | PE Key Vault signing embeds Authenticode; other remote helpers are digest-in/signature-out only | | Local-key signing | Top-level `sign` returns an explicit portable-not-implemented error | `sign-pe`, `sign-cab`, `sign-msi`, `sign-catalog`, `rdp` | Supported for PE, unsigned single-volume CAB, MSI/MSP, generic catalogs, and RDP local RSA signing; other Authenticode SIP subjects remain backlog | diff --git a/docs/psign-cli-matrix.json b/docs/psign-cli-matrix.json index 050fe42..4f8b647 100644 --- a/docs/psign-cli-matrix.json +++ b/docs/psign-cli-matrix.json @@ -6,6 +6,29 @@ "sdk_help_source": "parity-output/signtool-help-*.txt (local / gitignored)", "sdk_kit": "10.0.26100.0", "learn_doc": "https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool", + "last_audited": "2026-05-30", + "matrix_scope": { + "native_cli": "Windows SDK signtool.exe and rdpsign.exe switch parity where psign-tool delegates to Win32 signing, verify, timestamp, catalog, remove, and RDP code.", + "portable_lifecycle": "Linux/macOS-safe digest, CMS inspection, explicit-anchor trust, local/remote signing, timestamp helpers, and package-specific primitives surfaced through psign-tool portable and --mode portable.", + "package_orchestration": "dotnet/sign-style planning and guarded execution through psign-tool code for PE/WinMD, NuGet/SNuGet, VSIX, ZIP, MSIX/AppX prepare, ClickOnce/VSTO, and App Installer companion signatures." + }, + "capability_dimensions": [ + {"id": "argv-parity", "meaning": "Native-style slash aliases, response files, and global mode/verbosity behavior."}, + {"id": "sign-embed", "meaning": "Create a signature and embed it in the subject format or companion artifact."}, + {"id": "timestamp", "meaning": "Create or attach legacy/RFC3161 timestamp evidence to an existing or newly-created signature."}, + {"id": "verify-policy", "meaning": "Native WinTrust/CryptoAPI policy verification where available."}, + {"id": "digest-consistency", "meaning": "Recompute format-specific indirect data and compare it with PKCS#7/CMS contents without making an OS trust decision."}, + {"id": "explicit-anchor-trust", "meaning": "Portable CMS signature and certificate-chain verification to caller-supplied anchors, optionally with bounded AIA/OCSP/CRL fetches."}, + {"id": "inspect-extract", "meaning": "Read, list, or extract embedded PKCS#7/CMS/XMLDSig/package metadata for diagnostics and split-signing workflows."}, + {"id": "remove-mutate", "meaning": "Strip signatures, unauthenticated attributes, chains, or mutate signed package metadata."}, + {"id": "orchestrate", "meaning": "Plan and execute nested inside-out signing flows across files, manifests, and package containers."} + ], + "top_gap_ids": [ + "portable-msix-bundle-upload-final-signing", + "catalog-driver-package-authoring", + "wdac-ci-policy-signing" + ], + "top_gap_plan_doc": "docs/gap-analysis-signing-platforms.md#top-3-gaps-worth-filling-next", "global_options": [ {"native": "/q", "rust": "--quiet (-q)", "tier": "P0", "status": "implemented", "notes": "Suppress stdout on success"}, {"native": "/v", "rust": "--verbose (-v)", "tier": "P0", "status": "implemented", "notes": "Verbose diagnostics"}, @@ -210,6 +233,12 @@ "windows_mechanism": "`rdpsign.exe` parses RDP settings, signs the UTF-16 secure-settings blob with detached PKCS#7 (`CryptSignMessage`), serializes `{version,type,length,pkcs7}`, and writes `SignScope` / `Signature` records.", "rust_path": "`psign-tool rdp --sha256 file.rdp` implements the RDP-specific path directly; `psign-tool portable rdp --cert cert.pem --key key.pem file.rdp` uses the same RDP SignScope/secure-blob serializer and creates detached RSA/SHA-256 CMS without Win32. This is not a SIP/SignerSignEx3 file format.", "parity": "Unit fixtures under `tests/fixtures/rdp/` cover UTF-8, UTF-8 BOM, UTF-16 LE/BE with and without BOM, partial/stale SignScope/Signature records, malformed records, secure blob generation, serialized signature shape, and a signed `.rdp` generated with the repo test PFX. Portable CLI tests cover external PKCS#7 embedding and local RSA/SHA-256 cert+key signing; Windows tests exercise the same shared fixtures through the `psign` crate and optional integration can compare against native `rdpsign` output shape." + }, + { + "extensions": [".zip"], + "windows_mechanism": "No native signtool SIP peer for generic ZIP archives; package-specific ZIP containers such as VSIX, NuGet, and MSIX have their own signing formats.", + "rust_path": "`psign-tool portable verify-zip` and `trust-verify-zip` validate psign-defined custom ZIP Authenticode comment signatures. `psign-tool code` can also traverse generic ZIP containers for nested package/PE signing.", + "parity": "Portable tests cover `verify-zip` / `trust-verify-zip` and tamper rejection. See `docs/zip-authenticode-signing.md`; this is a psign compatibility format, not a WinTrust policy surface." } ], "portable_digest_cli": { @@ -223,13 +252,17 @@ {"name": "verify-pe", "maps_to_native_concept": "Consistency check after conceptual WinTrust success — similar to `psign-tool verify --rust-sip-pe-digest-check`"}, {"name": "trust-verify-pe", "maps_to_native_concept": "Explicit-anchor PKCS#7 trust + picky chain + PE digest (not WinTrust); optional `--as-of YYYY-MM-DD` for expired fixtures"}, {"name": "trust-verify-cab", "maps_to_native_concept": "`verify-cab` digest consistency then same trust stack as PE"}, + {"name": "trust-verify-msi", "maps_to_native_concept": "`verify-msi` digest consistency then same trust stack as PE"}, + {"name": "trust-verify-esd", "maps_to_native_concept": "`verify-esd` digest consistency then same trust stack as PE"}, {"name": "trust-verify-catalog", "maps_to_native_concept": "`verify-catalog` CMS digest consistency then picky trust when Authenticode-wrapped"}, {"name": "trust-verify-detached", "maps_to_native_concept": "Detached PKCS#7 vs raw content bytes (digest algo inferred from PKCS#7); blob normalization like Win32 detached helpers"}, + {"name": "trust-verify-zip", "maps_to_native_concept": "Custom ZIP Authenticode comment signature digest binding plus same explicit-anchor trust stack as PE"}, {"name": "pe-has-page-hashes", "maps_to_native_concept": "Subset of `verify /ph` — CMS signed-attribute probe plus raw OID TLV fallback"}, {"name": "pe-page-hash-info", "maps_to_native_concept": "CMS listing of SPC_PE_IMAGE_PAGE_HASHES V1/V2 attrs plus Authenticode flat-table parse (`parsed_page_hash_pairs`; `-` if unwrap fails)"}, {"name": "verify-pe-page-hashes", "maps_to_native_concept": "Experimental contiguous raw-file range verify vs parsed page-hash digests — not full WinTrust `/ph`"}, {"name": "pe-authenticode-ranges", "maps_to_native_concept": "Diagnostic listing of disjoint PE file ranges hashed by Authenticode image digest (`authenticode-rs` layout); aids `/ph` vs subject-digest reasoning"}, {"name": "verify-cab", "maps_to_native_concept": "`verify --rust-sip-cab-digest-check`"}, + {"name": "verify-zip", "maps_to_native_concept": "Custom ZIP Authenticode comment signature digest binding and reconstructed script digest"}, {"name": "verify-msi", "maps_to_native_concept": "`verify --rust-sip-msi-digest-check`"}, {"name": "verify-esd", "maps_to_native_concept": "`verify --rust-sip-esd-digest-check`"}, {"name": "verify-msix", "maps_to_native_concept": "`verify --rust-sip-msix-digest-check` (cleartext packages)"}, @@ -254,6 +287,8 @@ {"name": "catalog-signer-rs256-prehash", "maps_to_native_concept": "Same RS256 prehash on whole-file .cat PKCS#7 as pkcs7-signer-rs256-prehash; distinct from verify-catalog CTL scan"}, {"name": "pkcs7-signer-rs256-prehash", "maps_to_native_concept": "RS256 prehash from PKCS#7 DER alone (post-extract from PE/CAB/MSI or detached blob)"}, {"name": "pe-signer-rs256-prehash", "maps_to_native_concept": "RS256 prehash from embedded PE PKCS#7 row (index + signer-index)"}, + {"name": "inspect-pkcs7", "maps_to_native_concept": "Standalone PKCS#7/P7X inspection; accepts ContentInfo, bare SignedData, or AppX PKCX-wrapped AppxSignature.p7x"}, + {"name": "extract-pkcx-pkcs7", "maps_to_native_concept": "Strip AppX AppxSignature.p7x PKCX wrapper to inner PKCS#7 DER"}, {"name": "extract-pe-pkcs7", "maps_to_native_concept": "WIN_CERTIFICATE PKCS#7 row as raw DER (same index order as list-pe-pkcs7)"}, {"name": "list-pe-pkcs7", "maps_to_native_concept": "Enumerate embedded PKCS#7 rows (byte_len per index)"}, {"name": "inspect-pe-spc-indirect", "maps_to_native_concept": "JSON for SpcIndirectDataContent / digest in embedded PE PKCS#7"}, diff --git a/docs/psign-cli-matrix.md b/docs/psign-cli-matrix.md index 7423b4a..528d97f 100644 --- a/docs/psign-cli-matrix.md +++ b/docs/psign-cli-matrix.md @@ -1,6 +1,6 @@ # SignTool CLI parity matrix -This document summarizes native `signtool.exe` options plus the related `rdpsign.exe` RDP signing surface vs the **`psign-tool`** CLI (Rust package **`psign`**). The **machine-readable source of truth** is [`psign-cli-matrix.json`](psign-cli-matrix.json) (`commands.sign`, `commands.verify`, `commands.timestamp`, `commands.catdb`, `commands.remove`, `commands.rdp`, `global_options`, `invocation`, `code_sign_file_formats`). +This document summarizes native `signtool.exe` options plus the related `rdpsign.exe` RDP signing surface vs the **`psign-tool`** CLI (Rust package **`psign`**). The **machine-readable source of truth** is [`psign-cli-matrix.json`](psign-cli-matrix.json) (`commands.*`, `global_options`, `invocation`, `capability_dimensions`, `code_sign_file_formats`, `portable_digest_cli`, `top_gap_ids`). SDK help text used for cross-checking can be captured locally under **`parity-output/`** (`signtool-help-*.txt`; gitignored). The pinned kit version is recorded in this repo’s `sdk_kit` field in the JSON (currently aligned with `10.0.26100.0`). @@ -40,10 +40,34 @@ Full native ↔ Rust mappings, tiers, and per-flag notes are **only** maintained - **RDP signing**: `psign-tool rdp --sha256 file.rdp` ports `rdpsign.exe` by writing native `SignScope` / `Signature` records using detached PKCS#7 over the RDP secure-settings blob. `psign-tool portable rdp --cert cert.der --key key.pk8 file.rdp` uses the same RDP blob/record logic with portable RSA/SHA-256 CMS creation; fixtures cover UTF-8, UTF-16 with/without BOM, stale/partial signatures, malformed records, and a repo-test-cert signed sample. - **Artifact Signing REST for PE/WinMD**: `psign-tool portable sign-pe --artifact-signing-* --timestamp-url ...` and `psign-tool --mode portable sign --dmdf metadata.json --artifact-signing-* --timestamp-url ...` build, timestamp, and embed PE Authenticode signatures without Microsoft client DLLs. Windows dlib mode remains available for MSIX/AppX and other SIP formats. +## Expanded capability model + +The JSON now records the feature matrix across lifecycle dimensions, not just native switch spelling: + +| Dimension | Meaning | +|-----------|---------| +| `argv-parity` | Response files, slash aliases, and native-shaped global behavior | +| `sign-embed` / `timestamp` / `remove-mutate` | Signature creation, timestamp attachment, and signed-content mutation | +| `verify-policy` | WinTrust/CryptoAPI policy paths | +| `digest-consistency` / `explicit-anchor-trust` | Portable SIP/CMS verification without OS trust stores | +| `inspect-extract` / `orchestrate` | Diagnostics, split-signing primitives, and nested package planning/execution | + +`portable_digest_cli.commands` is checked against `psign-tool portable --help` by `tests/cli_matrix_docs.rs`, so newly-added portable subcommands must be reflected in the machine-readable matrix. + +## Top 3 gaps worth filling next + +The roadmap choice is maintained in [`gap-analysis-signing-platforms.md`](gap-analysis-signing-platforms.md#top-3-gaps-worth-filling-next); the JSON stores only the stable `top_gap_ids` for machine-readable cross-reference. Current priorities: + +| Gap id | Why it is high value | +|--------|----------------------| +| `portable-msix-bundle-upload-final-signing` | Closes the largest remaining Linux/Artifact Signing package gap after flat MSIX/AppX support. | +| `catalog-driver-package-authoring` | Turns existing catalog signing/member verification into a fuller driver/package catalog workflow. | +| `wdac-ci-policy-signing` | Builds on detached PKCS#7/catalog primitives for a security-policy workflow that is adjacent to existing Authenticode users. | + ## Gaps intentionally partial - **Split digest `/dg`, `/ds`, `/di`, `/dxml`**: Rust accepts equivalents; execution returns a structured error — use native `signtool` or atomic signing (`sign_digest_pipeline.rs`). -- **PKCS#7 product signing `/p7*`** (non-SIP): Flags exist; differs from PE SIP signing — partial in JSON. +- **PKCS#7 product signing `/p7*`** (non-SIP): Flags exist; differs from PE SIP signing. Portable `inspect-pkcs7` / `extract-pkcx-pkcs7` cover standalone inspection and AppX `PKCX` unwrap, but native-shaped product signing/export remains partial in JSON. - **Sign sealing / intent-to-seal / `/force` (sign)**, **`/c` template**, **`/sa`**, **`/fdchw` / `/tdchw` / `/rmc`**, seal warn flags**: CLI surfaces exist; many return explicit not-implemented errors (`sealing.rs`). - **Timestamp `/p7`, `/force`, `/nosealwarn`**: Explicit not-implemented errors. - **`/ms` (`--multiple-semantics`)**: Accepted; documented compatibility shim — WinTrust defaults vary by OS. diff --git a/docs/roadmap-authenticode-linux.md b/docs/roadmap-authenticode-linux.md index 8543188..15adac7 100644 --- a/docs/roadmap-authenticode-linux.md +++ b/docs/roadmap-authenticode-linux.md @@ -17,7 +17,7 @@ The primary `psign-tool` binary is unified: Windows mode depends on **`windows`* - **`crates/psign-sip-digest`** holds portable digest modules (**no `windows` dependency**). The Win32 binary re-exports them from **`src/win/sip_rust/mod.rs`** and keeps thin **`sign_*`** helpers that need **`GlobalOpts`**. - **`ci-unix`** runs **`cargo test -p psign-sip-digest --lib --locked`** (see `.github/workflows/ci-unix.yml`). -- **CLI:** **`psign-tool portable ...`** (runner in `crates/psign-digest-cli`) — `pe-digest`, `verify-pe` (digest-only PKCS#7 consistency), **`trust-verify-pe`** / **`trust-verify-cab`** / **`trust-verify-catalog`** / **`trust-verify-detached`** (explicit-anchor trust + picky chain), **`sign-pe`** (portable PE Authenticode CMS + `WIN_CERTIFICATE` embed with local RSA/SHA-2 keys), **`sign-cab`** (unsigned single-volume CAB reserve-header + tail PKCS#7 signing), **`sign-msi`** (MSI/MSP `DigitalSignature` stream signing), **`sign-catalog`** (portable generic CTL/catalog authoring + CMS signing), **`timestamp-pe-rfc3161`** (attach RFC3161 `timeStampToken` / granted `TimeStampResp` to PE `SignedData`), `pe-has-page-hashes`, `pe-page-hash-info`, `verify-pe-page-hashes`, `pe-authenticode-ranges` (Authenticode digest file segments), `verify-cab`, `verify-msi`, `verify-esd`, `verify-msix`, `verify-catalog`, `verify-script`, `cab-digest`, **`extract-cab-pkcs7`**, **`cab-signer-rs256-prehash`**, **`extract-msi-pkcs7`**, **`msi-signer-rs256-prehash`**, **`catalog-signer-rs256-prehash`**, **`inspect-pe-spc-indirect`**, **`extract-pe-pkcs7`**, **`list-pe-pkcs7`**, **`pe-signer-rs256-prehash`** (KV **`RS256`** CMS prehash; **`--signer-index`** for multi-**`SignerInfo`** **`SignedData`**), **`pkcs7-signer-rs256-prehash`**, **`append-pe-pkcs7`** (low-level append helper). Runs on Linux/macOS; does **`not`** call `WinVerifyTrust`. +- **CLI:** **`psign-tool portable ...`** (runner in `crates/psign-digest-cli`) — `pe-digest`, `verify-pe` (digest-only PKCS#7 consistency), **`trust-verify-pe`** / **`trust-verify-cab`** / **`trust-verify-catalog`** / **`trust-verify-detached`** (explicit-anchor trust + picky chain), **`sign-pe`** (portable PE Authenticode CMS + `WIN_CERTIFICATE` embed with local RSA/SHA-2 keys), **`sign-cab`** (unsigned single-volume CAB reserve-header + tail PKCS#7 signing), **`sign-msi`** (MSI/MSP `DigitalSignature` stream signing), **`sign-catalog`** (portable generic CTL/catalog authoring + CMS signing), **`timestamp-pe-rfc3161`** (attach RFC3161 `timeStampToken` / granted `TimeStampResp` to PE `SignedData`), `pe-has-page-hashes`, `pe-page-hash-info`, `verify-pe-page-hashes`, `pe-authenticode-ranges` (Authenticode digest file segments), `verify-cab`, `verify-msi`, `verify-esd`, `verify-msix`, `verify-catalog`, `verify-script`, `cab-digest`, **`extract-cab-pkcs7`**, **`cab-signer-rs256-prehash`**, **`extract-msi-pkcs7`**, **`msi-signer-rs256-prehash`**, **`catalog-signer-rs256-prehash`**, **`inspect-pkcs7`** / **`extract-pkcx-pkcs7`** (standalone PKCS#7 and AppX `PKCX` P7X helpers), **`inspect-pe-spc-indirect`**, **`extract-pe-pkcs7`**, **`list-pe-pkcs7`**, **`pe-signer-rs256-prehash`** (KV **`RS256`** CMS prehash; **`--signer-index`** for multi-**`SignerInfo`** **`SignedData`**), **`pkcs7-signer-rs256-prehash`**, **`append-pe-pkcs7`** (low-level append helper). Runs on Linux/macOS; does **`not`** call `WinVerifyTrust`. - **Trust library:** **`psign-authenticode-trust`** — see **[authenticode-trust-stack.md](authenticode-trust-stack.md)** and **[authroot-linux-verify.md](authroot-linux-verify.md)**. - Remaining Linux work: optional **revocation**, **PinRules**, and full CryptoAPI policy parity still need dedicated design; digest CLI extras (JSON output, stdin) remain optional. diff --git a/docs/rust-sip-gaps.md b/docs/rust-sip-gaps.md index 98bcacb..fe68dd0 100644 --- a/docs/rust-sip-gaps.md +++ b/docs/rust-sip-gaps.md @@ -25,7 +25,7 @@ Use **`verify --rust-sip-all-digest-checks`** to enable every experimental diges |---------|-----| | **Encrypted** MSIX / APPX (`.eappx`, `.emsix`, `.eappxbundle`, `.emsixbundle`) | **`EappxSip*`** / **`EappxBundleSip*`** — COM + **`EncryptedAppxHeader`** / keys; not a cleartext ZIP rehash. Rust checker returns an explicit error if you force MSIX digest parity on these extensions. | | **`ExtensionsSipGetSignedDataMsg`** | Dispatches to **optional third-party DLLs** enumerated from the package — not portable in-tree. | -| **Standalone `.p7x`** (**`P7xSip*`**) | Container extract (**PKCX** → inner PKCS#7); **`P7xSipVerifyIndirectData`** is effectively a null-check stub in **`AppxSip.dll`**. No separate “subject digest” to recompute beyond normal Authenticode. | +| **Standalone `.p7x`** (**`P7xSip*`**) | Container extract (**PKCX** → inner PKCS#7); **`P7xSipVerifyIndirectData`** is effectively a null-check stub in **`AppxSip.dll`**. Portable `inspect-pkcs7` / `extract-pkcx-pkcs7` cover the container-inspection slice; standalone `.p7x` signing/export remains outside the Rust SIP digest layer because there is no separate “subject digest” to recompute beyond normal Authenticode. | | **`mso.dll` / VBA** | Indirect data ultimately asks **`VBE7.DLL`** (`DllVbeGetHashOfCodeProjectEx`, …). Pure Rust would duplicate the VBA runtime and OLE project graph; optional future work is **FFI into `VBE7`**, not a small digest module. | ## Native `signtool` / Win32 backlog (not SIP-specific) diff --git a/tests/cli_matrix_docs.rs b/tests/cli_matrix_docs.rs new file mode 100644 index 0000000..50672f3 --- /dev/null +++ b/tests/cli_matrix_docs.rs @@ -0,0 +1,76 @@ +use assert_cmd::Command; +use serde_json::Value; +use std::collections::BTreeSet; + +fn portable_help_commands() -> BTreeSet { + let output = Command::cargo_bin("psign-tool") + .unwrap() + .arg("portable") + .arg("--help") + .output() + .expect("run psign-tool portable --help"); + assert!( + output.status.success(), + "portable help failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8_lossy(&output.stdout) + .lines() + .filter_map(|line| { + let trimmed = line.trim(); + if line.starts_with(" ") + && !trimmed.contains(' ') + && trimmed + .chars() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') + && trimmed != "help" + { + Some(trimmed.to_owned()) + } else { + None + } + }) + .collect() +} + +fn matrix_portable_commands() -> BTreeSet { + let matrix: Value = + serde_json::from_str(include_str!("../docs/psign-cli-matrix.json")).expect("matrix JSON"); + let mut commands = matrix["portable_digest_cli"]["commands"] + .as_array() + .expect("portable_digest_cli.commands array") + .iter() + .map(|entry| { + entry["name"] + .as_str() + .expect("portable command name") + .to_owned() + }) + .collect::>(); + + if !cfg!(feature = "artifact-signing-rest") { + commands.remove("artifact-signing-submit"); + } + if !cfg!(feature = "azure-kv-sign") { + commands.remove("azure-key-vault-sign-digest"); + } + if !cfg!(feature = "timestamp-http") { + commands.remove("rfc3161-timestamp-http-post"); + } + + commands +} + +#[test] +fn portable_cli_matrix_matches_help_commands() { + let help = portable_help_commands(); + let matrix = matrix_portable_commands(); + + let missing_from_matrix = help.difference(&matrix).cloned().collect::>(); + let stale_in_matrix = matrix.difference(&help).cloned().collect::>(); + + assert!( + missing_from_matrix.is_empty() && stale_in_matrix.is_empty(), + "portable_digest_cli.commands drifted from psign-tool portable --help\nmissing_from_matrix={missing_from_matrix:?}\nstale_in_matrix={stale_in_matrix:?}" + ); +} diff --git a/tests/cli_pe_digest.rs b/tests/cli_pe_digest.rs index f3555f5..0f72f34 100644 --- a/tests/cli_pe_digest.rs +++ b/tests/cli_pe_digest.rs @@ -86,6 +86,8 @@ fn help_lists_core_subcommands() { "pe-authenticode-ranges", "artifact-signing-metadata-check", "inspect-authenticode", + "inspect-pkcs7", + "extract-pkcx-pkcs7", "inspect-pe-spc-indirect", "extract-pe-pkcs7", "list-pe-pkcs7", @@ -3550,6 +3552,64 @@ fn inspect_pkcs7_parity_cli_stdout_matches_library_tiny32() { ); } +#[test] +fn inspect_pkcs7_cli_accepts_pkcx_wrapped_appx_signature() { + let pe_bytes = std::fs::read(tiny32_fixture()).expect("read tiny32"); + let der = verify_pe::pe_first_pkcs7_signed_data_der(&pe_bytes).expect("extract pkcs7"); + let dir = tempfile::tempdir().expect("tempdir"); + let blob = dir.path().join("AppxSignature.p7x"); + let mut p7x = b"PKCX".to_vec(); + p7x.extend_from_slice(&der); + std::fs::write(&blob, &p7x).expect("write p7x"); + + let mut cmd = portable_cmd(); + cmd.arg("inspect-pkcs7").arg(&blob); + let assert = cmd.assert().success(); + let out = std::str::from_utf8(&assert.get_output().stdout).expect("utf8"); + let cli_json: Value = serde_json::from_str(out.trim()).expect("CLI JSON"); + let lib_json = serde_json::to_value(inspect_authenticode_pkcs7_der(&der).expect("lib")) + .expect("serialize lib report"); + assert_eq!( + cli_json.get("authenticode_digest"), + lib_json.get("authenticode_digest") + ); +} + +#[test] +fn extract_pkcx_pkcs7_strips_wrapper_to_inner_der() { + let pe_bytes = std::fs::read(tiny32_fixture()).expect("read tiny32"); + let der = verify_pe::pe_first_pkcs7_signed_data_der(&pe_bytes).expect("extract pkcs7"); + let dir = tempfile::tempdir().expect("tempdir"); + let p7x_path = dir.path().join("AppxSignature.p7x"); + let out_path = dir.path().join("inner.p7"); + let mut p7x = b"PKCX".to_vec(); + p7x.extend_from_slice(&der); + std::fs::write(&p7x_path, &p7x).expect("write p7x"); + + let mut cmd = portable_cmd(); + cmd.arg("extract-pkcx-pkcs7") + .arg(&p7x_path) + .arg("--output") + .arg(&out_path); + cmd.assert().success(); + assert_eq!(std::fs::read(&out_path).expect("read output"), der); +} + +#[test] +fn extract_pkcx_pkcs7_rejects_raw_pkcs7_without_wrapper() { + let pe_bytes = std::fs::read(tiny32_fixture()).expect("read tiny32"); + let der = verify_pe::pe_first_pkcs7_signed_data_der(&pe_bytes).expect("extract pkcs7"); + let dir = tempfile::tempdir().expect("tempdir"); + let blob = dir.path().join("raw.p7"); + std::fs::write(&blob, &der).expect("write raw pkcs7"); + + let mut cmd = portable_cmd(); + cmd.arg("extract-pkcx-pkcs7").arg(&blob); + cmd.assert() + .failure() + .stderr(predicate::str::contains("missing PKCX wrapper")); +} + #[test] fn portable_verify_negative_inspect_authenticode_invalid_pkcs7_cli() { let dir = tempfile::tempdir().unwrap();