From 0388fa2c8b0a98fba654517c11b0ecd0107c40c6 Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sun, 29 Mar 2026 22:54:12 +0200 Subject: [PATCH 1/4] fix(server): enforce volumetric cap using past + current transfer value --- .../src/evm/policies/ether_transfer/mod.rs | 9 +++++---- .../src/evm/policies/ether_transfer/tests.rs | 8 ++++---- .../src/evm/policies/token_transfers/mod.rs | 9 +++++---- .../src/evm/policies/token_transfers/tests.rs | 10 +++++----- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs b/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs index b6c68c6..e823f07 100644 --- a/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs +++ b/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs @@ -91,6 +91,7 @@ async fn query_relevant_past_transaction( async fn check_rate_limits( grant: &Grant, + current_transfer_value: U256, db: &mut impl AsyncConnection, ) -> QueryResult> { let mut violations = Vec::new(); @@ -99,12 +100,12 @@ async fn check_rate_limits( let past_transaction = query_relevant_past_transaction(grant.id, window, db).await?; let window_start = chrono::Utc::now() - grant.settings.limit.window; - let cumulative_volume: U256 = past_transaction + let prospective_cumulative_volume: U256 = past_transaction .iter() .filter(|(_, timestamp)| timestamp >= &window_start) - .fold(U256::default(), |acc, (value, _)| acc + *value); + .fold(current_transfer_value, |acc, (value, _)| acc + *value); - if cumulative_volume > grant.settings.limit.max_volume { + if prospective_cumulative_volume > grant.settings.limit.max_volume { violations.push(EvalViolation::VolumetricLimitExceeded); } @@ -141,7 +142,7 @@ impl Policy for EtherTransfer { violations.push(EvalViolation::InvalidTarget { target: meaning.to }); } - let rate_violations = check_rate_limits(grant, db).await?; + let rate_violations = check_rate_limits(grant, meaning.value, db).await?; violations.extend(rate_violations); Ok(violations) diff --git a/server/crates/arbiter-server/src/evm/policies/ether_transfer/tests.rs b/server/crates/arbiter-server/src/evm/policies/ether_transfer/tests.rs index cba78b0..9ba48be 100644 --- a/server/crates/arbiter-server/src/evm/policies/ether_transfer/tests.rs +++ b/server/crates/arbiter-server/src/evm/policies/ether_transfer/tests.rs @@ -198,7 +198,7 @@ async fn evaluate_rejects_volume_over_limit() { grant_id, wallet_access_id: WALLET_ACCESS_ID, chain_id: CHAIN_ID as i32, - eth_value: utils::u256_to_bytes(U256::from(1_001u64)).to_vec(), + eth_value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(), signed_at: SqliteTimestamp(Utc::now()), }) .execute(&mut *conn) @@ -211,7 +211,7 @@ async fn evaluate_rejects_volume_over_limit() { shared: shared(), settings, }; - let context = ctx(ALLOWED, U256::from(100u64)); + let context = ctx(ALLOWED, U256::from(1u64)); let m = EtherTransfer::analyze(&context).unwrap(); let v = EtherTransfer::evaluate(&context, &m, &grant, &mut *conn) .await @@ -233,13 +233,13 @@ async fn evaluate_passes_at_exactly_volume_limit() { .await .unwrap(); - // Exactly at the limit — the check is `>`, so this should not violate + // Exactly at the limit including current transfer — check is `>`, so this should not violate insert_into(evm_transaction_log::table) .values(NewEvmTransactionLog { grant_id, wallet_access_id: WALLET_ACCESS_ID, chain_id: CHAIN_ID as i32, - eth_value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(), + eth_value: utils::u256_to_bytes(U256::from(900u64)).to_vec(), signed_at: SqliteTimestamp(Utc::now()), }) .execute(&mut *conn) diff --git a/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs b/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs index bfd8ba2..7dfec70 100644 --- a/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs +++ b/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs @@ -101,6 +101,7 @@ async fn query_relevant_past_transfers( async fn check_volume_rate_limits( grant: &Grant, + current_transfer_value: U256, db: &mut impl AsyncConnection, ) -> QueryResult> { let mut violations = Vec::new(); @@ -113,12 +114,12 @@ async fn check_volume_rate_limits( for limit in &grant.settings.volume_limits { let window_start = chrono::Utc::now() - limit.window; - let cumulative_volume: U256 = past_transfers + let prospective_cumulative_volume: U256 = past_transfers .iter() .filter(|(_, timestamp)| timestamp >= &window_start) - .fold(U256::default(), |acc, (value, _)| acc + *value); + .fold(current_transfer_value, |acc, (value, _)| acc + *value); - if cumulative_volume > limit.max_volume { + if prospective_cumulative_volume > limit.max_volume { violations.push(EvalViolation::VolumetricLimitExceeded); break; } @@ -163,7 +164,7 @@ impl Policy for TokenTransfer { violations.push(EvalViolation::InvalidTarget { target: meaning.to }); } - let rate_violations = check_volume_rate_limits(grant, db).await?; + let rate_violations = check_volume_rate_limits(grant, meaning.value, db).await?; violations.extend(rate_violations); Ok(violations) diff --git a/server/crates/arbiter-server/src/evm/policies/token_transfers/tests.rs b/server/crates/arbiter-server/src/evm/policies/token_transfers/tests.rs index d8a5947..2f1b72f 100644 --- a/server/crates/arbiter-server/src/evm/policies/token_transfers/tests.rs +++ b/server/crates/arbiter-server/src/evm/policies/token_transfers/tests.rs @@ -220,7 +220,7 @@ async fn evaluate_rejects_wrong_restricted_recipient() { } #[tokio::test] -async fn evaluate_passes_volume_within_limit() { +async fn evaluate_passes_volume_at_exact_limit() { let db = db::create_test_pool().await; let mut conn = db.get().await.unwrap(); @@ -230,7 +230,7 @@ async fn evaluate_passes_volume_within_limit() { .await .unwrap(); - // Record a past transfer of 500 (within 1000 limit) + // Record a past transfer of 900, with current transfer 100 => exactly 1000 limit use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log}; insert_into(evm_token_transfer_log::table) .values(NewEvmTokenTransferLog { @@ -239,7 +239,7 @@ async fn evaluate_passes_volume_within_limit() { chain_id: CHAIN_ID as i32, token_contract: DAI.to_vec(), recipient_address: RECIPIENT.to_vec(), - value: utils::u256_to_bytes(U256::from(500u64)).to_vec(), + value: utils::u256_to_bytes(U256::from(900u64)).to_vec(), }) .execute(&mut *conn) .await @@ -282,7 +282,7 @@ async fn evaluate_rejects_volume_over_limit() { chain_id: CHAIN_ID as i32, token_contract: DAI.to_vec(), recipient_address: RECIPIENT.to_vec(), - value: utils::u256_to_bytes(U256::from(1_001u64)).to_vec(), + value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(), }) .execute(&mut *conn) .await @@ -294,7 +294,7 @@ async fn evaluate_rejects_volume_over_limit() { shared: shared(), settings, }; - let calldata = transfer_calldata(RECIPIENT, U256::from(100u64)); + let calldata = transfer_calldata(RECIPIENT, U256::from(1u64)); let context = ctx(DAI, calldata); let m = TokenTransfer::analyze(&context).unwrap(); let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn) From 8feda7990c90c501cefa7044c0a040189ba4c2f6 Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sun, 29 Mar 2026 23:05:38 +0200 Subject: [PATCH 2/4] fix(auth): reject invalid challenge signatures instead of transitioning to AuthOk --- .../src/actors/user_agent/auth/state.rs | 13 ++-- .../arbiter-server/tests/user_agent/auth.rs | 66 +++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs index c422589..eea0661 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs @@ -210,13 +210,16 @@ where } }; - if valid { - self.transport - .send(Ok(Outbound::AuthSuccess)) - .await - .map_err(|_| Error::Transport)?; + if !valid { + error!("Invalid challenge solution signature"); + return Err(Error::InvalidChallengeSolution); } + self.transport + .send(Ok(Outbound::AuthSuccess)) + .await + .map_err(|_| Error::Transport)?; + Ok(key.clone()) } } diff --git a/server/crates/arbiter-server/tests/user_agent/auth.rs b/server/crates/arbiter-server/tests/user_agent/auth.rs index 285ddcf..3fd8c92 100644 --- a/server/crates/arbiter-server/tests/user_agent/auth.rs +++ b/server/crates/arbiter-server/tests/user_agent/auth.rs @@ -165,3 +165,69 @@ pub async fn test_challenge_auth() { task.await.unwrap().unwrap(); } + +#[tokio::test] +#[test_log::test] +pub async fn test_challenge_auth_rejects_invalid_signature() { + let db = db::create_test_pool().await; + let actors = GlobalActors::spawn(db.clone()).await.unwrap(); + + let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + + // Pre-register key with key_type + { + let mut conn = db.get().await.unwrap(); + insert_into(schema::useragent_client::table) + .values(( + schema::useragent_client::public_key.eq(pubkey_bytes.clone()), + schema::useragent_client::key_type.eq(1i32), + )) + .execute(&mut conn) + .await + .unwrap(); + } + + let (server_transport, mut test_transport) = ChannelTransport::new(); + let db_for_task = db.clone(); + let task = tokio::spawn(async move { + let mut props = UserAgentConnection::new(db_for_task, actors); + auth::authenticate(&mut props, server_transport).await + }); + + test_transport + .send(auth::Inbound::AuthChallengeRequest { + pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), + bootstrap_token: None, + }) + .await + .unwrap(); + + let response = test_transport + .recv() + .await + .expect("should receive challenge"); + let challenge = match response { + Ok(resp) => match resp { + auth::Outbound::AuthChallenge { nonce } => nonce, + other => panic!("Expected AuthChallenge, got {other:?}"), + }, + Err(err) => panic!("Expected Ok response, got Err({err:?})"), + }; + + // Sign a different challenge value so signature format is valid but verification must fail. + let wrong_challenge = arbiter_proto::format_challenge(challenge + 1, &pubkey_bytes); + let signature = new_key.sign(&wrong_challenge); + + test_transport + .send(auth::Inbound::AuthChallengeSolution { + signature: signature.to_bytes().to_vec(), + }) + .await + .unwrap(); + + assert!(matches!( + task.await.unwrap(), + Err(auth::Error::InvalidChallengeSolution) + )); +} From 63a4875fdb54b2ee8e4aceffcfbd323118dd70de Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sun, 29 Mar 2026 23:16:37 +0200 Subject: [PATCH 3/4] fix(keyholder): remove dead overwritten select in try_unseal query --- server/crates/arbiter-server/src/actors/keyholder/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/server/crates/arbiter-server/src/actors/keyholder/mod.rs b/server/crates/arbiter-server/src/actors/keyholder/mod.rs index 3a245af..fbf3d50 100644 --- a/server/crates/arbiter-server/src/actors/keyholder/mod.rs +++ b/server/crates/arbiter-server/src/actors/keyholder/mod.rs @@ -214,7 +214,6 @@ impl KeyHolder { let mut conn = self.db.get().await?; schema::root_key_history::table .filter(schema::root_key_history::id.eq(*root_key_history_id)) - .select(schema::root_key_history::data_encryption_nonce) .select(RootKeyHistory::as_select()) .first(&mut conn) .await? From 5bce9fd68ec70e4bc0957bebb4594643efe962ec Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sun, 29 Mar 2026 19:07:12 +0200 Subject: [PATCH 4/4] chore: bump mise deps --- mise.lock | 62 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/mise.lock b/mise.lock index 3cd025e..9cf1bee 100644 --- a/mise.lock +++ b/mise.lock @@ -8,10 +8,18 @@ backend = "aqua:ast-grep/ast-grep" checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip" +[tools.ast-grep."platforms.linux-arm64-musl"] +checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836" +url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip" + [tools.ast-grep."platforms.linux-x64"] checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip" +[tools.ast-grep."platforms.linux-x64-musl"] +checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651" +url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip" + [tools.ast-grep."platforms.macos-arm64"] checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip" @@ -32,10 +40,6 @@ backend = "cargo:cargo-audit" version = "0.13.9" backend = "cargo:cargo-edit" -[[tools."cargo:cargo-features"]] -version = "1.0.0" -backend = "cargo:cargo-features" - [[tools."cargo:cargo-features-manager"]] version = "0.11.1" backend = "cargo:cargo-features-manager" @@ -49,21 +53,13 @@ version = "0.9.126" backend = "cargo:cargo-nextest" [[tools."cargo:cargo-shear"]] -version = "1.9.1" +version = "1.11.2" backend = "cargo:cargo-shear" [[tools."cargo:cargo-vet"]] version = "0.10.2" backend = "cargo:cargo-vet" -[[tools."cargo:diesel-cli"]] -version = "2.3.6" -backend = "cargo:diesel-cli" - -[tools."cargo:diesel-cli".options] -default-features = "false" -features = "sqlite,sqlite-bundled" - [[tools."cargo:diesel_cli"]] version = "2.3.6" backend = "cargo:diesel_cli" @@ -72,10 +68,6 @@ backend = "cargo:diesel_cli" default-features = "false" features = "sqlite,sqlite-bundled" -[[tools."cargo:rinf_cli"]] -version = "8.9.1" -backend = "cargo:rinf_cli" - [[tools.flutter]] version = "3.38.9-stable" backend = "asdf:flutter" @@ -88,10 +80,18 @@ backend = "aqua:protocolbuffers/protobuf/protoc" checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79" url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip" +[tools.protoc."platforms.linux-arm64-musl"] +checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip" + [tools.protoc."platforms.linux-x64"] checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323" url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip" +[tools.protoc."platforms.linux-x64-musl"] +checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323" +url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip" + [tools.protoc."platforms.macos-arm64"] checksum = "sha256:b9576b5fa1a1ef3fe13a8c91d9d8204b46545759bea5ae155cd6ba2ea4cdaeed" url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-aarch_64.zip" @@ -109,24 +109,32 @@ version = "3.14.3" backend = "core:python" [tools.python."platforms.linux-arm64"] -checksum = "sha256:be0f4dc2932f762292b27d46ea7d3e8e66ddf3969a5eb0254a229015ed402625" -url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" +checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" + +[tools.python."platforms.linux-arm64-musl"] +checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" [tools.python."platforms.linux-x64"] -checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0" -url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" +checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" + +[tools.python."platforms.linux-x64-musl"] +checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" [tools.python."platforms.macos-arm64"] -checksum = "sha256:4703cdf18b26798fde7b49b6b66149674c25f97127be6a10dbcf29309bdcdcdb" -url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-apple-darwin-install_only_stripped.tar.gz" +checksum = "sha256:c43aecde4a663aebff99b9b83da0efec506479f1c3f98331442f33d2c43501f9" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-apple-darwin-install_only_stripped.tar.gz" [tools.python."platforms.macos-x64"] -checksum = "sha256:76f1cc26e3d262eae8ca546a93e8bded10cf0323613f7e246fea2e10a8115eb7" -url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-apple-darwin-install_only_stripped.tar.gz" +checksum = "sha256:9ab41dbc2f100a2a45d1833b9c11165f51051c558b5213eda9a9731d5948a0c0" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-apple-darwin-install_only_stripped.tar.gz" [tools.python."platforms.windows-x64"] -checksum = "sha256:950c5f21a015c1bdd1337f233456df2470fab71e4d794407d27a84cb8b9909a0" -url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-pc-windows-msvc-install_only_stripped.tar.gz" +checksum = "sha256:bbe19034b35b0267176a7442575ae7dc6343480fd4d35598cb7700173d431e09" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-pc-windows-msvc-install_only_stripped.tar.gz" [[tools.rust]] version = "1.93.0"