iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

commit ed0a98d9235fca4531ab45477c7a71177e39e353
parent aabf5a41af9c9f2f30ba663bb6194f596ec67208
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Wed, 19 Aug 2026 21:16:58 +0200

Add wallet config fuzz target

Diffstat:
MPLAN.md | 4+++-
MROADMAP.md | 3++-
Mdeployment.sh | 1+
Mdocs/security-review.md | 2++
Mfuzz/Cargo.toml | 6++++++
Mfuzz/README.md | 2++
Afuzz/corpus/wallet_config/config.json | 22++++++++++++++++++++++
Afuzz/corpus/wallet_config/wallet_encrypted.json | 12++++++++++++
Afuzz/corpus/wallet_config/wallet_plain.json | 5+++++
Afuzz/fuzz_targets/wallet_config.rs | 13+++++++++++++
Msrc/adapters/config_store.rs | 18+++++++++++++-----
Msrc/adapters/wallet_store.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++--
12 files changed, 130 insertions(+), 9 deletions(-)

diff --git a/PLAN.md b/PLAN.md @@ -5,7 +5,9 @@ candidate chain is treated as promotable to mainnet. ## 1. Fuzzing Harnesses -Status: initial repository harnesses and deployment smoke runs are in place. +Status: initial repository harnesses and deployment smoke runs are in place for +P2P gossip, compact snapshots, domain JSON, Stratum requests, and wallet/config +persistence metadata. Goal: continuously throw malformed and semi-valid input at every external data boundary and every compact persistence format. diff --git a/ROADMAP.md b/ROADMAP.md @@ -120,7 +120,8 @@ A release intended for deployment must pass: - `cargo test --locked` - `cargo check --locked --manifest-path fuzz/Cargo.toml` -- fuzz smoke runs for `p2p_envelope`, `compact_snapshot`, `domain_json`, and `stratum_request` +- fuzz smoke runs for `p2p_envelope`, `compact_snapshot`, `domain_json`, + `stratum_request`, and `wallet_config` - `cargo test --locked --release --test properties -- --ignored` Normal local development may skip ignored long-running property tests and long fuzzing sessions, but deployment must run the release gate smoke checks. diff --git a/deployment.sh b/deployment.sh @@ -80,6 +80,7 @@ run_release_tests() { cargo run --locked --manifest-path fuzz/Cargo.toml --bin compact_snapshot -- -runs=1 fuzz/corpus/compact_snapshot cargo run --locked --manifest-path fuzz/Cargo.toml --bin domain_json -- -runs=1 fuzz/corpus/domain_json cargo run --locked --manifest-path fuzz/Cargo.toml --bin stratum_request -- -runs=1 fuzz/corpus/stratum_request + cargo run --locked --manifest-path fuzz/Cargo.toml --bin wallet_config -- -runs=1 fuzz/corpus/wallet_config cargo test --locked --release --test properties -- --ignored } diff --git a/docs/security-review.md b/docs/security-review.md @@ -111,6 +111,7 @@ Evidence already in the tree: - HTTP/auth abuse tests for CSRF, lockout/backoff, session expiry, and forwarded-header spoofing; - wallet/config/chain crash-consistency tests; +- wallet/config persistence metadata fuzz target; - local-only UI guidance in `README.md`. ### Stratum @@ -148,6 +149,7 @@ cargo fuzz run p2p_envelope -- -runs=256 cargo fuzz run compact_snapshot -- -runs=256 cargo fuzz run domain_json -- -runs=256 cargo fuzz run stratum_request -- -runs=256 +cargo fuzz run wallet_config -- -runs=256 cargo test --locked --release --test properties -- --ignored ``` diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml @@ -35,3 +35,9 @@ name = "stratum_request" path = "fuzz_targets/stratum_request.rs" test = false doc = false + +[[bin]] +name = "wallet_config" +path = "fuzz_targets/wallet_config.rs" +test = false +doc = false diff --git a/fuzz/README.md b/fuzz/README.md @@ -15,6 +15,7 @@ cargo fuzz run p2p_envelope cargo fuzz run compact_snapshot cargo fuzz run domain_json cargo fuzz run stratum_request +cargo fuzz run wallet_config ``` Short smoke run without installing `cargo-fuzz`: @@ -24,6 +25,7 @@ cargo run --manifest-path fuzz/Cargo.toml --bin p2p_envelope -- -runs=1 fuzz/cor cargo run --manifest-path fuzz/Cargo.toml --bin compact_snapshot -- -runs=1 fuzz/corpus/compact_snapshot cargo run --manifest-path fuzz/Cargo.toml --bin domain_json -- -runs=1 fuzz/corpus/domain_json cargo run --manifest-path fuzz/Cargo.toml --bin stratum_request -- -runs=1 fuzz/corpus/stratum_request +cargo run --manifest-path fuzz/Cargo.toml --bin wallet_config -- -runs=1 fuzz/corpus/wallet_config ``` Targets intentionally accept malformed input. A parse error is fine; panics, diff --git a/fuzz/corpus/wallet_config/config.json b/fuzz/corpus/wallet_config/config.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "amount_unit": "microiuna", + "setup_complete": true, + "auth_password_hash": "argon2id:test", + "mining_enabled": true, + "pow_mining_enabled": true, + "pow_mining_workers": 4, + "burn_per_block": 100, + "burn_fee": 100, + "recovery_vdf_top_rank_percent": 50, + "keep_track_of_metrics": true, + "p2p_accept_inbound": true, + "p2p_bind_port": 9444, + "p2p_announce_addr": "203.0.113.10:9444", + "stratum_enabled": false, + "stratum_bind_port": 3333, + "peers": ["iuna.jhx.app:9444"], + "address_book": { + "0000000000000000000000000000000000000000000000000000000000000000": "Test" + } +} diff --git a/fuzz/corpus/wallet_config/wallet_encrypted.json b/fuzz/corpus/wallet_config/wallet_encrypted.json @@ -0,0 +1,12 @@ +{ + "version": 3, + "address": "0000000000000000000000000000000000000000000000000000000000000000", + "encryption": { + "algorithm": "chacha20poly1305", + "kdf": "pbkdf2-sha256", + "kdf_iterations": 210000, + "salt": "000102030405060708090a0b0c0d0e0f", + "nonce": "000102030405060708090a0b", + "ciphertext": "000102030405060708090a0b0c0d0e0f" + } +} diff --git a/fuzz/corpus/wallet_config/wallet_plain.json b/fuzz/corpus/wallet_config/wallet_plain.json @@ -0,0 +1,5 @@ +{ + "version": 2, + "seed": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", + "address": "8fcb9ff51abf624ef8ccd78cecf59be5c8cb3789668c32916fefc08c1864a4b0" +} diff --git a/fuzz/fuzz_targets/wallet_config.rs b/fuzz/fuzz_targets/wallet_config.rs @@ -0,0 +1,13 @@ +#![no_main] + +use iuna::adapters::{config_store, wallet_store}; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + if data.len() > 1024 * 1024 { + return; + } + + let _ = config_store::fuzz_parse_config(data); + let _ = wallet_store::fuzz_parse_wallet_metadata(data); +}); diff --git a/src/adapters/config_store.rs b/src/adapters/config_store.rs @@ -151,14 +151,17 @@ pub fn save(path: &Path, config: &UiConfig) -> Result<()> { fn load(path: &Path) -> Result<UiConfig> { let bytes = fs::read(path).with_context(|| format!("failed to read config file {}", path.display()))?; - let stored: ConfigFile = serde_json::from_slice(&bytes) - .with_context(|| format!("failed to parse config file {}", path.display()))?; + parse_config_bytes(&bytes, &path.display().to_string()) +} + +fn parse_config_bytes(bytes: &[u8], source: &str) -> Result<UiConfig> { + let stored: ConfigFile = serde_json::from_slice(bytes) + .with_context(|| format!("failed to parse config file {source}"))?; if stored.version != CONFIG_FILE_VERSION { bail!( - "unsupported config file version {} in {}", - stored.version, - path.display() + "unsupported config file version {} in {source}", + stored.version ); } @@ -201,6 +204,11 @@ fn load(path: &Path) -> Result<UiConfig> { }) } +#[cfg(feature = "fuzzing")] +pub fn fuzz_parse_config(bytes: &[u8]) -> Result<UiConfig> { + parse_config_bytes(bytes, "<fuzz>") +} + pub fn clamp_pow_mining_workers(workers: u8) -> u8 { workers.clamp(1, MAX_POW_MINING_WORKERS) } diff --git a/src/adapters/wallet_store.rs b/src/adapters/wallet_store.rs @@ -16,6 +16,8 @@ use serde::{Deserialize, Serialize}; use sha2::Sha256; use crate::domain::Wallet; +#[cfg(feature = "fuzzing")] +use crate::domain::validate_address; const WALLET_FILE_VERSION: u32 = 3; const PLAINTEXT_WALLET_FILE_VERSION: u32 = 2; @@ -250,8 +252,11 @@ fn wallet_seed(stored: &WalletFile, password: Option<&str>) -> Result<String> { fn read_wallet_file(path: &Path) -> Result<WalletFile> { let bytes = fs::read(path).with_context(|| format!("failed to read wallet file {}", path.display()))?; - serde_json::from_slice(&bytes) - .with_context(|| format!("failed to parse wallet file {}", path.display())) + parse_wallet_file_bytes(&bytes, &path.display().to_string()) +} + +fn parse_wallet_file_bytes(bytes: &[u8], source: &str) -> Result<WalletFile> { + serde_json::from_slice(bytes).with_context(|| format!("failed to parse wallet file {source}")) } enum WalletFileMode { @@ -464,6 +469,48 @@ fn decode_hex_nibble(byte: u8) -> Result<u8> { } } +#[cfg(feature = "fuzzing")] +pub fn fuzz_parse_wallet_metadata(bytes: &[u8]) -> Result<WalletMetadata> { + let stored = parse_wallet_file_bytes(bytes, "<fuzz>")?; + validate_wallet_file_metadata(&stored)?; + Ok(WalletMetadata { + address: stored.address, + encrypted: stored.encryption.is_some(), + }) +} + +#[cfg(feature = "fuzzing")] +fn validate_wallet_file_metadata(stored: &WalletFile) -> Result<()> { + if stored.version != WALLET_FILE_VERSION + && stored.version != PLAINTEXT_WALLET_FILE_VERSION + && stored.version != 1 + { + bail!("unsupported wallet file version {}", stored.version); + } + validate_address(&stored.address, "wallet address")?; + if let Some(encryption) = &stored.encryption { + if encryption.algorithm != WALLET_ENCRYPTION_ALGORITHM { + bail!("unsupported wallet encryption algorithm"); + } + if encryption.kdf != WALLET_ENCRYPTION_KDF { + bail!("unsupported wallet encryption kdf"); + } + let _ = decode_hex(&encryption.salt).context("invalid wallet encryption salt")?; + let nonce = decode_hex(&encryption.nonce).context("invalid wallet encryption nonce")?; + if nonce.len() != 12 { + bail!("invalid wallet encryption nonce length"); + } + let _ = decode_hex(&encryption.ciphertext).context("invalid wallet encrypted seed")?; + } else { + let seed = stored + .seed + .as_deref() + .context("wallet file does not contain a seed")?; + let _ = normalize_seed_phrase(seed)?; + } + Ok(()) +} + fn normalize_seed_phrase(seed_phrase: &str) -> Result<String> { let normalized = seed_phrase .split_whitespace()