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

@@ -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> {