Compare commits

..

1 Commits

Author SHA1 Message Date
CleverWild
a30bef11da fix: terminate VaultGate connection after vault lockout
Some checks failed
ci/woodpecker/pr/server-audit Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline was successful
2026-06-30 20:07:01 +02:00
20 changed files with 163 additions and 190 deletions

View File

@@ -22,5 +22,3 @@ run = '''
dart pub global activate protoc_plugin && \ dart pub global activate protoc_plugin && \
protoc --dart_out=grpc:useragent/lib/proto --proto_path=protobufs/ $(find protobufs -name '*.proto' | sort) protoc --dart_out=grpc:useragent/lib/proto --proto_path=protobufs/ $(find protobufs -name '*.proto' | sort)
''' '''
[tasks.generate_schema]

View File

@@ -43,24 +43,13 @@ create table if not exists arbiter_settings (
insert into arbiter_settings (id) values (1) on conflict do nothing; insert into arbiter_settings (id) values (1) on conflict do nothing;
-- ensure singleton row exists -- ensure singleton row exists
create table if not exists operator_identity ( create table if not exists operator_client (
id integer not null primary key, id integer not null primary key,
public_key blob not null, public_key blob not null,
created_at integer not null default(unixepoch ('now')), created_at integer not null default(unixepoch ('now')),
updated_at integer not null default(unixepoch ('now')) updated_at integer not null default(unixepoch ('now'))
) STRICT; ) STRICT;
create unique index if not exists uniq_operator_identity_public_key on operator_identity (public_key); create unique index if not exists uniq_operator_client_public_key on operator_client (public_key);
create table if not exists operator (
id integer primary key references operator_identity(id) on delete restrict, -- same id as operator_identity
share blob not null,
share_nonce blob not null,
created_at integer not null default(unixepoch ('now')),
updated_at integer not null default(unixepoch ('now'))
) STRICT;
create table if not exists client_metadata ( create table if not exists client_metadata (
id integer not null primary key, id integer not null primary key,

View File

@@ -68,7 +68,7 @@ impl Bootstrapper {
let row_count: i64 = { let row_count: i64 = {
let mut conn = db.get().await?; let mut conn = db.get().await?;
schema::operator::table schema::operator_client::table
.count() .count()
.get_result(&mut conn) .get_result(&mut conn)
.await? .await?

View File

@@ -3,7 +3,7 @@ use crate::{
crypto::integrity::{self, Integrable}, crypto::integrity::{self, Integrable},
db::{ db::{
DatabaseError, DatabasePool, DatabaseError, DatabasePool,
models::{self, EvmWalletId}, models::{self},
schema, schema,
}, },
evm::{ evm::{
@@ -162,7 +162,7 @@ impl EvmActor {
} }
#[message] #[message]
pub async fn list_wallets(&self) -> Result<Vec<(EvmWalletId, Address)>, Error> { pub async fn list_wallets(&self) -> Result<Vec<(i32, Address)>, Error> {
let mut conn = self.db.get().await.map_err(DatabaseError::from)?; let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
let rows: Vec<models::EvmWallet> = schema::evm_wallet::table let rows: Vec<models::EvmWallet> = schema::evm_wallet::table
.select(models::EvmWallet::as_select()) .select(models::EvmWallet::as_select())
@@ -295,18 +295,18 @@ impl EvmActor {
address: wallet_address, address: wallet_address,
aead_encrypted_id: wallet.aead_encrypted_id, aead_encrypted_id: wallet.aead_encrypted_id,
}, },
wallet.id.to_raw(), wallet.id,
) )
.await .await
.map_err(|e| { .map_err(|e| {
error!(?e, ?wallet.id, "EVM wallet integrity check failed"); error!(?e, wallet_id = wallet.id, "EVM wallet integrity check failed");
SignTransactionError::Internal SignTransactionError::Internal
})?; })?;
drop(conn); drop(conn);
if attestation != integrity::AttestationStatus::Attested { if attestation != integrity::AttestationStatus::Attested {
error!( error!(
?wallet.id, wallet_id = wallet.id,
"EVM wallet integrity unavailable; refusing to sign" "EVM wallet integrity unavailable; refusing to sign"
); );
return Err(SignTransactionError::Internal); return Err(SignTransactionError::Internal);

View File

@@ -6,7 +6,7 @@ use crate::{
}, },
db::{ db::{
self, self,
models::{self, RootKeyHistory, RootKeyHistoryId}, models::{self, RootKeyHistory},
schema::{self}, schema::{self},
}, },
}; };
@@ -25,6 +25,7 @@ use strum::{EnumDiscriminants, IntoDiscriminant};
use tracing::{error, info, warn}; use tracing::{error, info, warn};
pub mod events { pub mod events {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct Bootstrapped; pub struct Bootstrapped;
@@ -63,12 +64,12 @@ pub enum Error {
#[error("Broken database")] #[error("Broken database")]
BrokenDatabase, BrokenDatabase,
#[error("Integrity key version mismatch: envelope uses key {envelope:?}, current key is {current:?}")] #[error("Integrity key version mismatch: envelope uses key {envelope}, current key is {current}")]
KeyVersionMismatch { envelope: RootKeyHistoryId, current: RootKeyHistoryId }, KeyVersionMismatch { envelope: i32, current: i32 },
} }
struct Unsealed { struct Unsealed {
root_key_history_id: RootKeyHistoryId, root_key_history_id: i32,
root_key: KeyCell, root_key: KeyCell,
} }
@@ -77,9 +78,8 @@ struct Unsealed {
enum State { enum State {
#[default] #[default]
Unbootstrapped, Unbootstrapped,
Sealed { Sealed {
root_key_history_id: RootKeyHistoryId, root_key_history_id: i32,
}, },
Unsealed(Unsealed), Unsealed(Unsealed),
} }
@@ -123,10 +123,7 @@ impl Vault {
// Exclusive transaction to avoid race condtions if multiple vaults write // Exclusive transaction to avoid race condtions if multiple vaults write
// additional layer of protection against nonce-reuse // additional layer of protection against nonce-reuse
async fn get_new_nonce( async fn get_new_nonce(pool: &db::DatabasePool, root_key_id: i32) -> Result<Nonce, Error> {
pool: &db::DatabasePool,
root_key_id: RootKeyHistoryId,
) -> Result<Nonce, Error> {
let mut conn = pool.get().await?; let mut conn = pool.get().await?;
let nonce = conn let nonce = conn
@@ -139,7 +136,7 @@ impl Vault {
let mut nonce = Nonce::try_from(current_nonce.as_slice()).map_err(|()| { let mut nonce = Nonce::try_from(current_nonce.as_slice()).map_err(|()| {
error!( error!(
"Broken database: invalid nonce for root key history id={:#?}", "Broken database: invalid nonce for root key history id={}",
root_key_id root_key_id
); );
Error::BrokenDatabase Error::BrokenDatabase
@@ -195,7 +192,7 @@ impl Vault {
let data_encryption_nonce_bytes = data_encryption_nonce.to_vec(); let data_encryption_nonce_bytes = data_encryption_nonce.to_vec();
let root_key_history_id = conn let root_key_history_id = conn
.transaction(async |conn| { .transaction(async |conn| {
let root_key_history_id = insert_into(schema::root_key_history::table) let root_key_history_id: i32 = insert_into(schema::root_key_history::table)
.values(&models::NewRootKeyHistory { .values(&models::NewRootKeyHistory {
ciphertext: root_key_ciphertext.clone(), ciphertext: root_key_ciphertext.clone(),
tag: v1::ROOT_KEY_TAG.to_vec(), tag: v1::ROOT_KEY_TAG.to_vec(),
@@ -213,9 +210,7 @@ impl Vault {
.execute(&mut *conn) .execute(&mut *conn)
.await?; .await?;
Result::<_, diesel::result::Error>::Ok(RootKeyHistoryId::from_raw( Result::<_, diesel::result::Error>::Ok(root_key_history_id)
root_key_history_id,
))
}) })
.await?; .await?;
@@ -375,10 +370,7 @@ impl Vault {
} }
#[message] #[message]
pub fn sign_integrity( pub fn sign_integrity(&mut self, mac_input: Vec<u8>) -> Result<(i32, Vec<u8>), Error> {
&mut self,
mac_input: Vec<u8>,
) -> Result<(RootKeyHistoryId, Vec<u8>), Error> {
let Unsealed { let Unsealed {
root_key, root_key,
root_key_history_id, root_key_history_id,
@@ -388,7 +380,7 @@ impl Vault {
HmacSha256::new_from_slice(k) HmacSha256::new_from_slice(k)
.unwrap_or_else(|_| unreachable!("HMAC accepts keys of any size")) .unwrap_or_else(|_| unreachable!("HMAC accepts keys of any size"))
}); });
hmac.update(&root_key_history_id.to_raw().to_be_bytes()); hmac.update(&root_key_history_id.to_be_bytes());
hmac.update(&mac_input); hmac.update(&mac_input);
let mac = hmac.finalize().into_bytes().to_vec(); let mac = hmac.finalize().into_bytes().to_vec();
@@ -400,7 +392,7 @@ impl Vault {
&mut self, &mut self,
mac_input: Vec<u8>, mac_input: Vec<u8>,
expected_mac: Vec<u8>, expected_mac: Vec<u8>,
key_version: RootKeyHistoryId, key_version: i32,
) -> Result<bool, Error> { ) -> Result<bool, Error> {
let Unsealed { let Unsealed {
root_key, root_key,
@@ -418,7 +410,7 @@ impl Vault {
HmacSha256::new_from_slice(k) HmacSha256::new_from_slice(k)
.unwrap_or_else(|_| unreachable!("HMAC accepts keys of any size")) .unwrap_or_else(|_| unreachable!("HMAC accepts keys of any size"))
}); });
hmac.update(&key_version.to_raw().to_be_bytes()); hmac.update(&key_version.to_be_bytes());
hmac.update(&mac_input); hmac.update(&mac_input);
Ok(hmac.verify_slice(&expected_mac).is_ok()) Ok(hmac.verify_slice(&expected_mac).is_ok())
@@ -442,7 +434,6 @@ impl Vault {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::actors::GlobalActors; use crate::actors::GlobalActors;
use crate::db::models::RootKeyHistory;
use arbiter_crypto::safecell::SafeCellHandle as _; use arbiter_crypto::safecell::SafeCellHandle as _;
use super::*; use super::*;

View File

@@ -79,41 +79,10 @@ pub mod types {
} }
} }
macro_rules! declare_id { #[derive(Debug, FromSqlRow, AsExpression, Clone)]
($name:ident) => {
#[derive(Debug, FromSqlRow, AsExpression, Clone, Hash, Copy, PartialEq, Eq)]
#[diesel(sql_type = Integer)] #[diesel(sql_type = Integer)]
#[repr(transparent)] // hint compiler to optimize the wrapper struct away #[repr(transparent)] // hint compiler to optimize the wrapper struct away
pub struct $name(i32); pub struct ChainId(pub i32);
impl $name {
pub const fn to_raw(self) -> i32 {
self.0
}
pub const fn from_raw(raw: i32) -> Self {
Self(raw)
}
}
impl FromSql<Integer, Sqlite> for $name {
fn from_sql(
bytes: <Sqlite as diesel::backend::Backend>::RawValue<'_>,
) -> diesel::deserialize::Result<Self> {
FromSql::<Integer, Sqlite>::from_sql(bytes).map(Self)
}
}
impl ToSql<Integer, Sqlite> for $name {
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
) -> diesel::serialize::Result {
ToSql::<Integer, Sqlite>::to_sql(&self.0, out)
}
}
};
}
declare_id!(ChainId);
#[expect( #[expect(
clippy::cast_sign_loss, clippy::cast_sign_loss,
@@ -134,13 +103,21 @@ pub mod types {
} }
}; };
declare_id!(OperatorId); impl FromSql<Integer, Sqlite> for ChainId {
declare_id!(OperatorIdentityId); fn from_sql(
declare_id!(AeadEncryptedId); bytes: <Sqlite as diesel::backend::Backend>::RawValue<'_>,
declare_id!(RootKeyHistoryId); ) -> diesel::deserialize::Result<Self> {
declare_id!(TlsHistoryId); FromSql::<Integer, Sqlite>::from_sql(bytes).map(Self)
declare_id!(EvmWalletId); }
declare_id!(ClientId); }
impl ToSql<Integer, Sqlite> for ChainId {
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
) -> diesel::serialize::Result {
ToSql::<Integer, Sqlite>::to_sql(&self.0, out)
}
}
} }
pub use types::*; pub use types::*;
@@ -153,12 +130,12 @@ pub use types::*;
)] )]
#[diesel(table_name = aead_encrypted, check_for_backend(Sqlite))] #[diesel(table_name = aead_encrypted, check_for_backend(Sqlite))]
pub struct AeadEncrypted { pub struct AeadEncrypted {
pub id: AeadEncryptedId, pub id: i32,
pub ciphertext: Vec<u8>, pub ciphertext: Vec<u8>,
pub tag: Vec<u8>, pub tag: Vec<u8>,
pub current_nonce: Vec<u8>, pub current_nonce: Vec<u8>,
pub schema_version: i32, pub schema_version: i32,
pub associated_root_key_id: RootKeyHistoryId, pub associated_root_key_id: i32, // references root_key_history.id
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
} }
@@ -171,7 +148,7 @@ pub struct AeadEncrypted {
attributes_with = "deriveless" attributes_with = "deriveless"
)] )]
pub struct RootKeyHistory { pub struct RootKeyHistory {
pub id: RootKeyHistoryId, pub id: i32,
pub ciphertext: Vec<u8>, pub ciphertext: Vec<u8>,
pub tag: Vec<u8>, pub tag: Vec<u8>,
pub root_key_encryption_nonce: Vec<u8>, pub root_key_encryption_nonce: Vec<u8>,
@@ -189,7 +166,7 @@ pub struct RootKeyHistory {
attributes_with = "deriveless" attributes_with = "deriveless"
)] )]
pub struct TlsHistory { pub struct TlsHistory {
pub id: TlsHistoryId, pub id: i32,
pub cert: String, pub cert: String,
pub cert_key: String, // PEM Encoded private key pub cert_key: String, // PEM Encoded private key
pub ca_cert: String, // PEM Encoded certificate for cert signing pub ca_cert: String, // PEM Encoded certificate for cert signing
@@ -214,7 +191,7 @@ pub struct ArbiterSettings {
attributes_with = "deriveless" attributes_with = "deriveless"
)] )]
pub struct EvmWallet { pub struct EvmWallet {
pub id: EvmWalletId, pub id: i32,
pub address: Vec<u8>, pub address: Vec<u8>,
pub aead_encrypted_id: i32, pub aead_encrypted_id: i32,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
@@ -236,7 +213,7 @@ pub struct EvmWallet {
)] )]
pub struct EvmWalletAccess { pub struct EvmWalletAccess {
pub id: i32, pub id: i32,
pub wallet_id: EvmWalletId, pub wallet_id: i32,
pub client_id: i32, pub client_id: i32,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
} }
@@ -263,7 +240,7 @@ pub struct ProgramClientMetadataHistory {
#[derive(Models, Queryable, Debug, Insertable, Selectable)] #[derive(Models, Queryable, Debug, Insertable, Selectable)]
#[diesel(table_name = schema::program_client, check_for_backend(Sqlite))] #[diesel(table_name = schema::program_client, check_for_backend(Sqlite))]
pub struct ProgramClient { pub struct ProgramClient {
pub id: ClientId, pub id: i32,
pub public_key: Vec<u8>, pub public_key: Vec<u8>,
pub metadata_id: i32, pub metadata_id: i32,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
@@ -273,22 +250,12 @@ pub struct ProgramClient {
#[derive(Queryable, Debug)] #[derive(Queryable, Debug)]
#[diesel(table_name = schema::operator_client, check_for_backend(Sqlite))] #[diesel(table_name = schema::operator_client, check_for_backend(Sqlite))]
pub struct OperatorClient { pub struct OperatorClient {
pub id: OperatorIdentityId, pub id: i32,
pub public_key: Vec<u8>, pub public_key: Vec<u8>,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
pub updated_at: SqliteTimestamp, pub updated_at: SqliteTimestamp,
} }
#[derive(Queryable, Debug)]
#[diesel(table_name = schema::operator, check_for_backend(Sqlite))]
pub struct Operator {
pub id: OperatorId,
pub share: Vec<u8>,
pub share_nonce: Vec<u8>,
pub created_at: SqliteTimestamp,
pub updated_at: SqliteTimestamp,
}
#[derive(Models, Queryable, Debug, Insertable, Selectable)] #[derive(Models, Queryable, Debug, Insertable, Selectable)]
#[diesel(table_name = evm_ether_transfer_limit, check_for_backend(Sqlite))] #[diesel(table_name = evm_ether_transfer_limit, check_for_backend(Sqlite))]
#[view( #[view(
@@ -432,7 +399,7 @@ pub struct IntegrityEnvelope {
pub entity_kind: String, pub entity_kind: String,
pub entity_id: Vec<u8>, pub entity_id: Vec<u8>,
pub payload_version: i32, pub payload_version: i32,
pub key_version: RootKeyHistoryId, pub key_version: i32,
pub mac: Vec<u8>, pub mac: Vec<u8>,
pub signed_at: SqliteTimestamp, pub signed_at: SqliteTimestamp,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,

View File

@@ -152,25 +152,6 @@ diesel::table! {
} }
} }
diesel::table! {
operator (id) {
id -> Nullable<Integer>,
share -> Binary,
share_nonce -> Binary,
created_at -> Integer,
updated_at -> Integer,
}
}
diesel::table! {
operator_identity (id) {
id -> Integer,
public_key -> Binary,
created_at -> Integer,
updated_at -> Integer,
}
}
diesel::table! { diesel::table! {
program_client (id) { program_client (id) {
id -> Integer, id -> Integer,
@@ -204,6 +185,15 @@ diesel::table! {
} }
} }
diesel::table! {
operator_client (id) {
id -> Integer,
public_key -> Binary,
created_at -> Integer,
updated_at -> Integer,
}
}
diesel::joinable!(aead_encrypted -> root_key_history (associated_root_key_id)); diesel::joinable!(aead_encrypted -> root_key_history (associated_root_key_id));
diesel::joinable!(arbiter_settings -> root_key_history (root_key_id)); diesel::joinable!(arbiter_settings -> root_key_history (root_key_id));
diesel::joinable!(arbiter_settings -> tls_history (tls_id)); diesel::joinable!(arbiter_settings -> tls_history (tls_id));
@@ -222,7 +212,6 @@ diesel::joinable!(evm_transaction_log -> evm_wallet_access (wallet_access_id));
diesel::joinable!(evm_wallet -> aead_encrypted (aead_encrypted_id)); diesel::joinable!(evm_wallet -> aead_encrypted (aead_encrypted_id));
diesel::joinable!(evm_wallet_access -> evm_wallet (wallet_id)); diesel::joinable!(evm_wallet_access -> evm_wallet (wallet_id));
diesel::joinable!(evm_wallet_access -> program_client (client_id)); diesel::joinable!(evm_wallet_access -> program_client (client_id));
diesel::joinable!(operator -> operator_identity (id));
diesel::joinable!(program_client -> client_metadata (metadata_id)); diesel::joinable!(program_client -> client_metadata (metadata_id));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
@@ -241,9 +230,8 @@ diesel::allow_tables_to_appear_in_same_query!(
evm_wallet, evm_wallet,
evm_wallet_access, evm_wallet_access,
integrity_envelope, integrity_envelope,
operator,
operator_identity,
program_client, program_client,
root_key_history, root_key_history,
tls_history, tls_history,
operator_client,
); );

View File

@@ -514,8 +514,7 @@ mod tests {
use crate::db::{ use crate::db::{
self, DatabaseConnection, self, DatabaseConnection,
models::{ models::{
EvmBasicGrant, EvmWalletAccess, EvmWalletId, NewEvmBasicGrant, NewEvmTransactionLog, EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
SqliteTimestamp,
}, },
schema::{evm_basic_grant, evm_transaction_log}, schema::{evm_basic_grant, evm_transaction_log},
}; };
@@ -535,7 +534,7 @@ mod tests {
EvalContext { EvalContext {
target: EvmWalletAccess { target: EvmWalletAccess {
id: WALLET_ACCESS_ID, id: WALLET_ACCESS_ID,
wallet_id: EvmWalletId::from_raw(5), wallet_id: 10,
client_id: 20, client_id: 20,
created_at: SqliteTimestamp(Utc::now()), created_at: SqliteTimestamp(Utc::now()),
}, },
@@ -825,7 +824,7 @@ mod tests {
let wallet_access = EvmWalletAccess { let wallet_access = EvmWalletAccess {
id: WALLET_ACCESS_ID, id: WALLET_ACCESS_ID,
wallet_id: EvmWalletId::from_raw(10), wallet_id: 10,
client_id: 20, client_id: 20,
created_at: SqliteTimestamp(Utc::now()), created_at: SqliteTimestamp(Utc::now()),
}; };

View File

@@ -3,8 +3,7 @@ use crate::{
db::{ db::{
self, DatabaseConnection, self, DatabaseConnection,
models::{ models::{
EvmBasicGrant, EvmWalletAccess, EvmWalletId, NewEvmBasicGrant, NewEvmTransactionLog, EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
SqliteTimestamp,
}, },
schema::{evm_basic_grant, evm_transaction_log}, schema::{evm_basic_grant, evm_transaction_log},
}, },
@@ -32,7 +31,7 @@ fn ctx(to: Address, value: U256) -> EvalContext {
EvalContext { EvalContext {
target: EvmWalletAccess { target: EvmWalletAccess {
id: WALLET_ACCESS_ID, id: WALLET_ACCESS_ID,
wallet_id: EvmWalletId::from_raw(10), wallet_id: 10,
client_id: 20, client_id: 20,
created_at: SqliteTimestamp(Utc::now()), created_at: SqliteTimestamp(Utc::now()),
}, },

View File

@@ -2,7 +2,7 @@ use super::{Settings, TokenTransfer};
use crate::{ use crate::{
db::{ db::{
self, DatabaseConnection, self, DatabaseConnection,
models::{EvmBasicGrant, EvmWalletAccess, EvmWalletId, NewEvmBasicGrant, SqliteTimestamp}, models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp},
schema::evm_basic_grant, schema::evm_basic_grant,
}, },
evm::{ evm::{
@@ -45,7 +45,7 @@ fn ctx(to: Address, calldata: Bytes) -> EvalContext {
EvalContext { EvalContext {
target: EvmWalletAccess { target: EvmWalletAccess {
id: WALLET_ACCESS_ID, id: WALLET_ACCESS_ID,
wallet_id: EvmWalletId::from_raw(10), wallet_id: 10,
client_id: 20, client_id: 20,
created_at: SqliteTimestamp(Utc::now()), created_at: SqliteTimestamp(Utc::now()),
}, },

View File

@@ -90,7 +90,7 @@ async fn handle_wallet_list(
.into_iter() .into_iter()
.map(|(id, address)| WalletEntry { .map(|(id, address)| WalletEntry {
address: address.to_vec(), address: address.to_vec(),
id: id.to_raw(), id,
}) })
.collect(), .collect(),
}), }),

View File

@@ -1,10 +1,11 @@
use crate::{ use crate::{
db::models::{CoreEvmWalletAccess, EvmWalletId, NewEvmWalletAccess}, db::models::{CoreEvmWalletAccess, NewEvmWalletAccess},
evm::policies::{ evm::policies::{
SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, ether_transfer, SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, ether_transfer,
token_transfers, token_transfers,
}, },
grpc::{Convert, TryConvert}, grpc::Convert,
grpc::TryConvert,
}; };
use arbiter_proto::{ use arbiter_proto::{
proto::evm::{ proto::evm::{
@@ -150,7 +151,7 @@ impl Convert for WalletAccess {
fn convert(self) -> Self::Output { fn convert(self) -> Self::Output {
NewEvmWalletAccess { NewEvmWalletAccess {
wallet_id: EvmWalletId::from_raw(self.wallet_id), wallet_id: self.wallet_id,
client_id: self.sdk_client_id, client_id: self.sdk_client_id,
} }
} }
@@ -165,7 +166,7 @@ impl TryConvert for SdkClientWalletAccess {
return Err(Status::invalid_argument("Missing wallet access entry")); return Err(Status::invalid_argument("Missing wallet access entry"));
}; };
Ok(CoreEvmWalletAccess { Ok(CoreEvmWalletAccess {
wallet_id: EvmWalletId::from_raw(access.wallet_id), wallet_id: access.wallet_id,
client_id: access.sdk_client_id, client_id: access.sdk_client_id,
id: self.id, id: self.id,
}) })

View File

@@ -103,7 +103,7 @@ impl Convert for EvmWalletAccess {
Self::Output { Self::Output {
id: self.id, id: self.id,
access: Some(WalletAccess { access: Some(WalletAccess {
wallet_id: self.wallet_id.to_raw(), wallet_id: self.wallet_id,
sdk_client_id: self.client_id, sdk_client_id: self.client_id,
}), }),
} }

View File

@@ -2,7 +2,7 @@ use crate::{
db::models::NewEvmWalletAccess, db::models::NewEvmWalletAccess,
grpc::Convert, grpc::Convert,
peers::operator::{ peers::operator::{
OperatorSession, OutOfBand, OutOfBand, OperatorSession,
session::handlers::{ session::handlers::{
HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove, HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove,
HandleRevokeEvmWalletAccess, HandleSdkClientList, HandleRevokeEvmWalletAccess, HandleSdkClientList,
@@ -11,8 +11,8 @@ use crate::{
}; };
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_proto::proto::{ use arbiter_proto::proto::{
shared::ClientInfo as ProtoClientMetadata,
operator::{ operator::{
operator_response::Payload as OperatorResponsePayload,
sdk_client::{ sdk_client::{
self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel, self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel,
ConnectionRequest as ProtoSdkClientConnectionRequest, ConnectionRequest as ProtoSdkClientConnectionRequest,
@@ -24,8 +24,8 @@ use arbiter_proto::proto::{
request::Payload as SdkClientRequestPayload, request::Payload as SdkClientRequestPayload,
response::Payload as SdkClientResponsePayload, response::Payload as SdkClientResponsePayload,
}, },
operator_response::Payload as OperatorResponsePayload,
}, },
shared::ClientInfo as ProtoClientMetadata,
}; };
use kameo::actor::ActorRef; use kameo::actor::ActorRef;
@@ -115,7 +115,7 @@ async fn handle_list(
clients: clients clients: clients
.into_iter() .into_iter()
.map(|(client, metadata)| ProtoSdkClientEntry { .map(|(client, metadata)| ProtoSdkClientEntry {
id: client.id.to_raw(), id: client.id,
pubkey: client.public_key.clone(), pubkey: client.public_key.clone(),
info: Some(ProtoClientMetadata { info: Some(ProtoClientMetadata {
name: metadata.name, name: metadata.name,

View File

@@ -4,7 +4,7 @@ use super::{
}; };
use crate::{ use crate::{
actors::bootstrap::ConsumeToken, actors::bootstrap::ConsumeToken,
db::{DatabasePool, schema::operator_identity}, db::{DatabasePool, schema::operator_client},
peers::operator::auth::Outbound, peers::operator::auth::Outbound,
}; };
use arbiter_crypto::authn::{self, AuthChallenge, OPERATOR_CONTEXT}; use arbiter_crypto::authn::{self, AuthChallenge, OPERATOR_CONTEXT};
@@ -43,9 +43,9 @@ async fn get_client_id(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<O
Error::internal("Database unavailable") Error::internal("Database unavailable")
})?; })?;
operator_identity::table operator_client::table
.filter(operator_identity::public_key.eq(pubkey.to_bytes())) .filter(operator_client::public_key.eq(pubkey.to_bytes()))
.select(operator_identity::id) .select(operator_client::id)
.first::<i32>(&mut conn) .first::<i32>(&mut conn)
.await .await
.optional() .optional()
@@ -62,9 +62,9 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
Error::internal("Database unavailable") Error::internal("Database unavailable")
})?; })?;
let id: i32 = diesel::insert_into(operator_identity::table) let id: i32 = diesel::insert_into(operator_client::table)
.values((operator_identity::public_key.eq(pubkey_bytes),)) .values((operator_client::public_key.eq(pubkey_bytes),))
.returning(operator_identity::id) .returning(operator_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
.map_err(|e| { .map_err(|e| {

View File

@@ -1,17 +1,13 @@
use super::{Error, OperatorSession}; use super::{Error, OperatorSession};
use crate::{ use crate::{
actors::{ actors::evm::{
evm::{
ClientSignTransaction, Generate, ListWallets, OperatorCreateGrant, OperatorListGrants, ClientSignTransaction, Generate, ListWallets, OperatorCreateGrant, OperatorListGrants,
SignTransactionError as EvmSignError, SignTransactionError as EvmSignError,
}, },
flow_coordinator::{IsClientConnected, client_connect_approval::ClientApprovalAnswer}, actors::flow_coordinator::{IsClientConnected, client_connect_approval::ClientApprovalAnswer},
vault::VaultState, actors::vault::VaultState,
},
db::{ db::{
models::{ models::{EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata},
EvmWalletAccess, EvmWalletId, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
},
schema::program_client, schema::program_client,
}, },
evm::policies::{Grant, SpecificGrant}, evm::policies::{Grant, SpecificGrant},
@@ -80,9 +76,7 @@ impl OperatorSession {
} }
#[message] #[message]
pub(crate) async fn handle_evm_wallet_list( pub(crate) async fn handle_evm_wallet_list(&mut self) -> Result<Vec<(i32, Address)>, Error> {
&mut self,
) -> Result<Vec<(EvmWalletId, Address)>, Error> {
match self.props.actors.evm.ask(ListWallets {}).await { match self.props.actors.evm.ask(ListWallets {}).await {
Ok(wallets) => Ok(wallets), Ok(wallets) => Ok(wallets),
Err(err) => { Err(err) => {
@@ -334,7 +328,7 @@ impl OperatorSession {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::db::{self, models::{EvmWalletId, NewEvmWalletAccess}, schema::evm_wallet_access}; use crate::db::{self, models::NewEvmWalletAccess, schema::evm_wallet_access};
use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper}; use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper};
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
@@ -354,7 +348,7 @@ mod tests {
// entry B: id will be 2, wallet_id=1, client_id=20 // entry B: id will be 2, wallet_id=1, client_id=20
let entry_a = diesel::insert_into(evm_wallet_access::table) let entry_a = diesel::insert_into(evm_wallet_access::table)
.values(NewEvmWalletAccess { .values(NewEvmWalletAccess {
wallet_id: EvmWalletId::from_raw(1), wallet_id: 1,
client_id: 10, client_id: 10,
}) })
.returning(EvmWalletAccess::as_select()) .returning(EvmWalletAccess::as_select())
@@ -364,7 +358,7 @@ mod tests {
let entry_b = diesel::insert_into(evm_wallet_access::table) let entry_b = diesel::insert_into(evm_wallet_access::table)
.values(NewEvmWalletAccess { .values(NewEvmWalletAccess {
wallet_id: EvmWalletId::from_raw(1), wallet_id: 1,
client_id: 20, client_id: 20,
}) })
.returning(EvmWalletAccess::as_select()) .returning(EvmWalletAccess::as_select())
@@ -417,7 +411,7 @@ mod tests {
for i in 1_i32..=5 { for i in 1_i32..=5 {
diesel::insert_into(evm_wallet_access::table) diesel::insert_into(evm_wallet_access::table)
.values(NewEvmWalletAccess { .values(NewEvmWalletAccess {
wallet_id: EvmWalletId::from_raw(99), wallet_id: 99,
client_id: i, client_id: i,
}) })
.execute(&mut *conn) .execute(&mut *conn)
@@ -428,7 +422,7 @@ mod tests {
// Real target: wallet_id=1, will get id=6. // Real target: wallet_id=1, will get id=6.
let target = diesel::insert_into(evm_wallet_access::table) let target = diesel::insert_into(evm_wallet_access::table)
.values(NewEvmWalletAccess { .values(NewEvmWalletAccess {
wallet_id: EvmWalletId::from_raw(1), wallet_id: 1,
client_id: 1, client_id: 1,
}) })
.returning(EvmWalletAccess::as_select()) .returning(EvmWalletAccess::as_select())
@@ -438,7 +432,7 @@ mod tests {
// Sanity: target.id != target.wallet_id // Sanity: target.id != target.wallet_id
assert_ne!( assert_ne!(
target.id, target.wallet_id.to_raw(), target.id, target.wallet_id,
"test prerequisite: id and wallet_id must differ" "test prerequisite: id and wallet_id must differ"
); );

View File

@@ -11,7 +11,7 @@ use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use state::State; use state::State;
use chacha20poly1305::{AeadInPlace, KeyInit as _, XChaCha20Poly1305, XNonce}; use chacha20poly1305::{AeadInPlace, KeyInit as _, XChaCha20Poly1305, XNonce};
use kameo::{Actor, error::SendError, messages, prelude::Message}; use kameo::{Actor, error::SendError, messages, prelude::{Context, Message}};
use kameo_actors::message_bus::Register; use kameo_actors::message_bus::Register;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tracing::{error, info}; use tracing::{error, info};
@@ -143,12 +143,13 @@ impl VaultGate {
}) })
} }
#[message] #[message(ctx)]
pub async fn handle_unseal_encrypted_key( pub async fn handle_unseal_encrypted_key(
&mut self, &mut self,
nonce: Vec<u8>, nonce: Vec<u8>,
ciphertext: Vec<u8>, ciphertext: Vec<u8>,
associated_data: Vec<u8>, associated_data: Vec<u8>,
ctx: &mut Context<Self, Result<(), Error>>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let State::ReadyForExchange { secret, .. } = &self.state else { let State::ReadyForExchange { secret, .. } = &self.state else {
return Err(Error::State); return Err(Error::State);
@@ -172,7 +173,12 @@ impl VaultGate {
Ok(()) Ok(())
} }
Err(SendError::HandlerError(vault::Error::InvalidKey)) => Err(Error::InvalidKey), Err(SendError::HandlerError(vault::Error::InvalidKey)) => Err(Error::InvalidKey),
Err(SendError::HandlerError(vault::Error::LockedOut)) => Err(Error::LockedOut), Err(SendError::HandlerError(vault::Error::LockedOut)) => {
// Vault is permanently locked — terminate this gate so the
// run_vault_gate loop breaks and the connection is closed.
ctx.stop();
Err(Error::LockedOut)
}
Err(SendError::HandlerError(err)) => { Err(SendError::HandlerError(err)) => {
error!(?err, "Vault failed to unseal key"); error!(?err, "Vault failed to unseal key");
Err(Error::InvalidKey) Err(Error::InvalidKey)
@@ -245,7 +251,7 @@ impl Message<events::Bootstrapped> for VaultGate {
async fn handle( async fn handle(
&mut self, &mut self,
_: events::Bootstrapped, _: events::Bootstrapped,
ctx: &mut kameo::prelude::Context<Self, Self::Reply>, ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply { ) -> Self::Reply {
let result = async { let result = async {
let mut conn = self let mut conn = self
@@ -281,7 +287,7 @@ impl Message<events::Unsealed> for VaultGate {
async fn handle( async fn handle(
&mut self, &mut self,
_: events::Unsealed, _: events::Unsealed,
ctx: &mut kameo::prelude::Context<Self, Self::Reply>, ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply { ) -> Self::Reply {
if let Some(tx) = self.promotion_tx.take() { if let Some(tx) = self.promotion_tx.take() {
let _ = tx.send(Ok(())); let _ = tx.send(Ok(()));

View File

@@ -86,8 +86,8 @@ async fn insert_bootstrap_sentinel_operator(db: &db::DatabasePool) {
.0 .0
.to_vec(); .to_vec();
insert_into(schema::operator_identity::table) insert_into(schema::operator_client::table)
.values((schema::operator_identity::public_key.eq(sentinel_key),)) .values((schema::operator_client::public_key.eq(sentinel_key),))
.execute(&mut conn) .execute(&mut conn)
.await .await
.unwrap(); .unwrap();

View File

@@ -206,8 +206,8 @@ pub async fn bootstrap_token_auth() {
task.await.unwrap().unwrap(); task.await.unwrap().unwrap();
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let stored_pubkey: Vec<u8> = schema::operator_identity::table let stored_pubkey: Vec<u8> = schema::operator_client::table
.select(schema::operator_identity::public_key) .select(schema::operator_client::public_key)
.first::<Vec<u8>>(&mut conn) .first::<Vec<u8>>(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -259,7 +259,7 @@ pub async fn bootstrap_invalid_token_auth() {
)); ));
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let count: i64 = schema::operator_identity::table let count: i64 = schema::operator_client::table
.count() .count()
.get_result::<i64>(&mut conn) .get_result::<i64>(&mut conn)
.await .await
@@ -285,9 +285,9 @@ pub async fn challenge_auth() {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let id: i32 = insert_into(schema::operator_identity::table) let id: i32 = insert_into(schema::operator_client::table)
.values((schema::operator_identity::public_key.eq(pubkey_bytes.clone()),)) .values((schema::operator_client::public_key.eq(pubkey_bytes.clone()),))
.returning(schema::operator_identity::id) .returning(schema::operator_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -371,8 +371,8 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
insert_into(schema::operator_identity::table) insert_into(schema::operator_client::table)
.values((schema::operator_identity::public_key.eq(pubkey_bytes.clone()),)) .values((schema::operator_client::public_key.eq(pubkey_bytes.clone()),))
.execute(&mut conn) .execute(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -444,9 +444,9 @@ pub async fn challenge_auth_rejects_invalid_signature() {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let id: i32 = insert_into(schema::operator_identity::table) let id: i32 = insert_into(schema::operator_client::table)
.values((schema::operator_identity::public_key.eq(pubkey_bytes.clone()),)) .values((schema::operator_client::public_key.eq(pubkey_bytes.clone()),))
.returning(schema::operator_identity::id) .returning(schema::operator_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
.unwrap(); .unwrap();

View File

@@ -140,6 +140,47 @@ pub async fn unseal_corrupted_ciphertext() {
)); ));
} }
/// After MAX_UNSEAL_ATTEMPTS wrong keys, the vault locks and the VaultGate
/// must stop itself so the connection is dropped.
#[tokio::test]
#[test_log::test]
pub async fn lockout_stops_vault_gate() {
use kameo::error::SendError;
let seal_key = b"real-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
// Exhaust all MAX_UNSEAL_ATTEMPTS (5) with wrong keys; each returns InvalidKey.
for _ in 0..5 {
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await;
assert!(matches!(
gate.ask(encrypted_key).await,
Err(SendError::HandlerError(VaultGateError::InvalidKey))
));
}
// Sixth attempt: vault is now locked, returns LockedOut, and the gate stops itself.
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await;
assert!(matches!(
gate.ask(encrypted_key).await,
Err(SendError::HandlerError(VaultGateError::LockedOut))
));
// Give the actor scheduler time to process the stop signal.
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
// Any subsequent message must be rejected because the gate is stopped.
let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret);
assert!(
matches!(
gate.ask(HandleHandshake { client_pubkey: client_public }).await,
Err(SendError::ActorNotRunning(_))
),
"VaultGate must be stopped after LockedOut"
);
}
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn unseal_retry_after_invalid_key() { pub async fn unseal_retry_after_invalid_key() {