From f0677c0261e08fceb0e66cabc25faf6193ae09f6 Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Tue, 19 May 2026 17:34:26 -0400 Subject: [PATCH] add privacy test version --- Cargo.lock | 35 +++----- nssa/src/validated_state_diff.rs | 142 ++++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57135709..fcb4116c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -674,9 +674,9 @@ checksum = "4858a9d740c5007a9069007c3b4e91152d0506f13c1b31dd49051fd537656156" [[package]] name = "astral-tokio-tar" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ce73b17c62717c4b6a9af10b43e87c578b0cac27e00666d48304d3b7d2c0693" +checksum = "cb50a7aae84a03bf55b067832bc376f4961b790c97e64d3eacee97d389b90277" dependencies = [ "filetime", "futures-core", @@ -2275,7 +2275,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2606,7 +2606,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3585,7 +3585,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -6403,7 +6403,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]] @@ -7257,7 +7257,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.3", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -7294,9 +7294,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -8191,7 +8191,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -8249,7 +8249,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -9162,7 +9162,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -10464,7 +10464,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -10603,15 +10603,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.61.2" diff --git a/nssa/src/validated_state_diff.rs b/nssa/src/validated_state_diff.rs index 5deef6a4..d1a26de6 100644 --- a/nssa/src/validated_state_diff.rs +++ b/nssa/src/validated_state_diff.rs @@ -500,6 +500,146 @@ mod tests { validated_state_diff::ValidatedStateDiff, }; + /// Privacy-path version of the authorization-injection attack. + /// + /// `execute_and_prove` succeeds: all inner receipts are valid, and the outer circuit + /// honestly commits `victim(is_authorized=true)` to its journal. + /// `from_privacy_preserving_transaction` rejects the proof because the NSSA validator + /// independently reconstructs `public_pre_states` from chain state using + /// `signer_account_ids.contains(victim_id) = false`, producing `victim(is_authorized=false)`. + /// The committed journal and the expected output diverge, so `receipt.verify` fails. + #[test] + fn privacy_malicious_programs_cannot_drain_public_victim() { + use nssa_core::{ + Commitment, InputAccountIdentity, SharedSecretKey, + account::{Account, AccountWithMetadata}, + encryption::EphemeralPublicKey, + }; + + use crate::{ + PrivacyPreservingTransaction, + privacy_preserving_transaction::{ + circuit::{ProgramWithDependencies, execute_and_prove}, + message::Message, + witness_set::WitnessSet, + }, + state::{CommitmentSet, tests::test_private_account_keys_1}, + }; + + type InjectorInstruction = ( + nssa_core::program::ProgramId, // p2_id + nssa_core::program::ProgramId, // auth_transfer_id + [u8; 32], // victim_id_raw + u128, // victim_balance + u128, // victim_nonce + nssa_core::program::ProgramId, // victim_program_owner + [u8; 32], // recipient_id_raw + u128, // amount + ); + + // Attacker controls a private account. + let attacker_keys = test_private_account_keys_1(); + let attacker_id = AccountId::for_regular_private_account(&attacker_keys.npk(), 0); + let attacker_esk = [12_u8; 32]; + let attacker_ssk = SharedSecretKey::new(attacker_esk, &attacker_keys.vpk()); + let attacker_epk = EphemeralPublicKey::from_scalar(attacker_esk); + + let victim_id = AccountId::new([20_u8; 32]); + let recipient_id = AccountId::new([42_u8; 32]); + let victim_balance = 5_000_u128; + + // genesis sets program_owner = authenticated_transfer_program.id() on all accounts. + let mut state = V03State::new_with_genesis_accounts( + &[(victim_id, victim_balance), (recipient_id, 0)], + vec![], + 0, + ); + state.insert_program(Program::malicious_injector()); + state.insert_program(Program::malicious_launderer()); + + // Build attacker's private account and its local commitment tree. + let attacker_account = Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: 100, + ..Account::default() + }; + let attacker_commitment = Commitment::new(&attacker_id, &attacker_account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&attacker_commitment)); + let membership_proof = commitment_set + .get_proof_for(&attacker_commitment) + .expect("attacker commitment must be in the set"); + + let attacker_pre = AccountWithMetadata::new(attacker_account, true, attacker_id); + + let victim_account = state.get_account_by_id(victim_id); + let instruction: InjectorInstruction = ( + Program::malicious_launderer().id(), + Program::authenticated_transfer_program().id(), + *victim_id.value(), + victim_account.balance, + victim_account.nonce.0, + victim_account.program_owner, + *recipient_id.value(), + victim_balance, + ); + let instruction_data = Program::serialize_instruction(instruction).unwrap(); + + let p2 = Program::malicious_launderer(); + let at = Program::authenticated_transfer_program(); + let program_with_deps = ProgramWithDependencies::new( + Program::malicious_injector(), + [(p2.id(), p2), (at.id(), at)].into(), + ); + + // account_identities order must match self.pre_states as built by the circuit: + // [0] attacker — first seen in P1's program_output.pre_states + // [1] victim — first seen in authenticated_transfer's program_output.pre_states + // [2] recipient — first seen in authenticated_transfer's program_output.pre_states + let account_identities = vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: attacker_ssk, + nsk: attacker_keys.nsk, + membership_proof, + identifier: 0, + }, + InputAccountIdentity::Public, // victim + InputAccountIdentity::Public, // recipient + ]; + + // execute_and_prove succeeds: all inner receipts are valid. + // The outer circuit commits victim(is_authorized=true) to its journal. + let (circuit_output, proof) = execute_and_prove( + vec![attacker_pre], + instruction_data, + account_identities, + &program_with_deps, + ) + .expect("execute_and_prove should succeed \u{2014} the programs execute correctly"); + + // public_account_ids lists the Public entries from account_identities, in order. + // The single ciphertext belongs to attacker's private account update. + let message = Message::try_from_circuit_output( + vec![victim_id, recipient_id], + vec![], // no public signers, no nonces + vec![(attacker_keys.npk(), attacker_keys.vpk(), attacker_epk)], + circuit_output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0); + + assert!( + result.is_err(), + "attack privacy transaction should be rejected by the validator" + ); + assert_eq!(state.get_account_by_id(victim_id).balance, victim_balance); + assert_eq!(state.get_account_by_id(recipient_id).balance, 0); + } + /// Demonstrates the authorization-injection vulnerability: /// two malicious programs (injector + launderer) drain a victim's balance /// without the victim signing anything. @@ -514,7 +654,7 @@ mod tests { /// → `victim.is_authorized=true` passes check ({victim}.contains(victim)) /// → transfer executes. #[test] - fn malicious_programs_drain_victim_without_signature() { + fn malicious_programs_cannot_drain_victim_without_signature() { // p2_id, auth_transfer_id, victim_id_raw, victim_balance, victim_nonce, // victim_program_owner, recipient_id_raw, amount. // Primitives only — AccountId/Account cannot round-trip through instruction_data