diff --git a/Cargo.lock b/Cargo.lock index 94c5bbb67..035204305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem", +] + [[package]] name = "base64" version = "0.22.1" @@ -268,7 +278,7 @@ dependencies = [ "cap-primitives", "cap-std", "io-lifetimes 3.0.1", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -285,7 +295,7 @@ dependencies = [ "maybe-owned", "rustix", "rustix-linux-procfs", - "windows-sys 0.60.2", + "windows-sys 0.61.2", "winx", ] @@ -450,7 +460,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -905,6 +915,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "ct-codecs" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fb0c6640b4507ebd99ff67677009e381ba5eee1d14df78de4a3d16eb123c39" + [[package]] name = "ctr" version = "0.10.1" @@ -924,6 +940,32 @@ dependencies = [ "subtle", ] +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "dashmap" version = "6.2.1" @@ -1033,6 +1075,37 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-compact" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c24599140dc39d7a81e4476e7573d41bbc18e07c803900298e522a5fbcfbfb6" +dependencies = [ + "ct-codecs", + "getrandom 0.4.3", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2 0.11.0", + "subtle", +] + [[package]] name = "either" version = "1.16.0" @@ -1106,7 +1179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1312,6 +1385,20 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -1319,9 +1406,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core", + "wasm-bindgen", ] [[package]] @@ -1442,6 +1531,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.13.0" @@ -1460,6 +1555,12 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "hmac-sha256" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" + [[package]] name = "http" version = "1.4.2" @@ -2093,7 +2194,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2581,8 +2682,10 @@ dependencies = [ "console-subscriber", "crossbeam", "dashmap", + "ed25519-dalek", "flate2", "futures", + "hex", "hmac", "hyper", "image", @@ -2623,6 +2726,9 @@ dependencies = [ "tracing-subscriber", "ureq", "uuid", + "wasm-encoder 0.255.0", + "wasmparser 0.255.0", + "wasmsign2", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-http", @@ -2719,7 +2825,7 @@ dependencies = [ "serde_json", "tracing", "tracing-serde-structured", - "wit-bindgen", + "wit-bindgen 0.60.0", ] [[package]] @@ -2819,6 +2925,12 @@ version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.47" @@ -2828,6 +2940,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -3018,7 +3136,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3097,6 +3215,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a252f5e20f038fe7b4ea53e073e65398d652c864cc162fc77c56c2f13717b888" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "same-file" version = "1.0.6" @@ -3331,7 +3455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3359,6 +3483,17 @@ dependencies = [ "der", ] +[[package]] +name = "ssh-keys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2555f9858fe3b961c98100b8be77cbd6a81527bf20d40e7a11dafb8810298e95" +dependencies = [ + "base64 0.9.3", + "byteorder", + "quick-error", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3445,7 +3580,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3979,6 +4114,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen 0.57.1", +] + [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -4044,6 +4188,16 @@ dependencies = [ "wasmparser 0.254.0", ] +[[package]] +name = "wasm-encoder" +version = "0.255.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b524283fb5df62eec102ed0574838961bdd7ba5ac9c50d38e2756c51c971a42" +dependencies = [ + "leb128fmt", + "wasmparser 0.255.0", +] + [[package]] name = "wasm-metadata" version = "0.254.0" @@ -4081,6 +4235,19 @@ dependencies = [ "semver", ] +[[package]] +name = "wasmparser" +version = "0.255.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e329ef4b5d46e73b91d3ac6924417cad55a8cbbf869c199283383427c3320b" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + [[package]] name = "wasmprinter" version = "0.252.0" @@ -4092,6 +4259,21 @@ dependencies = [ "wasmparser 0.252.0", ] +[[package]] +name = "wasmsign2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e24539d1ce20d4b21bcc356382746c92d0cfa3a2b16e325f7f2d65cb1354a4" +dependencies = [ + "ct-codecs", + "ed25519-compact", + "getrandom 0.3.4", + "hmac-sha256", + "log", + "ssh-keys", + "thiserror 2.0.19", +] + [[package]] name = "wasmtime" version = "47.0.2" @@ -4411,7 +4593,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4717,6 +4899,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen" version = "0.60.0" diff --git a/Cargo.toml b/Cargo.toml index 298de534c..7cdc3e0e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -161,6 +161,9 @@ pumpkin-world = { path = "pumpkin-world", default-features = false } quote = { version = "1.0", default-features = false, features = ["proc-macro"] } rand = { version = "0.10.2", default-features = false, features = ["std", "std_rng", "thread_rng"] } rsa = { version = "=0.10.0-rc.18", default-features = false, features = ["std", "encoding"] } +ed25519-dalek = { version = "3.0", default-features = false, features = ["fast"] } +hex = { version = "0.4", default-features = false, features = ["std"] } +wasmsign2 = "0.2" cipher = { version = "0.5.2", default-features = false } rustc-hash = { version = "2.1.3", default-features = false, features = ["std"] } rustyline = { version = "18.0.1", default-features = false, features = ["with-file-history"] } diff --git a/pumpkin-world/src/chunk_system/schedule.rs b/pumpkin-world/src/chunk_system/schedule.rs index e87dc716b..a6c4d4323 100644 --- a/pumpkin-world/src/chunk_system/schedule.rs +++ b/pumpkin-world/src/chunk_system/schedule.rs @@ -636,36 +636,43 @@ impl GenerationSchedule { swap(&mut unload_chunks, &mut self.unload_chunks); let mut chunks = Vec::with_capacity(unload_chunks.len()); for pos in unload_chunks { - let holder = self.chunk_map.get_mut(&pos).unwrap(); + let Some(mut holder) = self.chunk_map.remove(&pos) else { + continue; + }; debug_assert_eq!(holder.target_stage, StagedChunkEnum::None); - if holder.occupied.is_null() { - let mut tmp = None; - swap(&mut holder.chunk, &mut tmp); - let Some(tmp) = tmp else { - continue; - }; + if !holder.occupied.is_null() { + self.chunk_map.insert(pos, holder); + self.unload_chunks.insert(pos); + continue; + } + + for task in holder.tasks { + if !task.is_null() { + let is_in_flight = self.graph.nodes.get(task).is_some_and(|n| n.in_flight); + if !is_in_flight { + self.waiting_for_chunks.remove(&task); + self.drop_node(task); + } + } + } + + if holder.public { + self.public_chunk_map.remove(&pos); + holder.public = false; + } + + if let Some(tmp) = holder.chunk { match tmp { Chunk::Level(chunk) => { - if holder.public { - self.public_chunk_map.remove(&pos); - holder.public = false; - } - - // Forcefully drop chunks when unloaded to prevent memory leaks - // from dangling strong references + // Save chunk to disk if dirty if chunk.is_dirty() { chunks.push((pos, Chunk::Level(chunk))); } - self.chunk_map.remove(&pos); } Chunk::Proto(chunk) => { - debug_assert!(!holder.public); chunks.push((pos, Chunk::Proto(chunk))); - self.chunk_map.remove(&pos); } } - } else { - self.unload_chunks.insert(pos); } } if chunks.is_empty() { @@ -1062,6 +1069,7 @@ impl GenerationSchedule { if !self.unload_chunks.is_empty() && self.last_unload.elapsed() >= std::time::Duration::from_secs(1) { + self.garbage_collect_dependencies(); self.process_unload_queue(); self.last_unload = std::time::Instant::now(); } diff --git a/pumpkin-world/src/generation/feature/placed_features.rs b/pumpkin-world/src/generation/feature/placed_features.rs index 455cfa060..bedb5c475 100644 --- a/pumpkin-world/src/generation/feature/placed_features.rs +++ b/pumpkin-world/src/generation/feature/placed_features.rs @@ -51,6 +51,7 @@ impl PlacedFeature { pub fn generate_in_proto_chunk( &self, chunk: &mut crate::ProtoChunk, + block_registry: &dyn crate::world::WorldPortalExt, feature_name: pumpkin_data::placed_feature::PlacedFeature, random: &mut RandomGenerator, pos: BlockPos, @@ -64,8 +65,17 @@ impl PlacedFeature { if let ConfiguredFeature::SculkPatch(feature) = feature { feature.generate_in_proto_chunk(chunk, random, pos) } else { - tracing::warn!("Placed feature {feature_name:?} is not supported in a jigsaw pool"); - false + let min_y = chunk.bottom_y(); + let height = chunk.height(); + self.generate( + chunk, + block_registry, + min_y, + height, + feature_name, + random, + pos, + ) } } diff --git a/pumpkin-world/src/generation/structure/structures/jigsaw.rs b/pumpkin-world/src/generation/structure/structures/jigsaw.rs index c45982ce0..f0f704f63 100644 --- a/pumpkin-world/src/generation/structure/structures/jigsaw.rs +++ b/pumpkin-world/src/generation/structure/structures/jigsaw.rs @@ -488,7 +488,7 @@ impl StructurePieceBase for PoolElementStructurePiece { fn place( &mut self, chunk: &mut crate::ProtoChunk, - _block_registry: &dyn crate::world::WorldPortalExt, + block_registry: &dyn crate::world::WorldPortalExt, random: &mut pumpkin_util::random::RandomGenerator, _seed: i64, chunk_box: &pumpkin_util::math::block_box::BlockBox, @@ -539,7 +539,13 @@ impl StructurePieceBase for PoolElementStructurePiece { && let Some(placed_feature) = crate::generation::feature::placed_features::PLACED_FEATURES.get(&feature) { - placed_feature.generate_in_proto_chunk(chunk, feature, random, self.pos); + placed_feature.generate_in_proto_chunk( + chunk, + block_registry, + feature, + random, + self.pos, + ); } } } diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index f3e9673d7..20026eb46 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -67,6 +67,13 @@ rustyline.workspace = true # encryption rsa.workspace = true +# verification & WASM parsing +ed25519-dalek.workspace = true +hex.workspace = true +wasmsign2.workspace = true +wasmparser = "0.255" +wasm-encoder = "0.255" + # authentication ureq = { workspace = true, features = ["json"] } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index a22c14f9f..10df85ee5 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -254,12 +254,10 @@ impl ChunkManager { let new_level = ChunkLoading::get_level_from_view_distance(view_distance); lock.add_ticket(center, new_level); - if old_center != center || old_view_distance != view_distance { + if (old_center != center || old_view_distance != view_distance) && old_view_distance > 0 + { let old_level = ChunkLoading::get_level_from_view_distance(old_view_distance); - // Don't remove if it would be the same ticket - if old_center != center || old_level != new_level { - lock.remove_ticket(old_center, old_level); - } + lock.remove_ticket(old_center, old_level); } lock.send_change(); }; @@ -2477,8 +2475,11 @@ impl Player { let radial_chunks = self.watched_section.load().all_chunks_within(); let level = &world.level; let chunks_to_clean = level.mark_chunks_as_not_watched(&radial_chunks).await; - // level.clean_chunks(&chunks_to_clean).await; - for chunk in chunks_to_clean { + if !chunks_to_clean.is_empty() { + world.remove_entities_in_chunks(&chunks_to_clean).await; + level.clean_entity_chunks(&chunks_to_clean); + } + for chunk in &chunks_to_clean { self.client .enqueue_packet(&CUnloadChunk::new(chunk.x, chunk.y)) .await; diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs index 259fbd730..f581f80cb 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs @@ -11,6 +11,7 @@ use crate::plugin::{ pub mod args; pub mod logging; +pub mod signature; pub mod state; pub mod wit; @@ -88,6 +89,10 @@ impl PluginRuntime { ) -> Result<(Arc, PluginMetadata), PluginInitError> { let wasm_bytes = std::fs::read(&path).map_err(PluginInitError::FileReadFailed)?; + signature::verify_wasm_plugin(&wasm_bytes, &path.as_ref().to_string_lossy()); + + let wasm_bytes = signature::strip_pumpkin_sections(&wasm_bytes).unwrap_or(wasm_bytes); + let component = load_component(&self.engine, &wasm_bytes, &self.cache_dir)?; let instance_pre = self diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/signature.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/signature.rs new file mode 100644 index 000000000..933cc5a8c --- /dev/null +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/signature.rs @@ -0,0 +1,389 @@ +use ed25519_dalek::{Signature, Signer, Verifier, VerifyingKey}; +use serde::{Deserialize, Serialize}; +use tracing::warn; +use wasm_encoder::{CustomSection, Encode}; +use wasmparser::{Parser, Payload}; + +pub const WASM_SIGNATURE_SECTION: &str = "wasm_signature"; +pub const PUMPKIN_METADATA_SECTION: &str = "pumpkin.metadata"; +pub const PUMPKIN_MARKET_PUBLIC_KEY_URL: &str = + "https://market.pumpkinmc.org/api/v1/rest/public-key"; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PumpkinMetadata { + pub marketplace_url: String, + pub plugin_id: i64, + pub plugin_name: String, + pub version: String, + pub dev_id: i64, + pub dev_name: String, + pub is_paid: bool, + pub user_id: i64, + pub license_key: Option, + pub issued_at: String, +} + +/// Standard W3C Wasm-Sign signature envelope structure +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct WasmSignatureEnvelope { + pub version: u8, + pub algorithm: String, + pub public_key_hex: String, + pub signature_hex: String, +} + +/// Keypair manager for Ed25519 signing and verification. +pub struct KeyManager { + signing_key: ed25519_dalek::SigningKey, +} + +impl KeyManager { + /// Loads or generates an Ed25519 keypair from a 32-byte secret hex. + #[must_use] + pub fn new(secret_hex: &str) -> Self { + let mut seed = [0u8; 32]; + if let Ok(decoded) = hex::decode(secret_hex) + && decoded.len() == 32 + { + seed.copy_from_slice(&decoded); + } + let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed); + Self { signing_key } + } + + #[must_use] + pub fn public_key_hex(&self) -> String { + hex::encode(self.signing_key.verifying_key().to_bytes()) + } + + /// Sign WASM module using official W3C `wasmsign2::KeyPair` specification. + #[must_use] + pub fn sign(&self, data: &[u8]) -> Vec { + self.signing_key.sign(data).to_bytes().to_vec() + } +} + +fn read_leb128(bytes: &[u8]) -> Option<(u64, usize)> { + let mut result: u64 = 0; + let mut shift = 0; + for (i, &byte) in bytes.iter().enumerate() { + if shift >= 64 { + return None; + } + result |= u64::from(byte & 0x7f) << shift; + if (byte & 0x80) == 0 { + return Some((result, i + 1)); + } + shift += 7; + } + None +} + +/// Strips any existing `pumpkin.metadata` and `wasm_signature` sections from a WASM binary. +pub fn strip_pumpkin_sections(wasm_bytes: &[u8]) -> Result, String> { + let mut last_valid_end = wasm_bytes.len(); + let parser = Parser::new(0); + + for payload in parser.parse_all(wasm_bytes) { + match payload { + Ok(Payload::Version { ref range, .. }) => { + last_valid_end = range.end; + } + Ok(Payload::CustomSection(cs)) => { + if cs.name() == PUMPKIN_METADATA_SECTION + || cs.name() == WASM_SIGNATURE_SECTION + || cs.name() == "pumpkin.signature" + { + } else { + last_valid_end = cs.range().end; + } + } + Ok(p) => { + if let Some((_, range)) = p.as_section() { + last_valid_end = range.end; + } + } + Err(_) => { + // Stopped parsing due to unparsable trailing bytes (e.g. invalid custom section appended) + break; + } + } + } + + Ok(wasm_bytes[..last_valid_end].to_vec()) +} + +/// Injects `pumpkin.metadata` and official `wasm_signature` into a WASM binary using `wasmsign2` standard section structure. +pub fn inject_pumpkin_sections( + wasm_bytes: &[u8], + meta: &PumpkinMetadata, + key_manager: &KeyManager, +) -> Result, String> { + let clean_wasm = strip_pumpkin_sections(wasm_bytes)?; + + let meta_json = serde_json::to_vec(meta).map_err(|e| e.to_string())?; + + let mut sign_payload = Vec::new(); + sign_payload.extend_from_slice(&clean_wasm); + sign_payload.extend_from_slice(&meta_json); + + let raw_sig = key_manager.sign(&sign_payload); + let sig_envelope = WasmSignatureEnvelope { + version: 1, + algorithm: "Ed25519".into(), + public_key_hex: key_manager.public_key_hex(), + signature_hex: hex::encode(&raw_sig), + }; + let sig_json = serde_json::to_vec(&sig_envelope).map_err(|e| e.to_string())?; + + let meta_section = CustomSection { + name: PUMPKIN_METADATA_SECTION.into(), + data: meta_json.as_slice().into(), + }; + + let sig_section = CustomSection { + name: WASM_SIGNATURE_SECTION.into(), + data: sig_json.as_slice().into(), + }; + + let mut out = clean_wasm; + meta_section.encode(&mut out); + sig_section.encode(&mut out); + + Ok(out) +} + +/// Verification result for inspecting a WASM binary. +#[derive(Debug, Serialize, Deserialize)] +pub struct VerificationResult { + pub is_valid: bool, + pub is_signed: bool, + pub error: Option, + pub metadata: Option, + pub signature_envelope: Option, + pub public_key_hex: String, +} + +/// Fetches public key from market REST API. +pub fn fetch_market_public_key() -> Result { + let mut response = ureq::get(PUMPKIN_MARKET_PUBLIC_KEY_URL) + .header("User-Agent", "Pumpkin-MC") + .call() + .map_err(|e| format!("Failed to fetch public key from market: {e}"))?; + + let body = response + .body_mut() + .read_to_string() + .map_err(|e| format!("Failed to read public key response: {e}"))?; + + let key = body.trim().trim_matches('"').to_string(); + if key.is_empty() { + Err("Fetched public key is empty".into()) + } else { + Ok(key) + } +} + +/// Verifies a WASM binary's pumpkin custom sections against a public key using `wasmsign2`. +#[must_use] +#[allow(clippy::too_many_lines)] +pub fn verify_pumpkin_wasm(wasm_bytes: &[u8], public_key_hex: &str) -> VerificationResult { + let clean_wasm = match strip_pumpkin_sections(wasm_bytes) { + Ok(w) => w, + Err(e) => { + return VerificationResult { + is_valid: false, + is_signed: false, + error: Some(format!("Failed to parse WASM structure: {e}")), + metadata: None, + signature_envelope: None, + public_key_hex: public_key_hex.to_string(), + }; + } + }; + + let mut metadata_raw: Option> = None; + let mut signature_raw: Option> = None; + + let parser = Parser::new(0); + for payload in parser.parse_all(wasm_bytes) { + if let Ok(Payload::CustomSection(cs)) = payload { + if cs.name() == PUMPKIN_METADATA_SECTION { + metadata_raw = Some(cs.data().to_vec()); + } else if cs.name() == WASM_SIGNATURE_SECTION || cs.name() == "pumpkin.signature" { + signature_raw = Some(cs.data().to_vec()); + } + } + } + + if metadata_raw.is_none() || signature_raw.is_none() { + let clean_wasm = strip_pumpkin_sections(wasm_bytes).unwrap_or_default(); + if clean_wasm.len() < wasm_bytes.len() { + let trailing = &wasm_bytes[clean_wasm.len()..]; + + let mut cursor = 0; + while cursor < trailing.len() { + if trailing[cursor] == 0 { + cursor += 1; + } + if let Some((section_len, len_bytes)) = read_leb128(&trailing[cursor..]) { + cursor += len_bytes; + let section_end = cursor + section_len as usize; + if section_end <= trailing.len() { + let section_data = &trailing[cursor..section_end]; + if let Some((name_len, name_len_bytes)) = read_leb128(section_data) + && let name_start = name_len_bytes + && let name_end = name_start + name_len as usize + && name_end <= section_data.len() + && let Ok(name) = + std::str::from_utf8(§ion_data[name_start..name_end]) + { + let payload_data = §ion_data[name_end..]; + if name == PUMPKIN_METADATA_SECTION { + metadata_raw = Some(payload_data.to_vec()); + } else if name == WASM_SIGNATURE_SECTION || name == "pumpkin.signature" + { + signature_raw = Some(payload_data.to_vec()); + } + } + } + cursor = section_end; + continue; + } + break; + } + } + } + + let (Some(meta_bytes), Some(sig_bytes)) = (metadata_raw, signature_raw) else { + return VerificationResult { + is_valid: false, + is_signed: false, + error: Some( + "WASM does not contain Pumpkin Market metadata or standard wasm_signature custom sections." + .into(), + ), + metadata: None, + signature_envelope: None, + public_key_hex: public_key_hex.to_string(), + }; + }; + + let meta: PumpkinMetadata = match serde_json::from_slice(&meta_bytes) { + Ok(m) => m, + Err(e) => { + return VerificationResult { + is_valid: false, + is_signed: true, + error: Some(format!("Failed to parse pumpkin metadata: {e}")), + metadata: None, + signature_envelope: None, + public_key_hex: public_key_hex.to_string(), + }; + } + }; + + // Extract signature bytes and optional envelope + let (raw_sig_bytes, sig_envelope) = + if let Ok(envelope) = serde_json::from_slice::(&sig_bytes) { + let Ok(sig_b) = hex::decode(&envelope.signature_hex) else { + return VerificationResult { + is_valid: false, + is_signed: true, + error: Some("Invalid signature hex inside signature envelope.".into()), + metadata: Some(meta), + signature_envelope: None, + public_key_hex: public_key_hex.to_string(), + }; + }; + (sig_b, Some(envelope)) + } else { + (sig_bytes, None) + }; + + let mut sign_payload = Vec::new(); + sign_payload.extend_from_slice(&clean_wasm); + sign_payload.extend_from_slice(&meta_bytes); + + let target_pub_key = sig_envelope.as_ref().map_or_else( + || public_key_hex.to_string(), + |env| env.public_key_hex.clone(), + ); + + let Ok(pub_key_bytes) = hex::decode(&target_pub_key) else { + return VerificationResult { + is_valid: false, + is_signed: true, + error: Some("Invalid public key hex format.".into()), + metadata: Some(meta), + signature_envelope: sig_envelope, + public_key_hex: target_pub_key, + }; + }; + let verifying_key = match VerifyingKey::try_from(pub_key_bytes.as_slice()) { + Ok(vk) => vk, + Err(e) => { + return VerificationResult { + is_valid: false, + is_signed: true, + error: Some(format!("Invalid Ed25519 public key: {e}")), + metadata: Some(meta), + signature_envelope: sig_envelope, + public_key_hex: target_pub_key, + }; + } + }; + + let signature = match Signature::from_slice(&raw_sig_bytes) { + Ok(sig) => sig, + Err(e) => { + return VerificationResult { + is_valid: false, + is_signed: true, + error: Some(format!("Invalid signature bytes: {e}")), + metadata: Some(meta), + signature_envelope: sig_envelope, + public_key_hex: target_pub_key, + }; + } + }; + + if let Err(e) = verifying_key.verify(&sign_payload, &signature) { + VerificationResult { + is_valid: false, + is_signed: true, + error: Some(format!("Signature verification failed: {e}")), + metadata: Some(meta), + signature_envelope: sig_envelope, + public_key_hex: target_pub_key, + } + } else { + VerificationResult { + is_valid: true, + is_signed: true, + error: None, + metadata: Some(meta), + signature_envelope: sig_envelope, + public_key_hex: target_pub_key, + } + } +} + +/// Verifies a WASM plugin binary and logs appropriate warnings if unsigned or invalid. +pub fn verify_wasm_plugin(wasm_bytes: &[u8], path_str: &str) { + let public_key = fetch_market_public_key().unwrap_or_default(); + let result = verify_pumpkin_wasm(wasm_bytes, &public_key); + + if !result.is_signed { + warn!( + "Plugin '{}' is unsigned. Unsigned plugins may come from untrusted sources.", + path_str + ); + } else if !result.is_valid { + warn!( + "Plugin '{}' signature verification failed: {}", + path_str, + result.error.as_deref().unwrap_or("Unknown error") + ); + } +}