fix(server::user_agent): useragents now self-sign themselves on bootstrap

This commit is contained in:
hdbg
2026-04-08 12:34:32 +02:00
parent 1585f90cae
commit 6b8da567dd
36 changed files with 352 additions and 229 deletions

View File

@@ -2,17 +2,9 @@ syntax = "proto3";
package arbiter.user_agent.auth;
enum KeyType {
KEY_TYPE_UNSPECIFIED = 0;
KEY_TYPE_ED25519 = 1;
KEY_TYPE_ECDSA_SECP256K1 = 2;
KEY_TYPE_RSA = 3;
}
message AuthChallengeRequest {
bytes pubkey = 1;
optional string bootstrap_token = 2;
KeyType key_type = 3;
}
message AuthChallenge {

View File

@@ -8,7 +8,10 @@ use kameo::{
use crate::{
actors::flow_coordinator::ApprovalError,
peers::{client::ClientProfile, user_agent::{UserAgentSession, session::BeginNewClientApproval}},
peers::{
client::ClientProfile,
user_agent::{UserAgentSession, session::BeginNewClientApproval},
},
};
pub struct Args {

View File

@@ -9,7 +9,13 @@ use kameo::{
};
use tracing::info;
use crate::{actors::flow_coordinator::client_connect_approval::ClientApprovalController, peers::{client::{ClientProfile, session::ClientSession}, user_agent::UserAgentSession}};
use crate::{
actors::flow_coordinator::client_connect_approval::ClientApprovalController,
peers::{
client::{ClientProfile, session::ClientSession},
user_agent::UserAgentSession,
},
};
pub mod client_connect_approval;

View File

@@ -4,8 +4,7 @@ use thiserror::Error;
use crate::{
actors::{
bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator,
vault::Vault,
bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator, vault::Vault,
},
db,
};

View File

@@ -147,7 +147,7 @@ impl Vault {
Ok(nonce)
}
fn expect_unsealed<'a>(state: &'a mut State) -> Result<&'a mut Unsealed, Error> {
fn expect_unsealed(state: &mut State) -> Result<&mut Unsealed, Error> {
match state {
State::Unsealed(unsealed) => Ok(unsealed),
State::Unbootstrapped => Err(Error::NotBootstrapped),

View File

@@ -1,6 +1,8 @@
use crate::{actors::vault, crypto::integrity::hashing::Hashable};
use arbiter_crypto::safecell::SafeCellHandle as _;
use hmac::{Hmac, Mac as _};
use crate::{
actors::vault::{self, GetState},
crypto::integrity::hashing::Hashable,
};
use hmac::Hmac;
use sha2::Sha256;
use diesel::{ExpressionMethods as _, QueryDsl, dsl::insert_into, sqlite::Sqlite};
@@ -199,6 +201,11 @@ pub async fn verify_entity<E: Integrable>(
}
}
pub async fn is_signing_available(vault: &ActorRef<Vault>) -> Result<bool, Error> {
let state = vault.ask(GetState).await.map_err(|_| Error::VaultSend)?;
Ok(matches!(state, vault::VaultState::Unsealed))
}
#[cfg(test)]
mod tests {
use diesel::{ExpressionMethods as _, QueryDsl};
@@ -208,7 +215,10 @@ mod tests {
use sha2::Digest;
use crate::{
actors::{GlobalActors, vault::{Bootstrap, Vault}},
actors::{
GlobalActors,
vault::{Bootstrap, Vault},
},
db::{self, schema},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};

View File

@@ -10,8 +10,8 @@ use tonic::Status;
use tracing::{info, warn};
use crate::{
peers::client::{ClientConnection, session::ClientSession},
grpc::request_tracker::RequestTracker,
peers::client::{ClientConnection, session::ClientSession},
};
mod auth;

View File

@@ -22,8 +22,8 @@ use tonic::Status;
use tracing::warn;
use crate::{
peers::client::{self, ClientConnection, auth},
grpc::request_tracker::RequestTracker,
peers::client::{self, ClientConnection, auth},
};
pub struct AuthTransportAdapter<'a> {

View File

@@ -16,11 +16,11 @@ use tonic::Status;
use tracing::warn;
use crate::{
peers::client::session::{ClientSession, HandleSignTransaction, SignTransactionRpcError},
grpc::{
Convert, TryConvert,
common::inbound::{RawEvmAddress, RawEvmTransaction},
},
peers::client::session::{ClientSession, HandleSignTransaction, SignTransactionRpcError},
};
fn wrap_response(payload: EvmResponsePayload) -> ClientResponsePayload {

View File

@@ -13,8 +13,8 @@ use tonic::Status;
use tracing::warn;
use crate::{
peers::client::session::{ClientSession, Error, HandleQueryVaultState},
actors::vault::VaultState,
peers::client::session::{ClientSession, Error, HandleQueryVaultState},
};
pub(super) async fn dispatch(

View File

@@ -10,8 +10,8 @@ use tonic::{Request, Response, Status, async_trait};
use tracing::info;
use crate::{
peers::{client::ClientConnection, user_agent::UserAgentConnection},
grpc::user_agent::start,
peers::{client::ClientConnection, user_agent::UserAgentConnection},
};
mod request_tracker;

View File

@@ -14,8 +14,8 @@ use tonic::Status;
use tracing::{error, info, warn};
use crate::{
peers::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession},
grpc::request_tracker::RequestTracker,
peers::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession},
};
mod auth;
@@ -124,7 +124,7 @@ pub async fn start(
) {
let mut request_tracker = RequestTracker::default();
let pubkey = match auth::start(&mut conn, &mut bi, &mut request_tracker).await {
let (id, pubkey) = match auth::start(&mut conn, &mut bi, &mut request_tracker).await {
Ok(pubkey) => pubkey,
Err(e) => {
warn!(error = ?e, "Authentication failed");
@@ -132,13 +132,19 @@ pub async fn start(
}
};
info!(?pubkey, "User authenticated successfully");
let (oob_sender, oob_receiver) = mpsc::channel(16);
let oob_adapter = OutOfBandAdapter(oob_sender);
let actor = UserAgentSession::spawn(UserAgentSession::new(conn, Box::new(oob_adapter)));
let actor = UserAgentSession::spawn(UserAgentSession::new(
conn,
id,
pubkey,
Box::new(oob_adapter),
));
let actor_for_cleanup = actor.clone();
info!(?pubkey, "User authenticated successfully");
dispatch_loop(bi, actor, oob_receiver, request_tracker).await;
actor_for_cleanup.kill();
}

View File

@@ -18,8 +18,8 @@ use tonic::Status;
use tracing::warn;
use crate::{
peers::user_agent::{UserAgentConnection, auth},
grpc::request_tracker::RequestTracker,
peers::user_agent::{UserAgentConnection, auth},
};
pub struct AuthTransportAdapter<'a> {
@@ -140,7 +140,6 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
AuthRequestPayload::ChallengeRequest(ProtoAuthChallengeRequest {
pubkey,
bootstrap_token,
key_type: _,
}) => {
let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else {
warn!(
@@ -168,7 +167,7 @@ pub async fn start(
conn: &mut UserAgentConnection,
bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>,
request_tracker: &mut RequestTracker,
) -> Result<authn::PublicKey, auth::Error> {
) -> Result<(i32, authn::PublicKey), auth::Error> {
let transport = AuthTransportAdapter::new(bi, request_tracker);
auth::authenticate(conn, transport).await
}

View File

@@ -23,6 +23,10 @@ use tonic::Status;
use tracing::warn;
use crate::{
grpc::{
Convert, TryConvert,
common::inbound::{RawEvmAddress, RawEvmTransaction},
},
peers::user_agent::{
UserAgentSession,
session::connection::{
@@ -31,10 +35,6 @@ use crate::{
SignTransactionError as SessionSignTransactionError,
},
},
grpc::{
Convert, TryConvert,
common::inbound::{RawEvmAddress, RawEvmTransaction},
},
};
fn wrap_evm_response(payload: EvmResponsePayload) -> UserAgentResponsePayload {

View File

@@ -21,6 +21,8 @@ use tonic::Status;
use tracing::{info, warn};
use crate::{
db::models::NewEvmWalletAccess,
grpc::Convert,
peers::user_agent::{
OutOfBand, UserAgentSession,
session::connection::{
@@ -28,8 +30,6 @@ use crate::{
HandleRevokeEvmWalletAccess, HandleSdkClientList,
},
},
db::models::NewEvmWalletAccess,
grpc::Convert,
};
fn wrap_sdk_client_response(payload: SdkClientResponsePayload) -> UserAgentResponsePayload {

View File

@@ -2,12 +2,12 @@
use crate::context::ServerContext;
pub mod actors;
pub mod peers;
pub mod context;
pub mod crypto;
pub mod db;
pub mod evm;
pub mod grpc;
pub mod peers;
pub mod utils;
pub struct Server {

View File

@@ -14,7 +14,9 @@ use tracing::error;
use crate::{
actors::{
GlobalActors, flow_coordinator::{self, RequestClientApproval}, vault::Vault
GlobalActors,
flow_coordinator::{self, RequestClientApproval},
vault::Vault,
},
crypto::integrity::{self, AttestationStatus},
db::{
@@ -187,10 +189,7 @@ async fn create_nonce(
.await
}
async fn approve_new_client(
actors: &GlobalActors,
profile: ClientProfile,
) -> Result<(), Error> {
async fn approve_new_client(actors: &GlobalActors, profile: ClientProfile) -> Result<(), Error> {
let result = actors
.flow_coordinator
.ask(RequestClientApproval { client: profile })

View File

@@ -6,7 +6,8 @@ use tracing::{error, info};
use crate::{
actors::GlobalActors,
crypto::integrity::{Integrable, hashing::Hashable},
db, peers::client::session::ClientSession,
db,
peers::client::session::ClientSession,
};
#[derive(Debug, Clone)]

View File

@@ -1,2 +1,2 @@
pub mod client;
pub mod user_agent;
pub mod client;

View File

@@ -69,7 +69,7 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents {
pub async fn authenticate<T>(
props: &mut UserAgentConnection,
transport: T,
) -> Result<authn::PublicKey, Error>
) -> Result<(i32, authn::PublicKey), Error>
where
T: Bi<Inbound, Result<Outbound, Error>> + Send,
{
@@ -82,7 +82,7 @@ where
};
match state.process_event(parse_auth_event(payload)).await {
Ok(AuthStates::AuthOk(key)) => return Ok(key.clone()),
Ok(AuthStates::AuthOk(result)) => return Ok((result.id, result.pubkey.clone())),
Err(AuthError::ActionFailed(err)) => {
error!(?err, "State machine action failed");
return Err(err);

View File

@@ -1,18 +1,15 @@
use super::super::{UserAgentConnection, UserAgentCredentials};
use arbiter_crypto::authn::{self, USERAGENT_CONTEXT};
use arbiter_proto::transport::Bi;
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::actor::ActorRef;
use tracing::error;
use super::super::{UserAgentCredentials, UserAgentConnection};
use super::Error;
use crate::peers::user_agent::auth::Outbound;
use crate::{
actors::{
bootstrap::ConsumeToken,
vault::Vault,
},
actors::{bootstrap::ConsumeToken, vault::Vault},
crypto::integrity,
db::{DatabasePool, schema::useragent_client},
};
@@ -27,6 +24,7 @@ pub struct BootstrapAuthRequest {
}
pub struct ChallengeContext {
pub id: i32,
pub challenge_nonce: i32,
pub key: authn::PublicKey,
}
@@ -35,13 +33,18 @@ pub struct ChallengeSolution {
pub solution: Vec<u8>,
}
pub struct AuthOk {
pub id: i32,
pub pubkey: authn::PublicKey,
}
smlang::statemachine!(
name: Auth,
custom_error: true,
transitions: {
*Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext),
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(authn::PublicKey),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(authn::PublicKey),
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthOk),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthOk),
}
);
@@ -110,12 +113,12 @@ async fn create_nonce(
db: &DatabasePool,
vault: &ActorRef<Vault>,
pubkey: &authn::PublicKey,
) -> Result<i32, Error> {
) -> Result<(i32, i32), Error> {
let mut db_conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::internal("Database unavailable")
})?;
let new_nonce = db_conn
let (id, new_nonce) = db_conn
.exclusive_transaction(|conn| {
Box::pin(async move {
let (id, new_nonce): (i32, i32) = update(useragent_client::table)
@@ -144,59 +147,36 @@ async fn create_nonce(
Error::internal("Database error")
})?;
Result::<_, Error>::Ok(new_nonce)
Result::<_, Error>::Ok((id, new_nonce))
})
})
.await?;
Ok(new_nonce)
Ok((id, new_nonce))
}
async fn register_key(
db: &DatabasePool,
vault: &ActorRef<Vault>,
pubkey: &authn::PublicKey,
) -> Result<(), Error> {
async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i32, Error> {
let pubkey_bytes = pubkey.to_bytes();
let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::internal("Database unavailable")
})?;
conn.transaction(|conn| {
Box::pin(async move {
const NONCE_START: i32 = 1;
const NONCE_START: i32 = 1;
let id: i32 = diesel::insert_into(useragent_client::table)
.values((
useragent_client::public_key.eq(pubkey_bytes),
useragent_client::nonce.eq(NONCE_START),
))
.returning(useragent_client::id)
.get_result(conn)
.await
.map_err(|e| {
error!(error = ?e, "Database error");
Error::internal("Database operation failed")
})?;
let id: i32 = diesel::insert_into(useragent_client::table)
.values((
useragent_client::public_key.eq(pubkey_bytes),
useragent_client::nonce.eq(NONCE_START),
))
.returning(useragent_client::id)
.get_result(&mut conn)
.await
.map_err(|e| {
error!(error = ?e, "Database error");
Error::internal("Database operation failed")
})?;
let entity = UserAgentCredentials {
pubkey: pubkey.clone(),
nonce: NONCE_START,
};
integrity::sign_entity(conn, vault, &entity, id)
.await
.map_err(|e| {
error!(error = ?e, "Failed to sign integrity tag for new user-agent key");
Error::internal("Failed to register public key")
})?;
Result::<_, Error>::Ok(())
})
})
.await?;
Ok(())
Ok(id)
}
pub struct AuthContext<'a, T> {
@@ -222,7 +202,7 @@ where
) -> Result<ChallengeContext, Self::Error> {
verify_integrity(&self.conn.db, &self.conn.actors.vault, &pubkey).await?;
let nonce = create_nonce(&self.conn.db, &self.conn.actors.vault, &pubkey).await?;
let (id, nonce) = create_nonce(&self.conn.db, &self.conn.actors.vault, &pubkey).await?;
self.transport
.send(Ok(Outbound::AuthChallenge { nonce }))
@@ -233,6 +213,7 @@ where
})?;
Ok(ChallengeContext {
id,
challenge_nonce: nonce,
key: pubkey,
})
@@ -243,7 +224,7 @@ where
async fn verify_bootstrap_token(
&mut self,
BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest,
) -> Result<authn::PublicKey, Self::Error> {
) -> Result<AuthOk, Self::Error> {
let token_ok: bool = self
.conn
.actors
@@ -264,12 +245,12 @@ where
match token_ok {
true => {
register_key(&self.conn.db, &self.conn.actors.vault, &pubkey).await?;
let id = register_key(&self.conn.db, &pubkey).await?;
self.transport
.send(Ok(Outbound::AuthSuccess))
.await
.map_err(|_| Error::Transport)?;
Ok(pubkey)
Ok(AuthOk { id, pubkey })
}
false => {
error!("Invalid bootstrap token provided");
@@ -287,11 +268,12 @@ where
async fn verify_solution(
&mut self,
ChallengeContext {
id,
challenge_nonce,
key,
}: &ChallengeContext,
ChallengeSolution { solution }: ChallengeSolution,
) -> Result<authn::PublicKey, Self::Error> {
) -> Result<AuthOk, Self::Error> {
let signature = authn::Signature::try_from(solution.as_slice()).map_err(|_| {
error!("Failed to decode signature in challenge solution");
Error::InvalidChallengeSolution
@@ -305,7 +287,7 @@ where
.send(Ok(Outbound::AuthSuccess))
.await
.map_err(|_| Error::Transport)?;
Ok(key.clone())
Ok(AuthOk { id: *id, pubkey: key.clone() })
}
false => {
self.transport

View File

@@ -1,7 +1,5 @@
use crate::{
actors::GlobalActors,
crypto::integrity::Integrable,
db, peers::client::ClientProfile,
actors::GlobalActors, crypto::integrity::Integrable, db, peers::client::ClientProfile,
};
use arbiter_crypto::authn;

View File

@@ -1,14 +1,22 @@
use arbiter_crypto::authn;
use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo_actors::message_bus::Register;
use std::{borrow::Cow, collections::HashMap};
use arbiter_proto::transport::Sender;
use async_trait::async_trait;
use kameo::{Actor, actor::ActorRef, messages};
use kameo::{Actor, actor::ActorRef, messages, prelude::Message};
use thiserror::Error;
use tracing::error;
use crate::{actors::flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController}, peers::client::ClientProfile};
use crate::{
actors::{
flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController},
vault::events,
}, crypto::integrity, db::schema::useragent_client, peers::{client::ClientProfile, user_agent::UserAgentCredentials}
};
mod state;
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
@@ -50,6 +58,8 @@ pub struct PendingClientApproval {
}
pub struct UserAgentSession {
id: i32,
pubkey: authn::PublicKey,
props: UserAgentConnection,
state: UserAgentStateMachine<DummyContext>,
sender: Box<dyn Sender<OutOfBand>>,
@@ -60,31 +70,22 @@ pub struct UserAgentSession {
pub mod connection;
impl UserAgentSession {
pub(crate) fn new(props: UserAgentConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self {
pub(crate) fn new(
props: UserAgentConnection,
id: i32,
pubkey: authn::PublicKey,
sender: Box<dyn Sender<OutOfBand>>,
) -> Self {
Self {
id,
props,
pubkey,
state: UserAgentStateMachine::new(DummyContext),
sender,
pending_client_approvals: Default::default(),
}
}
pub fn new_test(db: crate::db::DatabasePool, actors: crate::actors::GlobalActors) -> Self {
struct DummySender;
#[async_trait]
impl Sender<OutOfBand> for DummySender {
async fn send(
&mut self,
_item: OutOfBand,
) -> Result<(), arbiter_proto::transport::Error> {
Ok(())
}
}
Self::new(UserAgentConnection::new(db, actors), Box::new(DummySender))
}
fn transition(&mut self, event: UserAgentEvents) -> Result<(), Error> {
self.state.process_event(event).map_err(|e| {
error!(?e, "State transition failed");
@@ -127,6 +128,61 @@ impl UserAgentSession {
}
}
impl Message<events::VaultBootstrapped> for UserAgentSession {
type Reply = Result<(), Error>;
async fn handle(
&mut self,
_: events::VaultBootstrapped,
ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
) -> Self::Reply {
let Ok(mut conn) = self.props.db.get().await else {
error!("Failed to get database connection for vault bootstrapped event");
ctx.stop();
return Err(Error::internal("Failed to get database connection"));
};
let result = conn.exclusive_transaction(|conn| {
Box::pin(async {
let nonce: i32 = useragent_client::table
.filter(useragent_client::id.eq(self.id))
.select(useragent_client::nonce)
.first::<i32>(conn)
.await
.map_err(|e| {
error!(?e, "Failed to get nonce for useragent bootstrapping");
Error::internal("Failed to sign user agent credentials")
})?;
let entity = UserAgentCredentials {
pubkey: self.pubkey.clone(),
nonce,
};
integrity::sign_entity(conn, &self.props.actors.vault, &entity, self.id)
.await
.map_err(|e| {
error!(?e, "Failed to sign user agent credentials during vault bootstrapping");
Error::internal("Failed to sign user agent credentials")
})?;
Result::<_, Error>::Ok(())
})
}).await;
match result {
Ok(_) => Ok(()),
Err(err) => {
error!(?err, "Error during vault bootstrapping");
ctx.stop();
Err(err)
},
}
}
}
impl Actor for UserAgentSession {
type Args = Self;
@@ -136,6 +192,21 @@ impl Actor for UserAgentSession {
args: Self::Args,
this: kameo::prelude::ActorRef<Self>,
) -> Result<Self, Self::Error> {
args.props
.actors
.events
.tell(Register(
this.clone().recipient::<events::VaultBootstrapped>(),
))
.await
.map_err(|err| {
error!(
?err,
"Failed to register user agent connection with event bus"
);
Error::internal("Failed to register user agent connection with event bus")
})?;
args.props
.actors
.flow_coordinator

View File

@@ -14,7 +14,7 @@ use kameo::prelude::Context;
use tracing::{error, info};
use x25519_dalek::{EphemeralSecret, PublicKey};
use crate::{actors::vault::VaultState, peers::user_agent::session::state::{UnsealContext, UserAgentEvents}};
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
use crate::actors::{
evm::{
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
@@ -27,10 +27,11 @@ use crate::db::models::{
};
use crate::evm::policies::{Grant, SpecificGrant};
use crate::{
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer,
actors::vault::VaultState,
peers::user_agent::session::state::{UnsealContext, UserAgentEvents},
};
use super::{UserAgentSession, state, Error};
use super::{Error, UserAgentSession, state};
impl UserAgentSession {
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {

View File

@@ -5,14 +5,10 @@ use arbiter_crypto::{
use arbiter_proto::ClientMetadata;
use arbiter_proto::transport::{Receiver, Sender};
use arbiter_server::{
actors::{
GlobalActors,
vault::Bootstrap,
},
peers::client::{ClientConnection, ClientCredentials, auth, connect_client},
actors::{GlobalActors, vault::Bootstrap},
crypto::integrity,
db::{self, schema},
peers::client::{ClientConnection, ClientCredentials, auth, connect_client},
};
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
use diesel_async::RunQueryDsl;

View File

@@ -12,7 +12,9 @@ use tokio::sync::mpsc;
#[allow(dead_code)]
pub async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault {
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()).await.unwrap();
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
actor
.bootstrap(SafeCell::new(b"test-seal-key".to_vec()))
.await

View File

@@ -5,15 +5,10 @@ use arbiter_crypto::{
use arbiter_proto::transport::{Receiver, Sender};
use arbiter_server::{
actors::{
GlobalActors,
bootstrap::GetToken,
vault::Bootstrap,
},
peers::user_agent::{UserAgentConnection, UserAgentCredentials, auth},
actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap},
crypto::integrity,
db::{self, schema},
peers::user_agent::{UserAgentConnection, UserAgentCredentials, auth},
};
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
use diesel_async::RunQueryDsl;

View File

@@ -3,13 +3,12 @@ use arbiter_server::{
actors::{
GlobalActors,
vault::{Bootstrap, Seal},
},
peers::user_agent::{
UserAgentSession,
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
},
db,
peers::user_agent::{
UserAgentSession,
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
},
};
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};

View File

@@ -2,7 +2,10 @@ use std::collections::{HashMap, HashSet};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::{GlobalActors, vault::{CreateNew, Error, Vault}},
actors::{
GlobalActors,
vault::{CreateNew, Error, Vault},
},
db::{self, models, schema},
};
@@ -161,7 +164,9 @@ async fn decrypt_roundtrip_after_high_concurrency() {
let writes = write_concurrently(actor, "roundtrip", 40).await;
let expected: HashMap<i32, Vec<u8>> = writes.into_iter().collect();
let mut decryptor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()).await.unwrap();
let mut decryptor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
decryptor
.try_unseal(SafeCell::new(b"test-seal-key".to_vec()))
.await

View File

@@ -1,12 +1,11 @@
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::{GlobalActors, vault::{Error, Vault}},
actors::{
GlobalActors,
vault::{Error, Vault},
},
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG},
db::{self, models, schema},
peers::user_agent::{
UserAgentSession,
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
},
};
use diesel::{QueryDsl, SelectableHelper};
@@ -18,7 +17,9 @@ use crate::common;
#[test_log::test]
async fn test_bootstrap() {
let db = db::create_test_pool().await;
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()).await.unwrap();
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
actor.bootstrap(seal_key).await.unwrap();
@@ -52,7 +53,9 @@ async fn test_bootstrap_rejects_double() {
#[test_log::test]
async fn test_create_new_before_bootstrap_fails() {
let db = db::create_test_pool().await;
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus()).await.unwrap();
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus())
.await
.unwrap();
let err = actor
.create_new(SafeCell::new(b"data".to_vec()))
@@ -65,7 +68,9 @@ async fn test_create_new_before_bootstrap_fails() {
#[test_log::test]
async fn test_decrypt_before_bootstrap_fails() {
let db = db::create_test_pool().await;
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus()).await.unwrap();
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus())
.await
.unwrap();
let err = actor.decrypt(1).await.unwrap_err();
assert!(matches!(err, Error::NotBootstrapped));
@@ -78,7 +83,9 @@ async fn test_new_restores_sealed_state() {
let actor = common::bootstrapped_vault(&db).await;
drop(actor);
let mut actor2 = Vault::new(db, GlobalActors::spawn_message_bus()).await.unwrap();
let mut actor2 = Vault::new(db, GlobalActors::spawn_message_bus())
.await
.unwrap();
let err = actor2.decrypt(1).await.unwrap_err();
assert!(matches!(err, Error::NotBootstrapped));
}
@@ -96,7 +103,9 @@ async fn test_unseal_correct_password() {
.unwrap();
drop(actor);
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()).await.unwrap();
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
actor.try_unseal(seal_key).await.unwrap();
@@ -117,7 +126,9 @@ async fn test_unseal_wrong_then_correct_password() {
.unwrap();
drop(actor);
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()).await.unwrap();
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let bad_key = SafeCell::new(b"wrong-password".to_vec());
let err = actor.try_unseal(bad_key).await.unwrap_err();

View File

@@ -61,11 +61,6 @@ Future<Connection> connectAndAuthorize(
final req = ua_auth.AuthChallengeRequest(
pubkey: pubkey,
bootstrapToken: bootstrapToken,
keyType: switch (key.alg) {
KeyAlgorithm.rsa => ua_auth.KeyType.KEY_TYPE_RSA,
KeyAlgorithm.ecdsa => ua_auth.KeyType.KEY_TYPE_ECDSA_SECP256K1,
KeyAlgorithm.ed25519 => ua_auth.KeyType.KEY_TYPE_ED25519,
},
);
final response = await connection.ask(
UserAgentRequest(auth: ua_auth.Request(challengeRequest: req)),

View File

@@ -414,6 +414,79 @@ class GasLimitExceededViolation extends $pb.GeneratedMessage {
void clearMaxPriorityFeePerGas() => $_clearField(2);
}
class EvalViolation_ChainIdMismatch extends $pb.GeneratedMessage {
factory EvalViolation_ChainIdMismatch({
$fixnum.Int64? expected,
$fixnum.Int64? actual,
}) {
final result = create();
if (expected != null) result.expected = expected;
if (actual != null) result.actual = actual;
return result;
}
EvalViolation_ChainIdMismatch._();
factory EvalViolation_ChainIdMismatch.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EvalViolation_ChainIdMismatch.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EvalViolation.ChainIdMismatch',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.shared.evm'),
createEmptyInstance: create)
..a<$fixnum.Int64>(
1, _omitFieldNames ? '' : 'expected', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'actual', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EvalViolation_ChainIdMismatch clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EvalViolation_ChainIdMismatch copyWith(
void Function(EvalViolation_ChainIdMismatch) updates) =>
super.copyWith(
(message) => updates(message as EvalViolation_ChainIdMismatch))
as EvalViolation_ChainIdMismatch;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EvalViolation_ChainIdMismatch create() =>
EvalViolation_ChainIdMismatch._();
@$core.override
EvalViolation_ChainIdMismatch createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static EvalViolation_ChainIdMismatch getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<EvalViolation_ChainIdMismatch>(create);
static EvalViolation_ChainIdMismatch? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get expected => $_getI64(0);
@$pb.TagNumber(1)
set expected($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasExpected() => $_has(0);
@$pb.TagNumber(1)
void clearExpected() => $_clearField(1);
@$pb.TagNumber(2)
$fixnum.Int64 get actual => $_getI64(1);
@$pb.TagNumber(2)
set actual($fixnum.Int64 value) => $_setInt64(1, value);
@$pb.TagNumber(2)
$core.bool hasActual() => $_has(1);
@$pb.TagNumber(2)
void clearActual() => $_clearField(2);
}
enum EvalViolation_Kind {
invalidTarget,
gasLimitExceeded,
@@ -421,6 +494,7 @@ enum EvalViolation_Kind {
volumetricLimitExceeded,
invalidTime,
invalidTransactionType,
chainIdMismatch,
notSet
}
@@ -432,6 +506,7 @@ class EvalViolation extends $pb.GeneratedMessage {
$0.Empty? volumetricLimitExceeded,
$0.Empty? invalidTime,
$0.Empty? invalidTransactionType,
EvalViolation_ChainIdMismatch? chainIdMismatch,
}) {
final result = create();
if (invalidTarget != null) result.invalidTarget = invalidTarget;
@@ -442,6 +517,7 @@ class EvalViolation extends $pb.GeneratedMessage {
if (invalidTime != null) result.invalidTime = invalidTime;
if (invalidTransactionType != null)
result.invalidTransactionType = invalidTransactionType;
if (chainIdMismatch != null) result.chainIdMismatch = chainIdMismatch;
return result;
}
@@ -462,6 +538,7 @@ class EvalViolation extends $pb.GeneratedMessage {
4: EvalViolation_Kind.volumetricLimitExceeded,
5: EvalViolation_Kind.invalidTime,
6: EvalViolation_Kind.invalidTransactionType,
7: EvalViolation_Kind.chainIdMismatch,
0: EvalViolation_Kind.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@@ -469,7 +546,7 @@ class EvalViolation extends $pb.GeneratedMessage {
package:
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.shared.evm'),
createEmptyInstance: create)
..oo(0, [1, 2, 3, 4, 5, 6])
..oo(0, [1, 2, 3, 4, 5, 6, 7])
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'invalidTarget', $pb.PbFieldType.OY)
..aOM<GasLimitExceededViolation>(
@@ -483,6 +560,9 @@ class EvalViolation extends $pb.GeneratedMessage {
subBuilder: $0.Empty.create)
..aOM<$0.Empty>(6, _omitFieldNames ? '' : 'invalidTransactionType',
subBuilder: $0.Empty.create)
..aOM<EvalViolation_ChainIdMismatch>(
7, _omitFieldNames ? '' : 'chainIdMismatch',
subBuilder: EvalViolation_ChainIdMismatch.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -510,6 +590,7 @@ class EvalViolation extends $pb.GeneratedMessage {
@$pb.TagNumber(4)
@$pb.TagNumber(5)
@$pb.TagNumber(6)
@$pb.TagNumber(7)
EvalViolation_Kind whichKind() => _EvalViolation_KindByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
@@ -517,6 +598,7 @@ class EvalViolation extends $pb.GeneratedMessage {
@$pb.TagNumber(4)
@$pb.TagNumber(5)
@$pb.TagNumber(6)
@$pb.TagNumber(7)
void clearKind() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1)
@@ -582,6 +664,18 @@ class EvalViolation extends $pb.GeneratedMessage {
void clearInvalidTransactionType() => $_clearField(6);
@$pb.TagNumber(6)
$0.Empty ensureInvalidTransactionType() => $_ensure(5);
@$pb.TagNumber(7)
EvalViolation_ChainIdMismatch get chainIdMismatch => $_getN(6);
@$pb.TagNumber(7)
set chainIdMismatch(EvalViolation_ChainIdMismatch value) =>
$_setField(7, value);
@$pb.TagNumber(7)
$core.bool hasChainIdMismatch() => $_has(6);
@$pb.TagNumber(7)
void clearChainIdMismatch() => $_clearField(7);
@$pb.TagNumber(7)
EvalViolation_ChainIdMismatch ensureChainIdMismatch() => $_ensure(6);
}
/// Transaction was classified but no grant covers it

View File

@@ -195,12 +195,31 @@ const EvalViolation$json = {
'9': 0,
'10': 'invalidTransactionType'
},
{
'1': 'chain_id_mismatch',
'3': 7,
'4': 1,
'5': 11,
'6': '.arbiter.shared.evm.EvalViolation.ChainIdMismatch',
'9': 0,
'10': 'chainIdMismatch'
},
],
'3': [EvalViolation_ChainIdMismatch$json],
'8': [
{'1': 'kind'},
],
};
@$core.Deprecated('Use evalViolationDescriptor instead')
const EvalViolation_ChainIdMismatch$json = {
'1': 'ChainIdMismatch',
'2': [
{'1': 'expected', '3': 1, '4': 1, '5': 4, '10': 'expected'},
{'1': 'actual', '3': 2, '4': 1, '5': 4, '10': 'actual'},
],
};
/// Descriptor for `EvalViolation`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List evalViolationDescriptor = $convert.base64Decode(
'Cg1FdmFsVmlvbGF0aW9uEicKDmludmFsaWRfdGFyZ2V0GAEgASgMSABSDWludmFsaWRUYXJnZX'
@@ -211,7 +230,10 @@ final $typed_data.Uint8List evalViolationDescriptor = $convert.base64Decode(
'YuRW1wdHlIAFIXdm9sdW1ldHJpY0xpbWl0RXhjZWVkZWQSOwoMaW52YWxpZF90aW1lGAUgASgL'
'MhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5SABSC2ludmFsaWRUaW1lElIKGGludmFsaWRfdHJhbn'
'NhY3Rpb25fdHlwZRgGIAEoCzIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eUgAUhZpbnZhbGlkVHJh'
'bnNhY3Rpb25UeXBlQgYKBGtpbmQ=');
'bnNhY3Rpb25UeXBlEl8KEWNoYWluX2lkX21pc21hdGNoGAcgASgLMjEuYXJiaXRlci5zaGFyZW'
'QuZXZtLkV2YWxWaW9sYXRpb24uQ2hhaW5JZE1pc21hdGNoSABSD2NoYWluSWRNaXNtYXRjaBpF'
'Cg9DaGFpbklkTWlzbWF0Y2gSGgoIZXhwZWN0ZWQYASABKARSCGV4cGVjdGVkEhYKBmFjdHVhbB'
'gCIAEoBFIGYWN0dWFsQgYKBGtpbmQ=');
@$core.Deprecated('Use noMatchingGrantErrorDescriptor instead')
const NoMatchingGrantError$json = {

View File

@@ -24,12 +24,10 @@ class AuthChallengeRequest extends $pb.GeneratedMessage {
factory AuthChallengeRequest({
$core.List<$core.int>? pubkey,
$core.String? bootstrapToken,
KeyType? keyType,
}) {
final result = create();
if (pubkey != null) result.pubkey = pubkey;
if (bootstrapToken != null) result.bootstrapToken = bootstrapToken;
if (keyType != null) result.keyType = keyType;
return result;
}
@@ -50,8 +48,6 @@ class AuthChallengeRequest extends $pb.GeneratedMessage {
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY)
..aOS(2, _omitFieldNames ? '' : 'bootstrapToken')
..aE<KeyType>(3, _omitFieldNames ? '' : 'keyType',
enumValues: KeyType.values)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -90,15 +86,6 @@ class AuthChallengeRequest extends $pb.GeneratedMessage {
$core.bool hasBootstrapToken() => $_has(1);
@$pb.TagNumber(2)
void clearBootstrapToken() => $_clearField(2);
@$pb.TagNumber(3)
KeyType get keyType => $_getN(2);
@$pb.TagNumber(3)
set keyType(KeyType value) => $_setField(3, value);
@$pb.TagNumber(3)
$core.bool hasKeyType() => $_has(2);
@$pb.TagNumber(3)
void clearKeyType() => $_clearField(3);
}
class AuthChallenge extends $pb.GeneratedMessage {

View File

@@ -14,31 +14,6 @@ import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class KeyType extends $pb.ProtobufEnum {
static const KeyType KEY_TYPE_UNSPECIFIED =
KeyType._(0, _omitEnumNames ? '' : 'KEY_TYPE_UNSPECIFIED');
static const KeyType KEY_TYPE_ED25519 =
KeyType._(1, _omitEnumNames ? '' : 'KEY_TYPE_ED25519');
static const KeyType KEY_TYPE_ECDSA_SECP256K1 =
KeyType._(2, _omitEnumNames ? '' : 'KEY_TYPE_ECDSA_SECP256K1');
static const KeyType KEY_TYPE_RSA =
KeyType._(3, _omitEnumNames ? '' : 'KEY_TYPE_RSA');
static const $core.List<KeyType> values = <KeyType>[
KEY_TYPE_UNSPECIFIED,
KEY_TYPE_ED25519,
KEY_TYPE_ECDSA_SECP256K1,
KEY_TYPE_RSA,
];
static final $core.List<KeyType?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 3);
static KeyType? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const KeyType._(super.value, super.name);
}
class AuthResult extends $pb.ProtobufEnum {
static const AuthResult AUTH_RESULT_UNSPECIFIED =
AuthResult._(0, _omitEnumNames ? '' : 'AUTH_RESULT_UNSPECIFIED');

View File

@@ -15,22 +15,6 @@ import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use keyTypeDescriptor instead')
const KeyType$json = {
'1': 'KeyType',
'2': [
{'1': 'KEY_TYPE_UNSPECIFIED', '2': 0},
{'1': 'KEY_TYPE_ED25519', '2': 1},
{'1': 'KEY_TYPE_ECDSA_SECP256K1', '2': 2},
{'1': 'KEY_TYPE_RSA', '2': 3},
],
};
/// Descriptor for `KeyType`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List keyTypeDescriptor = $convert.base64Decode(
'CgdLZXlUeXBlEhgKFEtFWV9UWVBFX1VOU1BFQ0lGSUVEEAASFAoQS0VZX1RZUEVfRUQyNTUxOR'
'ABEhwKGEtFWV9UWVBFX0VDRFNBX1NFQ1AyNTZLMRACEhAKDEtFWV9UWVBFX1JTQRAD');
@$core.Deprecated('Use authResultDescriptor instead')
const AuthResult$json = {
'1': 'AuthResult',
@@ -67,14 +51,6 @@ const AuthChallengeRequest$json = {
'10': 'bootstrapToken',
'17': true
},
{
'1': 'key_type',
'3': 3,
'4': 1,
'5': 14,
'6': '.arbiter.user_agent.auth.KeyType',
'10': 'keyType'
},
],
'8': [
{'1': '_bootstrap_token'},
@@ -84,9 +60,8 @@ const AuthChallengeRequest$json = {
/// Descriptor for `AuthChallengeRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List authChallengeRequestDescriptor = $convert.base64Decode(
'ChRBdXRoQ2hhbGxlbmdlUmVxdWVzdBIWCgZwdWJrZXkYASABKAxSBnB1YmtleRIsCg9ib290c3'
'RyYXBfdG9rZW4YAiABKAlIAFIOYm9vdHN0cmFwVG9rZW6IAQESOwoIa2V5X3R5cGUYAyABKA4y'
'IC5hcmJpdGVyLnVzZXJfYWdlbnQuYXV0aC5LZXlUeXBlUgdrZXlUeXBlQhIKEF9ib290c3RyYX'
'BfdG9rZW4=');
'RyYXBfdG9rZW4YAiABKAlIAFIOYm9vdHN0cmFwVG9rZW6IAQFCEgoQX2Jvb3RzdHJhcF90b2tl'
'bg==');
@$core.Deprecated('Use authChallengeDescriptor instead')
const AuthChallenge$json = {