feat(poc): add terrors PoC crate scaffold and error types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ message ClientRequest {
|
|||||||
oneof payload {
|
oneof payload {
|
||||||
AuthChallengeRequest auth_challenge_request = 1;
|
AuthChallengeRequest auth_challenge_request = 1;
|
||||||
AuthChallengeSolution auth_challenge_solution = 2;
|
AuthChallengeSolution auth_challenge_solution = 2;
|
||||||
|
arbiter.evm.EvmSignTransactionRequest evm_sign_transaction = 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,55 @@ enum KeyType {
|
|||||||
KEY_TYPE_RSA = 3;
|
KEY_TYPE_RSA = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- SDK client management ---
|
||||||
|
|
||||||
|
enum SdkClientError {
|
||||||
|
SDK_CLIENT_ERROR_UNSPECIFIED = 0;
|
||||||
|
SDK_CLIENT_ERROR_ALREADY_EXISTS = 1;
|
||||||
|
SDK_CLIENT_ERROR_NOT_FOUND = 2;
|
||||||
|
SDK_CLIENT_ERROR_HAS_RELATED_DATA = 3; // hard-delete blocked by FK (client has grants or transaction logs)
|
||||||
|
SDK_CLIENT_ERROR_INTERNAL = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientApproveRequest {
|
||||||
|
bytes pubkey = 1; // 32-byte ed25519 public key
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientRevokeRequest {
|
||||||
|
int32 client_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientEntry {
|
||||||
|
int32 id = 1;
|
||||||
|
bytes pubkey = 2;
|
||||||
|
int32 created_at = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientList {
|
||||||
|
repeated SdkClientEntry clients = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientApproveResponse {
|
||||||
|
oneof result {
|
||||||
|
SdkClientEntry client = 1;
|
||||||
|
SdkClientError error = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientRevokeResponse {
|
||||||
|
oneof result {
|
||||||
|
google.protobuf.Empty ok = 1;
|
||||||
|
SdkClientError error = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientListResponse {
|
||||||
|
oneof result {
|
||||||
|
SdkClientList clients = 1;
|
||||||
|
SdkClientError error = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
message AuthChallengeRequest {
|
message AuthChallengeRequest {
|
||||||
bytes pubkey = 1;
|
bytes pubkey = 1;
|
||||||
optional string bootstrap_token = 2;
|
optional string bootstrap_token = 2;
|
||||||
@@ -57,16 +106,6 @@ enum VaultState {
|
|||||||
VAULT_STATE_ERROR = 4;
|
VAULT_STATE_ERROR = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ClientConnectionRequest {
|
|
||||||
bytes pubkey = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ClientConnectionResponse {
|
|
||||||
bool approved = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ClientConnectionCancel {}
|
|
||||||
|
|
||||||
message UserAgentRequest {
|
message UserAgentRequest {
|
||||||
oneof payload {
|
oneof payload {
|
||||||
AuthChallengeRequest auth_challenge_request = 1;
|
AuthChallengeRequest auth_challenge_request = 1;
|
||||||
@@ -79,7 +118,10 @@ message UserAgentRequest {
|
|||||||
arbiter.evm.EvmGrantCreateRequest evm_grant_create = 8;
|
arbiter.evm.EvmGrantCreateRequest evm_grant_create = 8;
|
||||||
arbiter.evm.EvmGrantDeleteRequest evm_grant_delete = 9;
|
arbiter.evm.EvmGrantDeleteRequest evm_grant_delete = 9;
|
||||||
arbiter.evm.EvmGrantListRequest evm_grant_list = 10;
|
arbiter.evm.EvmGrantListRequest evm_grant_list = 10;
|
||||||
ClientConnectionResponse client_connection_response = 11;
|
// field 11 reserved: was client_connection_response (online approval removed)
|
||||||
|
SdkClientApproveRequest sdk_client_approve = 12;
|
||||||
|
SdkClientRevokeRequest sdk_client_revoke = 13;
|
||||||
|
google.protobuf.Empty sdk_client_list = 14;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message UserAgentResponse {
|
message UserAgentResponse {
|
||||||
@@ -94,7 +136,9 @@ message UserAgentResponse {
|
|||||||
arbiter.evm.EvmGrantCreateResponse evm_grant_create = 8;
|
arbiter.evm.EvmGrantCreateResponse evm_grant_create = 8;
|
||||||
arbiter.evm.EvmGrantDeleteResponse evm_grant_delete = 9;
|
arbiter.evm.EvmGrantDeleteResponse evm_grant_delete = 9;
|
||||||
arbiter.evm.EvmGrantListResponse evm_grant_list = 10;
|
arbiter.evm.EvmGrantListResponse evm_grant_list = 10;
|
||||||
ClientConnectionRequest client_connection_request = 11;
|
// fields 11, 12 reserved: were client_connection_request, client_connection_cancel (online approval removed)
|
||||||
ClientConnectionCancel client_connection_cancel = 12;
|
SdkClientApproveResponse sdk_client_approve = 13;
|
||||||
|
SdkClientRevokeResponse sdk_client_revoke = 14;
|
||||||
|
SdkClientListResponse sdk_client_list = 15;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
server/Cargo.lock
generated
12
server/Cargo.lock
generated
@@ -678,6 +678,18 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "arbiter-client"
|
name = "arbiter-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"alloy",
|
||||||
|
"arbiter-proto",
|
||||||
|
"async-trait",
|
||||||
|
"ed25519-dalek",
|
||||||
|
"http",
|
||||||
|
"rustls-webpki",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tonic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arbiter-proto"
|
name = "arbiter-proto"
|
||||||
|
|||||||
@@ -5,4 +5,18 @@ edition = "2024"
|
|||||||
repository = "https://git.markettakers.org/MarketTakers/arbiter"
|
repository = "https://git.markettakers.org/MarketTakers/arbiter"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
arbiter-proto.path = "../arbiter-proto"
|
||||||
|
alloy.workspace = true
|
||||||
|
tonic.workspace = true
|
||||||
|
tonic.features = ["tls-aws-lc"]
|
||||||
|
tokio.workspace = true
|
||||||
|
tokio-stream.workspace = true
|
||||||
|
ed25519-dalek.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
http = "1.4.0"
|
||||||
|
rustls-webpki = { version = "0.103.9", features = ["aws-lc-rs"] }
|
||||||
|
async-trait.workspace = true
|
||||||
|
|||||||
@@ -1,14 +1,272 @@
|
|||||||
pub fn add(left: u64, right: u64) -> u64 {
|
use alloy::{
|
||||||
left + right
|
consensus::SignableTransaction,
|
||||||
|
network::TxSigner,
|
||||||
|
primitives::{Address, B256, ChainId, Signature},
|
||||||
|
signers::{Error, Result, Signer},
|
||||||
|
};
|
||||||
|
use arbiter_proto::{
|
||||||
|
format_challenge,
|
||||||
|
proto::{
|
||||||
|
arbiter_service_client::ArbiterServiceClient,
|
||||||
|
client::{
|
||||||
|
AuthChallengeRequest, AuthChallengeSolution, ClientRequest, ClientResponse,
|
||||||
|
client_connect_error, client_request::Payload as ClientRequestPayload,
|
||||||
|
client_response::Payload as ClientResponsePayload,
|
||||||
|
},
|
||||||
|
evm::{
|
||||||
|
EvmSignTransactionRequest, evm_sign_transaction_response::Result as SignResponseResult,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
url::ArbiterUrl,
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use ed25519_dalek::Signer as _;
|
||||||
|
use tokio::sync::{Mutex, mpsc};
|
||||||
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
|
use tonic::transport::ClientTlsConfig;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ConnectError {
|
||||||
|
#[error("Could not establish connection")]
|
||||||
|
Connection(#[from] tonic::transport::Error),
|
||||||
|
|
||||||
|
#[error("Invalid server URI")]
|
||||||
|
InvalidUri(#[from] http::uri::InvalidUri),
|
||||||
|
|
||||||
|
#[error("Invalid CA certificate")]
|
||||||
|
InvalidCaCert(#[from] webpki::Error),
|
||||||
|
|
||||||
|
#[error("gRPC error")]
|
||||||
|
Grpc(#[from] tonic::Status),
|
||||||
|
|
||||||
|
#[error("Auth challenge was not returned by server")]
|
||||||
|
MissingAuthChallenge,
|
||||||
|
|
||||||
|
#[error("Client approval denied by User Agent")]
|
||||||
|
ApprovalDenied,
|
||||||
|
|
||||||
|
#[error("No User Agents online to approve client")]
|
||||||
|
NoUserAgentsOnline,
|
||||||
|
|
||||||
|
#[error("Unexpected auth response payload")]
|
||||||
|
UnexpectedAuthResponse,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
mod tests {
|
enum ClientSignError {
|
||||||
use super::*;
|
#[error("Transport channel closed")]
|
||||||
|
ChannelClosed,
|
||||||
|
|
||||||
#[test]
|
#[error("Connection closed by server")]
|
||||||
fn it_works() {
|
ConnectionClosed,
|
||||||
let result = add(2, 2);
|
|
||||||
assert_eq!(result, 4);
|
#[error("Invalid response payload")]
|
||||||
|
InvalidResponse,
|
||||||
|
|
||||||
|
#[error("Remote signing was rejected")]
|
||||||
|
Rejected,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ClientTransport {
|
||||||
|
sender: mpsc::Sender<ClientRequest>,
|
||||||
|
receiver: tonic::Streaming<ClientResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientTransport {
|
||||||
|
async fn send(&mut self, request: ClientRequest) -> std::result::Result<(), ClientSignError> {
|
||||||
|
self.sender
|
||||||
|
.send(request)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ClientSignError::ChannelClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recv(&mut self) -> std::result::Result<ClientResponse, ClientSignError> {
|
||||||
|
match self.receiver.message().await {
|
||||||
|
Ok(Some(resp)) => Ok(resp),
|
||||||
|
Ok(None) => Err(ClientSignError::ConnectionClosed),
|
||||||
|
Err(_) => Err(ClientSignError::ConnectionClosed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ArbiterSigner {
|
||||||
|
transport: Mutex<ClientTransport>,
|
||||||
|
address: Address,
|
||||||
|
chain_id: Option<ChainId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArbiterSigner {
|
||||||
|
pub async fn connect_grpc(
|
||||||
|
url: ArbiterUrl,
|
||||||
|
key: ed25519_dalek::SigningKey,
|
||||||
|
address: Address,
|
||||||
|
) -> std::result::Result<Self, ConnectError> {
|
||||||
|
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
|
||||||
|
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
||||||
|
|
||||||
|
// NOTE: We intentionally keep the same URL construction strategy as the user-agent crate
|
||||||
|
// to avoid behavior drift between the two clients.
|
||||||
|
let channel = tonic::transport::Channel::from_shared(format!("{}:{}", url.host, url.port))?
|
||||||
|
.tls_config(tls)?
|
||||||
|
.connect()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut client = ArbiterServiceClient::new(channel);
|
||||||
|
let (tx, rx) = mpsc::channel(16);
|
||||||
|
let response_stream = client.client(ReceiverStream::new(rx)).await?.into_inner();
|
||||||
|
|
||||||
|
let mut transport = ClientTransport {
|
||||||
|
sender: tx,
|
||||||
|
receiver: response_stream,
|
||||||
|
};
|
||||||
|
|
||||||
|
authenticate(&mut transport, key).await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
transport: Mutex::new(transport),
|
||||||
|
address,
|
||||||
|
chain_id: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sign_transaction_via_arbiter(
|
||||||
|
&self,
|
||||||
|
tx: &mut dyn SignableTransaction<Signature>,
|
||||||
|
) -> Result<Signature> {
|
||||||
|
if let Some(chain_id) = self.chain_id
|
||||||
|
&& !tx.set_chain_id_checked(chain_id)
|
||||||
|
{
|
||||||
|
return Err(Error::TransactionChainIdMismatch {
|
||||||
|
signer: chain_id,
|
||||||
|
tx: tx.chain_id().unwrap(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rlp_transaction = Vec::new();
|
||||||
|
tx.encode_for_signing(&mut rlp_transaction);
|
||||||
|
|
||||||
|
let request = ClientRequest {
|
||||||
|
payload: Some(ClientRequestPayload::EvmSignTransaction(
|
||||||
|
EvmSignTransactionRequest {
|
||||||
|
wallet_address: self.address.as_slice().to_vec(),
|
||||||
|
rlp_transaction,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut transport = self.transport.lock().await;
|
||||||
|
transport.send(request).await.map_err(Error::other)?;
|
||||||
|
let response = transport.recv().await.map_err(Error::other)?;
|
||||||
|
|
||||||
|
let payload = response
|
||||||
|
.payload
|
||||||
|
.ok_or_else(|| Error::other(ClientSignError::InvalidResponse))?;
|
||||||
|
|
||||||
|
let ClientResponsePayload::EvmSignTransaction(sign_response) = payload else {
|
||||||
|
return Err(Error::other(ClientSignError::InvalidResponse));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(result) = sign_response.result else {
|
||||||
|
return Err(Error::other(ClientSignError::InvalidResponse));
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
SignResponseResult::Signature(bytes) => {
|
||||||
|
Signature::try_from(bytes.as_slice()).map_err(Error::other)
|
||||||
|
}
|
||||||
|
SignResponseResult::EvalError(_) | SignResponseResult::Error(_) => {
|
||||||
|
Err(Error::other(ClientSignError::Rejected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authenticate(
|
||||||
|
transport: &mut ClientTransport,
|
||||||
|
key: ed25519_dalek::SigningKey,
|
||||||
|
) -> std::result::Result<(), ConnectError> {
|
||||||
|
transport
|
||||||
|
.send(ClientRequest {
|
||||||
|
payload: Some(ClientRequestPayload::AuthChallengeRequest(
|
||||||
|
AuthChallengeRequest {
|
||||||
|
pubkey: key.verifying_key().to_bytes().to_vec(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| ConnectError::UnexpectedAuthResponse)?;
|
||||||
|
|
||||||
|
let response = transport
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ConnectError::MissingAuthChallenge)?;
|
||||||
|
|
||||||
|
let payload = response.payload.ok_or(ConnectError::MissingAuthChallenge)?;
|
||||||
|
match payload {
|
||||||
|
ClientResponsePayload::AuthChallenge(challenge) => {
|
||||||
|
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
|
||||||
|
let signature = key.sign(&challenge_payload).to_bytes().to_vec();
|
||||||
|
|
||||||
|
transport
|
||||||
|
.send(ClientRequest {
|
||||||
|
payload: Some(ClientRequestPayload::AuthChallengeSolution(
|
||||||
|
AuthChallengeSolution { signature },
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| ConnectError::UnexpectedAuthResponse)?;
|
||||||
|
|
||||||
|
// Current server flow does not emit `AuthOk` for SDK clients, so we proceed after
|
||||||
|
// sending the solution. If authentication fails, the first business request will return
|
||||||
|
// a `ClientConnectError` or the stream will close.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
ClientResponsePayload::ClientConnectError(err) => {
|
||||||
|
match client_connect_error::Code::try_from(err.code)
|
||||||
|
.unwrap_or(client_connect_error::Code::Unknown)
|
||||||
|
{
|
||||||
|
client_connect_error::Code::ApprovalDenied => Err(ConnectError::ApprovalDenied),
|
||||||
|
client_connect_error::Code::NoUserAgentsOnline => {
|
||||||
|
Err(ConnectError::NoUserAgentsOnline)
|
||||||
|
}
|
||||||
|
client_connect_error::Code::Unknown => Err(ConnectError::UnexpectedAuthResponse),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(ConnectError::UnexpectedAuthResponse),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Signer for ArbiterSigner {
|
||||||
|
async fn sign_hash(&self, _hash: &B256) -> Result<Signature> {
|
||||||
|
Err(Error::other(
|
||||||
|
"hash-only signing is not supported for ArbiterSigner; use transaction signing",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn address(&self) -> Address {
|
||||||
|
self.address
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chain_id(&self) -> Option<ChainId> {
|
||||||
|
self.chain_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
|
||||||
|
self.chain_id = chain_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TxSigner<Signature> for ArbiterSigner {
|
||||||
|
fn address(&self) -> Address {
|
||||||
|
self.address
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sign_transaction(
|
||||||
|
&self,
|
||||||
|
tx: &mut dyn SignableTransaction<Signature>,
|
||||||
|
) -> Result<Signature> {
|
||||||
|
self.sign_transaction_via_arbiter(tx).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP INDEX IF EXISTS program_client_public_key_unique;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE UNIQUE INDEX program_client_public_key_unique
|
||||||
|
ON program_client (public_key);
|
||||||
@@ -8,19 +8,13 @@ use arbiter_proto::{
|
|||||||
},
|
},
|
||||||
transport::expect_message,
|
transport::expect_message,
|
||||||
};
|
};
|
||||||
use diesel::{
|
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, update};
|
||||||
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update,
|
|
||||||
};
|
|
||||||
use diesel_async::RunQueryDsl as _;
|
use diesel_async::RunQueryDsl as _;
|
||||||
use ed25519_dalek::VerifyingKey;
|
use ed25519_dalek::VerifyingKey;
|
||||||
use kameo::error::SendError;
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::client::ClientConnection,
|
||||||
client::ClientConnection,
|
|
||||||
router::{self, RequestClientApproval},
|
|
||||||
},
|
|
||||||
db::{self, schema::program_client},
|
db::{self, schema::program_client},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -40,27 +34,20 @@ pub enum Error {
|
|||||||
DatabaseOperationFailed,
|
DatabaseOperationFailed,
|
||||||
#[error("Invalid challenge solution")]
|
#[error("Invalid challenge solution")]
|
||||||
InvalidChallengeSolution,
|
InvalidChallengeSolution,
|
||||||
#[error("Client approval request failed")]
|
#[error("Client not registered")]
|
||||||
ApproveError(#[from] ApproveError),
|
NotRegistered,
|
||||||
#[error("Internal error")]
|
#[error("Internal error")]
|
||||||
InternalError,
|
InternalError,
|
||||||
#[error("Transport error")]
|
#[error("Transport error")]
|
||||||
Transport,
|
Transport,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum ApproveError {
|
|
||||||
#[error("Internal error")]
|
|
||||||
Internal,
|
|
||||||
#[error("Client connection denied by user agents")]
|
|
||||||
Denied,
|
|
||||||
#[error("Upstream error: {0}")]
|
|
||||||
Upstream(router::ApprovalError),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Atomically reads and increments the nonce for a known client.
|
/// Atomically reads and increments the nonce for a known client.
|
||||||
/// Returns `None` if the pubkey is not registered.
|
/// Returns `None` if the pubkey is not registered.
|
||||||
async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Option<i32>, Error> {
|
async fn get_nonce(
|
||||||
|
db: &db::DatabasePool,
|
||||||
|
pubkey: &VerifyingKey,
|
||||||
|
) -> Result<Option<(i32, i32)>, Error> {
|
||||||
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
||||||
|
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
@@ -71,10 +58,10 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
|
|||||||
conn.exclusive_transaction(|conn| {
|
conn.exclusive_transaction(|conn| {
|
||||||
let pubkey_bytes = pubkey_bytes.clone();
|
let pubkey_bytes = pubkey_bytes.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let Some(current_nonce) = program_client::table
|
let Some((client_id, current_nonce)) = program_client::table
|
||||||
.filter(program_client::public_key.eq(&pubkey_bytes))
|
.filter(program_client::public_key.eq(&pubkey_bytes))
|
||||||
.select(program_client::nonce)
|
.select((program_client::id, program_client::nonce))
|
||||||
.first::<i32>(conn)
|
.first::<(i32, i32)>(conn)
|
||||||
.await
|
.await
|
||||||
.optional()?
|
.optional()?
|
||||||
else {
|
else {
|
||||||
@@ -87,7 +74,7 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
|
|||||||
.execute(conn)
|
.execute(conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Some(current_nonce))
|
Ok(Some((client_id, current_nonce)))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -97,59 +84,6 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn approve_new_client(
|
|
||||||
actors: &crate::actors::GlobalActors,
|
|
||||||
pubkey: VerifyingKey,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let result = actors
|
|
||||||
.router
|
|
||||||
.ask(RequestClientApproval {
|
|
||||||
client_pubkey: pubkey,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(true) => Ok(()),
|
|
||||||
Ok(false) => Err(Error::ApproveError(ApproveError::Denied)),
|
|
||||||
Err(SendError::HandlerError(e)) => {
|
|
||||||
error!(error = ?e, "Approval upstream error");
|
|
||||||
Err(Error::ApproveError(ApproveError::Upstream(e)))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!(error = ?e, "Approval request to router failed");
|
|
||||||
Err(Error::ApproveError(ApproveError::Internal))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_client(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<(), Error> {
|
|
||||||
let now = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs() as i32;
|
|
||||||
|
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
|
||||||
error!(error = ?e, "Database pool error");
|
|
||||||
Error::DatabasePoolUnavailable
|
|
||||||
})?;
|
|
||||||
|
|
||||||
insert_into(program_client::table)
|
|
||||||
.values((
|
|
||||||
program_client::public_key.eq(pubkey.as_bytes().to_vec()),
|
|
||||||
program_client::nonce.eq(1), // pre-incremented; challenge uses 0
|
|
||||||
program_client::created_at.eq(now),
|
|
||||||
program_client::updated_at.eq(now),
|
|
||||||
))
|
|
||||||
.execute(&mut conn)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!(error = ?e, "Failed to insert new client");
|
|
||||||
Error::DatabaseOperationFailed
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn challenge_client(
|
async fn challenge_client(
|
||||||
props: &mut ClientConnection,
|
props: &mut ClientConnection,
|
||||||
pubkey: VerifyingKey,
|
pubkey: VerifyingKey,
|
||||||
@@ -200,15 +134,12 @@ async fn challenge_client(
|
|||||||
|
|
||||||
fn connect_error_code(err: &Error) -> ConnectErrorCode {
|
fn connect_error_code(err: &Error) -> ConnectErrorCode {
|
||||||
match err {
|
match err {
|
||||||
Error::ApproveError(ApproveError::Denied) => ConnectErrorCode::ApprovalDenied,
|
Error::NotRegistered => ConnectErrorCode::ApprovalDenied,
|
||||||
Error::ApproveError(ApproveError::Upstream(
|
|
||||||
router::ApprovalError::NoUserAgentsConnected,
|
|
||||||
)) => ConnectErrorCode::NoUserAgentsOnline,
|
|
||||||
_ => ConnectErrorCode::Unknown,
|
_ => ConnectErrorCode::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn authenticate(props: &mut ClientConnection) -> Result<VerifyingKey, Error> {
|
async fn authenticate(props: &mut ClientConnection) -> Result<(VerifyingKey, i32), Error> {
|
||||||
let Some(ClientRequest {
|
let Some(ClientRequest {
|
||||||
payload: Some(ClientRequestPayload::AuthChallengeRequest(challenge)),
|
payload: Some(ClientRequestPayload::AuthChallengeRequest(challenge)),
|
||||||
}) = props.transport.recv().await
|
}) = props.transport.recv().await
|
||||||
@@ -223,23 +154,19 @@ async fn authenticate(props: &mut ClientConnection) -> Result<VerifyingKey, Erro
|
|||||||
let pubkey =
|
let pubkey =
|
||||||
VerifyingKey::from_bytes(pubkey_bytes).map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
|
VerifyingKey::from_bytes(pubkey_bytes).map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
|
||||||
|
|
||||||
let nonce = match get_nonce(&props.db, &pubkey).await? {
|
let (client_id, nonce) = match get_nonce(&props.db, &pubkey).await? {
|
||||||
Some(nonce) => nonce,
|
Some((client_id, nonce)) => (client_id, nonce),
|
||||||
None => {
|
None => return Err(Error::NotRegistered),
|
||||||
approve_new_client(&props.actors, pubkey).await?;
|
|
||||||
insert_client(&props.db, &pubkey).await?;
|
|
||||||
0
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
challenge_client(props, pubkey, nonce).await?;
|
challenge_client(props, pubkey, nonce).await?;
|
||||||
|
|
||||||
Ok(pubkey)
|
Ok((pubkey, client_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn authenticate_and_create(mut props: ClientConnection) -> Result<ClientSession, Error> {
|
pub async fn authenticate_and_create(mut props: ClientConnection) -> Result<ClientSession, Error> {
|
||||||
match authenticate(&mut props).await {
|
match authenticate(&mut props).await {
|
||||||
Ok(_pubkey) => Ok(ClientSession::new(props)),
|
Ok((_pubkey, client_id)) => Ok(ClientSession::new(props, client_id)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let code = connect_error_code(&err);
|
let code = connect_error_code(&err);
|
||||||
let _ = props
|
let _ = props
|
||||||
|
|||||||
@@ -1,19 +1,35 @@
|
|||||||
use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
|
use alloy::{consensus::TxEip1559, primitives::Address, rlp::Decodable};
|
||||||
|
use arbiter_proto::proto::{
|
||||||
|
client::{
|
||||||
|
ClientRequest, ClientResponse, client_request::Payload as ClientRequestPayload,
|
||||||
|
client_response::Payload as ClientResponsePayload,
|
||||||
|
},
|
||||||
|
evm::{
|
||||||
|
EvmError, EvmSignTransactionResponse, evm_sign_transaction_response::Result as SignResult,
|
||||||
|
},
|
||||||
|
};
|
||||||
use kameo::Actor;
|
use kameo::Actor;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{actors::{
|
use crate::{
|
||||||
GlobalActors, client::{ClientError, ClientConnection}, router::RegisterClient
|
actors::{
|
||||||
}, db};
|
GlobalActors,
|
||||||
|
client::{ClientConnection, ClientError},
|
||||||
|
evm::ClientSignTransaction,
|
||||||
|
router::RegisterClient,
|
||||||
|
},
|
||||||
|
db,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct ClientSession {
|
pub struct ClientSession {
|
||||||
props: ClientConnection,
|
props: ClientConnection,
|
||||||
|
client_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientSession {
|
impl ClientSession {
|
||||||
pub(crate) fn new(props: ClientConnection) -> Self {
|
pub(crate) fn new(props: ClientConnection, client_id: i32) -> Self {
|
||||||
Self { props }
|
Self { props, client_id }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn process_transport_inbound(&mut self, req: ClientRequest) -> Output {
|
pub async fn process_transport_inbound(&mut self, req: ClientRequest) -> Output {
|
||||||
@@ -22,8 +38,46 @@ impl ClientSession {
|
|||||||
ClientError::MissingRequestPayload
|
ClientError::MissingRequestPayload
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let _ = msg;
|
match msg {
|
||||||
Err(ClientError::UnexpectedRequestPayload)
|
ClientRequestPayload::EvmSignTransaction(sign_req) => {
|
||||||
|
let wallet_address: [u8; 20] = sign_req
|
||||||
|
.wallet_address
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| ClientError::UnexpectedRequestPayload)?;
|
||||||
|
|
||||||
|
let mut rlp_bytes: &[u8] = &sign_req.rlp_transaction;
|
||||||
|
let tx = TxEip1559::decode(&mut rlp_bytes)
|
||||||
|
.map_err(|_| ClientError::UnexpectedRequestPayload)?;
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.props
|
||||||
|
.actors
|
||||||
|
.evm
|
||||||
|
.ask(ClientSignTransaction {
|
||||||
|
client_id: self.client_id,
|
||||||
|
wallet_address: Address::from_slice(&wallet_address),
|
||||||
|
transaction: tx,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let response_result = match result {
|
||||||
|
Ok(signature) => SignResult::Signature(signature.as_bytes().to_vec()),
|
||||||
|
Err(err) => {
|
||||||
|
error!(?err, "client sign transaction failed");
|
||||||
|
SignResult::Error(EvmError::Internal.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ClientResponse {
|
||||||
|
payload: Some(ClientResponsePayload::EvmSignTransaction(
|
||||||
|
EvmSignTransactionResponse {
|
||||||
|
result: Some(response_result),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(ClientError::UnexpectedRequestPayload),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +143,9 @@ impl ClientSession {
|
|||||||
use arbiter_proto::transport::DummyTransport;
|
use arbiter_proto::transport::DummyTransport;
|
||||||
let transport: super::Transport = Box::new(DummyTransport::new());
|
let transport: super::Transport = Box::new(DummyTransport::new());
|
||||||
let props = ClientConnection::new(db, transport, actors);
|
let props = ClientConnection::new(db, transport, actors);
|
||||||
Self { props }
|
Self {
|
||||||
|
props,
|
||||||
|
client_id: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
use std::{collections::HashMap, ops::ControlFlow};
|
use std::{collections::HashMap, ops::ControlFlow};
|
||||||
|
|
||||||
use ed25519_dalek::VerifyingKey;
|
|
||||||
use kameo::{
|
use kameo::{
|
||||||
Actor,
|
Actor,
|
||||||
actor::{ActorId, ActorRef},
|
actor::{ActorId, ActorRef},
|
||||||
messages,
|
messages,
|
||||||
prelude::{ActorStopReason, Context, WeakActorRef},
|
prelude::{ActorStopReason, Context, WeakActorRef},
|
||||||
reply::DelegatedReply,
|
|
||||||
};
|
};
|
||||||
use tokio::{sync::watch, task::JoinSet};
|
use tracing::info;
|
||||||
use tracing::{info, warn};
|
|
||||||
|
|
||||||
use crate::actors::{
|
use crate::actors::{client::session::ClientSession, user_agent::session::UserAgentSession};
|
||||||
client::session::ClientSession,
|
|
||||||
user_agent::session::{RequestNewClientApproval, UserAgentSession},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct MessageRouter {
|
pub struct MessageRouter {
|
||||||
@@ -56,73 +50,6 @@ impl Actor for MessageRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub enum ApprovalError {
|
|
||||||
#[error("No user agents connected")]
|
|
||||||
NoUserAgentsConnected,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn request_client_approval(
|
|
||||||
user_agents: &[WeakActorRef<UserAgentSession>],
|
|
||||||
client_pubkey: VerifyingKey,
|
|
||||||
) -> Result<bool, ApprovalError> {
|
|
||||||
if user_agents.is_empty() {
|
|
||||||
return Err(ApprovalError::NoUserAgentsConnected);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pool = JoinSet::new();
|
|
||||||
let (cancel_tx, cancel_rx) = watch::channel(());
|
|
||||||
|
|
||||||
for weak_ref in user_agents {
|
|
||||||
match weak_ref.upgrade() {
|
|
||||||
Some(agent) => {
|
|
||||||
let cancel_rx = cancel_rx.clone();
|
|
||||||
pool.spawn(async move {
|
|
||||||
agent
|
|
||||||
.ask(RequestNewClientApproval {
|
|
||||||
client_pubkey,
|
|
||||||
cancel_flag: cancel_rx.clone(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
});
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
warn!(
|
|
||||||
id = weak_ref.id().to_string(),
|
|
||||||
actor = "MessageRouter",
|
|
||||||
event = "useragent.disconnected_before_approval"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(result) = pool.join_next().await {
|
|
||||||
match result {
|
|
||||||
Ok(Ok(approved)) => {
|
|
||||||
// cancel other pending requests
|
|
||||||
let _ = cancel_tx.send(());
|
|
||||||
return Ok(approved);
|
|
||||||
}
|
|
||||||
Ok(Err(err)) => {
|
|
||||||
warn!(
|
|
||||||
?err,
|
|
||||||
actor = "MessageRouter",
|
|
||||||
event = "useragent.approval_error"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!(
|
|
||||||
?err,
|
|
||||||
actor = "MessageRouter",
|
|
||||||
event = "useragent.approval_task_failed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(ApprovalError::NoUserAgentsConnected)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[messages]
|
#[messages]
|
||||||
impl MessageRouter {
|
impl MessageRouter {
|
||||||
#[message(ctx)]
|
#[message(ctx)]
|
||||||
@@ -146,29 +73,4 @@ impl MessageRouter {
|
|||||||
ctx.actor_ref().link(&actor).await;
|
ctx.actor_ref().link(&actor).await;
|
||||||
self.clients.insert(actor.id(), actor);
|
self.clients.insert(actor.id(), actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message(ctx)]
|
|
||||||
pub async fn request_client_approval(
|
|
||||||
&mut self,
|
|
||||||
client_pubkey: VerifyingKey,
|
|
||||||
ctx: &mut Context<Self, DelegatedReply<Result<bool, ApprovalError>>>,
|
|
||||||
) -> DelegatedReply<Result<bool, ApprovalError>> {
|
|
||||||
let (reply, Some(reply_sender)) = ctx.reply_sender() else {
|
|
||||||
panic!("Exptected `request_client_approval` to have callback channel");
|
|
||||||
};
|
|
||||||
|
|
||||||
let weak_refs = self
|
|
||||||
.user_agents
|
|
||||||
.values()
|
|
||||||
.map(|agent| agent.downgrade())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// handle in subtask to not to lock the actor
|
|
||||||
tokio::task::spawn(async move {
|
|
||||||
let result = request_client_approval(&weak_refs, client_pubkey).await;
|
|
||||||
reply_sender.send(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
reply
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,32 @@ use std::{ops::DerefMut, sync::Mutex};
|
|||||||
use arbiter_proto::proto::{
|
use arbiter_proto::proto::{
|
||||||
evm as evm_proto,
|
evm as evm_proto,
|
||||||
user_agent::{
|
user_agent::{
|
||||||
ClientConnectionCancel, ClientConnectionRequest, UnsealEncryptedKey, UnsealResult,
|
SdkClientApproveRequest, SdkClientApproveResponse, SdkClientEntry,
|
||||||
|
SdkClientError as ProtoSdkClientError, SdkClientList, SdkClientListResponse,
|
||||||
|
SdkClientRevokeRequest, SdkClientRevokeResponse, UnsealEncryptedKey, UnsealResult,
|
||||||
UnsealStart, UnsealStartResponse, UserAgentRequest, UserAgentResponse,
|
UnsealStart, UnsealStartResponse, UserAgentRequest, UserAgentResponse,
|
||||||
|
sdk_client_approve_response, sdk_client_list_response, sdk_client_revoke_response,
|
||||||
user_agent_request::Payload as UserAgentRequestPayload,
|
user_agent_request::Payload as UserAgentRequestPayload,
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||||
use ed25519_dalek::VerifyingKey;
|
use diesel::{ExpressionMethods as _, QueryDsl as _, dsl::insert_into};
|
||||||
use kameo::{Actor, error::SendError, messages, prelude::Context};
|
use diesel_async::RunQueryDsl as _;
|
||||||
|
use kameo::{Actor, error::SendError, prelude::Context};
|
||||||
use memsafe::MemSafe;
|
use memsafe::MemSafe;
|
||||||
use tokio::{select, sync::watch};
|
use tokio::select;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
use crate::actors::{
|
use crate::{
|
||||||
evm::{Generate, ListWallets},
|
actors::{
|
||||||
keyholder::{self, TryUnseal},
|
evm::{Generate, ListWallets},
|
||||||
router::RegisterUserAgent,
|
keyholder::{self, TryUnseal},
|
||||||
user_agent::{TransportResponseError, UserAgentConnection},
|
router::RegisterUserAgent,
|
||||||
|
user_agent::{TransportResponseError, UserAgentConnection},
|
||||||
|
},
|
||||||
|
db::schema::program_client,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod state;
|
mod state;
|
||||||
@@ -108,52 +115,6 @@ impl UserAgentSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[messages]
|
|
||||||
impl UserAgentSession {
|
|
||||||
// TODO: Think about refactoring it to state-machine based flow, as we already have one
|
|
||||||
#[message(ctx)]
|
|
||||||
pub async fn request_new_client_approval(
|
|
||||||
&mut self,
|
|
||||||
client_pubkey: VerifyingKey,
|
|
||||||
mut cancel_flag: watch::Receiver<()>,
|
|
||||||
ctx: &mut Context<Self, Result<bool, Error>>,
|
|
||||||
) -> Result<bool, Error> {
|
|
||||||
self.send_msg(
|
|
||||||
UserAgentResponsePayload::ClientConnectionRequest(ClientConnectionRequest {
|
|
||||||
pubkey: client_pubkey.as_bytes().to_vec(),
|
|
||||||
}),
|
|
||||||
ctx,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let extractor = |msg| {
|
|
||||||
if let UserAgentRequestPayload::ClientConnectionResponse(client_connection_response) =
|
|
||||||
msg
|
|
||||||
{
|
|
||||||
Some(client_connection_response)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
_ = cancel_flag.changed() => {
|
|
||||||
info!(actor = "useragent", "client connection approval cancelled");
|
|
||||||
self.send_msg(
|
|
||||||
UserAgentResponsePayload::ClientConnectionCancel(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
pub async fn process_transport_inbound(&mut self, req: UserAgentRequest) -> Output {
|
pub async fn process_transport_inbound(&mut self, req: UserAgentRequest) -> Output {
|
||||||
let msg = req.payload.ok_or_else(|| {
|
let msg = req.payload.ok_or_else(|| {
|
||||||
@@ -170,6 +131,13 @@ impl UserAgentSession {
|
|||||||
}
|
}
|
||||||
UserAgentRequestPayload::EvmWalletCreate(_) => self.handle_evm_wallet_create().await,
|
UserAgentRequestPayload::EvmWalletCreate(_) => self.handle_evm_wallet_create().await,
|
||||||
UserAgentRequestPayload::EvmWalletList(_) => self.handle_evm_wallet_list().await,
|
UserAgentRequestPayload::EvmWalletList(_) => self.handle_evm_wallet_list().await,
|
||||||
|
UserAgentRequestPayload::SdkClientApprove(req) => {
|
||||||
|
self.handle_sdk_client_approve(req).await
|
||||||
|
}
|
||||||
|
UserAgentRequestPayload::SdkClientRevoke(req) => {
|
||||||
|
self.handle_sdk_client_revoke(req).await
|
||||||
|
}
|
||||||
|
UserAgentRequestPayload::SdkClientList(_) => self.handle_sdk_client_list().await,
|
||||||
_ => Err(TransportResponseError::UnexpectedRequestPayload),
|
_ => Err(TransportResponseError::UnexpectedRequestPayload),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,6 +299,204 @@ impl UserAgentSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl UserAgentSession {
|
||||||
|
async fn handle_sdk_client_approve(&mut self, req: SdkClientApproveRequest) -> Output {
|
||||||
|
use sdk_client_approve_response::Result as ApproveResult;
|
||||||
|
|
||||||
|
if req.pubkey.len() != 32 {
|
||||||
|
return Ok(response(UserAgentResponsePayload::SdkClientApprove(
|
||||||
|
SdkClientApproveResponse {
|
||||||
|
result: Some(ApproveResult::Error(ProtoSdkClientError::Internal.into())),
|
||||||
|
},
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs() as i32;
|
||||||
|
|
||||||
|
let mut conn = match self.props.db.get().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
error!(?e, "Failed to get DB connection for sdk_client_approve");
|
||||||
|
return Ok(response(UserAgentResponsePayload::SdkClientApprove(
|
||||||
|
SdkClientApproveResponse {
|
||||||
|
result: Some(ApproveResult::Error(ProtoSdkClientError::Internal.into())),
|
||||||
|
},
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let pubkey_bytes = req.pubkey.clone();
|
||||||
|
let insert_result = insert_into(program_client::table)
|
||||||
|
.values((
|
||||||
|
program_client::public_key.eq(&pubkey_bytes),
|
||||||
|
program_client::nonce.eq(1), // pre-incremented; challenge will use nonce=0
|
||||||
|
program_client::created_at.eq(now),
|
||||||
|
program_client::updated_at.eq(now),
|
||||||
|
))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match insert_result {
|
||||||
|
Ok(_) => {
|
||||||
|
match program_client::table
|
||||||
|
.filter(program_client::public_key.eq(&pubkey_bytes))
|
||||||
|
.order(program_client::id.desc())
|
||||||
|
.select((
|
||||||
|
program_client::id,
|
||||||
|
program_client::public_key,
|
||||||
|
program_client::created_at,
|
||||||
|
))
|
||||||
|
.first::<(i32, Vec<u8>, i32)>(&mut conn)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((id, pubkey, created_at)) => Ok(response(
|
||||||
|
UserAgentResponsePayload::SdkClientApprove(SdkClientApproveResponse {
|
||||||
|
result: Some(ApproveResult::Client(SdkClientEntry {
|
||||||
|
id,
|
||||||
|
pubkey,
|
||||||
|
created_at,
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
Err(e) => {
|
||||||
|
error!(?e, "Failed to fetch inserted SDK client");
|
||||||
|
Ok(response(UserAgentResponsePayload::SdkClientApprove(
|
||||||
|
SdkClientApproveResponse {
|
||||||
|
result: Some(ApproveResult::Error(
|
||||||
|
ProtoSdkClientError::Internal.into(),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(diesel::result::Error::DatabaseError(
|
||||||
|
diesel::result::DatabaseErrorKind::UniqueViolation,
|
||||||
|
_,
|
||||||
|
)) => Ok(response(UserAgentResponsePayload::SdkClientApprove(
|
||||||
|
SdkClientApproveResponse {
|
||||||
|
result: Some(ApproveResult::Error(
|
||||||
|
ProtoSdkClientError::AlreadyExists.into(),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
))),
|
||||||
|
Err(e) => {
|
||||||
|
error!(?e, "Failed to insert SDK client");
|
||||||
|
Ok(response(UserAgentResponsePayload::SdkClientApprove(
|
||||||
|
SdkClientApproveResponse {
|
||||||
|
result: Some(ApproveResult::Error(ProtoSdkClientError::Internal.into())),
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_sdk_client_list(&mut self) -> Output {
|
||||||
|
let mut conn = match self.props.db.get().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
error!(?e, "Failed to get DB connection for sdk_client_list");
|
||||||
|
return Ok(response(UserAgentResponsePayload::SdkClientList(
|
||||||
|
SdkClientListResponse {
|
||||||
|
result: Some(sdk_client_list_response::Result::Error(
|
||||||
|
ProtoSdkClientError::Internal.into(),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match program_client::table
|
||||||
|
.select((
|
||||||
|
program_client::id,
|
||||||
|
program_client::public_key,
|
||||||
|
program_client::created_at,
|
||||||
|
))
|
||||||
|
.load::<(i32, Vec<u8>, i32)>(&mut conn)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(rows) => Ok(response(UserAgentResponsePayload::SdkClientList(
|
||||||
|
SdkClientListResponse {
|
||||||
|
result: Some(sdk_client_list_response::Result::Clients(SdkClientList {
|
||||||
|
clients: rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, pubkey, created_at)| SdkClientEntry {
|
||||||
|
id,
|
||||||
|
pubkey,
|
||||||
|
created_at,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
))),
|
||||||
|
Err(e) => {
|
||||||
|
error!(?e, "Failed to list SDK clients");
|
||||||
|
Ok(response(UserAgentResponsePayload::SdkClientList(
|
||||||
|
SdkClientListResponse {
|
||||||
|
result: Some(sdk_client_list_response::Result::Error(
|
||||||
|
ProtoSdkClientError::Internal.into(),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_sdk_client_revoke(&mut self, req: SdkClientRevokeRequest) -> Output {
|
||||||
|
use sdk_client_revoke_response::Result as RevokeResult;
|
||||||
|
|
||||||
|
let mut conn = match self.props.db.get().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
error!(?e, "Failed to get DB connection for sdk_client_revoke");
|
||||||
|
return Ok(response(UserAgentResponsePayload::SdkClientRevoke(
|
||||||
|
SdkClientRevokeResponse {
|
||||||
|
result: Some(RevokeResult::Error(ProtoSdkClientError::Internal.into())),
|
||||||
|
},
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match diesel::delete(program_client::table)
|
||||||
|
.filter(program_client::id.eq(req.client_id))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(0) => Ok(response(UserAgentResponsePayload::SdkClientRevoke(
|
||||||
|
SdkClientRevokeResponse {
|
||||||
|
result: Some(RevokeResult::Error(ProtoSdkClientError::NotFound.into())),
|
||||||
|
},
|
||||||
|
))),
|
||||||
|
Ok(_) => Ok(response(UserAgentResponsePayload::SdkClientRevoke(
|
||||||
|
SdkClientRevokeResponse {
|
||||||
|
result: Some(RevokeResult::Ok(())),
|
||||||
|
},
|
||||||
|
))),
|
||||||
|
Err(diesel::result::Error::DatabaseError(
|
||||||
|
diesel::result::DatabaseErrorKind::ForeignKeyViolation,
|
||||||
|
_,
|
||||||
|
)) => Ok(response(UserAgentResponsePayload::SdkClientRevoke(
|
||||||
|
SdkClientRevokeResponse {
|
||||||
|
result: Some(RevokeResult::Error(
|
||||||
|
ProtoSdkClientError::HasRelatedData.into(),
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
))),
|
||||||
|
Err(e) => {
|
||||||
|
error!(?e, "Failed to delete SDK client");
|
||||||
|
Ok(response(UserAgentResponsePayload::SdkClientRevoke(
|
||||||
|
SdkClientRevokeResponse {
|
||||||
|
result: Some(RevokeResult::Error(ProtoSdkClientError::Internal.into())),
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn map_evm_error<M>(op: &str, err: SendError<M, crate::actors::evm::Error>) -> evm_proto::EvmError {
|
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};
|
use crate::actors::{evm::Error as EvmError, keyholder::Error as KhError};
|
||||||
match err {
|
match err {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ fn client_auth_error_status(value: &client::auth::Error) -> Status {
|
|||||||
Status::invalid_argument("Failed to convert pubkey to VerifyingKey")
|
Status::invalid_argument("Failed to convert pubkey to VerifyingKey")
|
||||||
}
|
}
|
||||||
Error::InvalidChallengeSolution => Status::unauthenticated(value.to_string()),
|
Error::InvalidChallengeSolution => Status::unauthenticated(value.to_string()),
|
||||||
Error::ApproveError(_) => Status::permission_denied(value.to_string()),
|
Error::NotRegistered => Status::permission_denied(value.to_string()),
|
||||||
Error::Transport => Status::internal("Transport error"),
|
Error::Transport => Status::internal("Transport error"),
|
||||||
Error::DatabasePoolUnavailable => Status::internal("Database pool error"),
|
Error::DatabasePoolUnavailable => Status::internal("Database pool error"),
|
||||||
Error::DatabaseOperationFailed => Status::internal("Database error"),
|
Error::DatabaseOperationFailed => Status::internal("Database error"),
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
use arbiter_proto::proto::client::{
|
use alloy::{
|
||||||
AuthChallengeRequest, AuthChallengeSolution, ClientRequest,
|
consensus::TxEip1559,
|
||||||
client_request::Payload as ClientRequestPayload,
|
primitives::{Address, Bytes, TxKind, U256},
|
||||||
client_response::Payload as ClientResponsePayload,
|
rlp::Encodable,
|
||||||
|
};
|
||||||
|
use arbiter_proto::proto::{
|
||||||
|
client::{
|
||||||
|
AuthChallengeRequest, AuthChallengeSolution, ClientRequest,
|
||||||
|
client_request::Payload as ClientRequestPayload,
|
||||||
|
client_response::Payload as ClientResponsePayload,
|
||||||
|
},
|
||||||
|
evm::EvmSignTransactionRequest,
|
||||||
};
|
};
|
||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::transport::Bi;
|
||||||
use arbiter_server::actors::GlobalActors;
|
use arbiter_server::actors::GlobalActors;
|
||||||
@@ -109,3 +117,106 @@ pub async fn test_challenge_auth() {
|
|||||||
// Auth completes, session spawned
|
// Auth completes, session spawned
|
||||||
task.await.unwrap();
|
task.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
pub async fn test_evm_sign_request_payload_is_handled() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
|
||||||
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
insert_into(schema::program_client::table)
|
||||||
|
.values(schema::program_client::public_key.eq(pubkey_bytes.clone()))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
|
||||||
|
let props = ClientConnection::new(db.clone(), Box::new(server_transport), actors);
|
||||||
|
let task = tokio::spawn(connect_client(props));
|
||||||
|
|
||||||
|
test_transport
|
||||||
|
.send(ClientRequest {
|
||||||
|
payload: Some(ClientRequestPayload::AuthChallengeRequest(
|
||||||
|
AuthChallengeRequest {
|
||||||
|
pubkey: pubkey_bytes,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = test_transport
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.expect("should receive challenge");
|
||||||
|
let challenge = match response {
|
||||||
|
Ok(resp) => match resp.payload {
|
||||||
|
Some(ClientResponsePayload::AuthChallenge(c)) => c,
|
||||||
|
other => panic!("Expected AuthChallenge, got {other:?}"),
|
||||||
|
},
|
||||||
|
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let formatted_challenge = arbiter_proto::format_challenge(challenge.nonce, &challenge.pubkey);
|
||||||
|
let signature = new_key.sign(&formatted_challenge);
|
||||||
|
|
||||||
|
test_transport
|
||||||
|
.send(ClientRequest {
|
||||||
|
payload: Some(ClientRequestPayload::AuthChallengeSolution(
|
||||||
|
AuthChallengeSolution {
|
||||||
|
signature: signature.to_bytes().to_vec(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
task.await.unwrap();
|
||||||
|
|
||||||
|
let tx = TxEip1559 {
|
||||||
|
chain_id: 1,
|
||||||
|
nonce: 0,
|
||||||
|
gas_limit: 21_000,
|
||||||
|
max_fee_per_gas: 1,
|
||||||
|
max_priority_fee_per_gas: 1,
|
||||||
|
to: TxKind::Call(Address::from_slice(&[0x11; 20])),
|
||||||
|
value: U256::ZERO,
|
||||||
|
input: Bytes::new(),
|
||||||
|
access_list: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rlp_transaction = Vec::new();
|
||||||
|
tx.encode(&mut rlp_transaction);
|
||||||
|
|
||||||
|
test_transport
|
||||||
|
.send(ClientRequest {
|
||||||
|
payload: Some(ClientRequestPayload::EvmSignTransaction(
|
||||||
|
EvmSignTransactionRequest {
|
||||||
|
wallet_address: [0x22; 20].to_vec(),
|
||||||
|
rlp_transaction,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = test_transport
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.expect("should receive sign response");
|
||||||
|
|
||||||
|
match response {
|
||||||
|
Ok(resp) => match resp.payload {
|
||||||
|
Some(ClientResponsePayload::EvmSignTransaction(_)) => {}
|
||||||
|
other => panic!("Expected EvmSignTransaction response, got {other:?}"),
|
||||||
|
},
|
||||||
|
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@ mod common;
|
|||||||
|
|
||||||
#[path = "user_agent/auth.rs"]
|
#[path = "user_agent/auth.rs"]
|
||||||
mod auth;
|
mod auth;
|
||||||
|
#[path = "user_agent/sdk_client.rs"]
|
||||||
|
mod sdk_client;
|
||||||
#[path = "user_agent/unseal.rs"]
|
#[path = "user_agent/unseal.rs"]
|
||||||
mod unseal;
|
mod unseal;
|
||||||
|
|||||||
270
server/crates/arbiter-server/tests/user_agent/sdk_client.rs
Normal file
270
server/crates/arbiter-server/tests/user_agent/sdk_client.rs
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
use arbiter_proto::proto::user_agent::{
|
||||||
|
SdkClientApproveRequest, SdkClientError as ProtoSdkClientError, SdkClientRevokeRequest,
|
||||||
|
UserAgentRequest, sdk_client_approve_response, sdk_client_list_response,
|
||||||
|
sdk_client_revoke_response, user_agent_request::Payload as UserAgentRequestPayload,
|
||||||
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
|
};
|
||||||
|
use arbiter_server::{
|
||||||
|
actors::{GlobalActors, user_agent::session::UserAgentSession},
|
||||||
|
db,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Shared helper: create a session and register a client pubkey via sdk_client_approve.
|
||||||
|
async fn make_session(db: &db::DatabasePool) -> UserAgentSession {
|
||||||
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
UserAgentSession::new_test(db.clone(), actors)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
async fn test_sdk_client_approve_registers_client() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let mut session = make_session(&db).await;
|
||||||
|
|
||||||
|
let pubkey = [0x42u8; 32];
|
||||||
|
|
||||||
|
let response = session
|
||||||
|
.process_transport_inbound(UserAgentRequest {
|
||||||
|
payload: Some(UserAgentRequestPayload::SdkClientApprove(
|
||||||
|
SdkClientApproveRequest {
|
||||||
|
pubkey: pubkey.to_vec(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("handler should succeed");
|
||||||
|
|
||||||
|
let entry = match response.payload.unwrap() {
|
||||||
|
UserAgentResponsePayload::SdkClientApprove(resp) => match resp.result.unwrap() {
|
||||||
|
sdk_client_approve_response::Result::Client(e) => e,
|
||||||
|
sdk_client_approve_response::Result::Error(e) => {
|
||||||
|
panic!("Expected Client, got error {:?}", e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
other => panic!("Expected SdkClientApprove, got {other:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(entry.pubkey, pubkey.to_vec());
|
||||||
|
assert!(entry.id > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
async fn test_sdk_client_approve_duplicate_returns_already_exists() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let mut session = make_session(&db).await;
|
||||||
|
|
||||||
|
let pubkey = [0x11u8; 32];
|
||||||
|
let req = UserAgentRequest {
|
||||||
|
payload: Some(UserAgentRequestPayload::SdkClientApprove(
|
||||||
|
SdkClientApproveRequest {
|
||||||
|
pubkey: pubkey.to_vec(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
session
|
||||||
|
.process_transport_inbound(req.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = session
|
||||||
|
.process_transport_inbound(req)
|
||||||
|
.await
|
||||||
|
.expect("second insert should not panic");
|
||||||
|
|
||||||
|
match response.payload.unwrap() {
|
||||||
|
UserAgentResponsePayload::SdkClientApprove(resp) => match resp.result.unwrap() {
|
||||||
|
sdk_client_approve_response::Result::Error(code) => {
|
||||||
|
assert_eq!(code, ProtoSdkClientError::AlreadyExists as i32);
|
||||||
|
}
|
||||||
|
sdk_client_approve_response::Result::Client(_) => {
|
||||||
|
panic!("Expected AlreadyExists error for duplicate pubkey")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
other => panic!("Expected SdkClientApprove, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
async fn test_sdk_client_list_shows_registered_clients() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let mut session = make_session(&db).await;
|
||||||
|
|
||||||
|
let pubkey_a = [0x0Au8; 32];
|
||||||
|
let pubkey_b = [0x0Bu8; 32];
|
||||||
|
|
||||||
|
for pubkey in [pubkey_a, pubkey_b] {
|
||||||
|
session
|
||||||
|
.process_transport_inbound(UserAgentRequest {
|
||||||
|
payload: Some(UserAgentRequestPayload::SdkClientApprove(
|
||||||
|
SdkClientApproveRequest {
|
||||||
|
pubkey: pubkey.to_vec(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = session
|
||||||
|
.process_transport_inbound(UserAgentRequest {
|
||||||
|
payload: Some(UserAgentRequestPayload::SdkClientList(())),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("list should succeed");
|
||||||
|
|
||||||
|
let clients = match response.payload.unwrap() {
|
||||||
|
UserAgentResponsePayload::SdkClientList(resp) => match resp.result.unwrap() {
|
||||||
|
sdk_client_list_response::Result::Clients(list) => list.clients,
|
||||||
|
sdk_client_list_response::Result::Error(e) => {
|
||||||
|
panic!("Expected Clients, got error {:?}", e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
other => panic!("Expected SdkClientList, got {other:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(clients.len(), 2);
|
||||||
|
let pubkeys: Vec<Vec<u8>> = clients.into_iter().map(|e| e.pubkey).collect();
|
||||||
|
assert!(pubkeys.contains(&pubkey_a.to_vec()));
|
||||||
|
assert!(pubkeys.contains(&pubkey_b.to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
async fn test_sdk_client_revoke_removes_client() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let mut session = make_session(&db).await;
|
||||||
|
|
||||||
|
let pubkey = [0xBBu8; 32];
|
||||||
|
|
||||||
|
// Register a client and get its id
|
||||||
|
let approve_response = session
|
||||||
|
.process_transport_inbound(UserAgentRequest {
|
||||||
|
payload: Some(UserAgentRequestPayload::SdkClientApprove(
|
||||||
|
SdkClientApproveRequest {
|
||||||
|
pubkey: pubkey.to_vec(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let client_id = match approve_response.payload.unwrap() {
|
||||||
|
UserAgentResponsePayload::SdkClientApprove(resp) => match resp.result.unwrap() {
|
||||||
|
sdk_client_approve_response::Result::Client(e) => e.id,
|
||||||
|
sdk_client_approve_response::Result::Error(e) => panic!("approve failed: {:?}", e),
|
||||||
|
},
|
||||||
|
other => panic!("{other:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Revoke the client
|
||||||
|
let revoke_response = session
|
||||||
|
.process_transport_inbound(UserAgentRequest {
|
||||||
|
payload: Some(UserAgentRequestPayload::SdkClientRevoke(
|
||||||
|
SdkClientRevokeRequest { client_id },
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("revoke should succeed");
|
||||||
|
|
||||||
|
match revoke_response.payload.unwrap() {
|
||||||
|
UserAgentResponsePayload::SdkClientRevoke(resp) => match resp.result.unwrap() {
|
||||||
|
sdk_client_revoke_response::Result::Ok(_) => {}
|
||||||
|
sdk_client_revoke_response::Result::Error(e) => {
|
||||||
|
panic!("Expected Ok, got error {:?}", e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
other => panic!("Expected SdkClientRevoke, got {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// List should now be empty
|
||||||
|
let list_response = session
|
||||||
|
.process_transport_inbound(UserAgentRequest {
|
||||||
|
payload: Some(UserAgentRequestPayload::SdkClientList(())),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let clients = match list_response.payload.unwrap() {
|
||||||
|
UserAgentResponsePayload::SdkClientList(resp) => match resp.result.unwrap() {
|
||||||
|
sdk_client_list_response::Result::Clients(list) => list.clients,
|
||||||
|
sdk_client_list_response::Result::Error(e) => panic!("list error: {:?}", e),
|
||||||
|
},
|
||||||
|
other => panic!("{other:?}"),
|
||||||
|
};
|
||||||
|
assert!(clients.is_empty(), "client should be removed after revoke");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
async fn test_sdk_client_revoke_not_found_returns_error() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let mut session = make_session(&db).await;
|
||||||
|
|
||||||
|
let response = session
|
||||||
|
.process_transport_inbound(UserAgentRequest {
|
||||||
|
payload: Some(UserAgentRequestPayload::SdkClientRevoke(
|
||||||
|
SdkClientRevokeRequest { client_id: 9999 },
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match response.payload.unwrap() {
|
||||||
|
UserAgentResponsePayload::SdkClientRevoke(resp) => match resp.result.unwrap() {
|
||||||
|
sdk_client_revoke_response::Result::Error(code) => {
|
||||||
|
assert_eq!(code, ProtoSdkClientError::NotFound as i32);
|
||||||
|
}
|
||||||
|
sdk_client_revoke_response::Result::Ok(_) => {
|
||||||
|
panic!("Expected NotFound error for missing client_id")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
other => panic!("Expected SdkClientRevoke, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
async fn test_sdk_client_approve_rejected_client_cannot_auth() {
|
||||||
|
// Verify the core flow: only pre-approved clients can authenticate
|
||||||
|
use arbiter_proto::proto::client::{
|
||||||
|
AuthChallengeRequest, ClientRequest, client_request::Payload as ClientRequestPayload,
|
||||||
|
client_response::Payload as ClientResponsePayload,
|
||||||
|
};
|
||||||
|
use arbiter_proto::transport::Bi as _;
|
||||||
|
use arbiter_server::actors::client::{ClientConnection, connect_client};
|
||||||
|
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
|
||||||
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
|
let (server_transport, mut test_transport) = super::common::ChannelTransport::<_, _>::new();
|
||||||
|
let props = ClientConnection::new(db.clone(), Box::new(server_transport), actors.clone());
|
||||||
|
let task = tokio::spawn(connect_client(props));
|
||||||
|
|
||||||
|
test_transport
|
||||||
|
.send(ClientRequest {
|
||||||
|
payload: Some(ClientRequestPayload::AuthChallengeRequest(
|
||||||
|
AuthChallengeRequest {
|
||||||
|
pubkey: pubkey_bytes.clone(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = test_transport.recv().await.unwrap().unwrap();
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
response.payload.unwrap(),
|
||||||
|
ClientResponsePayload::ClientConnectError(_)
|
||||||
|
),
|
||||||
|
"unregistered client should be rejected"
|
||||||
|
);
|
||||||
|
|
||||||
|
task.await.unwrap();
|
||||||
|
}
|
||||||
7
server/crates/arbiter-terrors-poc/Cargo.toml
Normal file
7
server/crates/arbiter-terrors-poc/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "arbiter-terrors-poc"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
terrors = "0.3"
|
||||||
84
server/crates/arbiter-terrors-poc/src/errors.rs
Normal file
84
server/crates/arbiter-terrors-poc/src/errors.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use terrors::OneOf;
|
||||||
|
|
||||||
|
// Wire boundary type — what would go into a proto response
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ProtoError {
|
||||||
|
NotRegistered,
|
||||||
|
InvalidSignature,
|
||||||
|
Internal(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal terrors types
|
||||||
|
pub struct NotRegistered;
|
||||||
|
pub struct InvalidSignature;
|
||||||
|
pub struct Internal(pub String);
|
||||||
|
|
||||||
|
impl From<NotRegistered> for ProtoError {
|
||||||
|
fn from(_: NotRegistered) -> Self {
|
||||||
|
ProtoError::NotRegistered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<InvalidSignature> for ProtoError {
|
||||||
|
fn from(_: InvalidSignature) -> Self {
|
||||||
|
ProtoError::InvalidSignature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Internal> for ProtoError {
|
||||||
|
fn from(e: Internal) -> Self {
|
||||||
|
ProtoError::Internal(e.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts the narrowed remainder after handling NotRegistered
|
||||||
|
impl From<OneOf<(InvalidSignature, Internal)>> for ProtoError {
|
||||||
|
fn from(e: OneOf<(InvalidSignature, Internal)>) -> Self {
|
||||||
|
match e.narrow::<InvalidSignature, _>() {
|
||||||
|
Ok(_) => ProtoError::InvalidSignature,
|
||||||
|
Err(e) => {
|
||||||
|
let Internal(msg) = e.take();
|
||||||
|
ProtoError::Internal(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn not_registered_converts_to_proto() {
|
||||||
|
let e: ProtoError = NotRegistered.into();
|
||||||
|
assert!(matches!(e, ProtoError::NotRegistered));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_signature_converts_to_proto() {
|
||||||
|
let e: ProtoError = InvalidSignature.into();
|
||||||
|
assert!(matches!(e, ProtoError::InvalidSignature));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn internal_converts_to_proto() {
|
||||||
|
let e: ProtoError = Internal("boom".into()).into();
|
||||||
|
assert!(matches!(e, ProtoError::Internal(msg) if msg == "boom"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_of_remainder_converts_to_proto_invalid_signature() {
|
||||||
|
use terrors::OneOf;
|
||||||
|
let e: OneOf<(InvalidSignature, Internal)> = OneOf::new(InvalidSignature);
|
||||||
|
let proto = ProtoError::from(e);
|
||||||
|
assert!(matches!(proto, ProtoError::InvalidSignature));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_of_remainder_converts_to_proto_internal() {
|
||||||
|
use terrors::OneOf;
|
||||||
|
let e: OneOf<(InvalidSignature, Internal)> = OneOf::new(Internal("db fail".into()));
|
||||||
|
let proto = ProtoError::from(e);
|
||||||
|
assert!(matches!(proto, ProtoError::Internal(msg) if msg == "db fail"));
|
||||||
|
}
|
||||||
|
}
|
||||||
3
server/crates/arbiter-terrors-poc/src/main.rs
Normal file
3
server/crates/arbiter-terrors-poc/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod errors;
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
Reference in New Issue
Block a user