refactor(server): removed grpc adapter and replaced with concrete implementations
This commit is contained in:
@@ -3,12 +3,7 @@ use diesel::QueryDsl;
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::{Actor, messages};
|
||||
use miette::Diagnostic;
|
||||
use rand::{
|
||||
RngExt,
|
||||
distr::{Alphanumeric},
|
||||
make_rng,
|
||||
rngs::StdRng,
|
||||
};
|
||||
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::db::{self, DatabasePool, schema};
|
||||
@@ -61,7 +56,6 @@ impl Bootstrapper {
|
||||
|
||||
drop(conn);
|
||||
|
||||
|
||||
let token = if row_count == 0 {
|
||||
let token = generate_token().await?;
|
||||
Some(token)
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
use arbiter_proto::{
|
||||
format_challenge,
|
||||
proto::client::{
|
||||
AuthChallenge, AuthChallengeSolution, ClientConnectError, ClientRequest, ClientResponse,
|
||||
client_connect_error::Code as ConnectErrorCode,
|
||||
client_request::Payload as ClientRequestPayload,
|
||||
client_response::Payload as ClientResponsePayload,
|
||||
},
|
||||
transport::expect_message,
|
||||
};
|
||||
use arbiter_proto::{format_challenge, transport::expect_message};
|
||||
use diesel::{
|
||||
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update,
|
||||
};
|
||||
@@ -18,7 +9,7 @@ use tracing::error;
|
||||
|
||||
use crate::{
|
||||
actors::{
|
||||
client::ClientConnection,
|
||||
client::{ClientConnection, ConnectErrorCode, Request, Response},
|
||||
router::{self, RequestClientApproval},
|
||||
},
|
||||
db::{self, schema::program_client},
|
||||
@@ -155,15 +146,13 @@ async fn challenge_client(
|
||||
pubkey: VerifyingKey,
|
||||
nonce: i32,
|
||||
) -> Result<(), Error> {
|
||||
let challenge = AuthChallenge {
|
||||
pubkey: pubkey.as_bytes().to_vec(),
|
||||
nonce,
|
||||
};
|
||||
let challenge_pubkey = pubkey.as_bytes().to_vec();
|
||||
|
||||
props
|
||||
.transport
|
||||
.send(Ok(ClientResponse {
|
||||
payload: Some(ClientResponsePayload::AuthChallenge(challenge.clone())),
|
||||
.send(Ok(Response::AuthChallenge {
|
||||
pubkey: challenge_pubkey.clone(),
|
||||
nonce,
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -171,20 +160,17 @@ async fn challenge_client(
|
||||
Error::Transport
|
||||
})?;
|
||||
|
||||
let AuthChallengeSolution { signature } =
|
||||
expect_message(&mut *props.transport, |req: ClientRequest| {
|
||||
match req.payload? {
|
||||
ClientRequestPayload::AuthChallengeSolution(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Failed to receive challenge solution");
|
||||
Error::Transport
|
||||
})?;
|
||||
let signature = expect_message(&mut *props.transport, |req: Request| match req {
|
||||
Request::AuthChallengeSolution { signature } => Some(signature),
|
||||
_ => None,
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Failed to receive challenge solution");
|
||||
Error::Transport
|
||||
})?;
|
||||
|
||||
let formatted = format_challenge(nonce, &challenge.pubkey);
|
||||
let formatted = format_challenge(nonce, &challenge_pubkey);
|
||||
let sig = signature.as_slice().try_into().map_err(|_| {
|
||||
error!("Invalid signature length");
|
||||
Error::InvalidChallengeSolution
|
||||
@@ -209,15 +195,14 @@ fn connect_error_code(err: &Error) -> ConnectErrorCode {
|
||||
}
|
||||
|
||||
async fn authenticate(props: &mut ClientConnection) -> Result<VerifyingKey, Error> {
|
||||
let Some(ClientRequest {
|
||||
payload: Some(ClientRequestPayload::AuthChallengeRequest(challenge)),
|
||||
let Some(Request::AuthChallengeRequest {
|
||||
pubkey: challenge_pubkey,
|
||||
}) = props.transport.recv().await
|
||||
else {
|
||||
return Err(Error::Transport);
|
||||
};
|
||||
|
||||
let pubkey_bytes = challenge
|
||||
.pubkey
|
||||
let pubkey_bytes = challenge_pubkey
|
||||
.as_array()
|
||||
.ok_or(Error::InvalidClientPubkeyLength)?;
|
||||
let pubkey =
|
||||
@@ -244,11 +229,7 @@ pub async fn authenticate_and_create(mut props: ClientConnection) -> Result<Clie
|
||||
let code = connect_error_code(&err);
|
||||
let _ = props
|
||||
.transport
|
||||
.send(Ok(ClientResponse {
|
||||
payload: Some(ClientResponsePayload::ClientConnectError(
|
||||
ClientConnectError { code: code.into() },
|
||||
)),
|
||||
}))
|
||||
.send(Ok(Response::ClientConnectError { code }))
|
||||
.await;
|
||||
Err(err)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use arbiter_proto::{
|
||||
proto::client::{ClientRequest, ClientResponse},
|
||||
transport::Bi,
|
||||
};
|
||||
use arbiter_proto::transport::Bi;
|
||||
use kameo::actor::Spawn;
|
||||
use tracing::{error, info};
|
||||
|
||||
@@ -24,7 +21,27 @@ pub enum ClientError {
|
||||
Auth(#[from] auth::Error),
|
||||
}
|
||||
|
||||
pub type Transport = Box<dyn Bi<ClientRequest, Result<ClientResponse, ClientError>> + Send>;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnectErrorCode {
|
||||
Unknown,
|
||||
ApprovalDenied,
|
||||
NoUserAgentsOnline,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Request {
|
||||
AuthChallengeRequest { pubkey: Vec<u8> },
|
||||
AuthChallengeSolution { signature: Vec<u8> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Response {
|
||||
AuthChallenge { pubkey: Vec<u8>, nonce: i32 },
|
||||
AuthOk,
|
||||
ClientConnectError { code: ConnectErrorCode },
|
||||
}
|
||||
|
||||
pub type Transport = Box<dyn Bi<Request, Result<Response, ClientError>> + Send>;
|
||||
|
||||
pub struct ClientConnection {
|
||||
pub(crate) db: db::DatabasePool,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
|
||||
use kameo::Actor;
|
||||
use tokio::select;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{actors::{
|
||||
GlobalActors, client::{ClientError, ClientConnection}, router::RegisterClient
|
||||
}, db};
|
||||
use crate::{
|
||||
actors::{
|
||||
GlobalActors,
|
||||
client::{ClientConnection, ClientError, Request, Response},
|
||||
router::RegisterClient,
|
||||
},
|
||||
db,
|
||||
};
|
||||
|
||||
pub struct ClientSession {
|
||||
props: ClientConnection,
|
||||
@@ -16,18 +20,13 @@ impl ClientSession {
|
||||
Self { props }
|
||||
}
|
||||
|
||||
pub async fn process_transport_inbound(&mut self, req: ClientRequest) -> Output {
|
||||
let msg = req.payload.ok_or_else(|| {
|
||||
error!(actor = "client", "Received message with no payload");
|
||||
ClientError::MissingRequestPayload
|
||||
})?;
|
||||
|
||||
let _ = msg;
|
||||
pub async fn process_transport_inbound(&mut self, req: Request) -> Output {
|
||||
let _ = req;
|
||||
Err(ClientError::UnexpectedRequestPayload)
|
||||
}
|
||||
}
|
||||
|
||||
type Output = Result<ClientResponse, ClientError>;
|
||||
type Output = Result<Response, ClientError>;
|
||||
|
||||
impl Actor for ClientSession {
|
||||
type Args = Self;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
|
||||
use diesel::{ExpressionMethods, OptionalExtension as _, QueryDsl, SelectableHelper as _, dsl::insert_into};
|
||||
use diesel::{
|
||||
ExpressionMethods, OptionalExtension as _, QueryDsl, SelectableHelper as _, dsl::insert_into,
|
||||
};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::{Actor, actor::ActorRef, messages};
|
||||
use memsafe::MemSafe;
|
||||
@@ -7,13 +9,16 @@ use rand::{SeedableRng, rng, rngs::StdRng};
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
||||
db::{self, DatabasePool, models::{self, EvmBasicGrant, SqliteTimestamp}, schema},
|
||||
db::{
|
||||
self, DatabasePool,
|
||||
models::{self, EvmBasicGrant, SqliteTimestamp},
|
||||
schema,
|
||||
},
|
||||
evm::{
|
||||
self, RunKind,
|
||||
policies::{
|
||||
FullGrant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
||||
ether_transfer::EtherTransfer,
|
||||
token_transfers::TokenTransfer,
|
||||
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -88,7 +93,12 @@ impl EvmActor {
|
||||
// todo: audit
|
||||
let rng = StdRng::from_rng(&mut rng());
|
||||
let engine = evm::Engine::new(db.clone());
|
||||
Self { keyholder, db, rng, engine }
|
||||
Self {
|
||||
keyholder,
|
||||
db,
|
||||
rng,
|
||||
engine,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,12 +159,24 @@ impl EvmActor {
|
||||
match grant {
|
||||
SpecificGrant::EtherTransfer(settings) => {
|
||||
self.engine
|
||||
.create_grant::<EtherTransfer>(client_id, FullGrant { basic, specific: settings })
|
||||
.create_grant::<EtherTransfer>(
|
||||
client_id,
|
||||
FullGrant {
|
||||
basic,
|
||||
specific: settings,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
SpecificGrant::TokenTransfer(settings) => {
|
||||
self.engine
|
||||
.create_grant::<TokenTransfer>(client_id, FullGrant { basic, specific: settings })
|
||||
.create_grant::<TokenTransfer>(
|
||||
client_id,
|
||||
FullGrant {
|
||||
basic,
|
||||
specific: settings,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -204,8 +226,14 @@ impl EvmActor {
|
||||
.ok_or(SignTransactionError::WalletNotFound)?;
|
||||
drop(conn);
|
||||
|
||||
let meaning = self.engine
|
||||
.evaluate_transaction(wallet.id, client_id, transaction.clone(), RunKind::Execution)
|
||||
let meaning = self
|
||||
.engine
|
||||
.evaluate_transaction(
|
||||
wallet.id,
|
||||
client_id,
|
||||
transaction.clone(),
|
||||
RunKind::Execution,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(meaning)
|
||||
@@ -230,14 +258,21 @@ impl EvmActor {
|
||||
|
||||
let raw_key: MemSafe<Vec<u8>> = self
|
||||
.keyholder
|
||||
.ask(Decrypt { aead_id: wallet.aead_encrypted_id })
|
||||
.ask(Decrypt {
|
||||
aead_id: wallet.aead_encrypted_id,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| SignTransactionError::KeyholderSend)?;
|
||||
|
||||
let signer = safe_signer::SafeSigner::from_memsafe(raw_key)?;
|
||||
|
||||
self.engine
|
||||
.evaluate_transaction(wallet.id, client_id, transaction.clone(), RunKind::Execution)
|
||||
.evaluate_transaction(
|
||||
wallet.id,
|
||||
client_id,
|
||||
transaction.clone(),
|
||||
RunKind::Execution,
|
||||
)
|
||||
.await?;
|
||||
|
||||
use alloy::network::TxSignerSync as _;
|
||||
|
||||
@@ -313,7 +313,7 @@ impl KeyHolder {
|
||||
current_nonce: nonce.to_vec(),
|
||||
schema_version: 1,
|
||||
associated_root_key_id: *root_key_history_id,
|
||||
created_at: Utc::now().into()
|
||||
created_at: Utc::now().into(),
|
||||
})
|
||||
.returning(schema::aead_encrypted::id)
|
||||
.get_result(&mut conn)
|
||||
@@ -346,7 +346,7 @@ impl KeyHolder {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use diesel::SelectableHelper;
|
||||
|
||||
|
||||
use diesel_async::RunQueryDsl;
|
||||
use memsafe::MemSafe;
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use arbiter_proto::proto::user_agent::{
|
||||
AuthChallengeRequest, AuthChallengeSolution, KeyType as ProtoKeyType, UserAgentRequest,
|
||||
user_agent_request::Payload as UserAgentRequestPayload,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::actors::user_agent::{
|
||||
UserAgentConnection,
|
||||
auth::state::{AuthContext, AuthPublicKey, AuthStateMachine},
|
||||
Request, UserAgentConnection,
|
||||
auth::state::{AuthContext, AuthStateMachine},
|
||||
AuthPublicKey,
|
||||
session::UserAgentSession,
|
||||
};
|
||||
|
||||
@@ -37,54 +34,20 @@ pub enum Error {
|
||||
mod state;
|
||||
use state::*;
|
||||
|
||||
fn parse_pubkey(key_type: ProtoKeyType, pubkey: Vec<u8>) -> Result<AuthPublicKey, Error> {
|
||||
match key_type {
|
||||
// UNSPECIFIED treated as Ed25519 for backward compatibility
|
||||
ProtoKeyType::Unspecified | ProtoKeyType::Ed25519 => {
|
||||
let pubkey_bytes = pubkey.as_array().ok_or(Error::InvalidClientPubkeyLength)?;
|
||||
let key = ed25519_dalek::VerifyingKey::from_bytes(pubkey_bytes)
|
||||
.map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
|
||||
Ok(AuthPublicKey::Ed25519(key))
|
||||
}
|
||||
ProtoKeyType::EcdsaSecp256k1 => {
|
||||
// Public key is sent as 33-byte SEC1 compressed point
|
||||
let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(&pubkey)
|
||||
.map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
|
||||
Ok(AuthPublicKey::EcdsaSecp256k1(key))
|
||||
}
|
||||
ProtoKeyType::Rsa => {
|
||||
use rsa::pkcs8::DecodePublicKey as _;
|
||||
let key = rsa::RsaPublicKey::from_public_key_der(&pubkey)
|
||||
.map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
|
||||
Ok(AuthPublicKey::Rsa(key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_auth_event(payload: UserAgentRequestPayload) -> Result<AuthEvents, Error> {
|
||||
fn parse_auth_event(payload: Request) -> Result<AuthEvents, Error> {
|
||||
match payload {
|
||||
UserAgentRequestPayload::AuthChallengeRequest(AuthChallengeRequest {
|
||||
Request::AuthChallengeRequest {
|
||||
pubkey,
|
||||
bootstrap_token: None,
|
||||
key_type,
|
||||
}) => {
|
||||
let kt = ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified);
|
||||
Ok(AuthEvents::AuthRequest(ChallengeRequest {
|
||||
pubkey: parse_pubkey(kt, pubkey)?,
|
||||
}))
|
||||
}
|
||||
UserAgentRequestPayload::AuthChallengeRequest(AuthChallengeRequest {
|
||||
} => Ok(AuthEvents::AuthRequest(ChallengeRequest { pubkey })),
|
||||
Request::AuthChallengeRequest {
|
||||
pubkey,
|
||||
bootstrap_token: Some(token),
|
||||
key_type,
|
||||
}) => {
|
||||
let kt = ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified);
|
||||
Ok(AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest {
|
||||
pubkey: parse_pubkey(kt, pubkey)?,
|
||||
token,
|
||||
}))
|
||||
}
|
||||
UserAgentRequestPayload::AuthChallengeSolution(AuthChallengeSolution { signature }) => {
|
||||
} => Ok(AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest {
|
||||
pubkey,
|
||||
token,
|
||||
})),
|
||||
Request::AuthChallengeSolution { signature } => {
|
||||
Ok(AuthEvents::ReceivedSolution(ChallengeSolution {
|
||||
solution: signature,
|
||||
}))
|
||||
@@ -99,10 +62,7 @@ pub async fn authenticate(props: &mut UserAgentConnection) -> Result<AuthPublicK
|
||||
loop {
|
||||
// `state` holds a mutable reference to `props` so we can't access it directly here
|
||||
let transport = state.context_mut().conn.transport.as_mut();
|
||||
let Some(UserAgentRequest {
|
||||
payload: Some(payload),
|
||||
}) = transport.recv().await
|
||||
else {
|
||||
let Some(payload) = transport.recv().await else {
|
||||
return Err(Error::Transport);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,53 +1,16 @@
|
||||
use arbiter_proto::proto::user_agent::{
|
||||
AuthChallenge, AuthOk, UserAgentResponse,
|
||||
user_agent_response::Payload as UserAgentResponsePayload,
|
||||
};
|
||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use tracing::error;
|
||||
|
||||
use super::Error;
|
||||
use crate::{
|
||||
actors::{bootstrap::ConsumeToken, user_agent::UserAgentConnection},
|
||||
db::{models::KeyType, schema},
|
||||
actors::{
|
||||
bootstrap::ConsumeToken,
|
||||
user_agent::{AuthPublicKey, Response, UserAgentConnection},
|
||||
},
|
||||
db::schema,
|
||||
};
|
||||
|
||||
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
|
||||
#[derive(Clone)]
|
||||
pub enum AuthPublicKey {
|
||||
Ed25519(ed25519_dalek::VerifyingKey),
|
||||
/// Compressed SEC1 public key; signature bytes are raw 64-byte (r||s).
|
||||
EcdsaSecp256k1(k256::ecdsa::VerifyingKey),
|
||||
/// RSA-2048+ public key (Windows Hello / KeyCredentialManager); signature bytes are PSS+SHA-256.
|
||||
Rsa(rsa::RsaPublicKey),
|
||||
}
|
||||
|
||||
impl AuthPublicKey {
|
||||
/// Canonical bytes stored in DB and echoed back in the challenge.
|
||||
/// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI.
|
||||
pub fn to_stored_bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
AuthPublicKey::Ed25519(k) => k.to_bytes().to_vec(),
|
||||
// SEC1 compressed (33 bytes) is the natural compact format for secp256k1
|
||||
AuthPublicKey::EcdsaSecp256k1(k) => k.to_encoded_point(true).as_bytes().to_vec(),
|
||||
AuthPublicKey::Rsa(k) => {
|
||||
use rsa::pkcs8::EncodePublicKey as _;
|
||||
k.to_public_key_der()
|
||||
.expect("rsa SPKI encoding is infallible")
|
||||
.to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_type(&self) -> KeyType {
|
||||
match self {
|
||||
AuthPublicKey::Ed25519(_) => KeyType::Ed25519,
|
||||
AuthPublicKey::EcdsaSecp256k1(_) => KeyType::EcdsaSecp256k1,
|
||||
AuthPublicKey::Rsa(_) => KeyType::Rsa,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChallengeRequest {
|
||||
pub pubkey: AuthPublicKey,
|
||||
}
|
||||
@@ -58,7 +21,7 @@ pub struct BootstrapAuthRequest {
|
||||
}
|
||||
|
||||
pub struct ChallengeContext {
|
||||
pub challenge: AuthChallenge,
|
||||
pub challenge_nonce: i32,
|
||||
pub key: AuthPublicKey,
|
||||
}
|
||||
|
||||
@@ -155,16 +118,9 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
||||
let stored_bytes = pubkey.to_stored_bytes();
|
||||
let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
|
||||
|
||||
let challenge = AuthChallenge {
|
||||
pubkey: stored_bytes,
|
||||
nonce,
|
||||
};
|
||||
|
||||
self.conn
|
||||
.transport
|
||||
.send(Ok(UserAgentResponse {
|
||||
payload: Some(UserAgentResponsePayload::AuthChallenge(challenge.clone())),
|
||||
}))
|
||||
.send(Ok(Response::AuthChallenge { nonce }))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Failed to send auth challenge");
|
||||
@@ -172,7 +128,7 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
||||
})?;
|
||||
|
||||
Ok(ChallengeContext {
|
||||
challenge,
|
||||
challenge_nonce: nonce,
|
||||
key: pubkey,
|
||||
})
|
||||
}
|
||||
@@ -217,10 +173,10 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
||||
#[allow(clippy::unused_unit)]
|
||||
async fn verify_solution(
|
||||
&mut self,
|
||||
ChallengeContext { challenge, key }: &ChallengeContext,
|
||||
ChallengeContext { challenge_nonce, key }: &ChallengeContext,
|
||||
ChallengeSolution { solution }: ChallengeSolution,
|
||||
) -> Result<AuthPublicKey, Self::Error> {
|
||||
let formatted = arbiter_proto::format_challenge(challenge.nonce, &challenge.pubkey);
|
||||
let formatted = arbiter_proto::format_challenge(*challenge_nonce, &key.to_stored_bytes());
|
||||
|
||||
let valid = match key {
|
||||
AuthPublicKey::Ed25519(vk) => {
|
||||
@@ -252,9 +208,7 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
||||
if valid {
|
||||
self.conn
|
||||
.transport
|
||||
.send(Ok(UserAgentResponse {
|
||||
payload: Some(UserAgentResponsePayload::AuthOk(AuthOk {})),
|
||||
}))
|
||||
.send(Ok(Response::AuthOk))
|
||||
.await
|
||||
.map_err(|_| Error::Transport)?;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
use arbiter_proto::{
|
||||
proto::user_agent::{UserAgentRequest, UserAgentResponse},
|
||||
transport::Bi,
|
||||
};
|
||||
use alloy::primitives::Address;
|
||||
use arbiter_proto::transport::Bi;
|
||||
use kameo::actor::Spawn as _;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
actors::{GlobalActors, user_agent::session::UserAgentSession},
|
||||
db::{self},
|
||||
actors::{GlobalActors, evm, user_agent::session::UserAgentSession},
|
||||
db::{self, models::KeyType},
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
pub enum TransportResponseError {
|
||||
#[error("Expected message with payload")]
|
||||
MissingRequestPayload,
|
||||
#[error("Unexpected request payload")]
|
||||
UnexpectedRequestPayload,
|
||||
#[error("Invalid state for unseal encrypted key")]
|
||||
@@ -30,8 +26,106 @@ pub enum TransportResponseError {
|
||||
ConnectionRegistrationFailed,
|
||||
}
|
||||
|
||||
pub type Transport =
|
||||
Box<dyn Bi<UserAgentRequest, Result<UserAgentResponse, TransportResponseError>> + Send>;
|
||||
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AuthPublicKey {
|
||||
Ed25519(ed25519_dalek::VerifyingKey),
|
||||
/// Compressed SEC1 public key; signature bytes are raw 64-byte (r||s).
|
||||
EcdsaSecp256k1(k256::ecdsa::VerifyingKey),
|
||||
/// RSA-2048+ public key (Windows Hello / KeyCredentialManager); signature bytes are PSS+SHA-256.
|
||||
Rsa(rsa::RsaPublicKey),
|
||||
}
|
||||
|
||||
impl AuthPublicKey {
|
||||
/// Canonical bytes stored in DB and echoed back in the challenge.
|
||||
/// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI.
|
||||
pub fn to_stored_bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
AuthPublicKey::Ed25519(k) => k.to_bytes().to_vec(),
|
||||
// SEC1 compressed (33 bytes) is the natural compact format for secp256k1
|
||||
AuthPublicKey::EcdsaSecp256k1(k) => k.to_encoded_point(true).as_bytes().to_vec(),
|
||||
AuthPublicKey::Rsa(k) => {
|
||||
use rsa::pkcs8::EncodePublicKey as _;
|
||||
k.to_public_key_der()
|
||||
.expect("rsa SPKI encoding is infallible")
|
||||
.to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_type(&self) -> KeyType {
|
||||
match self {
|
||||
AuthPublicKey::Ed25519(_) => KeyType::Ed25519,
|
||||
AuthPublicKey::EcdsaSecp256k1(_) => KeyType::EcdsaSecp256k1,
|
||||
AuthPublicKey::Rsa(_) => KeyType::Rsa,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UnsealError {
|
||||
InvalidKey,
|
||||
Unbootstrapped,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BootstrapError {
|
||||
AlreadyBootstrapped,
|
||||
InvalidKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VaultState {
|
||||
Unbootstrapped,
|
||||
Sealed,
|
||||
Unsealed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Request {
|
||||
AuthChallengeRequest {
|
||||
pubkey: AuthPublicKey,
|
||||
bootstrap_token: Option<String>,
|
||||
},
|
||||
AuthChallengeSolution {
|
||||
signature: Vec<u8>,
|
||||
},
|
||||
UnsealStart {
|
||||
client_pubkey: x25519_dalek::PublicKey,
|
||||
},
|
||||
UnsealEncryptedKey {
|
||||
nonce: Vec<u8>,
|
||||
ciphertext: Vec<u8>,
|
||||
associated_data: Vec<u8>,
|
||||
},
|
||||
BootstrapEncryptedKey {
|
||||
nonce: Vec<u8>,
|
||||
ciphertext: Vec<u8>,
|
||||
associated_data: Vec<u8>,
|
||||
},
|
||||
QueryVaultState,
|
||||
EvmWalletCreate,
|
||||
EvmWalletList,
|
||||
ClientConnectionResponse {
|
||||
approved: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Response {
|
||||
AuthChallenge { nonce: i32 },
|
||||
AuthOk,
|
||||
UnsealStartResponse { server_pubkey: x25519_dalek::PublicKey },
|
||||
UnsealResult(Result<(), UnsealError>),
|
||||
BootstrapResult(Result<(), BootstrapError>),
|
||||
VaultState(VaultState),
|
||||
ClientConnectionRequest { pubkey: ed25519_dalek::VerifyingKey },
|
||||
ClientConnectionCancel,
|
||||
EvmWalletCreate(Result<(), evm::Error>),
|
||||
EvmWalletList(Vec<Address>),
|
||||
}
|
||||
|
||||
pub type Transport = Box<dyn Bi<Request, Result<Response, TransportResponseError>> + Send>;
|
||||
|
||||
pub struct UserAgentConnection {
|
||||
db: db::DatabasePool,
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
use std::{ops::DerefMut, sync::Mutex};
|
||||
|
||||
use arbiter_proto::proto::{
|
||||
evm as evm_proto,
|
||||
user_agent::{
|
||||
BootstrapEncryptedKey, BootstrapResult, ClientConnectionCancel, ClientConnectionRequest,
|
||||
UnsealEncryptedKey, UnsealResult, UnsealStart, UnsealStartResponse, UserAgentRequest,
|
||||
UserAgentResponse, user_agent_request::Payload as UserAgentRequestPayload,
|
||||
user_agent_response::Payload as UserAgentResponsePayload,
|
||||
},
|
||||
};
|
||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||
use ed25519_dalek::VerifyingKey;
|
||||
use kameo::{Actor, error::SendError, messages, prelude::Context};
|
||||
@@ -21,7 +12,10 @@ use crate::actors::{
|
||||
evm::{Generate, ListWallets},
|
||||
keyholder::{self, Bootstrap, TryUnseal},
|
||||
router::RegisterUserAgent,
|
||||
user_agent::{TransportResponseError, UserAgentConnection},
|
||||
user_agent::{
|
||||
BootstrapError, Request, Response, TransportResponseError, UnsealError,
|
||||
UserAgentConnection, VaultState,
|
||||
},
|
||||
};
|
||||
|
||||
mod state;
|
||||
@@ -60,21 +54,17 @@ impl UserAgentSession {
|
||||
|
||||
async fn send_msg<Reply: kameo::Reply>(
|
||||
&mut self,
|
||||
msg: UserAgentResponsePayload,
|
||||
msg: Response,
|
||||
_ctx: &mut Context<Self, Reply>,
|
||||
) -> Result<(), Error> {
|
||||
self.props
|
||||
.transport
|
||||
.send(Ok(response(msg)))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
error!(
|
||||
actor = "useragent",
|
||||
reason = "channel closed",
|
||||
"send.failed"
|
||||
);
|
||||
Error::ConnectionLost
|
||||
})
|
||||
self.props.transport.send(Ok(msg)).await.map_err(|_| {
|
||||
error!(
|
||||
actor = "useragent",
|
||||
reason = "channel closed",
|
||||
"send.failed"
|
||||
);
|
||||
Error::ConnectionLost
|
||||
})
|
||||
}
|
||||
|
||||
async fn expect_msg<Extractor, Msg, Reply>(
|
||||
@@ -83,7 +73,7 @@ impl UserAgentSession {
|
||||
ctx: &mut Context<Self, Reply>,
|
||||
) -> Result<Msg, Error>
|
||||
where
|
||||
Extractor: FnOnce(UserAgentRequestPayload) -> Option<Msg>,
|
||||
Extractor: FnOnce(Request) -> Option<Msg>,
|
||||
Reply: kameo::Reply,
|
||||
{
|
||||
let msg = self.props.transport.recv().await.ok_or_else(|| {
|
||||
@@ -96,7 +86,7 @@ impl UserAgentSession {
|
||||
Error::ConnectionLost
|
||||
})?;
|
||||
|
||||
msg.payload.and_then(extractor).ok_or_else(|| {
|
||||
extractor(msg).ok_or_else(|| {
|
||||
error!(
|
||||
actor = "useragent",
|
||||
reason = "unexpected message",
|
||||
@@ -119,18 +109,16 @@ impl UserAgentSession {
|
||||
ctx: &mut Context<Self, Result<bool, Error>>,
|
||||
) -> Result<bool, Error> {
|
||||
self.send_msg(
|
||||
UserAgentResponsePayload::ClientConnectionRequest(ClientConnectionRequest {
|
||||
pubkey: client_pubkey.as_bytes().to_vec(),
|
||||
}),
|
||||
Response::ClientConnectionRequest {
|
||||
pubkey: client_pubkey,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let extractor = |msg| {
|
||||
if let UserAgentRequestPayload::ClientConnectionResponse(client_connection_response) =
|
||||
msg
|
||||
{
|
||||
Some(client_connection_response)
|
||||
if let Request::ClientConnectionResponse { approved } = msg {
|
||||
Some(approved)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -140,53 +128,55 @@ impl UserAgentSession {
|
||||
_ = cancel_flag.changed() => {
|
||||
info!(actor = "useragent", "client connection approval cancelled");
|
||||
self.send_msg(
|
||||
UserAgentResponsePayload::ClientConnectionCancel(ClientConnectionCancel {}),
|
||||
Response::ClientConnectionCancel,
|
||||
ctx,
|
||||
).await?;
|
||||
Ok(false)
|
||||
}
|
||||
result = self.expect_msg(extractor, ctx) => {
|
||||
let result = result?;
|
||||
info!(actor = "useragent", "received client connection approval result: approved={}", result.approved);
|
||||
Ok(result.approved)
|
||||
info!(actor = "useragent", "received client connection approval result: approved={}", result);
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserAgentSession {
|
||||
pub async fn process_transport_inbound(&mut self, req: UserAgentRequest) -> Output {
|
||||
let msg = req.payload.ok_or_else(|| {
|
||||
error!(actor = "useragent", "Received message with no payload");
|
||||
TransportResponseError::MissingRequestPayload
|
||||
})?;
|
||||
|
||||
match msg {
|
||||
UserAgentRequestPayload::UnsealStart(unseal_start) => {
|
||||
self.handle_unseal_request(unseal_start).await
|
||||
pub async fn process_transport_inbound(&mut self, req: Request) -> Output {
|
||||
match req {
|
||||
Request::UnsealStart { client_pubkey } => {
|
||||
self.handle_unseal_request(client_pubkey).await
|
||||
}
|
||||
UserAgentRequestPayload::UnsealEncryptedKey(unseal_encrypted_key) => {
|
||||
self.handle_unseal_encrypted_key(unseal_encrypted_key).await
|
||||
}
|
||||
UserAgentRequestPayload::BootstrapEncryptedKey(bootstrap_encrypted_key) => {
|
||||
self.handle_bootstrap_encrypted_key(bootstrap_encrypted_key)
|
||||
Request::UnsealEncryptedKey {
|
||||
nonce,
|
||||
ciphertext,
|
||||
associated_data,
|
||||
} => {
|
||||
self.handle_unseal_encrypted_key(nonce, ciphertext, associated_data)
|
||||
.await
|
||||
}
|
||||
UserAgentRequestPayload::QueryVaultState(_) => self.handle_query_vault_state().await,
|
||||
UserAgentRequestPayload::EvmWalletCreate(_) => self.handle_evm_wallet_create().await,
|
||||
UserAgentRequestPayload::EvmWalletList(_) => self.handle_evm_wallet_list().await,
|
||||
_ => Err(TransportResponseError::UnexpectedRequestPayload),
|
||||
Request::BootstrapEncryptedKey {
|
||||
nonce,
|
||||
ciphertext,
|
||||
associated_data,
|
||||
} => {
|
||||
self.handle_bootstrap_encrypted_key(nonce, ciphertext, associated_data)
|
||||
.await
|
||||
}
|
||||
Request::QueryVaultState => self.handle_query_vault_state().await,
|
||||
Request::EvmWalletCreate => self.handle_evm_wallet_create().await,
|
||||
Request::EvmWalletList => self.handle_evm_wallet_list().await,
|
||||
Request::AuthChallengeRequest { .. }
|
||||
| Request::AuthChallengeSolution { .. }
|
||||
| Request::ClientConnectionResponse { .. } => {
|
||||
Err(TransportResponseError::UnexpectedRequestPayload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Output = Result<UserAgentResponse, TransportResponseError>;
|
||||
|
||||
fn response(payload: UserAgentResponsePayload) -> UserAgentResponse {
|
||||
UserAgentResponse {
|
||||
payload: Some(payload),
|
||||
}
|
||||
}
|
||||
type Output = Result<Response, TransportResponseError>;
|
||||
|
||||
impl UserAgentSession {
|
||||
fn take_unseal_secret(
|
||||
@@ -242,37 +232,31 @@ impl UserAgentSession {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_unseal_request(&mut self, req: UnsealStart) -> Output {
|
||||
async fn handle_unseal_request(&mut self, client_pubkey: x25519_dalek::PublicKey) -> Output {
|
||||
let secret = EphemeralSecret::random();
|
||||
let public_key = PublicKey::from(&secret);
|
||||
|
||||
let client_pubkey_bytes: [u8; 32] = req
|
||||
.client_pubkey
|
||||
.try_into()
|
||||
.map_err(|_| TransportResponseError::InvalidClientPubkeyLength)?;
|
||||
|
||||
let client_public_key = PublicKey::from(client_pubkey_bytes);
|
||||
|
||||
self.transition(UserAgentEvents::UnsealRequest(UnsealContext {
|
||||
secret: Mutex::new(Some(secret)),
|
||||
client_public_key,
|
||||
client_public_key: client_pubkey
|
||||
}))?;
|
||||
|
||||
Ok(response(UserAgentResponsePayload::UnsealStartResponse(
|
||||
UnsealStartResponse {
|
||||
server_pubkey: public_key.as_bytes().to_vec(),
|
||||
},
|
||||
)))
|
||||
Ok(Response::UnsealStartResponse {
|
||||
server_pubkey: public_key,
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_unseal_encrypted_key(&mut self, req: UnsealEncryptedKey) -> Output {
|
||||
async fn handle_unseal_encrypted_key(
|
||||
&mut self,
|
||||
nonce: Vec<u8>,
|
||||
ciphertext: Vec<u8>,
|
||||
associated_data: Vec<u8>,
|
||||
) -> Output {
|
||||
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
|
||||
Ok(values) => values,
|
||||
Err(TransportResponseError::StateTransitionFailed) => {
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
return Ok(response(UserAgentResponsePayload::UnsealResult(
|
||||
UnsealResult::InvalidKey.into(),
|
||||
)));
|
||||
return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)));
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
@@ -280,16 +264,14 @@ impl UserAgentSession {
|
||||
let seal_key_buffer = match Self::decrypt_client_key_material(
|
||||
ephemeral_secret,
|
||||
client_public_key,
|
||||
&req.nonce,
|
||||
&req.ciphertext,
|
||||
&req.associated_data,
|
||||
&nonce,
|
||||
&ciphertext,
|
||||
&associated_data,
|
||||
) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(()) => {
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
return Ok(response(UserAgentResponsePayload::UnsealResult(
|
||||
UnsealResult::InvalidKey.into(),
|
||||
)));
|
||||
return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -305,22 +287,16 @@ impl UserAgentSession {
|
||||
Ok(_) => {
|
||||
info!("Successfully unsealed key with client-provided key");
|
||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||
Ok(response(UserAgentResponsePayload::UnsealResult(
|
||||
UnsealResult::Success.into(),
|
||||
)))
|
||||
Ok(Response::UnsealResult(Ok(())))
|
||||
}
|
||||
Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => {
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
Ok(response(UserAgentResponsePayload::UnsealResult(
|
||||
UnsealResult::InvalidKey.into(),
|
||||
)))
|
||||
Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)))
|
||||
}
|
||||
Err(SendError::HandlerError(err)) => {
|
||||
error!(?err, "Keyholder failed to unseal key");
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
Ok(response(UserAgentResponsePayload::UnsealResult(
|
||||
UnsealResult::InvalidKey.into(),
|
||||
)))
|
||||
Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)))
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to send unseal request to keyholder");
|
||||
@@ -330,14 +306,17 @@ impl UserAgentSession {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_bootstrap_encrypted_key(&mut self, req: BootstrapEncryptedKey) -> Output {
|
||||
async fn handle_bootstrap_encrypted_key(
|
||||
&mut self,
|
||||
nonce: Vec<u8>,
|
||||
ciphertext: Vec<u8>,
|
||||
associated_data: Vec<u8>,
|
||||
) -> Output {
|
||||
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
|
||||
Ok(values) => values,
|
||||
Err(TransportResponseError::StateTransitionFailed) => {
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
return Ok(response(UserAgentResponsePayload::BootstrapResult(
|
||||
BootstrapResult::InvalidKey.into(),
|
||||
)));
|
||||
return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)));
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
@@ -345,16 +324,14 @@ impl UserAgentSession {
|
||||
let seal_key_buffer = match Self::decrypt_client_key_material(
|
||||
ephemeral_secret,
|
||||
client_public_key,
|
||||
&req.nonce,
|
||||
&req.ciphertext,
|
||||
&req.associated_data,
|
||||
&nonce,
|
||||
&ciphertext,
|
||||
&associated_data,
|
||||
) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(()) => {
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
return Ok(response(UserAgentResponsePayload::BootstrapResult(
|
||||
BootstrapResult::InvalidKey.into(),
|
||||
)));
|
||||
return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -370,22 +347,18 @@ impl UserAgentSession {
|
||||
Ok(_) => {
|
||||
info!("Successfully bootstrapped vault with client-provided key");
|
||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||
Ok(response(UserAgentResponsePayload::BootstrapResult(
|
||||
BootstrapResult::Success.into(),
|
||||
)))
|
||||
Ok(Response::BootstrapResult(Ok(())))
|
||||
}
|
||||
Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => {
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
Ok(response(UserAgentResponsePayload::BootstrapResult(
|
||||
BootstrapResult::AlreadyBootstrapped.into(),
|
||||
Ok(Response::BootstrapResult(Err(
|
||||
BootstrapError::AlreadyBootstrapped,
|
||||
)))
|
||||
}
|
||||
Err(SendError::HandlerError(err)) => {
|
||||
error!(?err, "Keyholder failed to bootstrap vault");
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
Ok(response(UserAgentResponsePayload::BootstrapResult(
|
||||
BootstrapResult::InvalidKey.into(),
|
||||
)))
|
||||
Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)))
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to send bootstrap request to keyholder");
|
||||
@@ -399,7 +372,6 @@ impl UserAgentSession {
|
||||
impl UserAgentSession {
|
||||
async fn handle_query_vault_state(&mut self) -> Output {
|
||||
use crate::actors::keyholder::{GetState, StateDiscriminants};
|
||||
use arbiter_proto::proto::user_agent::VaultState;
|
||||
|
||||
let vault_state = match self.props.actors.key_holder.ask(GetState {}).await {
|
||||
Ok(StateDiscriminants::Unbootstrapped) => VaultState::Unbootstrapped,
|
||||
@@ -407,70 +379,34 @@ impl UserAgentSession {
|
||||
Ok(StateDiscriminants::Unsealed) => VaultState::Unsealed,
|
||||
Err(err) => {
|
||||
error!(?err, actor = "useragent", "keyholder.query.failed");
|
||||
VaultState::Error
|
||||
return Err(TransportResponseError::KeyHolderActorUnreachable);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(response(UserAgentResponsePayload::VaultState(
|
||||
vault_state.into(),
|
||||
)))
|
||||
Ok(Response::VaultState(vault_state))
|
||||
}
|
||||
}
|
||||
|
||||
impl UserAgentSession {
|
||||
async fn handle_evm_wallet_create(&mut self) -> Output {
|
||||
use evm_proto::wallet_create_response::Result as CreateResult;
|
||||
|
||||
let result = match self.props.actors.evm.ask(Generate {}).await {
|
||||
Ok(address) => CreateResult::Wallet(evm_proto::WalletEntry {
|
||||
address: address.as_slice().to_vec(),
|
||||
}),
|
||||
Err(err) => CreateResult::Error(map_evm_error("wallet create", err).into()),
|
||||
Ok(_address) => return Ok(Response::EvmWalletCreate(Ok(()))),
|
||||
Err(SendError::HandlerError(err)) => Err(err),
|
||||
Err(err) => {
|
||||
error!(?err, "EVM actor unreachable during wallet create");
|
||||
return Err(TransportResponseError::KeyHolderActorUnreachable);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(response(UserAgentResponsePayload::EvmWalletCreate(
|
||||
evm_proto::WalletCreateResponse {
|
||||
result: Some(result),
|
||||
},
|
||||
)))
|
||||
Ok(Response::EvmWalletCreate(result))
|
||||
}
|
||||
|
||||
async fn handle_evm_wallet_list(&mut self) -> Output {
|
||||
use evm_proto::wallet_list_response::Result as ListResult;
|
||||
|
||||
let result = match self.props.actors.evm.ask(ListWallets {}).await {
|
||||
Ok(wallets) => ListResult::Wallets(evm_proto::WalletList {
|
||||
wallets: wallets
|
||||
.into_iter()
|
||||
.map(|addr| evm_proto::WalletEntry {
|
||||
address: addr.as_slice().to_vec(),
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
Err(err) => ListResult::Error(map_evm_error("wallet list", err).into()),
|
||||
};
|
||||
|
||||
Ok(response(UserAgentResponsePayload::EvmWalletList(
|
||||
evm_proto::WalletListResponse {
|
||||
result: Some(result),
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn map_evm_error<M>(op: &str, err: SendError<M, crate::actors::evm::Error>) -> evm_proto::EvmError {
|
||||
use crate::actors::{evm::Error as EvmError, keyholder::Error as KhError};
|
||||
match err {
|
||||
SendError::HandlerError(EvmError::Keyholder(KhError::NotBootstrapped)) => {
|
||||
evm_proto::EvmError::VaultSealed
|
||||
}
|
||||
SendError::HandlerError(err) => {
|
||||
error!(?err, "EVM {op} failed");
|
||||
evm_proto::EvmError::Internal
|
||||
}
|
||||
_ => {
|
||||
error!("EVM actor unreachable during {op}");
|
||||
evm_proto::EvmError::Internal
|
||||
match self.props.actors.evm.ask(ListWallets {}).await {
|
||||
Ok(wallets) => Ok(Response::EvmWalletList(wallets)),
|
||||
Err(err) => {
|
||||
error!(?err, "EVM wallet list failed");
|
||||
Err(TransportResponseError::KeyHolderActorUnreachable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use rcgen::{
|
||||
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
|
||||
IsCa, Issuer, KeyPair, KeyUsagePurpose,
|
||||
};
|
||||
use rustls::pki_types::{pem::PemObject};
|
||||
use rustls::pki_types::pem::PemObject;
|
||||
use thiserror::Error;
|
||||
use tonic::transport::CertificateDer;
|
||||
|
||||
@@ -59,10 +59,7 @@ pub enum InitError {
|
||||
pub type PemCert = String;
|
||||
|
||||
pub fn encode_cert_to_pem(cert: &CertificateDer) -> PemCert {
|
||||
pem::encode_config(
|
||||
&Pem::new("CERTIFICATE", cert.to_vec()),
|
||||
ENCODE_CONFIG,
|
||||
)
|
||||
pem::encode_config(&Pem::new("CERTIFICATE", cert.to_vec()), ENCODE_CONFIG)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
|
||||
@@ -117,9 +117,7 @@ async fn check_shared_constraints(
|
||||
let now = Utc::now();
|
||||
|
||||
// Validity window
|
||||
if shared.valid_from.is_some_and(|t| now < t)
|
||||
|| shared.valid_until.is_some_and(|t| now > t)
|
||||
{
|
||||
if shared.valid_from.is_some_and(|t| now < t) || shared.valid_until.is_some_and(|t| now > t) {
|
||||
violations.push(EvalViolation::InvalidTime);
|
||||
}
|
||||
|
||||
@@ -127,9 +125,9 @@ async fn check_shared_constraints(
|
||||
let fee_exceeded = shared
|
||||
.max_gas_fee_per_gas
|
||||
.is_some_and(|cap| U256::from(context.max_fee_per_gas) > cap);
|
||||
let priority_exceeded = shared.max_priority_fee_per_gas.is_some_and(|cap| {
|
||||
U256::from(context.max_priority_fee_per_gas) > cap
|
||||
});
|
||||
let priority_exceeded = shared
|
||||
.max_priority_fee_per_gas
|
||||
.is_some_and(|cap| U256::from(context.max_priority_fee_per_gas) > cap);
|
||||
if fee_exceeded || priority_exceeded {
|
||||
violations.push(EvalViolation::GasLimitExceeded {
|
||||
max_gas_fee_per_gas: shared.max_gas_fee_per_gas,
|
||||
|
||||
@@ -73,7 +73,6 @@ pub struct Grant<PolicySettings> {
|
||||
pub settings: PolicySettings,
|
||||
}
|
||||
|
||||
|
||||
pub trait Policy: Sized {
|
||||
type Settings: Send + Sync + 'static + Into<SpecificGrant>;
|
||||
type Meaning: Display + std::fmt::Debug + Send + Sync + 'static + Into<SpecificMeaning>;
|
||||
|
||||
@@ -9,9 +9,7 @@ use crate::db::{
|
||||
schema::{evm_basic_grant, evm_transaction_log},
|
||||
};
|
||||
use crate::evm::{
|
||||
policies::{
|
||||
EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, VolumeRateLimit,
|
||||
},
|
||||
policies::{EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, VolumeRateLimit},
|
||||
utils,
|
||||
};
|
||||
|
||||
|
||||
@@ -140,10 +140,18 @@ async fn evaluate_rejects_nonzero_eth_value() {
|
||||
let mut context = ctx(DAI, calldata);
|
||||
context.value = U256::from(1u64); // ETH attached to an ERC-20 call
|
||||
|
||||
let m = TokenTransfer::analyze(&EvalContext { value: U256::ZERO, ..context.clone() })
|
||||
let m = TokenTransfer::analyze(&EvalContext {
|
||||
value: U256::ZERO,
|
||||
..context.clone()
|
||||
})
|
||||
.unwrap();
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
|
||||
assert!(v.iter().any(|e| matches!(e, EvalViolation::InvalidTransactionType)));
|
||||
assert!(
|
||||
v.iter()
|
||||
.any(|e| matches!(e, EvalViolation::InvalidTransactionType))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -160,7 +168,9 @@ async fn evaluate_passes_any_recipient_when_no_restriction() {
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||
let context = ctx(DAI, calldata);
|
||||
let m = TokenTransfer::analyze(&context).unwrap();
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(v.is_empty());
|
||||
}
|
||||
|
||||
@@ -178,7 +188,9 @@ async fn evaluate_passes_matching_restricted_recipient() {
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||
let context = ctx(DAI, calldata);
|
||||
let m = TokenTransfer::analyze(&context).unwrap();
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(v.is_empty());
|
||||
}
|
||||
|
||||
@@ -196,8 +208,13 @@ async fn evaluate_rejects_wrong_restricted_recipient() {
|
||||
let calldata = transfer_calldata(OTHER, U256::from(100u64));
|
||||
let context = ctx(DAI, calldata);
|
||||
let m = TokenTransfer::analyze(&context).unwrap();
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
|
||||
assert!(v.iter().any(|e| matches!(e, EvalViolation::InvalidTarget { .. })));
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
v.iter()
|
||||
.any(|e| matches!(e, EvalViolation::InvalidTarget { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -207,7 +224,9 @@ async fn evaluate_passes_volume_within_limit() {
|
||||
|
||||
let basic = insert_basic(&mut conn, false).await;
|
||||
let settings = make_settings(None, Some(1_000));
|
||||
let grant_id = TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
|
||||
let grant_id = TokenTransfer::create_grant(&basic, &settings, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Record a past transfer of 500 (within 1000 limit)
|
||||
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
|
||||
@@ -224,12 +243,22 @@ async fn evaluate_passes_volume_within_limit() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let grant = Grant { id: grant_id, shared_grant_id: basic.id, shared: shared(), settings };
|
||||
let grant = Grant {
|
||||
id: grant_id,
|
||||
shared_grant_id: basic.id,
|
||||
shared: shared(),
|
||||
settings,
|
||||
};
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||
let context = ctx(DAI, calldata);
|
||||
let m = TokenTransfer::analyze(&context).unwrap();
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
|
||||
assert!(!v.iter().any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded)));
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!v.iter()
|
||||
.any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -239,7 +268,9 @@ async fn evaluate_rejects_volume_over_limit() {
|
||||
|
||||
let basic = insert_basic(&mut conn, false).await;
|
||||
let settings = make_settings(None, Some(1_000));
|
||||
let grant_id = TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
|
||||
let grant_id = TokenTransfer::create_grant(&basic, &settings, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
|
||||
insert_into(evm_token_transfer_log::table)
|
||||
@@ -255,12 +286,22 @@ async fn evaluate_rejects_volume_over_limit() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let grant = Grant { id: grant_id, shared_grant_id: basic.id, shared: shared(), settings };
|
||||
let grant = Grant {
|
||||
id: grant_id,
|
||||
shared_grant_id: basic.id,
|
||||
shared: shared(),
|
||||
settings,
|
||||
};
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||
let context = ctx(DAI, calldata);
|
||||
let m = TokenTransfer::analyze(&context).unwrap();
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
|
||||
assert!(v.iter().any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded)));
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
v.iter()
|
||||
.any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -277,8 +318,13 @@ async fn evaluate_no_volume_limits_always_passes() {
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(u64::MAX));
|
||||
let context = ctx(DAI, calldata);
|
||||
let m = TokenTransfer::analyze(&context).unwrap();
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
|
||||
assert!(!v.iter().any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded)));
|
||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!v.iter()
|
||||
.any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded))
|
||||
);
|
||||
}
|
||||
|
||||
// ── try_find_grant ───────────────────────────────────────────────────────
|
||||
@@ -290,7 +336,9 @@ async fn try_find_grant_roundtrip() {
|
||||
|
||||
let basic = insert_basic(&mut conn, false).await;
|
||||
let settings = make_settings(Some(RECIPIENT), Some(5_000));
|
||||
TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
|
||||
TokenTransfer::create_grant(&basic, &settings, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||
let found = TokenTransfer::try_find_grant(&ctx(DAI, calldata), &mut *conn)
|
||||
@@ -312,7 +360,9 @@ async fn try_find_grant_revoked_returns_none() {
|
||||
|
||||
let basic = insert_basic(&mut conn, true).await;
|
||||
let settings = make_settings(None, None);
|
||||
TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
|
||||
TokenTransfer::create_grant(&basic, &settings, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(1u64));
|
||||
let found = TokenTransfer::try_find_grant(&ctx(DAI, calldata), &mut *conn)
|
||||
@@ -328,7 +378,9 @@ async fn try_find_grant_unknown_token_returns_none() {
|
||||
|
||||
let basic = insert_basic(&mut conn, false).await;
|
||||
let settings = make_settings(None, None);
|
||||
TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
|
||||
TokenTransfer::create_grant(&basic, &settings, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Query with a different token contract
|
||||
let calldata = transfer_calldata(RECIPIENT, U256::from(1u64));
|
||||
@@ -355,9 +407,13 @@ async fn find_all_grants_excludes_revoked() {
|
||||
|
||||
let settings = make_settings(None, Some(1_000));
|
||||
let active = insert_basic(&mut conn, false).await;
|
||||
TokenTransfer::create_grant(&active, &settings, &mut *conn).await.unwrap();
|
||||
TokenTransfer::create_grant(&active, &settings, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
let revoked = insert_basic(&mut conn, true).await;
|
||||
TokenTransfer::create_grant(&revoked, &settings, &mut *conn).await.unwrap();
|
||||
TokenTransfer::create_grant(&revoked, &settings, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
|
||||
assert_eq!(all.len(), 1);
|
||||
@@ -370,12 +426,17 @@ async fn find_all_grants_loads_volume_limits() {
|
||||
|
||||
let basic = insert_basic(&mut conn, false).await;
|
||||
let settings = make_settings(None, Some(9_999));
|
||||
TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
|
||||
TokenTransfer::create_grant(&basic, &settings, &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
|
||||
assert_eq!(all.len(), 1);
|
||||
assert_eq!(all[0].settings.volume_limits.len(), 1);
|
||||
assert_eq!(all[0].settings.volume_limits[0].max_volume, U256::from(9_999u64));
|
||||
assert_eq!(
|
||||
all[0].settings.volume_limits[0].max_volume,
|
||||
U256::from(9_999u64)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -388,9 +449,13 @@ async fn find_all_grants_multiple_grants_batch_loaded() {
|
||||
.await
|
||||
.unwrap();
|
||||
let b2 = insert_basic(&mut conn, false).await;
|
||||
TokenTransfer::create_grant(&b2, &make_settings(Some(RECIPIENT), Some(2_000)), &mut *conn)
|
||||
.await
|
||||
.unwrap();
|
||||
TokenTransfer::create_grant(
|
||||
&b2,
|
||||
&make_settings(Some(RECIPIENT), Some(2_000)),
|
||||
&mut *conn,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
|
||||
assert_eq!(all.len(), 2);
|
||||
|
||||
@@ -3,11 +3,11 @@ use std::sync::Mutex;
|
||||
use alloy::{
|
||||
consensus::SignableTransaction,
|
||||
network::{TxSigner, TxSignerSync},
|
||||
primitives::{Address, ChainId, Signature, B256},
|
||||
primitives::{Address, B256, ChainId, Signature},
|
||||
signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId, SigningKey};
|
||||
use k256::ecdsa::{self, RecoveryId, SigningKey, signature::hazmat::PrehashSigner};
|
||||
use memsafe::MemSafe;
|
||||
|
||||
/// An Ethereum signer that stores its secp256k1 secret key inside a
|
||||
@@ -90,10 +90,7 @@ impl SafeSigner {
|
||||
Ok(sig.into())
|
||||
}
|
||||
|
||||
fn sign_tx_inner(
|
||||
&self,
|
||||
tx: &mut dyn SignableTransaction<Signature>,
|
||||
) -> Result<Signature> {
|
||||
fn sign_tx_inner(&self, tx: &mut dyn SignableTransaction<Signature>) -> Result<Signature> {
|
||||
if let Some(chain_id) = self.chain_id
|
||||
&& !tx.set_chain_id_checked(chain_id)
|
||||
{
|
||||
@@ -102,7 +99,8 @@ impl SafeSigner {
|
||||
tx: tx.chain_id().unwrap(),
|
||||
});
|
||||
}
|
||||
self.sign_hash_inner(&tx.signature_hash()).map_err(Error::other)
|
||||
self.sign_hash_inner(&tx.signature_hash())
|
||||
.map_err(Error::other)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
137
server/crates/arbiter-server/src/grpc/client.rs
Normal file
137
server/crates/arbiter-server/src/grpc/client.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use arbiter_proto::{
|
||||
proto::client::{
|
||||
AuthChallenge as ProtoAuthChallenge,
|
||||
AuthChallengeRequest as ProtoAuthChallengeRequest,
|
||||
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthOk as ProtoAuthOk,
|
||||
ClientConnectError, ClientRequest, ClientResponse,
|
||||
client_connect_error::Code as ProtoClientConnectErrorCode,
|
||||
client_request::Payload as ClientRequestPayload,
|
||||
client_response::Payload as ClientResponsePayload,
|
||||
},
|
||||
transport::{Bi, Error as TransportError},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt as _;
|
||||
use tokio::sync::mpsc;
|
||||
use tonic::{Status, Streaming};
|
||||
|
||||
use crate::actors::client::{
|
||||
self, ClientError, ConnectErrorCode, Request as DomainRequest, Response as DomainResponse,
|
||||
};
|
||||
|
||||
pub struct GrpcTransport {
|
||||
sender: mpsc::Sender<Result<ClientResponse, Status>>,
|
||||
receiver: Streaming<ClientRequest>,
|
||||
}
|
||||
|
||||
impl GrpcTransport {
|
||||
pub fn new(
|
||||
sender: mpsc::Sender<Result<ClientResponse, Status>>,
|
||||
receiver: Streaming<ClientRequest>,
|
||||
) -> Self {
|
||||
Self { sender, receiver }
|
||||
}
|
||||
|
||||
fn request_to_domain(request: ClientRequest) -> Result<DomainRequest, Status> {
|
||||
match request.payload {
|
||||
Some(ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest {
|
||||
pubkey,
|
||||
})) => Ok(DomainRequest::AuthChallengeRequest { pubkey }),
|
||||
Some(ClientRequestPayload::AuthChallengeSolution(
|
||||
ProtoAuthChallengeSolution { signature },
|
||||
)) => Ok(DomainRequest::AuthChallengeSolution { signature }),
|
||||
None => Err(Status::invalid_argument("Missing client request payload")),
|
||||
}
|
||||
}
|
||||
|
||||
fn response_to_proto(response: DomainResponse) -> ClientResponse {
|
||||
let payload = match response {
|
||||
DomainResponse::AuthChallenge { pubkey, nonce } => {
|
||||
ClientResponsePayload::AuthChallenge(ProtoAuthChallenge { pubkey, nonce })
|
||||
}
|
||||
DomainResponse::AuthOk => ClientResponsePayload::AuthOk(ProtoAuthOk {}),
|
||||
DomainResponse::ClientConnectError { code } => {
|
||||
ClientResponsePayload::ClientConnectError(ClientConnectError {
|
||||
code: match code {
|
||||
ConnectErrorCode::Unknown => ProtoClientConnectErrorCode::Unknown,
|
||||
ConnectErrorCode::ApprovalDenied => {
|
||||
ProtoClientConnectErrorCode::ApprovalDenied
|
||||
}
|
||||
ConnectErrorCode::NoUserAgentsOnline => {
|
||||
ProtoClientConnectErrorCode::NoUserAgentsOnline
|
||||
}
|
||||
}
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
ClientResponse {
|
||||
payload: Some(payload),
|
||||
}
|
||||
}
|
||||
|
||||
fn error_to_status(value: ClientError) -> Status {
|
||||
match value {
|
||||
ClientError::MissingRequestPayload | ClientError::UnexpectedRequestPayload => {
|
||||
Status::invalid_argument("Expected message with payload")
|
||||
}
|
||||
ClientError::StateTransitionFailed => Status::internal("State machine error"),
|
||||
ClientError::Auth(ref err) => auth_error_status(err),
|
||||
ClientError::ConnectionRegistrationFailed => {
|
||||
Status::internal("Connection registration failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Bi<DomainRequest, Result<DomainResponse, ClientError>> for GrpcTransport {
|
||||
async fn send(&mut self, item: Result<DomainResponse, ClientError>) -> Result<(), TransportError> {
|
||||
let outbound = match item {
|
||||
Ok(message) => Ok(Self::response_to_proto(message)),
|
||||
Err(err) => Err(Self::error_to_status(err)),
|
||||
};
|
||||
|
||||
self.sender
|
||||
.send(outbound)
|
||||
.await
|
||||
.map_err(|_| TransportError::ChannelClosed)
|
||||
}
|
||||
|
||||
async fn recv(&mut self) -> Option<DomainRequest> {
|
||||
match self.receiver.next().await {
|
||||
Some(Ok(item)) => match Self::request_to_domain(item) {
|
||||
Ok(request) => Some(request),
|
||||
Err(status) => {
|
||||
let _ = self.sender.send(Err(status)).await;
|
||||
None
|
||||
}
|
||||
},
|
||||
Some(Err(error)) => {
|
||||
tracing::error!(error = ?error, "grpc client recv failed; closing stream");
|
||||
None
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_error_status(value: &client::auth::Error) -> Status {
|
||||
use client::auth::Error;
|
||||
|
||||
match value {
|
||||
Error::UnexpectedMessagePayload | Error::InvalidClientPubkeyLength => {
|
||||
Status::invalid_argument(value.to_string())
|
||||
}
|
||||
Error::InvalidAuthPubkeyEncoding => {
|
||||
Status::invalid_argument("Failed to convert pubkey to VerifyingKey")
|
||||
}
|
||||
Error::InvalidChallengeSolution => Status::unauthenticated(value.to_string()),
|
||||
Error::ApproveError(_) => Status::permission_denied(value.to_string()),
|
||||
Error::Transport => Status::internal("Transport error"),
|
||||
Error::DatabasePoolUnavailable => Status::internal("Database pool error"),
|
||||
Error::DatabaseOperationFailed => Status::internal("Database error"),
|
||||
Error::InternalError => Status::internal("Internal error"),
|
||||
}
|
||||
}
|
||||
65
server/crates/arbiter-server/src/grpc/mod.rs
Normal file
65
server/crates/arbiter-server/src/grpc/mod.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
use arbiter_proto::proto::{
|
||||
client::{ClientRequest, ClientResponse},
|
||||
user_agent::{UserAgentRequest, UserAgentResponse},
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tonic::{Request, Response, Status, async_trait};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
DEFAULT_CHANNEL_SIZE,
|
||||
actors::{client::{ClientConnection, connect_client}, user_agent::{UserAgentConnection, connect_user_agent}},
|
||||
};
|
||||
|
||||
pub mod client;
|
||||
pub mod user_agent;
|
||||
|
||||
#[async_trait]
|
||||
impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Server {
|
||||
type UserAgentStream = ReceiverStream<Result<UserAgentResponse, Status>>;
|
||||
type ClientStream = ReceiverStream<Result<ClientResponse, Status>>;
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
async fn client(
|
||||
&self,
|
||||
request: Request<tonic::Streaming<ClientRequest>>,
|
||||
) -> Result<Response<Self::ClientStream>, Status> {
|
||||
let req_stream = request.into_inner();
|
||||
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
|
||||
|
||||
let transport = client::GrpcTransport::new(tx, req_stream);
|
||||
let props = ClientConnection::new(
|
||||
self.context.db.clone(),
|
||||
Box::new(transport),
|
||||
self.context.actors.clone(),
|
||||
);
|
||||
tokio::spawn(connect_client(props));
|
||||
|
||||
info!(event = "connection established", "grpc.client");
|
||||
|
||||
Ok(Response::new(ReceiverStream::new(rx)))
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
async fn user_agent(
|
||||
&self,
|
||||
request: Request<tonic::Streaming<UserAgentRequest>>,
|
||||
) -> Result<Response<Self::UserAgentStream>, Status> {
|
||||
let req_stream = request.into_inner();
|
||||
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
|
||||
|
||||
let transport = user_agent::GrpcTransport::new(tx, req_stream);
|
||||
let props = UserAgentConnection::new(
|
||||
self.context.db.clone(),
|
||||
self.context.actors.clone(),
|
||||
Box::new(transport),
|
||||
);
|
||||
tokio::spawn(connect_user_agent(props));
|
||||
|
||||
info!(event = "connection established", "grpc.user_agent");
|
||||
|
||||
Ok(Response::new(ReceiverStream::new(rx)))
|
||||
}
|
||||
}
|
||||
288
server/crates/arbiter-server/src/grpc/user_agent.rs
Normal file
288
server/crates/arbiter-server/src/grpc/user_agent.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
use arbiter_proto::{
|
||||
proto::{
|
||||
evm::{
|
||||
EvmError as ProtoEvmError, WalletCreateResponse, WalletEntry, WalletList,
|
||||
WalletListResponse, wallet_create_response::Result as WalletCreateResult,
|
||||
wallet_list_response::Result as WalletListResult,
|
||||
},
|
||||
user_agent::{
|
||||
AuthChallenge as ProtoAuthChallenge,
|
||||
AuthChallengeRequest as ProtoAuthChallengeRequest,
|
||||
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthOk as ProtoAuthOk,
|
||||
BootstrapEncryptedKey as ProtoBootstrapEncryptedKey,
|
||||
BootstrapResult as ProtoBootstrapResult, ClientConnectionCancel,
|
||||
ClientConnectionRequest, ClientConnectionResponse, KeyType as ProtoKeyType,
|
||||
UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult,
|
||||
UnsealStart, UnsealStartResponse, UserAgentRequest, UserAgentResponse,
|
||||
VaultState as ProtoVaultState,
|
||||
user_agent_request::Payload as UserAgentRequestPayload,
|
||||
user_agent_response::Payload as UserAgentResponsePayload,
|
||||
},
|
||||
},
|
||||
transport::{Bi, Error as TransportError},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt as _;
|
||||
use tokio::sync::mpsc;
|
||||
use tonic::{Status, Streaming};
|
||||
|
||||
use crate::actors::user_agent::{
|
||||
self, AuthPublicKey, BootstrapError, Request as DomainRequest, Response as DomainResponse,
|
||||
TransportResponseError, UnsealError, VaultState,
|
||||
};
|
||||
|
||||
pub struct GrpcTransport {
|
||||
sender: mpsc::Sender<Result<UserAgentResponse, Status>>,
|
||||
receiver: Streaming<UserAgentRequest>,
|
||||
}
|
||||
|
||||
impl GrpcTransport {
|
||||
pub fn new(
|
||||
sender: mpsc::Sender<Result<UserAgentResponse, Status>>,
|
||||
receiver: Streaming<UserAgentRequest>,
|
||||
) -> Self {
|
||||
Self { sender, receiver }
|
||||
}
|
||||
|
||||
fn request_to_domain(request: UserAgentRequest) -> Result<DomainRequest, Status> {
|
||||
match request.payload {
|
||||
Some(UserAgentRequestPayload::AuthChallengeRequest(
|
||||
ProtoAuthChallengeRequest {
|
||||
pubkey,
|
||||
bootstrap_token,
|
||||
key_type,
|
||||
},
|
||||
)) => Ok(DomainRequest::AuthChallengeRequest {
|
||||
pubkey: parse_auth_pubkey(key_type, pubkey)?,
|
||||
bootstrap_token,
|
||||
}),
|
||||
Some(UserAgentRequestPayload::AuthChallengeSolution(
|
||||
ProtoAuthChallengeSolution { signature },
|
||||
)) => Ok(DomainRequest::AuthChallengeSolution { signature }),
|
||||
Some(UserAgentRequestPayload::UnsealStart(UnsealStart { client_pubkey })) => {
|
||||
let client_pubkey: [u8; 32] = client_pubkey
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| Status::invalid_argument("client_pubkey must be 32 bytes"))?;
|
||||
Ok(DomainRequest::UnsealStart {
|
||||
client_pubkey: x25519_dalek::PublicKey::from(client_pubkey),
|
||||
})
|
||||
}
|
||||
Some(UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey {
|
||||
nonce,
|
||||
ciphertext,
|
||||
associated_data,
|
||||
})) => Ok(DomainRequest::UnsealEncryptedKey {
|
||||
nonce,
|
||||
ciphertext,
|
||||
associated_data,
|
||||
}),
|
||||
Some(UserAgentRequestPayload::BootstrapEncryptedKey(
|
||||
ProtoBootstrapEncryptedKey {
|
||||
nonce,
|
||||
ciphertext,
|
||||
associated_data,
|
||||
},
|
||||
)) => Ok(DomainRequest::BootstrapEncryptedKey {
|
||||
nonce,
|
||||
ciphertext,
|
||||
associated_data,
|
||||
}),
|
||||
Some(UserAgentRequestPayload::QueryVaultState(_)) => {
|
||||
Ok(DomainRequest::QueryVaultState)
|
||||
}
|
||||
Some(UserAgentRequestPayload::EvmWalletCreate(_)) => Ok(DomainRequest::EvmWalletCreate),
|
||||
Some(UserAgentRequestPayload::EvmWalletList(_)) => Ok(DomainRequest::EvmWalletList),
|
||||
Some(UserAgentRequestPayload::ClientConnectionResponse(
|
||||
ClientConnectionResponse { approved },
|
||||
)) => Ok(DomainRequest::ClientConnectionResponse { approved }),
|
||||
Some(_) => Err(Status::invalid_argument(
|
||||
"Unexpected user-agent request payload",
|
||||
)),
|
||||
None => Err(Status::invalid_argument("Missing user-agent request payload")),
|
||||
}
|
||||
}
|
||||
|
||||
fn response_to_proto(response: DomainResponse) -> UserAgentResponse {
|
||||
let payload = match response {
|
||||
DomainResponse::AuthChallenge { nonce } => {
|
||||
UserAgentResponsePayload::AuthChallenge(ProtoAuthChallenge {
|
||||
pubkey: Vec::new(),
|
||||
nonce,
|
||||
})
|
||||
}
|
||||
DomainResponse::AuthOk => UserAgentResponsePayload::AuthOk(ProtoAuthOk {}),
|
||||
DomainResponse::UnsealStartResponse { server_pubkey } => {
|
||||
UserAgentResponsePayload::UnsealStartResponse(UnsealStartResponse {
|
||||
server_pubkey: server_pubkey.as_bytes().to_vec(),
|
||||
})
|
||||
}
|
||||
DomainResponse::UnsealResult(result) => UserAgentResponsePayload::UnsealResult(
|
||||
match result {
|
||||
Ok(()) => ProtoUnsealResult::Success,
|
||||
Err(UnsealError::InvalidKey) => ProtoUnsealResult::InvalidKey,
|
||||
Err(UnsealError::Unbootstrapped) => ProtoUnsealResult::Unbootstrapped,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
DomainResponse::BootstrapResult(result) => UserAgentResponsePayload::BootstrapResult(
|
||||
match result {
|
||||
Ok(()) => ProtoBootstrapResult::Success,
|
||||
Err(BootstrapError::AlreadyBootstrapped) => {
|
||||
ProtoBootstrapResult::AlreadyBootstrapped
|
||||
}
|
||||
Err(BootstrapError::InvalidKey) => ProtoBootstrapResult::InvalidKey,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
DomainResponse::VaultState(state) => UserAgentResponsePayload::VaultState(
|
||||
match state {
|
||||
VaultState::Unbootstrapped => ProtoVaultState::Unbootstrapped,
|
||||
VaultState::Sealed => ProtoVaultState::Sealed,
|
||||
VaultState::Unsealed => ProtoVaultState::Unsealed,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
DomainResponse::ClientConnectionRequest { pubkey } => {
|
||||
UserAgentResponsePayload::ClientConnectionRequest(ClientConnectionRequest {
|
||||
pubkey: pubkey.to_bytes().to_vec(),
|
||||
})
|
||||
}
|
||||
DomainResponse::ClientConnectionCancel => {
|
||||
UserAgentResponsePayload::ClientConnectionCancel(ClientConnectionCancel {})
|
||||
}
|
||||
DomainResponse::EvmWalletCreate(result) => {
|
||||
UserAgentResponsePayload::EvmWalletCreate(WalletCreateResponse {
|
||||
result: Some(match result {
|
||||
Ok(()) => WalletCreateResult::Wallet(WalletEntry {
|
||||
address: Vec::new(),
|
||||
}),
|
||||
Err(_) => WalletCreateResult::Error(ProtoEvmError::Internal.into()),
|
||||
}),
|
||||
})
|
||||
}
|
||||
DomainResponse::EvmWalletList(wallets) => {
|
||||
UserAgentResponsePayload::EvmWalletList(WalletListResponse {
|
||||
result: Some(WalletListResult::Wallets(WalletList {
|
||||
wallets: wallets
|
||||
.into_iter()
|
||||
.map(|addr| WalletEntry {
|
||||
address: addr.as_slice().to_vec(),
|
||||
})
|
||||
.collect(),
|
||||
})),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
UserAgentResponse {
|
||||
payload: Some(payload),
|
||||
}
|
||||
}
|
||||
|
||||
fn error_to_status(value: TransportResponseError) -> Status {
|
||||
match value {
|
||||
TransportResponseError::UnexpectedRequestPayload => {
|
||||
Status::invalid_argument("Expected message with payload")
|
||||
}
|
||||
TransportResponseError::InvalidStateForUnsealEncryptedKey => {
|
||||
Status::failed_precondition("Invalid state for unseal encrypted key")
|
||||
}
|
||||
TransportResponseError::InvalidClientPubkeyLength => {
|
||||
Status::invalid_argument("client_pubkey must be 32 bytes")
|
||||
}
|
||||
TransportResponseError::StateTransitionFailed => Status::internal("State machine error"),
|
||||
TransportResponseError::KeyHolderActorUnreachable => {
|
||||
Status::internal("Vault is not available")
|
||||
}
|
||||
TransportResponseError::Auth(ref err) => auth_error_status(err),
|
||||
TransportResponseError::ConnectionRegistrationFailed => {
|
||||
Status::internal("Failed registering connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Bi<DomainRequest, Result<DomainResponse, TransportResponseError>> for GrpcTransport {
|
||||
async fn send(
|
||||
&mut self,
|
||||
item: Result<DomainResponse, TransportResponseError>,
|
||||
) -> Result<(), TransportError> {
|
||||
let outbound = match item {
|
||||
Ok(message) => Ok(Self::response_to_proto(message)),
|
||||
Err(err) => Err(Self::error_to_status(err)),
|
||||
};
|
||||
|
||||
self.sender
|
||||
.send(outbound)
|
||||
.await
|
||||
.map_err(|_| TransportError::ChannelClosed)
|
||||
}
|
||||
|
||||
async fn recv(&mut self) -> Option<DomainRequest> {
|
||||
match self.receiver.next().await {
|
||||
Some(Ok(item)) => match Self::request_to_domain(item) {
|
||||
Ok(request) => Some(request),
|
||||
Err(status) => {
|
||||
let _ = self.sender.send(Err(status)).await;
|
||||
None
|
||||
}
|
||||
},
|
||||
Some(Err(error)) => {
|
||||
tracing::error!(error = ?error, "grpc user-agent recv failed; closing stream");
|
||||
None
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_auth_pubkey(key_type: i32, pubkey: Vec<u8>) -> Result<AuthPublicKey, Status> {
|
||||
match ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified) {
|
||||
ProtoKeyType::Unspecified | ProtoKeyType::Ed25519 => {
|
||||
let bytes: [u8; 32] = pubkey
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| Status::invalid_argument("invalid Ed25519 public key length"))?;
|
||||
let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes)
|
||||
.map_err(|_| Status::invalid_argument("invalid Ed25519 public key encoding"))?;
|
||||
Ok(AuthPublicKey::Ed25519(key))
|
||||
}
|
||||
ProtoKeyType::EcdsaSecp256k1 => {
|
||||
let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(&pubkey)
|
||||
.map_err(|_| Status::invalid_argument("invalid secp256k1 public key encoding"))?;
|
||||
Ok(AuthPublicKey::EcdsaSecp256k1(key))
|
||||
}
|
||||
ProtoKeyType::Rsa => {
|
||||
use rsa::pkcs8::DecodePublicKey as _;
|
||||
|
||||
let key = rsa::RsaPublicKey::from_public_key_der(&pubkey)
|
||||
.map_err(|_| Status::invalid_argument("invalid RSA public key encoding"))?;
|
||||
Ok(AuthPublicKey::Rsa(key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_error_status(value: &user_agent::auth::Error) -> Status {
|
||||
use user_agent::auth::Error;
|
||||
|
||||
match value {
|
||||
Error::UnexpectedMessagePayload | Error::InvalidClientPubkeyLength => {
|
||||
Status::invalid_argument(value.to_string())
|
||||
}
|
||||
Error::InvalidAuthPubkeyEncoding => {
|
||||
Status::invalid_argument("Failed to convert pubkey to VerifyingKey")
|
||||
}
|
||||
Error::PublicKeyNotRegistered | Error::InvalidChallengeSolution => {
|
||||
Status::unauthenticated(value.to_string())
|
||||
}
|
||||
Error::InvalidBootstrapToken => Status::invalid_argument("Invalid bootstrap token"),
|
||||
Error::Transport => Status::internal("Transport error"),
|
||||
Error::BootstrapperActorUnreachable => {
|
||||
Status::internal("Bootstrap token consumption failed")
|
||||
}
|
||||
Error::DatabasePoolUnavailable => Status::internal("Database pool error"),
|
||||
Error::DatabaseOperationFailed => Status::internal("Database error"),
|
||||
}
|
||||
}
|
||||
@@ -1,137 +1,16 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use arbiter_proto::{
|
||||
proto::{
|
||||
client::{ClientRequest, ClientResponse},
|
||||
user_agent::{UserAgentRequest, UserAgentResponse},
|
||||
},
|
||||
transport::{IdentityRecvConverter, SendConverter, grpc},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
use tonic::{Request, Response, Status};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
actors::{
|
||||
client::{self, ClientConnection as ClientConnectionProps, ClientError, connect_client},
|
||||
user_agent::{self, TransportResponseError, UserAgentConnection, connect_user_agent},
|
||||
},
|
||||
context::ServerContext,
|
||||
};
|
||||
use crate::context::ServerContext;
|
||||
|
||||
pub mod actors;
|
||||
pub mod context;
|
||||
pub mod db;
|
||||
pub mod evm;
|
||||
pub mod grpc;
|
||||
|
||||
const DEFAULT_CHANNEL_SIZE: usize = 1000;
|
||||
|
||||
struct UserAgentGrpcSender;
|
||||
|
||||
impl SendConverter for UserAgentGrpcSender {
|
||||
type Input = Result<UserAgentResponse, TransportResponseError>;
|
||||
type Output = Result<UserAgentResponse, Status>;
|
||||
|
||||
fn convert(&self, item: Self::Input) -> Self::Output {
|
||||
match item {
|
||||
Ok(message) => Ok(message),
|
||||
Err(err) => Err(user_agent_error_status(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientGrpcSender;
|
||||
|
||||
impl SendConverter for ClientGrpcSender {
|
||||
type Input = Result<ClientResponse, ClientError>;
|
||||
type Output = Result<ClientResponse, Status>;
|
||||
|
||||
fn convert(&self, item: Self::Input) -> Self::Output {
|
||||
match item {
|
||||
Ok(message) => Ok(message),
|
||||
Err(err) => Err(client_error_status(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn client_error_status(value: ClientError) -> Status {
|
||||
match value {
|
||||
ClientError::MissingRequestPayload | ClientError::UnexpectedRequestPayload => {
|
||||
Status::invalid_argument("Expected message with payload")
|
||||
}
|
||||
ClientError::StateTransitionFailed => Status::internal("State machine error"),
|
||||
ClientError::Auth(ref err) => client_auth_error_status(err),
|
||||
ClientError::ConnectionRegistrationFailed => {
|
||||
Status::internal("Connection registration failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn client_auth_error_status(value: &client::auth::Error) -> Status {
|
||||
use client::auth::Error;
|
||||
match value {
|
||||
Error::UnexpectedMessagePayload | Error::InvalidClientPubkeyLength => {
|
||||
Status::invalid_argument(value.to_string())
|
||||
}
|
||||
Error::InvalidAuthPubkeyEncoding => {
|
||||
Status::invalid_argument("Failed to convert pubkey to VerifyingKey")
|
||||
}
|
||||
Error::InvalidChallengeSolution => Status::unauthenticated(value.to_string()),
|
||||
Error::ApproveError(_) => Status::permission_denied(value.to_string()),
|
||||
Error::Transport => Status::internal("Transport error"),
|
||||
Error::DatabasePoolUnavailable => Status::internal("Database pool error"),
|
||||
Error::DatabaseOperationFailed => Status::internal("Database error"),
|
||||
Error::InternalError => Status::internal("Internal error"),
|
||||
}
|
||||
}
|
||||
|
||||
fn user_agent_error_status(value: TransportResponseError) -> Status {
|
||||
match value {
|
||||
TransportResponseError::MissingRequestPayload
|
||||
| TransportResponseError::UnexpectedRequestPayload => {
|
||||
Status::invalid_argument("Expected message with payload")
|
||||
}
|
||||
TransportResponseError::InvalidStateForUnsealEncryptedKey => {
|
||||
Status::failed_precondition("Invalid state for unseal encrypted key")
|
||||
}
|
||||
TransportResponseError::InvalidClientPubkeyLength => {
|
||||
Status::invalid_argument("client_pubkey must be 32 bytes")
|
||||
}
|
||||
TransportResponseError::StateTransitionFailed => Status::internal("State machine error"),
|
||||
TransportResponseError::KeyHolderActorUnreachable => {
|
||||
Status::internal("Vault is not available")
|
||||
}
|
||||
TransportResponseError::Auth(ref err) => auth_error_status(err),
|
||||
TransportResponseError::ConnectionRegistrationFailed => {
|
||||
Status::internal("Failed registering connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_error_status(value: &user_agent::auth::Error) -> Status {
|
||||
use user_agent::auth::Error;
|
||||
match value {
|
||||
Error::UnexpectedMessagePayload | Error::InvalidClientPubkeyLength => {
|
||||
Status::invalid_argument(value.to_string())
|
||||
}
|
||||
Error::InvalidAuthPubkeyEncoding => {
|
||||
Status::invalid_argument("Failed to convert pubkey to VerifyingKey")
|
||||
}
|
||||
Error::PublicKeyNotRegistered | Error::InvalidChallengeSolution => {
|
||||
Status::unauthenticated(value.to_string())
|
||||
}
|
||||
Error::InvalidBootstrapToken => Status::invalid_argument("Invalid bootstrap token"),
|
||||
Error::Transport => Status::internal("Transport error"),
|
||||
Error::BootstrapperActorUnreachable => {
|
||||
Status::internal("Bootstrap token consumption failed")
|
||||
}
|
||||
Error::DatabasePoolUnavailable => Status::internal("Database pool error"),
|
||||
Error::DatabaseOperationFailed => Status::internal("Database error"),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
context: ServerContext,
|
||||
}
|
||||
@@ -142,60 +21,4 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl arbiter_proto::proto::arbiter_service_server::ArbiterService for Server {
|
||||
type UserAgentStream = ReceiverStream<Result<UserAgentResponse, Status>>;
|
||||
type ClientStream = ReceiverStream<Result<ClientResponse, Status>>;
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
async fn client(
|
||||
&self,
|
||||
request: Request<tonic::Streaming<ClientRequest>>,
|
||||
) -> Result<Response<Self::ClientStream>, Status> {
|
||||
let req_stream = request.into_inner();
|
||||
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
|
||||
|
||||
let transport = grpc::GrpcAdapter::new(
|
||||
tx,
|
||||
req_stream,
|
||||
IdentityRecvConverter::<ClientRequest>::new(),
|
||||
ClientGrpcSender,
|
||||
);
|
||||
let props = ClientConnectionProps::new(
|
||||
self.context.db.clone(),
|
||||
Box::new(transport),
|
||||
self.context.actors.clone(),
|
||||
);
|
||||
tokio::spawn(connect_client(props));
|
||||
|
||||
info!(event = "connection established", "grpc.client");
|
||||
|
||||
Ok(Response::new(ReceiverStream::new(rx)))
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
async fn user_agent(
|
||||
&self,
|
||||
request: Request<tonic::Streaming<UserAgentRequest>>,
|
||||
) -> Result<Response<Self::UserAgentStream>, Status> {
|
||||
let req_stream = request.into_inner();
|
||||
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
|
||||
|
||||
let transport = grpc::GrpcAdapter::new(
|
||||
tx,
|
||||
req_stream,
|
||||
IdentityRecvConverter::<UserAgentRequest>::new(),
|
||||
UserAgentGrpcSender,
|
||||
);
|
||||
let props = UserAgentConnection::new(
|
||||
self.context.db.clone(),
|
||||
self.context.actors.clone(),
|
||||
Box::new(transport),
|
||||
);
|
||||
tokio::spawn(connect_user_agent(props));
|
||||
|
||||
info!(event = "connection established", "grpc.user_agent");
|
||||
|
||||
Ok(Response::new(ReceiverStream::new(rx)))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user