iuna

iuna

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

commit c04eca7dd9e3333bd44be5b6f89a103a06623b63
parent c2d08cb3ab0629002aae90639eb48e74ad4bb527
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Sun, 30 Aug 2026 07:23:20 +0200

Fix vulnerable desktop dependencies

Diffstat:
A.github/workflows/security.yml | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 1+
MDockerfile.local-testnet | 2+-
MREADME.md | 8+++++---
MROADMAP.md | 4++++
Mdeployment.sh | 5+++--
Mdocs/security-review.md | 12++++++++++++
Mfuzz/Cargo.toml | 2++
Mrust-toolchain.toml | 2+-
Ascripts/check-dependencies.sh | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc-tauri/Cargo.lock | 24++++++++++++------------
Msrc-tauri/Cargo.toml | 2++
Msrc/adapters/chain_store/compact.rs | 5+----
Msrc/adapters/p2p/line_codec.rs | 4++--
Msrc/domain/adversarial_tests.rs | 10+++-------
Msrc/domain/hex.rs | 2+-
Msrc/domain/ledger_queries.rs | 8+++++---
Msrc/domain/ticket.rs | 7++-----
Msrc/main.rs | 2+-
19 files changed, 192 insertions(+), 42 deletions(-)

diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml @@ -0,0 +1,70 @@ +name: Security and desktop builds + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + dependency-policy: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Install pinned Rust toolchain + run: rustup toolchain install 1.88.0 --profile minimal + - name: Install pinned cargo-audit + run: cargo +1.88.0 install cargo-audit --version 0.22.1 --locked + - name: Audit dependencies and enforce source/license policy + run: ./scripts/check-dependencies.sh + - name: Test node + run: cargo test --locked + - name: Check fuzz targets + run: cargo check --locked --manifest-path fuzz/Cargo.toml + + desktop-build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Install Linux desktop dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libwebkit2gtk-4.1-dev \ + build-essential \ + libxdo-dev \ + libssl-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev + - name: Install pinned Rust toolchain + run: rustup toolchain install 1.88.0 --profile minimal + - name: Build host sidecar + run: | + cargo +1.88.0 build --locked + target_triple="$(rustc +1.88.0 -vV | sed -n 's/^host: //p')" + mkdir -p src-tauri/binaries + if [ "${{ runner.os }}" = "Windows" ]; then + cp target/debug/iuna.exe "src-tauri/binaries/iuna-sidecar-${target_triple}.exe" + else + cp target/debug/iuna "src-tauri/binaries/iuna-sidecar-${target_triple}" + fi + - name: Test desktop crate + run: cargo +1.88.0 test --locked --manifest-path src-tauri/Cargo.toml + - name: Build desktop crate + run: cargo +1.88.0 build --locked --manifest-path src-tauri/Cargo.toml diff --git a/Cargo.toml b/Cargo.toml @@ -2,6 +2,7 @@ name = "iuna" version = "0.4.3" edition = "2024" +rust-version = "1.88" license = "Apache-2.0" [dependencies] diff --git a/Dockerfile.local-testnet b/Dockerfile.local-testnet @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -FROM rust:1.86.0-bookworm AS builder +FROM rust:1.88.0-bookworm AS builder WORKDIR /src/iuna COPY . . diff --git a/README.md b/README.md @@ -68,9 +68,11 @@ Release and deploy with: ./deployment.sh 0.2.48 ``` -By default, deployment runs the regular unit tests and verifies that the fuzz -targets compile against their locked dependencies. Run the extended adversarial, -fuzz, and release-property suites explicitly when needed: +By default, deployment audits all three Rust lockfiles, enforces the dependency +source/license policy, runs the regular unit tests, and verifies that the fuzz +targets compile against their locked dependencies. Release hosts therefore need +`cargo-audit` and `jq` in addition to the pinned Rust 1.88 toolchain. Run the +extended adversarial, fuzz, and release-property suites explicitly when needed: ```sh ./deployment.sh --full-tests 0.2.48 diff --git a/ROADMAP.md b/ROADMAP.md @@ -118,8 +118,12 @@ Focus: improve usability, tooling, and governance after the base network is stab A release intended for deployment must pass: +- `./scripts/check-dependencies.sh`, which audits `Cargo.lock`, + `fuzz/Cargo.lock`, and `src-tauri/Cargo.lock` and enforces the dependency + source/license allowlist - `cargo test --locked` - `cargo check --locked --manifest-path fuzz/Cargo.toml` +- desktop test/build jobs on Linux, macOS, and Windows - fuzz gate runs with `256` iterations each for `p2p_envelope`, `compact_snapshot`, `domain_json`, `stratum_request`, and `wallet_config` - `cargo test --locked --release --test properties -- --ignored` diff --git a/deployment.sh b/deployment.sh @@ -101,6 +101,7 @@ run_release_tests() { require_command cargo + ./scripts/check-dependencies.sh cargo test --locked cargo check --locked --manifest-path fuzz/Cargo.toml @@ -222,7 +223,7 @@ build_windows_desktop_in_docker_if_possible() { -v iuna-windows-tauri-target:/work/iuna/src-tauri/target \ -v "$(pwd):/src/iuna:ro" \ -v "$(pwd)/downloads:/out" \ - rust:1.86-bookworm \ + rust:1.88-bookworm \ bash -c ' set -euo pipefail @@ -299,7 +300,7 @@ build_linux_cli_archives() { -v "$(pwd):/src/iuna:ro" \ -v "$(pwd)/downloads:/out" \ -v "$(pwd)/.docker-build:/node-out" \ - rust:1.86-bookworm \ + rust:1.88-bookworm \ bash -c ' set -euo pipefail diff --git a/docs/security-review.md b/docs/security-review.md @@ -158,6 +158,7 @@ Evidence already in the tree: Before the mainnet-candidate launch, attach or publish logs for: ```sh +./scripts/check-dependencies.sh cargo test --locked cargo check --locked --manifest-path fuzz/Cargo.toml cargo run --locked --manifest-path fuzz/Cargo.toml --bin p2p_envelope -- -runs=256 fuzz/corpus/p2p_envelope @@ -246,6 +247,17 @@ see which revision was tested. playbooks are the maintained in-tree references. - Release evidence: keep successful release-gate logs from the exact tagged candidate revision. +- Desktop dependency security: the project and release builders use Rust 1.88. + The desktop lockfile pins `quick-xml 0.41.0`, `plist 1.10.0`, and + `time 0.3.47`, removing RUSTSEC-2026-0194, RUSTSEC-2026-0195, and + RUSTSEC-2026-0009. `scripts/check-dependencies.sh` audits the root, fuzz, and + desktop lockfiles without vulnerability ignores and rejects unapproved + dependency sources or license identifiers. It is mandatory in + `deployment.sh` and the CI security job. CI also test-builds the desktop crate + on Linux, macOS, and Windows. RustSec still reports non-vulnerability warnings + for Tauri's Linux GTK3 stack (unmaintained crates and the `glib::VariantStrIter` + advisory); iuna does not call that iterator API directly. Reassess this + transitive stack on every Tauri upgrade and no later than 2026-11-30. - Height `1000` activation: the release activates both grinding resistance and transaction signing format v1 automatically. All candidate nodes must upgrade before activation; the height activation itself requires no chain-state reset diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml @@ -2,6 +2,8 @@ name = "iuna-fuzz" version = "0.0.0" edition = "2024" +rust-version = "1.88" +license = "Apache-2.0" publish = false [package.metadata] diff --git a/rust-toolchain.toml b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.86.0" +channel = "1.88.0" components = ["rustfmt", "clippy"] profile = "minimal" diff --git a/scripts/check-dependencies.sh b/scripts/check-dependencies.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +require_command() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: missing required command: $1" >&2 + exit 1 + } +} + +require_command cargo +require_command cargo-audit +require_command jq + +lockfiles=(Cargo.lock fuzz/Cargo.lock src-tauri/Cargo.lock) +for lockfile in "${lockfiles[@]}"; do + cargo audit --file "$lockfile" +done + +manifests=(Cargo.toml fuzz/Cargo.toml src-tauri/Cargo.toml) +metadata_file="$(mktemp)" +trap 'rm -f "$metadata_file"' EXIT + +for manifest in "${manifests[@]}"; do + cargo metadata --locked --format-version 1 --manifest-path "$manifest" | + jq -r '.packages[] | [.name, (.license // "MISSING"), (.source // "LOCAL")] | @tsv' +done | sort -u > "$metadata_file" + +policy_failed=false +while IFS=$'\t' read -r package license source; do + case "$source" in + LOCAL|registry+https://github.com/rust-lang/crates.io-index) ;; + *) + echo "error: dependency ${package} uses unapproved source ${source}" >&2 + policy_failed=true + ;; + esac + + if [ "$license" = "MISSING" ]; then + echo "error: dependency ${package} has no declared license" >&2 + policy_failed=true + continue + fi + + license_tokens="$(printf '%s\n' "$license" | + sed -E 's/[()\/]/ /g; s/(^|[[:space:]])(AND|OR|WITH)([[:space:]]|$)/ /g')" + for license_token in $license_tokens; do + case "$license_token" in + 0BSD|Apache-2.0|BSD-1-Clause|BSD-2-Clause|BSD-3-Clause|BSL-1.0|CC0-1.0|ISC|LGPL-2.1-or-later|MIT|MIT-0|MPL-2.0|NCSA|Unicode-3.0|Unlicense|Zlib|LLVM-exception) ;; + *) + echo "error: dependency ${package} uses unapproved license token ${license_token} (${license})" >&2 + policy_failed=true + ;; + esac + done +done < "$metadata_file" + +if [ "$policy_failed" = "true" ]; then + exit 1 +fi + +echo "Dependency audit, source policy, and license policy passed for all manifests." diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock @@ -1814,9 +1814,9 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -2198,9 +2198,9 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", @@ -2320,9 +2320,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] @@ -3341,9 +3341,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -3356,15 +3356,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml @@ -2,6 +2,8 @@ name = "iuna-desktop" version = "0.4.3" edition = "2024" +rust-version = "1.88" +license = "Apache-2.0" publish = false [build-dependencies] diff --git a/src/adapters/chain_store/compact.rs b/src/adapters/chain_store/compact.rs @@ -131,10 +131,7 @@ pub(super) fn encode_compact_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<u8 ); } if block.prev_hash != expected_prev_hash { - bail!( - "chain snapshot block {} has non-canonical previous hash", - height - ); + bail!("chain snapshot block {height} has non-canonical previous hash"); } if block.hash != block.compute_hash() { bail!("chain snapshot block {height} has a non-canonical hash"); diff --git a/src/adapters/p2p/line_codec.rs b/src/adapters/p2p/line_codec.rs @@ -39,7 +39,7 @@ impl<R: AsyncRead + Unpin> LimitedLineReader<R> { if let Some(newline) = available.iter().position(|byte| *byte == b'\n') { if self.pending.len() + newline > MAX_GOSSIP_LINE_BYTES { - anyhow::bail!("p2p message exceeds {} byte limit", MAX_GOSSIP_LINE_BYTES); + anyhow::bail!("p2p message exceeds {MAX_GOSSIP_LINE_BYTES} byte limit"); } self.pending.extend_from_slice(&available[..newline]); self.reader.consume(newline + 1); @@ -53,7 +53,7 @@ impl<R: AsyncRead + Unpin> LimitedLineReader<R> { } if self.pending.len() + available.len() > MAX_GOSSIP_LINE_BYTES { - anyhow::bail!("p2p message exceeds {} byte limit", MAX_GOSSIP_LINE_BYTES); + anyhow::bail!("p2p message exceeds {MAX_GOSSIP_LINE_BYTES} byte limit"); } let consumed = available.len(); self.pending.extend_from_slice(available); diff --git a/src/domain/adversarial_tests.rs b/src/domain/adversarial_tests.rs @@ -1396,12 +1396,9 @@ fn mini_weighted_ticket_draw( format!("{}:{}", parent.hash, parent.vdf_output) }; let seed = if rank == 0 { - format!("iuna-ticket-draw:{}:{}", target_height, parent_randomness) + format!("iuna-ticket-draw:{target_height}:{parent_randomness}") } else { - format!( - "iuna-ticket-draw-rank:{}:{}:{}", - target_height, rank, parent_randomness - ) + format!("iuna-ticket-draw-rank:{target_height}:{rank}:{parent_randomness}") }; let digest = Sha256::digest(seed.as_bytes()); let mut bytes = [0_u8; 16]; @@ -3209,8 +3206,7 @@ fn mini_ticket_oracle_matches_ledger_across_maturity_expiry_and_consumption() { && ticket.eligible_from_height == expected_from && ticket.eligible_until_height == expected_until }), - "burn ticket {} was not scheduled with expected maturity/expiry", - signature + "burn ticket {signature} was not scheduled with expected maturity/expiry" ); } } diff --git a/src/domain/hex.rs b/src/domain/hex.rs @@ -10,7 +10,7 @@ pub(super) fn decode_hex_array<const N: usize>(input: &str) -> Result<[u8; N]> { let len = bytes.len(); bytes .try_into() - .map_err(|_| anyhow!("expected {} hex bytes, got {len}", N)) + .map_err(|_| anyhow!("expected {N} hex bytes, got {len}")) } pub(super) fn decode_hex(input: &str) -> Result<Vec<u8>> { diff --git a/src/domain/ledger_queries.rs b/src/domain/ledger_queries.rs @@ -113,9 +113,11 @@ impl Ledger { launch_profile_hash: self.launch_profile.hash(), mine_reward: self.mine_reward, current_mine_difficulty_bits: self.current_mine_difficulty_bits(), - balances: include_balances - .then(|| balances_from_utxos(&self.utxos)) - .unwrap_or_default(), + balances: if include_balances { + balances_from_utxos(&self.utxos) + } else { + Default::default() + }, pending_transactions: self.pending.len(), } } diff --git a/src/domain/ticket.rs b/src/domain/ticket.rs @@ -86,12 +86,9 @@ pub(super) fn draw_parent_randomness(parent: &Block, target_height: u64) -> Stri fn ticket_draw_seed(parent: &Block, target_height: u64, rank: u32) -> String { let parent_randomness = draw_parent_randomness(parent, target_height); if rank == 0 { - format!("iuna-ticket-draw:{}:{}", target_height, parent_randomness) + format!("iuna-ticket-draw:{target_height}:{parent_randomness}") } else { - format!( - "iuna-ticket-draw-rank:{}:{}:{}", - target_height, rank, parent_randomness - ) + format!("iuna-ticket-draw-rank:{target_height}:{rank}:{parent_randomness}") } } diff --git a/src/main.rs b/src/main.rs @@ -187,7 +187,7 @@ async fn main() -> Result<()> { println!("UI data database: {}", ui_data_store.path().display()); println!("management UI: http://{}", opts.http_addr); if p2p_accept_inbound { - println!("p2p listener: {}", configured_p2p_addr); + println!("p2p listener: {configured_p2p_addr}"); } else { println!("p2p listener: disabled (outbound-only)"); }