Compare commits
1 Commits
4bb2c062dc
...
check-uac-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e1ab51398 |
@@ -90,7 +90,6 @@ message EvmGrantCreateResponse {
|
|||||||
|
|
||||||
message EvmGrantDeleteRequest {
|
message EvmGrantDeleteRequest {
|
||||||
int32 grant_id = 1;
|
int32 grant_id = 1;
|
||||||
int32 wallet_access_id = 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message EvmGrantDeleteResponse {
|
message EvmGrantDeleteResponse {
|
||||||
|
|||||||
@@ -158,15 +158,28 @@ impl EvmActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn useragent_delete_grant(
|
pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> {
|
||||||
&mut self,
|
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
grant_id: i32,
|
// let keyholder = self.keyholder.clone();
|
||||||
wallet_access_id: i32,
|
|
||||||
) -> Result<(), Error> {
|
// diesel_async::AsyncConnection::transaction(&mut conn, |conn| {
|
||||||
self.engine
|
// Box::pin(async move {
|
||||||
.revoke_grant(grant_id, wallet_access_id)
|
// diesel::update(schema::evm_basic_grant::table)
|
||||||
.await
|
// .filter(schema::evm_basic_grant::id.eq(grant_id))
|
||||||
.map_err(Error::from)
|
// .set(schema::evm_basic_grant::revoked_at.eq(SqliteTimestamp::now()))
|
||||||
|
// .execute(conn)
|
||||||
|
// .await?;
|
||||||
|
|
||||||
|
// let signed = integrity::evm::load_signed_grant_by_basic_id(conn, grant_id).await?;
|
||||||
|
|
||||||
|
// diesel::result::QueryResult::Ok(())
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// .await
|
||||||
|
// .map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
|
// Ok(())
|
||||||
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
|
|||||||
@@ -360,13 +360,12 @@ impl UserAgentSession {
|
|||||||
pub(crate) async fn handle_grant_delete(
|
pub(crate) async fn handle_grant_delete(
|
||||||
&mut self,
|
&mut self,
|
||||||
grant_id: i32,
|
grant_id: i32,
|
||||||
wallet_access_id: i32,
|
|
||||||
) -> Result<(), GrantMutationError> {
|
) -> Result<(), GrantMutationError> {
|
||||||
// match self
|
// match self
|
||||||
// .props
|
// .props
|
||||||
// .actors
|
// .actors
|
||||||
// .evm
|
// .evm
|
||||||
// .ask(UseragentDeleteGrant { grant_id, wallet_access_id })
|
// .ask(UseragentDeleteGrant { grant_id })
|
||||||
// .await
|
// .await
|
||||||
// {
|
// {
|
||||||
// Ok(()) => Ok(()),
|
// Ok(()) => Ok(()),
|
||||||
@@ -375,7 +374,7 @@ impl UserAgentSession {
|
|||||||
// Err(GrantMutationError::Internal)
|
// Err(GrantMutationError::Internal)
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
let _ = (grant_id, wallet_access_id);
|
let _ = grant_id;
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
pub mod abi;
|
pub mod abi;
|
||||||
pub mod safe_signer;
|
pub mod safe_signer;
|
||||||
|
|
||||||
use alloy::primitives::Address;
|
|
||||||
use alloy::{
|
use alloy::{
|
||||||
consensus::TxEip1559,
|
consensus::TxEip1559,
|
||||||
primitives::{TxKind, U256},
|
primitives::{TxKind, U256},
|
||||||
};
|
};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use diesel::{
|
use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite};
|
||||||
ExpressionMethods as _, OptionalExtension, QueryDsl as _, QueryResult, SelectableHelper,
|
|
||||||
insert_into, sqlite::Sqlite, update,
|
|
||||||
};
|
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
use kameo::actor::ActorRef;
|
use kameo::actor::ActorRef;
|
||||||
|
|
||||||
@@ -20,16 +16,14 @@ use crate::{
|
|||||||
db::{
|
db::{
|
||||||
self, DatabaseError,
|
self, DatabaseError,
|
||||||
models::{
|
models::{
|
||||||
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget,
|
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
|
||||||
EvmEtherTransferLimit, EvmTokenTransferGrant, EvmTokenTransferVolumeLimit,
|
|
||||||
EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
|
|
||||||
},
|
},
|
||||||
schema::{self, evm_transaction_log},
|
schema::{self, evm_transaction_log},
|
||||||
},
|
},
|
||||||
evm::policies::{
|
evm::policies::{
|
||||||
CombinedSettings, DatabaseID, EvalContext, EvalViolation, Grant, Policy,
|
CombinedSettings, DatabaseID, EvalContext, EvalViolation, Grant, Policy,
|
||||||
SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
|
SharedGrantSettings, SpecificGrant, SpecificMeaning, ether_transfer::EtherTransfer,
|
||||||
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
token_transfers::TokenTransfer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -276,156 +270,6 @@ impl Engine {
|
|||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn revoke_grant(
|
|
||||||
&self,
|
|
||||||
basic_grant_id: i32,
|
|
||||||
wallet_access_id: i32,
|
|
||||||
) -> Result<(), DatabaseError> {
|
|
||||||
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
|
||||||
let keyholder = self.keyholder.clone();
|
|
||||||
|
|
||||||
conn.transaction(|conn| {
|
|
||||||
Box::pin(async move {
|
|
||||||
use crate::db::schema::{
|
|
||||||
evm_basic_grant, evm_ether_transfer_grant, evm_ether_transfer_grant_target,
|
|
||||||
evm_ether_transfer_limit, evm_token_transfer_grant,
|
|
||||||
evm_token_transfer_volume_limit,
|
|
||||||
};
|
|
||||||
|
|
||||||
update(evm_basic_grant::table)
|
|
||||||
.filter(evm_basic_grant::id.eq(basic_grant_id))
|
|
||||||
.filter(evm_basic_grant::wallet_access_id.eq(wallet_access_id))
|
|
||||||
.set(evm_basic_grant::revoked_at.eq(SqliteTimestamp(Utc::now())))
|
|
||||||
.execute(conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let basic_grant: EvmBasicGrant = evm_basic_grant::table
|
|
||||||
.filter(evm_basic_grant::id.eq(basic_grant_id))
|
|
||||||
.filter(evm_basic_grant::wallet_access_id.eq(wallet_access_id))
|
|
||||||
.select(EvmBasicGrant::as_select())
|
|
||||||
.first(conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let shared = SharedGrantSettings::try_from_model(basic_grant)?;
|
|
||||||
|
|
||||||
if let Some(ether_grant) = evm_ether_transfer_grant::table
|
|
||||||
.filter(evm_ether_transfer_grant::basic_grant_id.eq(basic_grant_id))
|
|
||||||
.select(EvmEtherTransferGrant::as_select())
|
|
||||||
.first(conn)
|
|
||||||
.await
|
|
||||||
.optional()?
|
|
||||||
{
|
|
||||||
let target_rows: Vec<EvmEtherTransferGrantTarget> =
|
|
||||||
evm_ether_transfer_grant_target::table
|
|
||||||
.filter(evm_ether_transfer_grant_target::grant_id.eq(ether_grant.id))
|
|
||||||
.select(EvmEtherTransferGrantTarget::as_select())
|
|
||||||
.load(conn)
|
|
||||||
.await?;
|
|
||||||
let targets: Vec<Address> = target_rows
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|target| {
|
|
||||||
let arr: [u8; 20] = target.address.try_into().ok()?;
|
|
||||||
Some(Address::from(arr))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let limit: EvmEtherTransferLimit = evm_ether_transfer_limit::table
|
|
||||||
.filter(evm_ether_transfer_limit::id.eq(ether_grant.limit_id))
|
|
||||||
.select(EvmEtherTransferLimit::as_select())
|
|
||||||
.first(conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let settings = CombinedSettings {
|
|
||||||
shared: shared.clone(),
|
|
||||||
specific: crate::evm::policies::ether_transfer::Settings {
|
|
||||||
target: targets,
|
|
||||||
limit: VolumeRateLimit {
|
|
||||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(
|
|
||||||
|err| {
|
|
||||||
diesel::result::Error::DeserializationError(Box::new(err))
|
|
||||||
},
|
|
||||||
)?,
|
|
||||||
window: chrono::Duration::seconds(limit.window_secs as i64),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
integrity::sign_entity(conn, &keyholder, &settings, basic_grant_id)
|
|
||||||
.await
|
|
||||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
|
||||||
|
|
||||||
return QueryResult::Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(token_grant) = evm_token_transfer_grant::table
|
|
||||||
.filter(evm_token_transfer_grant::basic_grant_id.eq(basic_grant_id))
|
|
||||||
.select(EvmTokenTransferGrant::as_select())
|
|
||||||
.first(conn)
|
|
||||||
.await
|
|
||||||
.optional()?
|
|
||||||
{
|
|
||||||
let volume_limit_rows: Vec<EvmTokenTransferVolumeLimit> =
|
|
||||||
evm_token_transfer_volume_limit::table
|
|
||||||
.filter(evm_token_transfer_volume_limit::grant_id.eq(token_grant.id))
|
|
||||||
.select(EvmTokenTransferVolumeLimit::as_select())
|
|
||||||
.load(conn)
|
|
||||||
.await?;
|
|
||||||
let volume_limits: Vec<VolumeRateLimit> = volume_limit_rows
|
|
||||||
.into_iter()
|
|
||||||
.map(|row| {
|
|
||||||
Ok(VolumeRateLimit {
|
|
||||||
max_volume: utils::try_bytes_to_u256(&row.max_volume).map_err(
|
|
||||||
|err| {
|
|
||||||
diesel::result::Error::DeserializationError(Box::new(err))
|
|
||||||
},
|
|
||||||
)?,
|
|
||||||
window: chrono::Duration::seconds(row.window_secs as i64),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<QueryResult<Vec<_>>>()?;
|
|
||||||
|
|
||||||
let target: Option<Address> = match token_grant.receiver {
|
|
||||||
None => None,
|
|
||||||
Some(bytes) => {
|
|
||||||
let arr: [u8; 20] = bytes.try_into().map_err(|_| {
|
|
||||||
diesel::result::Error::DeserializationError(
|
|
||||||
"Invalid receiver address length".into(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
Some(Address::from(arr))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let token_contract: [u8; 20] =
|
|
||||||
token_grant.token_contract.clone().try_into().map_err(|_| {
|
|
||||||
diesel::result::Error::DeserializationError(
|
|
||||||
"Invalid token contract address length".into(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let settings = CombinedSettings {
|
|
||||||
shared,
|
|
||||||
specific: crate::evm::policies::token_transfers::Settings {
|
|
||||||
token_contract: Address::from(token_contract),
|
|
||||||
target,
|
|
||||||
volume_limits,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
integrity::sign_entity(conn, &keyholder, &settings, basic_grant_id)
|
|
||||||
.await
|
|
||||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
|
||||||
|
|
||||||
return QueryResult::Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(diesel::result::Error::NotFound)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(DatabaseError::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_one_kind<Kind: Policy, Y>(
|
async fn list_one_kind<Kind: Policy, Y>(
|
||||||
&self,
|
&self,
|
||||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
@@ -505,15 +349,11 @@ impl Engine {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use alloy::primitives::{Address, Bytes, U256, address};
|
use alloy::primitives::{Address, Bytes, U256, address};
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use diesel::{SelectableHelper, insert_into};
|
use diesel::{SelectableHelper, insert_into};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use kameo::{actor::ActorRef, prelude::Spawn};
|
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::actors::keyholder::{Bootstrap, KeyHolder};
|
|
||||||
use crate::crypto::integrity;
|
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
self, DatabaseConnection,
|
self, DatabaseConnection,
|
||||||
models::{
|
models::{
|
||||||
@@ -521,10 +361,8 @@ mod tests {
|
|||||||
},
|
},
|
||||||
schema::{evm_basic_grant, evm_transaction_log},
|
schema::{evm_basic_grant, evm_transaction_log},
|
||||||
};
|
};
|
||||||
use crate::evm::policies::ether_transfer::EtherTransfer;
|
|
||||||
use crate::evm::policies::{
|
use crate::evm::policies::{
|
||||||
CombinedSettings, EvalContext, EvalViolation, Policy, SharedGrantSettings,
|
EvalContext, EvalViolation, SharedGrantSettings, TransactionRateLimit,
|
||||||
TransactionRateLimit, VolumeRateLimit,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::check_shared_constraints;
|
use super::check_shared_constraints;
|
||||||
@@ -556,7 +394,6 @@ mod tests {
|
|||||||
chain: CHAIN_ID,
|
chain: CHAIN_ID,
|
||||||
valid_from: None,
|
valid_from: None,
|
||||||
valid_until: None,
|
valid_until: None,
|
||||||
revoked_at: None,
|
|
||||||
max_gas_fee_per_gas: None,
|
max_gas_fee_per_gas: None,
|
||||||
max_priority_fee_per_gas: None,
|
max_priority_fee_per_gas: None,
|
||||||
rate_limit: None,
|
rate_limit: None,
|
||||||
@@ -759,111 +596,4 @@ mod tests {
|
|||||||
assert!(violations.is_empty());
|
assert!(violations.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn bootstrapped_keyholder(db: &db::DatabasePool) -> ActorRef<KeyHolder> {
|
|
||||||
let actor = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
|
||||||
actor
|
|
||||||
.ask(Bootstrap {
|
|
||||||
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
actor
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn revoke_grant_preserves_revoked_integrity() {
|
|
||||||
use crate::db::schema::evm_basic_grant;
|
|
||||||
use diesel::ExpressionMethods as _;
|
|
||||||
|
|
||||||
let db = db::create_test_pool().await;
|
|
||||||
let keyholder = bootstrapped_keyholder(&db).await;
|
|
||||||
let engine = super::Engine::new(db.clone(), keyholder.clone());
|
|
||||||
|
|
||||||
let full_grant = CombinedSettings {
|
|
||||||
shared: SharedGrantSettings {
|
|
||||||
wallet_access_id: WALLET_ACCESS_ID,
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
specific: crate::evm::policies::ether_transfer::Settings {
|
|
||||||
target: vec![RECIPIENT],
|
|
||||||
limit: VolumeRateLimit {
|
|
||||||
max_volume: U256::from(100u64),
|
|
||||||
window: Duration::hours(1),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let grant_id = engine
|
|
||||||
.create_grant::<EtherTransfer>(full_grant)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
engine.revoke_grant(grant_id, WALLET_ACCESS_ID).await.unwrap();
|
|
||||||
|
|
||||||
let mut conn = db.get().await.unwrap();
|
|
||||||
diesel::update(evm_basic_grant::table)
|
|
||||||
.filter(evm_basic_grant::id.eq(grant_id))
|
|
||||||
.set(evm_basic_grant::revoked_at.eq::<Option<SqliteTimestamp>>(None))
|
|
||||||
.execute(&mut conn)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let wallet_access = EvmWalletAccess {
|
|
||||||
id: WALLET_ACCESS_ID,
|
|
||||||
wallet_id: 10,
|
|
||||||
client_id: 20,
|
|
||||||
created_at: SqliteTimestamp(Utc::now()),
|
|
||||||
};
|
|
||||||
let context = EvalContext {
|
|
||||||
target: wallet_access,
|
|
||||||
chain: CHAIN_ID,
|
|
||||||
to: RECIPIENT,
|
|
||||||
value: U256::ONE,
|
|
||||||
calldata: Bytes::new(),
|
|
||||||
max_fee_per_gas: 1,
|
|
||||||
max_priority_fee_per_gas: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let grant = crate::evm::policies::ether_transfer::EtherTransfer::try_find_grant(
|
|
||||||
&context, &mut conn,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let result =
|
|
||||||
integrity::verify_entity(&mut conn, &keyholder, &grant.settings, grant.id).await;
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
result,
|
|
||||||
Err(crate::crypto::integrity::Error::MacMismatch { .. })
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,7 +146,6 @@ pub struct SharedGrantSettings {
|
|||||||
|
|
||||||
pub valid_from: Option<DateTime<Utc>>,
|
pub valid_from: Option<DateTime<Utc>>,
|
||||||
pub valid_until: Option<DateTime<Utc>>,
|
pub valid_until: Option<DateTime<Utc>>,
|
||||||
pub revoked_at: Option<DateTime<Utc>>,
|
|
||||||
|
|
||||||
pub max_gas_fee_per_gas: Option<U256>,
|
pub max_gas_fee_per_gas: Option<U256>,
|
||||||
pub max_priority_fee_per_gas: Option<U256>,
|
pub max_priority_fee_per_gas: Option<U256>,
|
||||||
@@ -161,7 +160,6 @@ 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
|
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_from: model.valid_from.map(Into::into),
|
||||||
valid_until: model.valid_until.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: model
|
||||||
.max_gas_fee_per_gas
|
.max_gas_fee_per_gas
|
||||||
.map(|b| utils::try_bytes_to_u256(&b))
|
.map(|b| utils::try_bytes_to_u256(&b))
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ fn shared() -> SharedGrantSettings {
|
|||||||
chain: CHAIN_ID,
|
chain: CHAIN_ID,
|
||||||
valid_from: None,
|
valid_from: None,
|
||||||
valid_until: None,
|
valid_until: None,
|
||||||
revoked_at: None,
|
|
||||||
max_gas_fee_per_gas: None,
|
max_gas_fee_per_gas: None,
|
||||||
max_priority_fee_per_gas: None,
|
max_priority_fee_per_gas: None,
|
||||||
rate_limit: None,
|
rate_limit: None,
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ fn shared() -> SharedGrantSettings {
|
|||||||
chain: CHAIN_ID,
|
chain: CHAIN_ID,
|
||||||
valid_from: None,
|
valid_from: None,
|
||||||
valid_until: None,
|
valid_until: None,
|
||||||
revoked_at: None,
|
|
||||||
max_gas_fee_per_gas: None,
|
max_gas_fee_per_gas: None,
|
||||||
max_priority_fee_per_gas: None,
|
max_priority_fee_per_gas: None,
|
||||||
rate_limit: None,
|
rate_limit: None,
|
||||||
|
|||||||
@@ -170,7 +170,6 @@ async fn handle_grant_delete(
|
|||||||
let result = match actor
|
let result = match actor
|
||||||
.ask(HandleGrantDelete {
|
.ask(HandleGrantDelete {
|
||||||
grant_id: req.grant_id,
|
grant_id: req.grant_id,
|
||||||
wallet_access_id: req.wallet_access_id,
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ impl TryConvert for ProtoSharedSettings {
|
|||||||
.valid_until
|
.valid_until
|
||||||
.map(ProtoTimestamp::try_convert)
|
.map(ProtoTimestamp::try_convert)
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
revoked_at: None,
|
|
||||||
max_gas_fee_per_gas: self
|
max_gas_fee_per_gas: self
|
||||||
.max_gas_fee_per_gas
|
.max_gas_fee_per_gas
|
||||||
.as_deref()
|
.as_deref()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
|||||||
import 'package:arbiter/features/connection/connection.dart';
|
import 'package:arbiter/features/connection/connection.dart';
|
||||||
import 'package:arbiter/features/connection/server_info_storage.dart';
|
import 'package:arbiter/features/connection/server_info_storage.dart';
|
||||||
import 'package:arbiter/features/identity/pk_manager.dart';
|
import 'package:arbiter/features/identity/pk_manager.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
|
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
|
||||||
import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth;
|
import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth;
|
||||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
@@ -45,6 +46,18 @@ class ConnectionException implements Exception {
|
|||||||
String toString() => message;
|
String toString() => message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String certificateFingerprintHex(List<int> derBytes) {
|
||||||
|
return sha256.convert(derBytes).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPinnedServerCertificate({
|
||||||
|
required String expectedFingerprint,
|
||||||
|
required List<int> certificateDer,
|
||||||
|
}) {
|
||||||
|
return certificateFingerprintHex(certificateDer) ==
|
||||||
|
expectedFingerprint.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
Future<Connection> connectAndAuthorize(
|
Future<Connection> connectAndAuthorize(
|
||||||
StoredServerInfo serverInfo,
|
StoredServerInfo serverInfo,
|
||||||
KeyHandle key, {
|
KeyHandle key, {
|
||||||
@@ -155,7 +168,12 @@ Future<Connection> _connect(StoredServerInfo serverInfo) async {
|
|||||||
connectTimeout: const Duration(seconds: 10),
|
connectTimeout: const Duration(seconds: 10),
|
||||||
credentials: ChannelCredentials.secure(
|
credentials: ChannelCredentials.secure(
|
||||||
onBadCertificate: (cert, host) {
|
onBadCertificate: (cert, host) {
|
||||||
return true;
|
final isExpectedHost = host == serverInfo.address;
|
||||||
|
final isPinnedCert = isPinnedServerCertificate(
|
||||||
|
expectedFingerprint: serverInfo.caCertFingerprint,
|
||||||
|
certificateDer: cert.der,
|
||||||
|
);
|
||||||
|
return isExpectedHost && isPinnedCert;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:arbiter/features/connection/auth.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('certificate pinning helpers', () {
|
||||||
|
test('certificateFingerprintHex returns SHA-256 in hex', () {
|
||||||
|
final fingerprint = certificateFingerprintHex('abc'.codeUnits);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
fingerprint,
|
||||||
|
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isPinnedServerCertificate matches expected fingerprint', () {
|
||||||
|
final matches = isPinnedServerCertificate(
|
||||||
|
expectedFingerprint:
|
||||||
|
'BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD',
|
||||||
|
certificateDer: 'abc'.codeUnits,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(matches, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isPinnedServerCertificate rejects mismatched fingerprint', () {
|
||||||
|
final matches = isPinnedServerCertificate(
|
||||||
|
expectedFingerprint:
|
||||||
|
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||||
|
certificateDer: 'abc'.codeUnits,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(matches, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user