From 5a3446322837cfdd5aeee558bec3a39ba823b8f7 Mon Sep 17 00:00:00 2001 From: CleverWild Date: Wed, 8 Apr 2026 12:09:54 +0200 Subject: [PATCH] security(server): bind grant revocation state (revoked_at) to integrity hash --- server/crates/arbiter-server/src/evm/mod.rs | 21 +++++++++++++++++++ .../crates/arbiter-server/src/evm/policies.rs | 2 ++ .../src/evm/policies/ether_transfer/tests.rs | 1 + .../src/evm/policies/token_transfers/tests.rs | 1 + .../src/grpc/user_agent/inbound.rs | 1 + 5 files changed, 26 insertions(+) diff --git a/server/crates/arbiter-server/src/evm/mod.rs b/server/crates/arbiter-server/src/evm/mod.rs index 15ac999..fe603db 100644 --- a/server/crates/arbiter-server/src/evm/mod.rs +++ b/server/crates/arbiter-server/src/evm/mod.rs @@ -394,6 +394,7 @@ mod tests { chain: CHAIN_ID, valid_from: None, valid_until: None, + revoked_at: None, max_gas_fee_per_gas: None, max_priority_fee_per_gas: None, rate_limit: None, @@ -596,4 +597,24 @@ mod tests { assert!(violations.is_empty()); } } + + #[test] + fn shared_settings_hash_changes_when_revoked_at_changes() { + use arbiter_crypto::hashing::Hashable; + use sha2::Digest; + + let active = shared_settings(); + let revoked = SharedGrantSettings { + revoked_at: Some(Utc::now()), + ..shared_settings() + }; + + let mut active_hash = sha2::Sha256::new(); + active.hash(&mut active_hash); + + let mut revoked_hash = sha2::Sha256::new(); + revoked.hash(&mut revoked_hash); + + assert_ne!(active_hash.finalize(), revoked_hash.finalize()); + } } diff --git a/server/crates/arbiter-server/src/evm/policies.rs b/server/crates/arbiter-server/src/evm/policies.rs index 828c52e..984caef 100644 --- a/server/crates/arbiter-server/src/evm/policies.rs +++ b/server/crates/arbiter-server/src/evm/policies.rs @@ -146,6 +146,7 @@ pub struct SharedGrantSettings { pub valid_from: Option>, pub valid_until: Option>, + pub revoked_at: Option>, pub max_gas_fee_per_gas: Option, pub max_priority_fee_per_gas: Option, @@ -160,6 +161,7 @@ impl SharedGrantSettings { chain: model.chain_id as u64, // safe because chain_id is stored as i32 but is guaranteed to be a valid ChainId by the API when creating grants valid_from: model.valid_from.map(Into::into), valid_until: model.valid_until.map(Into::into), + revoked_at: model.revoked_at.map(Into::into), max_gas_fee_per_gas: model .max_gas_fee_per_gas .map(|b| utils::try_bytes_to_u256(&b)) 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 5253a25..22cafb0 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 @@ -78,6 +78,7 @@ fn shared() -> SharedGrantSettings { chain: CHAIN_ID, valid_from: None, valid_until: None, + revoked_at: None, max_gas_fee_per_gas: None, max_priority_fee_per_gas: None, rate_limit: None, 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 c059b0b..790b4df 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 @@ -95,6 +95,7 @@ fn shared() -> SharedGrantSettings { chain: CHAIN_ID, valid_from: None, valid_until: None, + revoked_at: None, max_gas_fee_per_gas: None, max_priority_fee_per_gas: None, rate_limit: None, diff --git a/server/crates/arbiter-server/src/grpc/user_agent/inbound.rs b/server/crates/arbiter-server/src/grpc/user_agent/inbound.rs index 6cfb2e5..52fa516 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/inbound.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/inbound.rs @@ -87,6 +87,7 @@ impl TryConvert for ProtoSharedSettings { .valid_until .map(ProtoTimestamp::try_convert) .transpose()?, + revoked_at: None, max_gas_fee_per_gas: self .max_gas_fee_per_gas .as_deref()