From 8226d1ccde095b42945afd3a3facc9abf87071ae Mon Sep 17 00:00:00 2001 From: Marc-Andre Moreau Date: Thu, 2 Jul 2026 11:11:30 -0400 Subject: [PATCH 1/3] Add portable AKV PowerShell signing Extend --mode portable sign with Azure Key Vault-backed signing for PowerShell Authenticode script targets such as .psd1, reusing the portable core path and covering it with CLI regression tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Cargo.toml | 2 + crates/psign-portable-core/src/lib.rs | 19 +++++- docs/gap-analysis-signing-platforms.md | 8 +-- docs/migration-azuresigntool.md | 6 +- src/portable_sign.rs | 90 ++++++++++++++++++++++---- tests/cli_pe_digest.rs | 39 +++++++++++ 6 files changed, 143 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 83f17c2..bd90284 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,9 +46,11 @@ default = [ ] ## Azure Key Vault signing (`AuthenticatorDigestSign` callback + REST); enables Azure-shaped CLI flags on `sign`. azure-kv-sign = [ + "dep:psign-portable-core", "dep:psign-azure-kv-rest", "dep:reqwest", "psign-digest-cli/azure-kv-sign-portable", + "psign-portable-core/azure-kv-sign", ] ## Azure Artifact Signing / Trusted Signing **data-plane** hash signing (REST LRO); experimental helper command `artifact-signing-submit`. artifact-signing-rest = [ diff --git a/crates/psign-portable-core/src/lib.rs b/crates/psign-portable-core/src/lib.rs index a9003af..7612515 100644 --- a/crates/psign-portable-core/src/lib.rs +++ b/crates/psign-portable-core/src/lib.rs @@ -311,6 +311,8 @@ pub struct PortableSignRequest { #[serde(default)] pub azure_key_vault_certificate: Option, #[serde(default)] + pub azure_key_vault_certificate_version: Option, + #[serde(default)] pub azure_key_vault_access_token: Option, #[serde(default)] pub azure_key_vault_client_id: Option, @@ -320,6 +322,8 @@ pub struct PortableSignRequest { pub azure_key_vault_tenant_id: Option, #[serde(default)] pub azure_key_vault_managed_identity: Option, + #[serde(default)] + pub azure_authority: Option, // Azure Artifact Signing / Trusted Signing #[serde(default)] pub artifact_signing_endpoint: Option, @@ -367,11 +371,13 @@ impl Default for PortableSignRequest { timestamp_hash_algorithm: None, azure_key_vault_url: None, azure_key_vault_certificate: None, + azure_key_vault_certificate_version: None, azure_key_vault_access_token: None, azure_key_vault_client_id: None, azure_key_vault_client_secret: None, azure_key_vault_tenant_id: None, azure_key_vault_managed_identity: None, + azure_authority: None, artifact_signing_endpoint: None, artifact_signing_account_name: None, artifact_signing_profile_name: None, @@ -1437,11 +1443,16 @@ fn load_azure_key_vault_signing_provider(request: &PortableSignRequest) -> Resul tenant_id: request.azure_key_vault_tenant_id.as_deref(), client_id: request.azure_key_vault_client_id.as_deref(), client_secret: request.azure_key_vault_client_secret.as_deref(), - authority: None, + authority: request.azure_authority.as_deref(), }; let token = psign_azure_kv_rest::acquire_kv_access_token(&auth)?; - let key_vault_certificate = - psign_azure_kv_rest::fetch_kv_certificate(&http, &vault_url, &certificate, None, &token)?; + let key_vault_certificate = psign_azure_kv_rest::fetch_kv_certificate( + &http, + &vault_url, + &certificate, + request.azure_key_vault_certificate_version.as_deref(), + &token, + )?; let signer_cert_der = psign_azure_kv_rest::kv_decode_cer_b64(&key_vault_certificate.cer)?; let signer_cert = rdp::parse_certificate(&signer_cert_der).context("parse Key Vault signer certificate")?; @@ -3889,11 +3900,13 @@ mod tests { timestamp_hash_algorithm: None, azure_key_vault_url: None, azure_key_vault_certificate: None, + azure_key_vault_certificate_version: None, azure_key_vault_access_token: None, azure_key_vault_client_id: None, azure_key_vault_client_secret: None, azure_key_vault_tenant_id: None, azure_key_vault_managed_identity: None, + azure_authority: None, artifact_signing_endpoint: None, artifact_signing_account_name: None, artifact_signing_profile_name: None, diff --git a/docs/gap-analysis-signing-platforms.md b/docs/gap-analysis-signing-platforms.md index 08156e0..a39e4e3 100644 --- a/docs/gap-analysis-signing-platforms.md +++ b/docs/gap-analysis-signing-platforms.md @@ -84,13 +84,13 @@ The committed corpus already includes generated unsigned and signed vectors for | Goal | Today | Gap | |------|--------|-----| | **Drop-in Linux replacement for `signtool.exe` sign/verify** | Not supported | Signing and WinTrust-backed verify require Windows CryptAPI/SIP (`SignerSignEx3`, `WinVerifyTrust`). | -| **Drop-in Linux replacement for AzureSignTool** | Partial | **`psign-tool portable sign-pe --azure-key-vault-* --timestamp-url ...`** and **`psign-tool --mode portable sign --azure-key-vault-* --timestamp-url ...`** can build timestamped PE Authenticode signatures with Key Vault RSA signing. **`azure-key-vault-sign-digest`** remains available for lower-level **`keys/sign`** workflows. Gaps: non-PE remote-sign embedding still requires Windows mode or future portable signer support. | +| **Drop-in Linux replacement for AzureSignTool** | Partial | **`psign-tool portable sign-pe --azure-key-vault-* --timestamp-url ...`** and **`psign-tool --mode portable sign --azure-key-vault-* --timestamp-url ...`** can build timestamped PE Authenticode signatures with Key Vault RSA signing, and native-shaped portable sign now also covers PowerShell Authenticode script formats (`.ps1`, `.psd1`, `.psm1`, `.ps1xml`, `.psc1`, `.cdxml`, `.mof`). **`azure-key-vault-sign-digest`** remains available for lower-level **`keys/sign`** workflows. Gaps: other non-PE remote-sign embedders still require Windows mode or future portable signer support. | | **Drop-in Linux replacement for Artifact Signing (dlib / REST)** | Partial | PE/WinMD is supported through **`psign-tool portable sign-pe --artifact-signing-* --timestamp-url ...`** and **`psign-tool --mode portable sign --dmdf ... --artifact-signing-* --timestamp-url ...`**. CAB and MSI/MSP are supported through scoped portable commands and native-shaped in-place Artifact Signing; generic catalogs are supported through **`portable sign-catalog --artifact-signing-*`**. Native-shaped portable Artifact Signing supports input file lists, skip-signed, continue-on-error, and max parallelism for supported targets. The lower-level **`artifact-signing-submit`** helper remains available for digest → JSON workflows. Gaps: MSIX/AppX, non-PE timestamp mutation, and other SIP formats still require Windows dlib mode or future portable embedders. | | **Linux verify + digest parity for many Authenticode formats** | Supported | **`psign-tool portable`** covers PE, CAB, MSI, ESD/WIM, cleartext MSIX, catalog, scripts; **`trust-verify-*`** adds anchor-based CMS trust (see [`authenticode-trust-stack.md`](authenticode-trust-stack.md)). | | **Maximum Windows-mode Authenticode subject formats** | Windows mode delegates most SIP-registered subjects to OS providers | Remaining gaps are first-class CLI affordances, parity fixtures, generic SIP remove, catalog authoring/member policy, Office/VBA ergonomics, extension SIP coverage, and standalone `.p7x` handling. | | **Maximum portable-mode Authenticode subject formats** | Portable mode covers digest/trust for PE, CAB, MSI, ESD/WIM, cleartext MSIX, catalogs, scripts, and detached PKCS#7; local signing for PE/CAB/MSI/generic catalogs is explicitly scoped; Artifact Signing REST can sign PE/WinMD, CAB, MSI/MSP, and generic catalogs | Portable gaps include MSIX signing/embed, non-PE timestamp mutation, WinTrust/CryptoAPI policy, encrypted MSIX, extension SIPs, Office/VBA, standalone `.p7x`, and package-specific ecosystems. | -**Practical Linux path today:** Use **`psign-tool portable`** for **digest computation**, **local signing** of PE/CAB/MSI/generic catalogs, **Key Vault PE signing** (`portable sign-pe` or `--mode portable sign`), **Artifact Signing REST PE signing** (`portable sign-pe --artifact-signing-*` or `--mode portable sign --dmdf ... --artifact-signing-*`), **Key Vault `keys/sign`** on digest files (**`azure-key-vault-sign-digest`** with **`--features azure-kv-sign-portable`**), low-level **`:sign` REST** (**`artifact-signing-submit`** with **`--features artifact-signing-rest`**), **inspect**, and **verify/trust** across supported formats. Broader native-shaped signing and unsupported SIP embedders still require **`psign-tool`** / **`SignerSignEx3`** (or native **`signtool.exe`**). Cookbook: [`linux-signing-pipelines.md`](linux-signing-pipelines.md). +**Practical Linux path today:** Use **`psign-tool portable`** for **digest computation**, **local signing** of PE/CAB/MSI/generic catalogs, **Key Vault PE and PowerShell script signing** (`portable sign-pe` or `--mode portable sign`), **Artifact Signing REST PE signing** (`portable sign-pe --artifact-signing-*` or `--mode portable sign --dmdf ... --artifact-signing-*`), **Key Vault `keys/sign`** on digest files (**`azure-key-vault-sign-digest`** with **`--features azure-kv-sign-portable`**), low-level **`:sign` REST** (**`artifact-signing-submit`** with **`--features artifact-signing-rest`**), **inspect**, and **verify/trust** across supported formats. Broader native-shaped signing and unsupported SIP embedders still require **`psign-tool`** / **`SignerSignEx3`** (or native **`signtool.exe`**). Cookbook: [`linux-signing-pipelines.md`](linux-signing-pipelines.md). **Long-term Linux signing** (if required): extend the portable **CMS `SignerInfo` production** (inside **`SignedData`**) + **format-specific embedding** beyond the current PE/CAB/MSI/catalog subset to MSIX `ContentTypes` / manifest glue and other package-native formats, then combine with **remote signing** (KV REST, Artifact Signing `:sign` LRO). [`pkcs7.rs`](crates/psign-sip-digest/src/pkcs7.rs) holds parse/replace helpers, **`signed_data_replace_first_signer_info`**, **`encode_pkcs7_content_info_signed_data_der`**, **RSA PKCS#1 RS256** prehash ↔ **`SignerInfo.signature`** parity tests (`rsa_pkcs1v15_signed_attrs_verify`), and **`signer_info_sha256_digest_over_signed_attrs`** (documented KV **`RS256`** input shape); [`pe_embed.rs`](crates/psign-sip-digest/src/pe_embed.rs) can **wrap PKCS#7**, **append** rows (including after signer splice experiments), and **recompute `CheckSum`**. **`psign-tool portable pe-signer-rs256-prehash`** surfaces the **32-byte** prehash for Linux KV workflows; MSIX signing/embed and non-PE timestamp mutation remain backlog (see [`rust-sip-gaps.md`](rust-sip-gaps.md)). @@ -198,7 +198,7 @@ Portable support is intentionally split by lifecycle stage. This keeps Linux/mac | 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`, `inspect-pkcs7`, `extract-pkcx-pkcs7`, `extract-pe-pkcs7`, `extract-cab-pkcs7`, `extract-msi-pkcs7`, `list-pe-pkcs7` | Supported diagnostics; no trust decision by itself | | Portable trust verification | `verify` routes to portable trust commands by default when automatic AuthRoot is enabled; explicit trust inputs still force trust routing | `trust-verify-pe`, `trust-verify-cab`, `trust-verify-msi`, `trust-verify-esd`, `trust-verify-catalog`, `trust-verify-detached` | Supported with automatic AuthRoot CAB cache or explicit anchors plus 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 | +| Remote hash/signing | PE plus PowerShell Authenticode script 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 | Top-level Key Vault signing embeds Authenticode for PE/WinMD and PowerShell script targets; 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 | | CMS creation from scratch | Not exposed through the native-shaped verb | PE/CAB/MSI Authenticode CMS creation through `sign-pe`, `sign-cab`, `sign-msi`, generic CTL/catalog CMS creation through `sign-catalog`, and `psign-sip-digest` helpers | Supported for PE, CAB, MSI, and generic catalog RSA/SHA-2; reusable CMS work remains to extend MSIX | | Format-specific Authenticode embed | Not implemented | `sign-pe` for PE, `sign-cab` for unsigned single-volume CABs, `sign-msi` for MSI/MSP `DigitalSignature` streams, `sign-catalog` for CTL `eContent` authoring; `append-pe-pkcs7` remains lower-level PE append plumbing | PE supported; CAB initial signing supported; MSI stream signing supported; generic catalog authoring supported; MSIX production embedder is backlog | @@ -208,7 +208,7 @@ Portable support is intentionally split by lifecycle stage. This keeps Linux/mac The compatibility rule is: **portable mode may prove digest/CMS consistency and explicit-anchor trust, but it must not silently emulate Windows policy.** When a user asks for a Windows-only lifecycle stage, the CLI should fail with an explicit unsupported/not-implemented message and point to the closest portable helper. -**Remote signing steps:** With **`--features azure-kv-sign-portable`**, **`sign-pe --azure-key-vault-*`** performs full PE Authenticode signing with Key Vault RSA signatures, while **`azure-key-vault-sign-digest`** performs Azure Key Vault **`keys/sign`** on a **raw digest file** for lower-level workflows. **`pe-signer-rs256-prehash`**, **`cab-signer-rs256-prehash`**, **`msi-signer-rs256-prehash`**, and **`catalog-signer-rs256-prehash`** (**`--encoding raw`**) emit the **32-byte** **`RS256`** input over **`SignerInfo.signedAttrs`** (distinct from subject-layout digests and from **`verify-catalog`**’s CTL **`eContent`** / PKCS#9 checks). With **`--features artifact-signing-rest`**, **`artifact-signing-submit`** calls Trusted Signing **`:sign`**, and **`sign-pe --artifact-signing-*`** / top-level **`--mode portable sign --artifact-signing-*`** use that REST signature to embed PE/WinMD Authenticode. CAB/MSI/catalog remote-sign CLI routing, MSIX embedding, and broader native-shaped remote-sign routing remain future portable embedder work. +**Remote signing steps:** With **`--features azure-kv-sign-portable`**, **`sign-pe --azure-key-vault-*`** performs full PE Authenticode signing with Key Vault RSA signatures, and top-level **`--mode portable sign --azure-key-vault-*`** now covers PE/WinMD plus PowerShell Authenticode script targets. **`azure-key-vault-sign-digest`** performs Azure Key Vault **`keys/sign`** on a **raw digest file** for lower-level workflows. **`pe-signer-rs256-prehash`**, **`cab-signer-rs256-prehash`**, **`msi-signer-rs256-prehash`**, and **`catalog-signer-rs256-prehash`** (**`--encoding raw`**) emit the **32-byte** **`RS256`** input over **`SignerInfo.signedAttrs`** (distinct from subject-layout digests and from **`verify-catalog`**’s CTL **`eContent`** / PKCS#9 checks). With **`--features artifact-signing-rest`**, **`artifact-signing-submit`** calls Trusted Signing **`:sign`**, and **`sign-pe --artifact-signing-*`** / top-level **`--mode portable sign --artifact-signing-*`** use that REST signature to embed PE/WinMD Authenticode. CAB/MSI/catalog remote-sign CLI routing, MSIX embedding, and broader native-shaped remote-sign routing remain future portable embedder work. **RFC 3161 TSA helpers:** **`rfc3161-timestamp-req`** builds **`TimeStampReq`** DER from **`--digest-hex`** / **`--digest-file`** (message-imprint preimage; optional **`--nonce`**, **`--cert-req`**) for **`curl`** / OpenSSL **`ts`** against a timestamp URL. **`rfc3161-timestamp-resp-inspect`** prints **`pki_status`** / **`pki_status_int`** (raw status INTEGER) / **`granted`** / token length, **`time_stamp_token_prefix_hex`** (first **16** octets of the raw **`timeStampToken`** TLV, or **`-`** when absent — handy for **`ContentInfo`** / CMS shape checks), **`status_strings_json`** (**`PKIFreeText`**), **`fail_info_tlv_hex`**, and **`fail_info_flags_json`** (RFC 2510 Appendix A **`PKIFailureInfo`** bit names through **`badPOP`**, then **`bit_N`**; **`null`** when the **`BIT STRING`** body is not decodable). Parseable CMS **`id-ct-TSTInfo`** tokens also surface structural **`tst_info_*`** diagnostics: policy OID, message-imprint digest OID/hash, serial, **`genTime`**, and nonce. Optional **`rfc3161-timestamp-http-post`** (**`--features timestamp-http`**) performs the HTTPS POST without **`curl`**. **`timestamp-pe-rfc3161`** can then attach a raw **`timeStampToken`** or granted **`TimeStampResp`** token to an existing PE Authenticode `SignerInfo` as the Microsoft RFC3161 unsigned attribute. This still does not clone every **`SignerTimeStampEx3`** policy branch or timestamp non-PE subjects. diff --git a/docs/migration-azuresigntool.md b/docs/migration-azuresigntool.md index de0b13f..396f9c6 100644 --- a/docs/migration-azuresigntool.md +++ b/docs/migration-azuresigntool.md @@ -1,6 +1,6 @@ # Migrating from AzureSignTool -This project can replace **AzureSignTool** for Windows signing when built with **`--features azure-kv-sign`**. **`psign-tool portable`** covers digest checks, verification, and (with **`--features azure-kv-sign-portable`**) Key Vault **`keys/sign`** on digest files plus PE Authenticode signing through **`portable sign-pe`** or the PE subset of **`--mode portable sign`**. Windows mode remains the broader native-shaped signing path. +This project can replace **AzureSignTool** for Windows signing when built with **`--features azure-kv-sign`**. **`psign-tool portable`** covers digest checks, verification, and (with **`--features azure-kv-sign-portable`**) Key Vault **`keys/sign`** on digest files plus PE Authenticode signing through **`portable sign-pe`** and native-shaped **`--mode portable sign`** for PE/WinMD plus PowerShell Authenticode script formats (`.ps1`, `.psd1`, `.psm1`, `.ps1xml`, `.psc1`, `.cdxml`, `.mof`). Windows mode remains the broader native-shaped signing path. **Azure Artifact Signing (Trusted Signing)** via Microsoft’s decoupled **`Azure.CodeSigning.Dlib.dll`** is **not** the Key Vault path: use **`--dlib`** / **`--trusted-signing-dlib-root`** with **`--dmdf`** only (never mixed with **`--azure-key-vault-url`**). See [`migration-artifact-signing.md`](migration-artifact-signing.md). PowerShell OpenAuthenticode overlap (inspect JSON, REST submit, EKU prefix selection) is summarized in [`psa-interoperability.md`](psa-interoperability.md). @@ -44,7 +44,7 @@ psign-tool.exe sign ^ | `-coe` | `--continue-on-error` | | `-mdop` | `--max-degree-of-parallelism` | -**`-s` (skip signed)** in AzureSignTool conflicts with native **`/s` (certificate store name)** in this tool. Use **`--skip-signed`** instead. In `--mode portable sign`, PE/WinMD targets are skipped only when the embedded Authenticode digest verifies; unsigned files still sign normally, and corrupt existing signatures fail. +**`-s` (skip signed)** in AzureSignTool conflicts with native **`/s` (certificate store name)** in this tool. Use **`--skip-signed`** instead. In `--mode portable sign`, PE/WinMD targets are skipped only when the embedded Authenticode digest verifies; unsigned files still sign normally, and corrupt existing signatures fail. PowerShell Authenticode script targets now sign through the same Key Vault-backed portable path, but they do not yet have native-shaped `--skip-signed` detection. ### Authentication notes @@ -146,4 +146,4 @@ Use the appropriate portable subcommands for your format (`verify-pe`, catalog c ## Integration testing -Automated CI uses **`psign-server azure-key-vault-server`** as a local Key Vault replacement. Non-ignored E2E tests cover both **`psign-tool portable azure-key-vault-sign-digest`** and full portable PE signing through **`portable sign-pe`** / **`--mode portable sign`** with a dummy access token, then verify the embedded Authenticode signature without contacting Azure. +Automated CI uses **`psign-server azure-key-vault-server`** as a local Key Vault replacement. Non-ignored E2E tests cover both **`psign-tool portable azure-key-vault-sign-digest`** and full portable signing through **`portable sign-pe`** / **`--mode portable sign`** with a dummy access token, including PowerShell Authenticode script coverage, then verify the resulting signature without contacting Azure. diff --git a/src/portable_sign.rs b/src/portable_sign.rs index 8c6f177..8641c85 100644 --- a/src/portable_sign.rs +++ b/src/portable_sign.rs @@ -597,15 +597,23 @@ fn sign_one_target( fn sign_one_target_azure_key_vault(target: &Path, args: &SignArgs) -> Result<()> { let ext = target_extension_lower(target); - if !is_pe_winmd_extension(&ext) { - return Err(anyhow!( - "portable Azure Key Vault signing is currently implemented only for PE/WinMD targets; got {}", + let tmp = temporary_output_path(target); + let result = match ext.as_str() { + ext if is_pe_winmd_extension(ext) => run_portable_sign_pe_azure_key_vault(target, &tmp, args), + ext if is_portable_powershell_script_extension(ext) => { + if args.append_signature { + Err(anyhow!( + "--as/--append-signature is only supported for portable PE/WinMD signing" + )) + } else { + run_portable_sign_script_azure_key_vault(target, &tmp, args) + } + } + _ => Err(anyhow!( + "portable Azure Key Vault signing is currently implemented for PE/WinMD and PowerShell Authenticode script targets (.ps1, .psd1, .psm1, .ps1xml, .psc1, .cdxml, .mof); got {}", target.display() - )); + )), } - - let tmp = temporary_output_path(target); - let result = run_portable_sign_pe_azure_key_vault(target, &tmp, args) .and_then(|_| { std::fs::copy(&tmp, target) .with_context(|| format!("replace '{}' with signed output", target.display()))?; @@ -667,6 +675,10 @@ fn is_pe_winmd_extension(ext: &str) -> bool { matches!(ext, "exe" | "dll" | "sys" | "ocx" | "efi" | "winmd") } +fn is_portable_powershell_script_extension(ext: &str) -> bool { + psign_sip_digest::ps_script::extension_supported(ext) +} + fn target_has_valid_existing_pe_signature(target: &Path) -> Result { let ext = target_extension_lower(target); if !is_pe_winmd_extension(&ext) { @@ -760,6 +772,62 @@ fn run_portable_sign_pe_azure_key_vault( .map_err(|_| anyhow!("portable sign-pe runner panicked"))? } +#[cfg(feature = "azure-kv-sign")] +fn run_portable_sign_script_azure_key_vault( + target: &Path, + output: &Path, + args: &SignArgs, +) -> Result<()> { + let request = psign_portable_core::PortableSignRequest { + path: target.to_path_buf(), + output_path: Some(output.to_path_buf()), + hash_algorithm: portable_core_digest(args.digest)?, + chain_certificate_paths: args.additional_certs.clone(), + timestamp_server: text_opt(args.timestamp_url.as_deref()).map(ToOwned::to_owned), + timestamp_hash_algorithm: args + .timestamp_digest + .map(portable_core_timestamp_digest) + .transpose()?, + azure_key_vault_url: text_opt(args.azure_key_vault_url.as_deref()).map(ToOwned::to_owned), + azure_key_vault_certificate: text_opt(args.azure_key_vault_certificate.as_deref()) + .map(ToOwned::to_owned), + azure_key_vault_certificate_version: text_opt( + args.azure_key_vault_certificate_version.as_deref(), + ) + .map(ToOwned::to_owned), + azure_key_vault_access_token: text_opt(args.azure_key_vault_access_token.as_deref()) + .map(ToOwned::to_owned), + azure_key_vault_client_id: text_opt(args.azure_key_vault_client_id.as_deref()) + .map(ToOwned::to_owned), + azure_key_vault_client_secret: text_opt(args.azure_key_vault_client_secret.as_deref()) + .map(ToOwned::to_owned), + azure_key_vault_tenant_id: text_opt(args.azure_key_vault_tenant_id.as_deref()) + .map(ToOwned::to_owned), + azure_key_vault_managed_identity: Some(effective_azure_key_vault_managed_identity(args)), + azure_authority: text_opt(args.azure_authority.as_deref()).map(ToOwned::to_owned), + ..Default::default() + }; + psign_portable_core::portable_sign(request) + .map(|_| ()) + .with_context(|| { + format!( + "portable Azure Key Vault PowerShell script target '{}'", + target.display() + ) + }) +} + +#[cfg(not(feature = "azure-kv-sign"))] +fn run_portable_sign_script_azure_key_vault( + _target: &Path, + _output: &Path, + _args: &SignArgs, +) -> Result<()> { + Err(anyhow!( + "portable Azure Key Vault signing support is not compiled into this build (feature: azure-kv-sign)" + )) +} + fn run_portable_sign_pe_artifact_signing( target: &Path, output: &Path, @@ -1067,7 +1135,7 @@ fn artifact_signing_metadata(args: &SignArgs) -> Result Result { @@ -1077,13 +1145,13 @@ fn portable_core_digest( crate::cli::DigestAlgorithm::Sha512 => psign_portable_core::PortableDigestAlgorithm::Sha512, crate::cli::DigestAlgorithm::Sha1 | crate::cli::DigestAlgorithm::CertHash => { return Err(anyhow!( - "portable MSIX/AppX Artifact Signing supports SHA256, SHA384, and SHA512 file digests" + "portable cloud signing supports SHA256, SHA384, and SHA512 file digests" )); } }) } -#[cfg(feature = "artifact-signing-rest")] +#[cfg(any(feature = "artifact-signing-rest", feature = "azure-kv-sign"))] fn portable_core_timestamp_digest( digest: crate::cli::DigestAlgorithm, ) -> Result { @@ -1102,7 +1170,7 @@ fn portable_core_timestamp_digest( } crate::cli::DigestAlgorithm::CertHash => { return Err(anyhow!( - "portable MSIX/AppX Artifact Signing timestamp digest does not support certHash" + "portable cloud signing timestamp digest does not support certHash" )); } }) diff --git a/tests/cli_pe_digest.rs b/tests/cli_pe_digest.rs index 4d68b6a..acba6a3 100644 --- a/tests/cli_pe_digest.rs +++ b/tests/cli_pe_digest.rs @@ -4757,6 +4757,45 @@ fn mode_portable_sign_uses_azure_key_vault_for_pe() { verify.assert().success(); } +#[cfg(all(feature = "timestamp-server", feature = "azure-kv-sign"))] +#[test] +fn mode_portable_sign_uses_azure_key_vault_for_psd1() { + let dir = tempfile::tempdir().unwrap(); + let manifest_path = dir.path().join("sample.kv-mode-portable-signed.psd1"); + std::fs::copy( + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/unsigned-sample.psd1"), + &manifest_path, + ) + .expect("copy unsigned psd1"); + + let (mut guard, url, certificate) = spawn_psign_azure_key_vault_server(2); + let mut cmd = Command::cargo_bin("psign-tool").unwrap(); + cmd.arg("--mode") + .arg("portable") + .arg("sign") + .arg("--digest") + .arg("sha256") + .arg("--azure-key-vault-url") + .arg(&url) + .arg("--azure-key-vault-certificate") + .arg(&certificate) + .arg("--azure-key-vault-accesstoken") + .arg("test-token") + .arg(&manifest_path); + cmd.assert() + .success() + .stdout(predicate::str::contains("Signed:")); + let status = guard.0.wait().expect("server exit"); + assert!(status.success(), "server failed with {status}"); + + let mut verify = portable_cmd(); + verify.arg("verify-script").arg(&manifest_path); + verify.assert().success(); + + let signed = std::fs::read_to_string(&manifest_path).expect("read signed psd1"); + assert!(signed.contains("# SIG # Begin signature block")); +} + #[cfg(all(feature = "timestamp-server", feature = "azure-kv-sign"))] #[test] fn mode_portable_sign_azure_key_vault_skip_signed_skips_valid_pe_without_service() { From f605576fe6e57e961a0be3fdf5517d5ad9448c0b Mon Sep 17 00:00:00 2001 From: Marc-Andre Moreau Date: Thu, 2 Jul 2026 11:20:59 -0400 Subject: [PATCH 2/3] Preserve MSIX fixture line endings Keep the minimal AppxManifest fixture out of Git text normalization so the committed vector manifest stays stable across Windows and non-Windows checkouts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .gitattributes | 1 + tests/fixtures/msix-minimal/AppxManifest.xml | 68 ++++++++++---------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/.gitattributes b/.gitattributes index 6b16159..49ebc2f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ # Fixture bytes are test inputs; do not let Git normalize line endings or encodings. +tests/fixtures/msix-minimal/AppxManifest.xml -text tests/fixtures/*.js -text tests/fixtures/*.ps1 -text tests/fixtures/*.psd1 -text diff --git a/tests/fixtures/msix-minimal/AppxManifest.xml b/tests/fixtures/msix-minimal/AppxManifest.xml index 73efb4a..f4da5be 100644 --- a/tests/fixtures/msix-minimal/AppxManifest.xml +++ b/tests/fixtures/msix-minimal/AppxManifest.xml @@ -1,34 +1,34 @@ - - - - - psign parity minimal - psign - Assets\StoreLogo.png - - - - - - - - - - - - - - - - + + + + + psign parity minimal + psign + Assets\StoreLogo.png + + + + + + + + + + + + + + + + From f2809d5b5155faeb59a343fcfb50d294c286a502 Mon Sep 17 00:00:00 2001 From: Marc-Andre Moreau Date: Thu, 2 Jul 2026 11:25:02 -0400 Subject: [PATCH 3/3] Bump version to 0.6.1 Update the workspace and release-facing package metadata for the next psign release using the repo's version bump script. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- .../Devolutions.Psign/Devolutions.Psign.psd1 | 2 +- README.md | 2 +- crates/psign-authenticode-trust/Cargo.toml | 2 +- crates/psign-azure-kv-rest/Cargo.toml | 2 +- crates/psign-codesigning-rest/Cargo.toml | 2 +- crates/psign-digest-cli/Cargo.toml | 2 +- crates/psign-opc-sign/Cargo.toml | 2 +- crates/psign-portable-core/Cargo.toml | 2 +- crates/psign-portable-ffi/Cargo.toml | 2 +- crates/psign-sip-digest/Cargo.toml | 2 +- nuget/tool/Devolutions.Psign.Tool.csproj | 2 +- 14 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 39ef30b..fcb7010 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: Release version to build/publish (for example 0.6.0) + description: Release version to build/publish (for example 0.6.1) required: true type: string publish_nuget: diff --git a/Cargo.lock b/Cargo.lock index 7565838..dd39abd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2010,7 +2010,7 @@ dependencies = [ [[package]] name = "psign" -version = "0.6.0" +version = "0.6.1" dependencies = [ "anyhow", "assert_cmd", @@ -2047,7 +2047,7 @@ dependencies = [ [[package]] name = "psign-authenticode-trust" -version = "0.6.0" +version = "0.6.1" dependencies = [ "anyhow", "authenticode", @@ -2072,7 +2072,7 @@ dependencies = [ [[package]] name = "psign-azure-kv-rest" -version = "0.6.0" +version = "0.6.1" dependencies = [ "anyhow", "base64", @@ -2087,7 +2087,7 @@ dependencies = [ [[package]] name = "psign-codesigning-rest" -version = "0.6.0" +version = "0.6.1" dependencies = [ "anyhow", "base64", @@ -2099,7 +2099,7 @@ dependencies = [ [[package]] name = "psign-digest-cli" -version = "0.6.0" +version = "0.6.1" dependencies = [ "anyhow", "assert_cmd", @@ -2125,7 +2125,7 @@ dependencies = [ [[package]] name = "psign-opc-sign" -version = "0.6.0" +version = "0.6.1" dependencies = [ "anyhow", "base64", @@ -2136,7 +2136,7 @@ dependencies = [ [[package]] name = "psign-portable-core" -version = "0.6.0" +version = "0.6.1" dependencies = [ "anyhow", "authenticode", @@ -2160,7 +2160,7 @@ dependencies = [ [[package]] name = "psign-portable-ffi" -version = "0.6.0" +version = "0.6.1" dependencies = [ "anyhow", "psign-portable-core", @@ -2170,7 +2170,7 @@ dependencies = [ [[package]] name = "psign-sip-digest" -version = "0.6.0" +version = "0.6.1" dependencies = [ "anyhow", "authenticode", diff --git a/Cargo.toml b/Cargo.toml index bd90284..0d264f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ repository = "https://github.com/Devolutions/psign" [package] name = "psign" -version = "0.6.0" +version = "0.6.1" edition = "2024" description = "Rust port of the Windows SDK signtool.exe (Authenticode sign/verify/timestamp) with portable digest helpers." license.workspace = true diff --git a/PowerShell/Devolutions.Psign/Devolutions.Psign.psd1 b/PowerShell/Devolutions.Psign/Devolutions.Psign.psd1 index 3a38d4c..53778a3 100644 --- a/PowerShell/Devolutions.Psign/Devolutions.Psign.psd1 +++ b/PowerShell/Devolutions.Psign/Devolutions.Psign.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'Devolutions.Psign.psm1' - ModuleVersion = '0.6.0' + ModuleVersion = '0.6.1' GUID = 'e6e50e4b-bf25-4ed6-a343-49f904e79f8f' Author = 'Devolutions' CompanyName = 'Devolutions' diff --git a/README.md b/README.md index ba8c3a8..3f15dd5 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ dotnet tool run psign-tool -- --help Create local dotnet tool packages from prebuilt release artifacts: ```powershell -pwsh ./nuget/pack-psign-dotnet-tool.ps1 -Version 0.6.0 -ArtifactsRoot ./dist -OutputDir ./dist/nuget +pwsh ./nuget/pack-psign-dotnet-tool.ps1 -Version 0.6.1 -ArtifactsRoot ./dist -OutputDir ./dist/nuget ``` The package is built from native `psign-tool` artifacts for `win-x64`, `win-arm64`, `linux-x64`, `linux-arm64`, `osx-x64`, and `osx-arm64`, plus an `any` fallback package for unsupported runtimes. diff --git a/crates/psign-authenticode-trust/Cargo.toml b/crates/psign-authenticode-trust/Cargo.toml index b770075..37023ee 100644 --- a/crates/psign-authenticode-trust/Cargo.toml +++ b/crates/psign-authenticode-trust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psign-authenticode-trust" -version = "0.6.0" +version = "0.6.1" edition = "2024" description = "Portable Authenticode PKCS#7 trust verification (anchors, chain, EKU) using picky-rs" license.workspace = true diff --git a/crates/psign-azure-kv-rest/Cargo.toml b/crates/psign-azure-kv-rest/Cargo.toml index 80ab6b6..a31312c 100644 --- a/crates/psign-azure-kv-rest/Cargo.toml +++ b/crates/psign-azure-kv-rest/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psign-azure-kv-rest" -version = "0.6.0" +version = "0.6.1" edition = "2024" description = "Azure Key Vault certificate metadata + keys/sign REST (portable, blocking HTTP)" license.workspace = true diff --git a/crates/psign-codesigning-rest/Cargo.toml b/crates/psign-codesigning-rest/Cargo.toml index ceafa21..076c047 100644 --- a/crates/psign-codesigning-rest/Cargo.toml +++ b/crates/psign-codesigning-rest/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psign-codesigning-rest" -version = "0.6.0" +version = "0.6.1" edition = "2024" description = "Azure Code Signing data-plane CertificateProfileOperations Sign LRO (portable, blocking HTTP)" license.workspace = true diff --git a/crates/psign-digest-cli/Cargo.toml b/crates/psign-digest-cli/Cargo.toml index f6c1aa5..0e6f0e0 100644 --- a/crates/psign-digest-cli/Cargo.toml +++ b/crates/psign-digest-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psign-digest-cli" -version = "0.6.0" +version = "0.6.1" edition = "2024" description = "Linux/macOS-friendly CLI over portable Authenticode SIP digests (psign-sip-digest)" license.workspace = true diff --git a/crates/psign-opc-sign/Cargo.toml b/crates/psign-opc-sign/Cargo.toml index b0a301b..039d2ea 100644 --- a/crates/psign-opc-sign/Cargo.toml +++ b/crates/psign-opc-sign/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psign-opc-sign" -version = "0.6.0" +version = "0.6.1" edition = "2024" description = "Portable OPC, VSIX, and NuGet package signing primitives" license.workspace = true diff --git a/crates/psign-portable-core/Cargo.toml b/crates/psign-portable-core/Cargo.toml index c9c5769..390d6fa 100644 --- a/crates/psign-portable-core/Cargo.toml +++ b/crates/psign-portable-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psign-portable-core" -version = "0.6.0" +version = "0.6.1" edition = "2024" description = "Reusable portable Authenticode signing and inspection APIs for psign" license.workspace = true diff --git a/crates/psign-portable-ffi/Cargo.toml b/crates/psign-portable-ffi/Cargo.toml index 232fc68..3e8ccb3 100644 --- a/crates/psign-portable-ffi/Cargo.toml +++ b/crates/psign-portable-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psign-portable-ffi" -version = "0.6.0" +version = "0.6.1" edition = "2024" description = "C ABI shared library for psign portable Authenticode operations" license.workspace = true diff --git a/crates/psign-sip-digest/Cargo.toml b/crates/psign-sip-digest/Cargo.toml index b798b2c..a5c55b8 100644 --- a/crates/psign-sip-digest/Cargo.toml +++ b/crates/psign-sip-digest/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psign-sip-digest" -version = "0.6.0" +version = "0.6.1" edition = "2024" description = "Portable Authenticode SIP digest recomputation (PE, CAB, MSI, MSIX, scripts, …) without Win32" license.workspace = true diff --git a/nuget/tool/Devolutions.Psign.Tool.csproj b/nuget/tool/Devolutions.Psign.Tool.csproj index 2dd6374..baf3ce3 100644 --- a/nuget/tool/Devolutions.Psign.Tool.csproj +++ b/nuget/tool/Devolutions.Psign.Tool.csproj @@ -8,7 +8,7 @@ psign-tool Devolutions.Psign.Tool - 0.6.0 + 0.6.1 Devolutions RID-specific dotnet tool wrapper around prebuilt psign-tool native executables. README.md