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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
- name: Portable digest CLI integration test
run: cargo test -p psign --test cli_pe_digest --locked

- name: Portable MSIX/AppX native makeappx.exe validation
run: cargo test -p psign --test msix_native_makeappx --locked

- name: Cross-CLI parity (portable verify-pe vs Windows rust-sip PE digest routine)
run: cargo test -p psign --test cross_cli_windows --locked

Expand Down
129 changes: 128 additions & 1 deletion crates/psign-portable-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3421,7 +3421,12 @@ fn build_flat_msix_block_map(
r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?><BlockMap xmlns="http://schemas.microsoft.com/appx/2010/blockmap" HashMethod="{hash_method}">"#
);
for (name, data) in payloads {
let escaped_name = xml_escape_attr(name);
// `AppxBlockMap.xml` `File/@Name` uses Windows-style backslash separators
// (matching native `AppxSip`/`makeappx` output), even though the physical
// ZIP entry name uses forward slashes. Getting this wrong makes real Windows
// AppX package validation fail with 0x80080205 ("block map is not valid").
let block_map_name = name.replace('/', "\\");
let escaped_name = xml_escape_attr(&block_map_name);
xml.push_str(&format!(
r#"<File Name="{escaped_name}" Size="{}" LfhSize="{}">"#,
data.len(),
Expand Down Expand Up @@ -3599,6 +3604,128 @@ mod tests {
);
}

/// Regression test for the HRESULT 0x80080205 ("The Appx package's block map
/// is invalid") corruption reported against `--mode portable sign` for flat
/// `.msix`/`.appx` packages.
///
/// Root cause: `build_flat_msix_block_map` emitted `AppxBlockMap.xml`
/// `File/@Name` attributes using the physical ZIP entry's forward-slash path
/// separators (e.g. `Assets/StoreLogo.png`), but native AppX packages (as
/// produced by `makeappx`/`AppxSip.dll`, and required by the real Windows
/// AppX package validator) always use backslash separators in the block map
/// (`Assets\StoreLogo.png`) even though the physical ZIP entry name itself
/// stays forward-slash. This test signs a real MSIX fixture and then
/// independently re-parses the produced ZIP container (via a fresh
/// `ZipArchive` read, not by reusing `msix_digest`'s internal state) to
/// assert that every `AppxBlockMap.xml` `File/@Name` uses backslash
/// separators, while the physical ZIP entry names remain forward-slash.
#[test]
fn signed_flat_msix_block_map_uses_backslash_separators() {
let fixture_dir = PathBuf::from("../../tests/fixtures/devolutions-authenticode");
let source = PathBuf::from("../../tests/fixtures/generated-unsigned/msix/sample.msix");

let temp_dir = std::env::temp_dir().join(format!(
"psign-portable-msix-blockmap-sep-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
std::fs::create_dir_all(&temp_dir).expect("create temp dir");
let output = temp_dir.join("sample.signed.msix");

portable_sign(PortableSignRequest {
path: source,
output_path: Some(output.clone()),
pfx_path: Some(fixture_dir.join("authenticode-test-cert.pfx")),
pfx_password: Some("CodeSign123!".to_string()),
..default_sign_request()
})
.expect("sign flat MSIX package");

let signed_bytes = std::fs::read(&output).expect("read signed MSIX package");
let mut archive =
ZipArchive::new(std::io::Cursor::new(&signed_bytes)).expect("open signed MSIX zip");

// Independently collect the physical ZIP entry names: these must stay
// forward-slash (this is the correct, unaffected convention).
let mut physical_names = Vec::new();
for i in 0..archive.len() {
let entry = archive.by_index(i).expect("read zip entry");
physical_names.push(entry.name().to_string());
}
assert!(
physical_names.iter().any(|n| n == "Assets/StoreLogo.png"),
"expected physical zip entry with forward-slash name, got: {physical_names:?}"
);
assert!(
!physical_names.iter().any(|n| n.contains('\\')),
"physical zip entry names must never contain backslashes, got: {physical_names:?}"
);

let block_map_xml = {
let mut entry = archive
.by_name("AppxBlockMap.xml")
.expect("AppxBlockMap.xml entry present");
let mut buf = String::new();
std::io::Read::read_to_string(&mut entry, &mut buf).expect("read AppxBlockMap.xml");
buf
};

// Independently extract every `File Name="..."` attribute value using a
// small ad-hoc parse (deliberately not reusing any block-map-writing
// helper from this module).
let mut file_names = Vec::new();
let marker = "<File Name=\"";
let mut rest = block_map_xml.as_str();
while let Some(start) = rest.find(marker) {
rest = &rest[start + marker.len()..];
let end = rest.find('"').expect("closing quote for File Name");
file_names.push(rest[..end].to_string());
rest = &rest[end..];
}
assert!(
!file_names.is_empty(),
"expected at least one File entry in AppxBlockMap.xml"
);

let logo_entry = file_names
.iter()
.find(|n| n.contains("StoreLogo.png"))
.unwrap_or_else(|| panic!("expected StoreLogo.png entry in {file_names:?}"));
assert_eq!(
logo_entry, "Assets\\StoreLogo.png",
"AppxBlockMap.xml File/@Name must use backslash separators to match \
native AppX semantics, got: {logo_entry}"
);
assert!(
!file_names.iter().any(|n| n.contains('/')),
"AppxBlockMap.xml File/@Name values must not contain forward slashes, \
got: {file_names:?}"
);

// Every payload entry present in the physical zip (other than the
// signature part, which is never listed in the block map) must have a
// corresponding, backslash-converted entry in the block map.
for physical in &physical_names {
if matches!(
physical.as_str(),
"AppxSignature.p7x" | "AppxBlockMap.xml" | "[Content_Types].xml"
) {
continue;
}
let expected = physical.replace('/', "\\");
assert!(
file_names.contains(&expected),
"physical entry {physical:?} missing from AppxBlockMap.xml (expected \
Name={expected:?}); block map entries: {file_names:?}"
);
}

let _ = std::fs::remove_dir_all(temp_dir);
}

#[test]
fn appends_script_signature_block_using_source_utf16_encoding() {
let block = "\r\n# SIG # Begin signature block\r\n";
Expand Down
2 changes: 1 addition & 1 deletion docs/rust-sip-gaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ VSIX and NuGet package signatures should not be modeled as Rust SIP gaps. VSIX u

| Item | Status |
|------|--------|
| PE/CAB/MSI **PKCS#7 encode** + format embed entirely in Rust | **Implemented for PE RSA/SHA-2:** **`psign-tool portable sign-pe`** computes the PE Authenticode digest, creates Authenticode **`SignedData`** from scratch, wraps it in a **`WIN_CERTIFICATE`**, replaces existing PE signatures by default, supports explicit append mode with **`--append-signature`**, and recomputes **`CheckSum`**. **Implemented for unsigned single-volume CAB RSA/SHA-2:** **`psign-tool portable sign-cab`** inserts the CAB Authenticode reserve header, creates CAB **`SpcIndirectDataContent`**, appends tail PKCS#7, and verifies through **`verify-cab`**. **Implemented for MSI/MSP RSA/SHA-2:** **`psign-tool portable sign-msi`** creates MSI **`SpcSigInfo`** indirect data and writes the root **`\u{5}DigitalSignature`** stream. **Implemented for flat `.msix`/`.appx` and `.msixbundle`/`.appxbundle` RSA/SHA-2:** native-shaped **`--mode portable sign`** (local PFX/certificate-store, Azure Key Vault, or Artifact Signing) stages the package, builds the APPX **`SpcIndirectDataContent`** blob, embeds **`AppxSignature.p7x`** (**`PKCX`**), and validates through **`verify-msix`**; bundle staging follows native **`AppxBundleSip`** semantics (manifest-only block map, byte-identical children, recursive child verification — children must be signed before the bundle). **`pe_embed`** still exposes **`wrap_pkcs7_der_authenticode_win_certificate`** + **`pe_append_authenticode_pkcs7_certificate`** for lower-level PE flows. **`pkcs7.rs`** now includes PE, CAB, and MSI **`SpcIndirectDataContent`** construction, local RSA/SHA-256/384/512 CMS signing, and remote RSA signature injection helpers in addition to PKCS#9 **`messageDigest`** extract/replace, authenticated-attribute **`SET OF Attribute`** DER, **`signer_info_sha256_digest_over_signed_attrs`**, **`signer_info_clone_with_signed_attrs`** / **`signer_info_clone_with_signature_octets`**, **`signed_data_replace_signer_info_at`**, and **`signed_data_replace_first_signer_info`**. Remaining gaps: ECDSA attribute-sign rules, optional attr tweaks beyond PKCS#9 **`messageDigest`**, broad top-level `sign` routing, CAB replacement/multivolume cases, `MsiDigitalSignatureEx` authoring, upload-container (`.appxupload`/`.msixupload`) final signing, and non-PE timestamp mutation. |
| PE/CAB/MSI **PKCS#7 encode** + format embed entirely in Rust | **Implemented for PE RSA/SHA-2:** **`psign-tool portable sign-pe`** computes the PE Authenticode digest, creates Authenticode **`SignedData`** from scratch, wraps it in a **`WIN_CERTIFICATE`**, replaces existing PE signatures by default, supports explicit append mode with **`--append-signature`**, and recomputes **`CheckSum`**. **Implemented for unsigned single-volume CAB RSA/SHA-2:** **`psign-tool portable sign-cab`** inserts the CAB Authenticode reserve header, creates CAB **`SpcIndirectDataContent`**, appends tail PKCS#7, and verifies through **`verify-cab`**. **Implemented for MSI/MSP RSA/SHA-2:** **`psign-tool portable sign-msi`** creates MSI **`SpcSigInfo`** indirect data and writes the root **`\u{5}DigitalSignature`** stream. **Implemented for flat `.msix`/`.appx` and `.msixbundle`/`.appxbundle` RSA/SHA-2:** native-shaped **`--mode portable sign`** (local PFX/certificate-store, Azure Key Vault, or Artifact Signing) stages the package, builds the APPX **`SpcIndirectDataContent`** blob, embeds **`AppxSignature.p7x`** (**`PKCX`**), and runs an internal digest self-check (**`verify-msix`**'s underlying routine) before the CLI will replace the original file — a non-`Valid` self-check status now fails the `sign` command instead of being discarded. `AppxBlockMap.xml` `File/@Name` entries use Windows-style backslash path separators (matching native `AppxSip`/`makeappx` output), even though the physical ZIP entry name uses forward slashes; getting this wrong produced real packages that looked self-consistent under this repo's own digest check yet were rejected by the real Windows AppX package validator with HRESULT `0x80080205` ("The Appx package's block map is invalid") — regression-tested both by an independent re-parse of the produced ZIP/block-map bytes (`psign-portable-core`'s `signed_flat_msix_block_map_uses_backslash_separators` test) and, on Windows hosts with the SDK installed, by shelling out to the real `makeappx.exe unpack` validator (`tests/msix_native_makeappx.rs`). Bundle staging follows native **`AppxBundleSip`** semantics (manifest-only block map, byte-identical children, recursive child verification — children must be signed before the bundle). **`pe_embed`** still exposes **`wrap_pkcs7_der_authenticode_win_certificate`** + **`pe_append_authenticode_pkcs7_certificate`** for lower-level PE flows. **`pkcs7.rs`** now includes PE, CAB, and MSI **`SpcIndirectDataContent`** construction, local RSA/SHA-256/384/512 CMS signing, and remote RSA signature injection helpers in addition to PKCS#9 **`messageDigest`** extract/replace, authenticated-attribute **`SET OF Attribute`** DER, **`signer_info_sha256_digest_over_signed_attrs`**, **`signer_info_clone_with_signed_attrs`** / **`signer_info_clone_with_signature_octets`**, **`signed_data_replace_signer_info_at`**, and **`signed_data_replace_first_signer_info`**. Remaining gaps: ECDSA attribute-sign rules, optional attr tweaks beyond PKCS#9 **`messageDigest`**, broad top-level `sign` routing, CAB replacement/multivolume cases, `MsiDigitalSignatureEx` authoring, upload-container (`.appxupload`/`.msixupload`) final signing, and non-PE timestamp mutation. |
| **MSIX/Appx `CryptSIPDllCreateIndirectData`** | **`AppxSipCreateIndirectData`** / **`AppxBundleSipCreateIndirectData`** build the **APPX `SpcIndirectData`** blob at sign time; **`msix_digest`** only **verifies** recomputed AX\* vs PKCS#7 — see [`windows-signing-components.md`](windows-signing-components.md) (**AppxSip.dll**) and [`rust-sip-spec-refs.md`](rust-sip-spec-refs.md). |
| **RFC3161** timestamp construction in Rust | **Partial:** `crates/psign-sip-digest/src/timestamp.rs` — **`build_timestamp_request_bytes`** encodes **DER** **`TimeStampReq`** (version 1 + **`MessageImprint`** + optional **`nonce`** / **`certReq`**) for SHA-1 / SHA-256 / SHA-384 / SHA-512; **`parse_time_stamp_resp_der`** reads **`PKIStatusInfo.status`**, optional **`statusString`**, optional **`failInfo`**, plus optional raw **`timeStampToken`**; **`parse_time_stamp_token_tst_info`** structurally extracts CMS **`id-ct-TSTInfo`** policy OID, message-imprint digest OID/hash, serial, **`genTime`**, and nonce. **`psign-tool portable`** exposes **`rfc3161-timestamp-req`**, **`rfc3161-timestamp-resp-inspect`**, optional **`rfc3161-timestamp-http-post`** with **`--features timestamp-http`**, **`sign-pe --timestamp-url --timestamp-digest`** for PE sign-time timestamping, and **`timestamp-pe-rfc3161`** to attach a raw token or granted **`TimeStampResp`** token to a PE `SignerInfo` unsigned attribute. Portable trust uses cryptographic RFC3161 validation when both **`--prefer-timestamp-signing-time`** and **`--require-valid-timestamp`** are set: nested token **`MessageImprint`** over primary **`SignerInfo.signature`**, timestamp CMS **`messageDigest`**, RSA/SHA-256 timestamp signature, TSA `timeStamping` EKU, and explicit-anchor TSA chain. Microsoft/ACS tokens with fractional-second **`genTime`** and attribute certificates in the timestamp bag are handled (fractional seconds truncated; non-X.509 **`CertificateChoices`** stripped). PKCS#9 **`signing-time`** still works only for non-required instant selection. Remaining gaps: delegated/non-RSA TSA support, non-PE timestamp mutation, and full Windows **`CryptVerifyTimeStampSignature`** parity. |
| **`/ph`** **page hashes** (`SPC_PE_IMAGE_PAGE_HASHES`) | Portable **CMS extract** + **payload peel** + **flat `(offset,digest)*` parse** + **experimental contiguous file-offset verify** (`page_hashes`, CLI **`pe-has-page-hashes`** / **`pe-page-hash-info`** / **`verify-pe-page-hashes`**). Differs from **`WinVerifyTrust`** where checksum / security-directory handling diverges — native **`verify --verify-page-hashes`** remains the strict `/ph` reference. |
Expand Down
Loading
Loading