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
22 changes: 12 additions & 10 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ members = [
[workspace.dependencies]
camino = "1.1"
indexmap = { version = "2", features = ["serde"] }
ltk_wad = "0.2.13"
ltk_wad = "0.3.0"
sysinfo = "0.32"
19 changes: 16 additions & 3 deletions crates/ltk_overlay/src/wad_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
//! and a set of override chunks, and produces a new WAD file containing all original
//! chunks plus the overrides.
//!
//! # Signature Preservation
//!
//! The output header carries the original WAD's signature and checksum **verbatim**.
//! Riot's RSA signature covers the original TOC, so it does not validate the patched
//! TOC — it is preserved as provenance: verifiers (e.g. `ltk_sig`'s `WadMod` records)
//! use it to prove which Riot-signed WAD the overlay was derived from.
//!
//! # Compression Strategy
//!
//! **Non-overridden chunks** are passed through as raw compressed bytes from the
Expand Down Expand Up @@ -56,6 +63,9 @@ pub struct PatchedWadStats {
/// exist in the source WAD are treated as **new entries** and inserted at the correct
/// sorted position in the TOC.
///
/// The original WAD's header signature and checksum are copied through verbatim
/// (see the [module docs](self) on signature preservation).
///
/// Parent directories for `dst_wad_path` are created automatically.
///
/// The WAD is written to a sibling `.tmp` file and renamed into place atomically once
Expand Down Expand Up @@ -159,9 +169,12 @@ fn write_patched_wad<B: AsRef<[u8]>>(
writer.write_u8(3)?; // major version
writer.write_u8(4)?; // minor version

// Write dummy ECDSA signature (256 bytes) + checksum (8 bytes)
writer.write_all(&[0u8; 256])?;
writer.write_u64::<LE>(0)?;
// Carry the source WAD's signature (256 bytes) + checksum (8 bytes) through
// verbatim. The signature covers the *original* TOC, not the patched one —
// preserving it lets verifiers (ltk_sig's WadMod records) recover the
// Riot-signed original TOC provenance from the overlay file.
writer.write_all(wad.signature())?;
writer.write_u64::<LE>(wad.checksum())?;

// Write chunk count
writer.write_u32::<LE>(ordered.len() as u32)?;
Expand Down
47 changes: 47 additions & 0 deletions crates/ltk_overlay/tests/rebuild.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,50 @@ fn failed_wad_build_leaves_no_partial_file() {
.unwrap_or_default();
assert!(leftovers.is_empty(), "unexpected leftovers: {leftovers:?}");
}

/// The patched WAD must carry the source WAD's header signature and checksum
/// verbatim — Riot's RSA signature over the original TOC is the provenance
/// record that ltk_sig's merged-WAD verification recovers from overlay files.
#[test]
fn patched_wad_preserves_original_signature_and_checksum() {
let tmp = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap();

let signature = [0xA5u8; 256];
let checksum = 0xDEAD_BEEF_CAFE_F00D_u64;

// A "signed" game WAD with one chunk.
let mut cursor = Cursor::new(Vec::new());
WadBuilder::default()
.with_prebuilt_signature(&signature)
.with_prebuilt_checksum(checksum)
.with_chunk(
WadChunkBuilder::default()
.with_path(CHUNK_PATH)
.with_force_compression(WadChunkCompression::None),
)
.build_to_writer(&mut cursor, |_hash, writer| {
writer.write_all(b"GAME_ORIGINAL")?;
Ok(())
})
.unwrap();
let src = root.join(GAME_WAD);
fs::write(src.as_std_path(), cursor.into_inner()).unwrap();

// Replace the existing chunk and insert a brand-new entry, covering both
// TOC-mutating paths.
let override_hash = resolve_chunk_hash(Utf8Path::new(CHUNK_PATH), b"").unwrap();
let new_hash = resolve_chunk_hash(Utf8Path::new("assets/new/entry.bin"), b"").unwrap();
let dst = root.join("out").join(GAME_WAD);
build_patched_wad(
&src,
&dst,
&HashSet::from([override_hash, new_hash]),
|_hash| -> Result<Vec<u8>> { Ok(b"MODDED".to_vec()) },
)
.unwrap();

let patched = Wad::mount(fs::File::open(dst.as_std_path()).unwrap()).unwrap();
assert_eq!(patched.signature(), &signature);
assert_eq!(patched.checksum(), checksum);
}
Loading