refactor(server): removed grpc adapter and replaced with concrete implementations
This commit is contained in:
24
server/Cargo.lock
generated
24
server/Cargo.lock
generated
@@ -755,30 +755,6 @@ dependencies = [
|
|||||||
"alloy",
|
"alloy",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arbiter-useragent"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"arbiter-proto",
|
|
||||||
"async-trait",
|
|
||||||
"ed25519-dalek",
|
|
||||||
"http",
|
|
||||||
"k256",
|
|
||||||
"kameo",
|
|
||||||
"rand 0.10.0",
|
|
||||||
"rsa",
|
|
||||||
"rustls-webpki",
|
|
||||||
"sha2 0.10.9",
|
|
||||||
"smlang",
|
|
||||||
"spki",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"tokio-stream",
|
|
||||||
"tonic",
|
|
||||||
"tracing",
|
|
||||||
"x25519-dalek",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argon2"
|
name = "argon2"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|||||||
@@ -1,78 +1,39 @@
|
|||||||
//! Transport-facing abstractions for protocol/session code.
|
//! Transport-facing abstractions shared by protocol/session code.
|
||||||
//!
|
//!
|
||||||
//! This module separates three concerns:
|
//! This module defines a small duplex interface, [`Bi`], that actors and other
|
||||||
//!
|
//! protocol code can depend on without knowing anything about the concrete
|
||||||
//! - protocol/session logic wants a small duplex interface ([`Bi`])
|
//! transport underneath.
|
||||||
//! - transport adapters push concrete stream items to an underlying IO layer
|
|
||||||
//! - transport boundaries translate between protocol-facing and transport-facing
|
|
||||||
//! item types via direction-specific converters
|
|
||||||
//!
|
//!
|
||||||
//! [`Bi`] is intentionally minimal and transport-agnostic:
|
//! [`Bi`] is intentionally minimal and transport-agnostic:
|
||||||
//! - [`Bi::recv`] yields inbound protocol messages
|
//! - [`Bi::recv`] yields inbound messages
|
||||||
//! - [`Bi::send`] accepts outbound protocol/domain items
|
//! - [`Bi::send`] accepts outbound messages
|
||||||
|
//!
|
||||||
|
//! Transport-specific adapters, including protobuf or gRPC bridges, live in the
|
||||||
|
//! crates that own those boundaries rather than in `arbiter-proto`.
|
||||||
//!
|
//!
|
||||||
//! # Generic Ordering Rule
|
//! # Generic Ordering Rule
|
||||||
//!
|
//!
|
||||||
//! This module uses a single convention consistently: when a type or trait is
|
//! This module consistently uses `Inbound` first and `Outbound` second in
|
||||||
//! parameterized by protocol message directions, the generic parameters are
|
//! generic parameter lists.
|
||||||
//! declared as `Inbound` first, then `Outbound`.
|
|
||||||
//!
|
//!
|
||||||
//! For [`Bi`], that means `Bi<Inbound, Outbound>`:
|
//! For [`Bi`], that means `Bi<Inbound, Outbound>`:
|
||||||
//! - `recv() -> Option<Inbound>`
|
//! - `recv() -> Option<Inbound>`
|
||||||
//! - `send(Outbound)`
|
//! - `send(Outbound)`
|
||||||
//!
|
//!
|
||||||
//! For adapter types that are parameterized by direction-specific converters,
|
//! [`expect_message`] is a small helper for request/response style flows: it
|
||||||
//! inbound-related converter parameters are declared before outbound-related
|
//! reads one inbound message from a transport and extracts a typed value from
|
||||||
//! converter parameters.
|
//! it, failing if the channel closes or the message shape is not what the
|
||||||
|
//! caller expected.
|
||||||
//!
|
//!
|
||||||
//! [`RecvConverter`] and [`SendConverter`] are infallible conversion traits used
|
//! [`DummyTransport`] is a no-op implementation useful for tests and local
|
||||||
//! by adapters to map between protocol-facing and transport-facing item types.
|
//! actor execution where no real stream exists.
|
||||||
//! The traits themselves are not result-aware; adapters decide how transport
|
|
||||||
//! errors are handled before (or instead of) conversion.
|
|
||||||
//!
|
|
||||||
//! [`grpc::GrpcAdapter`] combines:
|
|
||||||
//! - a tonic inbound stream
|
|
||||||
//! - a Tokio sender for outbound transport items
|
|
||||||
//! - a [`RecvConverter`] for the receive path
|
|
||||||
//! - a [`SendConverter`] for the send path
|
|
||||||
//!
|
|
||||||
//! [`DummyTransport`] is a no-op implementation useful for tests and local actor
|
|
||||||
//! execution where no real network stream exists.
|
|
||||||
//!
|
|
||||||
//! # Component Interaction
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! inbound (network -> protocol)
|
|
||||||
//! ============================
|
|
||||||
//!
|
|
||||||
//! tonic::Streaming<RecvTransport>
|
|
||||||
//! -> grpc::GrpcAdapter::recv()
|
|
||||||
//! |
|
|
||||||
//! +--> on `Ok(item)`: RecvConverter::convert(RecvTransport) -> Inbound
|
|
||||||
//! +--> on `Err(status)`: log error and close stream (`None`)
|
|
||||||
//! -> Bi::recv()
|
|
||||||
//! -> protocol/session actor
|
|
||||||
//!
|
|
||||||
//! outbound (protocol -> network)
|
|
||||||
//! ==============================
|
|
||||||
//!
|
|
||||||
//! protocol/session actor
|
|
||||||
//! -> Bi::send(Outbound)
|
|
||||||
//! -> grpc::GrpcAdapter::send()
|
|
||||||
//! |
|
|
||||||
//! +--> SendConverter::convert(Outbound) -> SendTransport
|
|
||||||
//! -> Tokio mpsc::Sender<SendTransport>
|
|
||||||
//! -> tonic response stream
|
|
||||||
//! ```
|
|
||||||
//!
|
//!
|
||||||
//! # Design Notes
|
//! # Design Notes
|
||||||
//!
|
//!
|
||||||
//! - `send()` returns [`Error`] only for transport delivery failures (for
|
//! - [`Bi::send`] returns [`Error`] only for transport delivery failures, such
|
||||||
//! example, when the outbound channel is closed).
|
//! as a closed outbound channel.
|
||||||
//! - [`grpc::GrpcAdapter`] logs tonic receive errors and treats them as stream
|
//! - [`Bi::recv`] returns `None` when the underlying transport closes.
|
||||||
//! closure (`None`).
|
//! - Message translation is intentionally out of scope for this module.
|
||||||
//! - When protocol-facing and transport-facing types are identical, use
|
|
||||||
//! [`IdentityRecvConverter`] / [`IdentitySendConverter`].
|
|
||||||
|
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
@@ -114,162 +75,6 @@ pub trait Bi<Inbound, Outbound>: Send + Sync + 'static {
|
|||||||
async fn recv(&mut self) -> Option<Inbound>;
|
async fn recv(&mut self) -> Option<Inbound>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts transport-facing inbound items into protocol-facing inbound items.
|
|
||||||
pub trait RecvConverter: Send + Sync + 'static {
|
|
||||||
type Input;
|
|
||||||
type Output;
|
|
||||||
|
|
||||||
fn convert(&self, item: Self::Input) -> Self::Output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts protocol/domain outbound items into transport-facing outbound items.
|
|
||||||
pub trait SendConverter: Send + Sync + 'static {
|
|
||||||
type Input;
|
|
||||||
type Output;
|
|
||||||
|
|
||||||
fn convert(&self, item: Self::Input) -> Self::Output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A [`RecvConverter`] that forwards values unchanged.
|
|
||||||
pub struct IdentityRecvConverter<T> {
|
|
||||||
_marker: PhantomData<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> IdentityRecvConverter<T> {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
_marker: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Default for IdentityRecvConverter<T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> RecvConverter for IdentityRecvConverter<T>
|
|
||||||
where
|
|
||||||
T: Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
type Input = T;
|
|
||||||
type Output = T;
|
|
||||||
|
|
||||||
fn convert(&self, item: Self::Input) -> Self::Output {
|
|
||||||
item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A [`SendConverter`] that forwards values unchanged.
|
|
||||||
pub struct IdentitySendConverter<T> {
|
|
||||||
_marker: PhantomData<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> IdentitySendConverter<T> {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
_marker: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Default for IdentitySendConverter<T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> SendConverter for IdentitySendConverter<T>
|
|
||||||
where
|
|
||||||
T: Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
type Input = T;
|
|
||||||
type Output = T;
|
|
||||||
|
|
||||||
fn convert(&self, item: Self::Input) -> Self::Output {
|
|
||||||
item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// gRPC-specific transport adapters and helpers.
|
|
||||||
pub mod grpc {
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use futures::StreamExt;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tonic::Streaming;
|
|
||||||
|
|
||||||
use super::{Bi, Error, RecvConverter, SendConverter};
|
|
||||||
|
|
||||||
/// [`Bi`] adapter backed by a tonic gRPC bidirectional stream.
|
|
||||||
///
|
|
||||||
/// Tonic receive errors are logged and treated as stream closure (`None`).
|
|
||||||
/// The receive converter is only invoked for successful inbound transport
|
|
||||||
/// items.
|
|
||||||
pub struct GrpcAdapter<InboundConverter, OutboundConverter>
|
|
||||||
where
|
|
||||||
InboundConverter: RecvConverter,
|
|
||||||
OutboundConverter: SendConverter,
|
|
||||||
{
|
|
||||||
sender: mpsc::Sender<OutboundConverter::Output>,
|
|
||||||
receiver: Streaming<InboundConverter::Input>,
|
|
||||||
inbound_converter: InboundConverter,
|
|
||||||
outbound_converter: OutboundConverter,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<InboundTransport, Inbound, InboundConverter, OutboundConverter>
|
|
||||||
GrpcAdapter<InboundConverter, OutboundConverter>
|
|
||||||
where
|
|
||||||
InboundConverter: RecvConverter<Input = InboundTransport, Output = Inbound>,
|
|
||||||
OutboundConverter: SendConverter,
|
|
||||||
{
|
|
||||||
pub fn new(
|
|
||||||
sender: mpsc::Sender<OutboundConverter::Output>,
|
|
||||||
receiver: Streaming<InboundTransport>,
|
|
||||||
inbound_converter: InboundConverter,
|
|
||||||
outbound_converter: OutboundConverter,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
sender,
|
|
||||||
receiver,
|
|
||||||
inbound_converter,
|
|
||||||
outbound_converter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<InboundConverter, OutboundConverter> Bi<InboundConverter::Output, OutboundConverter::Input>
|
|
||||||
for GrpcAdapter<InboundConverter, OutboundConverter>
|
|
||||||
where
|
|
||||||
InboundConverter: RecvConverter,
|
|
||||||
OutboundConverter: SendConverter,
|
|
||||||
OutboundConverter::Input: Send + 'static,
|
|
||||||
OutboundConverter::Output: Send + 'static,
|
|
||||||
{
|
|
||||||
#[tracing::instrument(level = "trace", skip(self, item))]
|
|
||||||
async fn send(&mut self, item: OutboundConverter::Input) -> Result<(), Error> {
|
|
||||||
let outbound = self.outbound_converter.convert(item);
|
|
||||||
self.sender
|
|
||||||
.send(outbound)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::ChannelClosed)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(level = "trace", skip(self))]
|
|
||||||
async fn recv(&mut self) -> Option<InboundConverter::Output> {
|
|
||||||
match self.receiver.next().await {
|
|
||||||
Some(Ok(item)) => Some(self.inbound_converter.convert(item)),
|
|
||||||
Some(Err(error)) => {
|
|
||||||
tracing::error!(error = ?error, "grpc transport recv failed; closing stream");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No-op [`Bi`] transport for tests and manual actor usage.
|
/// No-op [`Bi`] transport for tests and manual actor usage.
|
||||||
///
|
///
|
||||||
/// `send` drops all items and succeeds. [`Bi::recv`] never resolves and therefore
|
/// `send` drops all items and succeeds. [`Bi::recv`] never resolves and therefore
|
||||||
|
|||||||
@@ -3,12 +3,7 @@ use diesel::QueryDsl;
|
|||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use kameo::{Actor, messages};
|
use kameo::{Actor, messages};
|
||||||
use miette::Diagnostic;
|
use miette::Diagnostic;
|
||||||
use rand::{
|
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
|
||||||
RngExt,
|
|
||||||
distr::{Alphanumeric},
|
|
||||||
make_rng,
|
|
||||||
rngs::StdRng,
|
|
||||||
};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::db::{self, DatabasePool, schema};
|
use crate::db::{self, DatabasePool, schema};
|
||||||
@@ -61,7 +56,6 @@ impl Bootstrapper {
|
|||||||
|
|
||||||
drop(conn);
|
drop(conn);
|
||||||
|
|
||||||
|
|
||||||
let token = if row_count == 0 {
|
let token = if row_count == 0 {
|
||||||
let token = generate_token().await?;
|
let token = generate_token().await?;
|
||||||
Some(token)
|
Some(token)
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::{format_challenge, transport::expect_message};
|
||||||
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 diesel::{
|
use diesel::{
|
||||||
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update,
|
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update,
|
||||||
};
|
};
|
||||||
@@ -18,7 +9,7 @@ use tracing::error;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
client::ClientConnection,
|
client::{ClientConnection, ConnectErrorCode, Request, Response},
|
||||||
router::{self, RequestClientApproval},
|
router::{self, RequestClientApproval},
|
||||||
},
|
},
|
||||||
db::{self, schema::program_client},
|
db::{self, schema::program_client},
|
||||||
@@ -155,15 +146,13 @@ async fn challenge_client(
|
|||||||
pubkey: VerifyingKey,
|
pubkey: VerifyingKey,
|
||||||
nonce: i32,
|
nonce: i32,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let challenge = AuthChallenge {
|
let challenge_pubkey = pubkey.as_bytes().to_vec();
|
||||||
pubkey: pubkey.as_bytes().to_vec(),
|
|
||||||
nonce,
|
|
||||||
};
|
|
||||||
|
|
||||||
props
|
props
|
||||||
.transport
|
.transport
|
||||||
.send(Ok(ClientResponse {
|
.send(Ok(Response::AuthChallenge {
|
||||||
payload: Some(ClientResponsePayload::AuthChallenge(challenge.clone())),
|
pubkey: challenge_pubkey.clone(),
|
||||||
|
nonce,
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -171,20 +160,17 @@ async fn challenge_client(
|
|||||||
Error::Transport
|
Error::Transport
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let AuthChallengeSolution { signature } =
|
let signature = expect_message(&mut *props.transport, |req: Request| match req {
|
||||||
expect_message(&mut *props.transport, |req: ClientRequest| {
|
Request::AuthChallengeSolution { signature } => Some(signature),
|
||||||
match req.payload? {
|
_ => None,
|
||||||
ClientRequestPayload::AuthChallengeSolution(s) => Some(s),
|
})
|
||||||
_ => None,
|
.await
|
||||||
}
|
.map_err(|e| {
|
||||||
})
|
error!(error = ?e, "Failed to receive challenge solution");
|
||||||
.await
|
Error::Transport
|
||||||
.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(|_| {
|
let sig = signature.as_slice().try_into().map_err(|_| {
|
||||||
error!("Invalid signature length");
|
error!("Invalid signature length");
|
||||||
Error::InvalidChallengeSolution
|
Error::InvalidChallengeSolution
|
||||||
@@ -209,15 +195,14 @@ fn connect_error_code(err: &Error) -> ConnectErrorCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn authenticate(props: &mut ClientConnection) -> Result<VerifyingKey, Error> {
|
async fn authenticate(props: &mut ClientConnection) -> Result<VerifyingKey, Error> {
|
||||||
let Some(ClientRequest {
|
let Some(Request::AuthChallengeRequest {
|
||||||
payload: Some(ClientRequestPayload::AuthChallengeRequest(challenge)),
|
pubkey: challenge_pubkey,
|
||||||
}) = props.transport.recv().await
|
}) = props.transport.recv().await
|
||||||
else {
|
else {
|
||||||
return Err(Error::Transport);
|
return Err(Error::Transport);
|
||||||
};
|
};
|
||||||
|
|
||||||
let pubkey_bytes = challenge
|
let pubkey_bytes = challenge_pubkey
|
||||||
.pubkey
|
|
||||||
.as_array()
|
.as_array()
|
||||||
.ok_or(Error::InvalidClientPubkeyLength)?;
|
.ok_or(Error::InvalidClientPubkeyLength)?;
|
||||||
let pubkey =
|
let pubkey =
|
||||||
@@ -244,11 +229,7 @@ pub async fn authenticate_and_create(mut props: ClientConnection) -> Result<Clie
|
|||||||
let code = connect_error_code(&err);
|
let code = connect_error_code(&err);
|
||||||
let _ = props
|
let _ = props
|
||||||
.transport
|
.transport
|
||||||
.send(Ok(ClientResponse {
|
.send(Ok(Response::ClientConnectError { code }))
|
||||||
payload: Some(ClientResponsePayload::ClientConnectError(
|
|
||||||
ClientConnectError { code: code.into() },
|
|
||||||
)),
|
|
||||||
}))
|
|
||||||
.await;
|
.await;
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::transport::Bi;
|
||||||
proto::client::{ClientRequest, ClientResponse},
|
|
||||||
transport::Bi,
|
|
||||||
};
|
|
||||||
use kameo::actor::Spawn;
|
use kameo::actor::Spawn;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
@@ -24,7 +21,27 @@ pub enum ClientError {
|
|||||||
Auth(#[from] auth::Error),
|
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 struct ClientConnection {
|
||||||
pub(crate) db: db::DatabasePool,
|
pub(crate) db: db::DatabasePool,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
|
|
||||||
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, Request, Response},
|
||||||
|
router::RegisterClient,
|
||||||
|
},
|
||||||
|
db,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct ClientSession {
|
pub struct ClientSession {
|
||||||
props: ClientConnection,
|
props: ClientConnection,
|
||||||
@@ -16,18 +20,13 @@ impl ClientSession {
|
|||||||
Self { props }
|
Self { props }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn process_transport_inbound(&mut self, req: ClientRequest) -> Output {
|
pub async fn process_transport_inbound(&mut self, req: Request) -> Output {
|
||||||
let msg = req.payload.ok_or_else(|| {
|
let _ = req;
|
||||||
error!(actor = "client", "Received message with no payload");
|
|
||||||
ClientError::MissingRequestPayload
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let _ = msg;
|
|
||||||
Err(ClientError::UnexpectedRequestPayload)
|
Err(ClientError::UnexpectedRequestPayload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Output = Result<ClientResponse, ClientError>;
|
type Output = Result<Response, ClientError>;
|
||||||
|
|
||||||
impl Actor for ClientSession {
|
impl Actor for ClientSession {
|
||||||
type Args = Self;
|
type Args = Self;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
|
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 diesel_async::RunQueryDsl;
|
||||||
use kameo::{Actor, actor::ActorRef, messages};
|
use kameo::{Actor, actor::ActorRef, messages};
|
||||||
use memsafe::MemSafe;
|
use memsafe::MemSafe;
|
||||||
@@ -7,13 +9,16 @@ use rand::{SeedableRng, rng, rngs::StdRng};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
||||||
db::{self, DatabasePool, models::{self, EvmBasicGrant, SqliteTimestamp}, schema},
|
db::{
|
||||||
|
self, DatabasePool,
|
||||||
|
models::{self, EvmBasicGrant, SqliteTimestamp},
|
||||||
|
schema,
|
||||||
|
},
|
||||||
evm::{
|
evm::{
|
||||||
self, RunKind,
|
self, RunKind,
|
||||||
policies::{
|
policies::{
|
||||||
FullGrant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
FullGrant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
||||||
ether_transfer::EtherTransfer,
|
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
||||||
token_transfers::TokenTransfer,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -88,7 +93,12 @@ impl EvmActor {
|
|||||||
// todo: audit
|
// todo: audit
|
||||||
let rng = StdRng::from_rng(&mut rng());
|
let rng = StdRng::from_rng(&mut rng());
|
||||||
let engine = evm::Engine::new(db.clone());
|
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 {
|
match grant {
|
||||||
SpecificGrant::EtherTransfer(settings) => {
|
SpecificGrant::EtherTransfer(settings) => {
|
||||||
self.engine
|
self.engine
|
||||||
.create_grant::<EtherTransfer>(client_id, FullGrant { basic, specific: settings })
|
.create_grant::<EtherTransfer>(
|
||||||
|
client_id,
|
||||||
|
FullGrant {
|
||||||
|
basic,
|
||||||
|
specific: settings,
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
SpecificGrant::TokenTransfer(settings) => {
|
SpecificGrant::TokenTransfer(settings) => {
|
||||||
self.engine
|
self.engine
|
||||||
.create_grant::<TokenTransfer>(client_id, FullGrant { basic, specific: settings })
|
.create_grant::<TokenTransfer>(
|
||||||
|
client_id,
|
||||||
|
FullGrant {
|
||||||
|
basic,
|
||||||
|
specific: settings,
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,8 +226,14 @@ impl EvmActor {
|
|||||||
.ok_or(SignTransactionError::WalletNotFound)?;
|
.ok_or(SignTransactionError::WalletNotFound)?;
|
||||||
drop(conn);
|
drop(conn);
|
||||||
|
|
||||||
let meaning = self.engine
|
let meaning = self
|
||||||
.evaluate_transaction(wallet.id, client_id, transaction.clone(), RunKind::Execution)
|
.engine
|
||||||
|
.evaluate_transaction(
|
||||||
|
wallet.id,
|
||||||
|
client_id,
|
||||||
|
transaction.clone(),
|
||||||
|
RunKind::Execution,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(meaning)
|
Ok(meaning)
|
||||||
@@ -230,14 +258,21 @@ impl EvmActor {
|
|||||||
|
|
||||||
let raw_key: MemSafe<Vec<u8>> = self
|
let raw_key: MemSafe<Vec<u8>> = self
|
||||||
.keyholder
|
.keyholder
|
||||||
.ask(Decrypt { aead_id: wallet.aead_encrypted_id })
|
.ask(Decrypt {
|
||||||
|
aead_id: wallet.aead_encrypted_id,
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| SignTransactionError::KeyholderSend)?;
|
.map_err(|_| SignTransactionError::KeyholderSend)?;
|
||||||
|
|
||||||
let signer = safe_signer::SafeSigner::from_memsafe(raw_key)?;
|
let signer = safe_signer::SafeSigner::from_memsafe(raw_key)?;
|
||||||
|
|
||||||
self.engine
|
self.engine
|
||||||
.evaluate_transaction(wallet.id, client_id, transaction.clone(), RunKind::Execution)
|
.evaluate_transaction(
|
||||||
|
wallet.id,
|
||||||
|
client_id,
|
||||||
|
transaction.clone(),
|
||||||
|
RunKind::Execution,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
use alloy::network::TxSignerSync as _;
|
use alloy::network::TxSignerSync as _;
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ impl KeyHolder {
|
|||||||
current_nonce: nonce.to_vec(),
|
current_nonce: nonce.to_vec(),
|
||||||
schema_version: 1,
|
schema_version: 1,
|
||||||
associated_root_key_id: *root_key_history_id,
|
associated_root_key_id: *root_key_history_id,
|
||||||
created_at: Utc::now().into()
|
created_at: Utc::now().into(),
|
||||||
})
|
})
|
||||||
.returning(schema::aead_encrypted::id)
|
.returning(schema::aead_encrypted::id)
|
||||||
.get_result(&mut conn)
|
.get_result(&mut conn)
|
||||||
@@ -346,7 +346,7 @@ impl KeyHolder {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use diesel::SelectableHelper;
|
use diesel::SelectableHelper;
|
||||||
|
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use memsafe::MemSafe;
|
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 tracing::error;
|
||||||
|
|
||||||
use crate::actors::user_agent::{
|
use crate::actors::user_agent::{
|
||||||
UserAgentConnection,
|
Request, UserAgentConnection,
|
||||||
auth::state::{AuthContext, AuthPublicKey, AuthStateMachine},
|
auth::state::{AuthContext, AuthStateMachine},
|
||||||
|
AuthPublicKey,
|
||||||
session::UserAgentSession,
|
session::UserAgentSession,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,54 +34,20 @@ pub enum Error {
|
|||||||
mod state;
|
mod state;
|
||||||
use state::*;
|
use state::*;
|
||||||
|
|
||||||
fn parse_pubkey(key_type: ProtoKeyType, pubkey: Vec<u8>) -> Result<AuthPublicKey, Error> {
|
fn parse_auth_event(payload: Request) -> Result<AuthEvents, 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> {
|
|
||||||
match payload {
|
match payload {
|
||||||
UserAgentRequestPayload::AuthChallengeRequest(AuthChallengeRequest {
|
Request::AuthChallengeRequest {
|
||||||
pubkey,
|
pubkey,
|
||||||
bootstrap_token: None,
|
bootstrap_token: None,
|
||||||
key_type,
|
} => Ok(AuthEvents::AuthRequest(ChallengeRequest { pubkey })),
|
||||||
}) => {
|
Request::AuthChallengeRequest {
|
||||||
let kt = ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified);
|
|
||||||
Ok(AuthEvents::AuthRequest(ChallengeRequest {
|
|
||||||
pubkey: parse_pubkey(kt, pubkey)?,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
UserAgentRequestPayload::AuthChallengeRequest(AuthChallengeRequest {
|
|
||||||
pubkey,
|
pubkey,
|
||||||
bootstrap_token: Some(token),
|
bootstrap_token: Some(token),
|
||||||
key_type,
|
} => Ok(AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest {
|
||||||
}) => {
|
pubkey,
|
||||||
let kt = ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified);
|
token,
|
||||||
Ok(AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest {
|
})),
|
||||||
pubkey: parse_pubkey(kt, pubkey)?,
|
Request::AuthChallengeSolution { signature } => {
|
||||||
token,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
UserAgentRequestPayload::AuthChallengeSolution(AuthChallengeSolution { signature }) => {
|
|
||||||
Ok(AuthEvents::ReceivedSolution(ChallengeSolution {
|
Ok(AuthEvents::ReceivedSolution(ChallengeSolution {
|
||||||
solution: signature,
|
solution: signature,
|
||||||
}))
|
}))
|
||||||
@@ -99,10 +62,7 @@ pub async fn authenticate(props: &mut UserAgentConnection) -> Result<AuthPublicK
|
|||||||
loop {
|
loop {
|
||||||
// `state` holds a mutable reference to `props` so we can't access it directly here
|
// `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 transport = state.context_mut().conn.transport.as_mut();
|
||||||
let Some(UserAgentRequest {
|
let Some(payload) = transport.recv().await else {
|
||||||
payload: Some(payload),
|
|
||||||
}) = transport.recv().await
|
|
||||||
else {
|
|
||||||
return Err(Error::Transport);
|
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::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use super::Error;
|
use super::Error;
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{bootstrap::ConsumeToken, user_agent::UserAgentConnection},
|
actors::{
|
||||||
db::{models::KeyType, schema},
|
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 struct ChallengeRequest {
|
||||||
pub pubkey: AuthPublicKey,
|
pub pubkey: AuthPublicKey,
|
||||||
}
|
}
|
||||||
@@ -58,7 +21,7 @@ pub struct BootstrapAuthRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChallengeContext {
|
pub struct ChallengeContext {
|
||||||
pub challenge: AuthChallenge,
|
pub challenge_nonce: i32,
|
||||||
pub key: AuthPublicKey,
|
pub key: AuthPublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,16 +118,9 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
|||||||
let stored_bytes = pubkey.to_stored_bytes();
|
let stored_bytes = pubkey.to_stored_bytes();
|
||||||
let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
|
let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
|
||||||
|
|
||||||
let challenge = AuthChallenge {
|
|
||||||
pubkey: stored_bytes,
|
|
||||||
nonce,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.conn
|
self.conn
|
||||||
.transport
|
.transport
|
||||||
.send(Ok(UserAgentResponse {
|
.send(Ok(Response::AuthChallenge { nonce }))
|
||||||
payload: Some(UserAgentResponsePayload::AuthChallenge(challenge.clone())),
|
|
||||||
}))
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(?e, "Failed to send auth challenge");
|
error!(?e, "Failed to send auth challenge");
|
||||||
@@ -172,7 +128,7 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(ChallengeContext {
|
Ok(ChallengeContext {
|
||||||
challenge,
|
challenge_nonce: nonce,
|
||||||
key: pubkey,
|
key: pubkey,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -217,10 +173,10 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
|||||||
#[allow(clippy::unused_unit)]
|
#[allow(clippy::unused_unit)]
|
||||||
async fn verify_solution(
|
async fn verify_solution(
|
||||||
&mut self,
|
&mut self,
|
||||||
ChallengeContext { challenge, key }: &ChallengeContext,
|
ChallengeContext { challenge_nonce, key }: &ChallengeContext,
|
||||||
ChallengeSolution { solution }: ChallengeSolution,
|
ChallengeSolution { solution }: ChallengeSolution,
|
||||||
) -> Result<AuthPublicKey, Self::Error> {
|
) -> 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 {
|
let valid = match key {
|
||||||
AuthPublicKey::Ed25519(vk) => {
|
AuthPublicKey::Ed25519(vk) => {
|
||||||
@@ -252,9 +208,7 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
|||||||
if valid {
|
if valid {
|
||||||
self.conn
|
self.conn
|
||||||
.transport
|
.transport
|
||||||
.send(Ok(UserAgentResponse {
|
.send(Ok(Response::AuthOk))
|
||||||
payload: Some(UserAgentResponsePayload::AuthOk(AuthOk {})),
|
|
||||||
}))
|
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::Transport)?;
|
.map_err(|_| Error::Transport)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
use arbiter_proto::{
|
use alloy::primitives::Address;
|
||||||
proto::user_agent::{UserAgentRequest, UserAgentResponse},
|
use arbiter_proto::transport::Bi;
|
||||||
transport::Bi,
|
|
||||||
};
|
|
||||||
use kameo::actor::Spawn as _;
|
use kameo::actor::Spawn as _;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{GlobalActors, user_agent::session::UserAgentSession},
|
actors::{GlobalActors, evm, user_agent::session::UserAgentSession},
|
||||||
db::{self},
|
db::{self, models::KeyType},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||||
pub enum TransportResponseError {
|
pub enum TransportResponseError {
|
||||||
#[error("Expected message with payload")]
|
|
||||||
MissingRequestPayload,
|
|
||||||
#[error("Unexpected request payload")]
|
#[error("Unexpected request payload")]
|
||||||
UnexpectedRequestPayload,
|
UnexpectedRequestPayload,
|
||||||
#[error("Invalid state for unseal encrypted key")]
|
#[error("Invalid state for unseal encrypted key")]
|
||||||
@@ -30,8 +26,106 @@ pub enum TransportResponseError {
|
|||||||
ConnectionRegistrationFailed,
|
ConnectionRegistrationFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Transport =
|
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
|
||||||
Box<dyn Bi<UserAgentRequest, Result<UserAgentResponse, TransportResponseError>> + Send>;
|
#[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 {
|
pub struct UserAgentConnection {
|
||||||
db: db::DatabasePool,
|
db: db::DatabasePool,
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
use std::{ops::DerefMut, sync::Mutex};
|
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 chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||||
use ed25519_dalek::VerifyingKey;
|
use ed25519_dalek::VerifyingKey;
|
||||||
use kameo::{Actor, error::SendError, messages, prelude::Context};
|
use kameo::{Actor, error::SendError, messages, prelude::Context};
|
||||||
@@ -21,7 +12,10 @@ use crate::actors::{
|
|||||||
evm::{Generate, ListWallets},
|
evm::{Generate, ListWallets},
|
||||||
keyholder::{self, Bootstrap, TryUnseal},
|
keyholder::{self, Bootstrap, TryUnseal},
|
||||||
router::RegisterUserAgent,
|
router::RegisterUserAgent,
|
||||||
user_agent::{TransportResponseError, UserAgentConnection},
|
user_agent::{
|
||||||
|
BootstrapError, Request, Response, TransportResponseError, UnsealError,
|
||||||
|
UserAgentConnection, VaultState,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod state;
|
mod state;
|
||||||
@@ -60,21 +54,17 @@ impl UserAgentSession {
|
|||||||
|
|
||||||
async fn send_msg<Reply: kameo::Reply>(
|
async fn send_msg<Reply: kameo::Reply>(
|
||||||
&mut self,
|
&mut self,
|
||||||
msg: UserAgentResponsePayload,
|
msg: Response,
|
||||||
_ctx: &mut Context<Self, Reply>,
|
_ctx: &mut Context<Self, Reply>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.props
|
self.props.transport.send(Ok(msg)).await.map_err(|_| {
|
||||||
.transport
|
error!(
|
||||||
.send(Ok(response(msg)))
|
actor = "useragent",
|
||||||
.await
|
reason = "channel closed",
|
||||||
.map_err(|_| {
|
"send.failed"
|
||||||
error!(
|
);
|
||||||
actor = "useragent",
|
Error::ConnectionLost
|
||||||
reason = "channel closed",
|
})
|
||||||
"send.failed"
|
|
||||||
);
|
|
||||||
Error::ConnectionLost
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn expect_msg<Extractor, Msg, Reply>(
|
async fn expect_msg<Extractor, Msg, Reply>(
|
||||||
@@ -83,7 +73,7 @@ impl UserAgentSession {
|
|||||||
ctx: &mut Context<Self, Reply>,
|
ctx: &mut Context<Self, Reply>,
|
||||||
) -> Result<Msg, Error>
|
) -> Result<Msg, Error>
|
||||||
where
|
where
|
||||||
Extractor: FnOnce(UserAgentRequestPayload) -> Option<Msg>,
|
Extractor: FnOnce(Request) -> Option<Msg>,
|
||||||
Reply: kameo::Reply,
|
Reply: kameo::Reply,
|
||||||
{
|
{
|
||||||
let msg = self.props.transport.recv().await.ok_or_else(|| {
|
let msg = self.props.transport.recv().await.ok_or_else(|| {
|
||||||
@@ -96,7 +86,7 @@ impl UserAgentSession {
|
|||||||
Error::ConnectionLost
|
Error::ConnectionLost
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
msg.payload.and_then(extractor).ok_or_else(|| {
|
extractor(msg).ok_or_else(|| {
|
||||||
error!(
|
error!(
|
||||||
actor = "useragent",
|
actor = "useragent",
|
||||||
reason = "unexpected message",
|
reason = "unexpected message",
|
||||||
@@ -119,18 +109,16 @@ impl UserAgentSession {
|
|||||||
ctx: &mut Context<Self, Result<bool, Error>>,
|
ctx: &mut Context<Self, Result<bool, Error>>,
|
||||||
) -> Result<bool, Error> {
|
) -> Result<bool, Error> {
|
||||||
self.send_msg(
|
self.send_msg(
|
||||||
UserAgentResponsePayload::ClientConnectionRequest(ClientConnectionRequest {
|
Response::ClientConnectionRequest {
|
||||||
pubkey: client_pubkey.as_bytes().to_vec(),
|
pubkey: client_pubkey,
|
||||||
}),
|
},
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let extractor = |msg| {
|
let extractor = |msg| {
|
||||||
if let UserAgentRequestPayload::ClientConnectionResponse(client_connection_response) =
|
if let Request::ClientConnectionResponse { approved } = msg {
|
||||||
msg
|
Some(approved)
|
||||||
{
|
|
||||||
Some(client_connection_response)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -140,53 +128,55 @@ impl UserAgentSession {
|
|||||||
_ = cancel_flag.changed() => {
|
_ = cancel_flag.changed() => {
|
||||||
info!(actor = "useragent", "client connection approval cancelled");
|
info!(actor = "useragent", "client connection approval cancelled");
|
||||||
self.send_msg(
|
self.send_msg(
|
||||||
UserAgentResponsePayload::ClientConnectionCancel(ClientConnectionCancel {}),
|
Response::ClientConnectionCancel,
|
||||||
ctx,
|
ctx,
|
||||||
).await?;
|
).await?;
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
result = self.expect_msg(extractor, ctx) => {
|
result = self.expect_msg(extractor, ctx) => {
|
||||||
let result = result?;
|
let result = result?;
|
||||||
info!(actor = "useragent", "received client connection approval result: approved={}", result.approved);
|
info!(actor = "useragent", "received client connection approval result: approved={}", result);
|
||||||
Ok(result.approved)
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
pub async fn process_transport_inbound(&mut self, req: UserAgentRequest) -> Output {
|
pub async fn process_transport_inbound(&mut self, req: Request) -> Output {
|
||||||
let msg = req.payload.ok_or_else(|| {
|
match req {
|
||||||
error!(actor = "useragent", "Received message with no payload");
|
Request::UnsealStart { client_pubkey } => {
|
||||||
TransportResponseError::MissingRequestPayload
|
self.handle_unseal_request(client_pubkey).await
|
||||||
})?;
|
|
||||||
|
|
||||||
match msg {
|
|
||||||
UserAgentRequestPayload::UnsealStart(unseal_start) => {
|
|
||||||
self.handle_unseal_request(unseal_start).await
|
|
||||||
}
|
}
|
||||||
UserAgentRequestPayload::UnsealEncryptedKey(unseal_encrypted_key) => {
|
Request::UnsealEncryptedKey {
|
||||||
self.handle_unseal_encrypted_key(unseal_encrypted_key).await
|
nonce,
|
||||||
}
|
ciphertext,
|
||||||
UserAgentRequestPayload::BootstrapEncryptedKey(bootstrap_encrypted_key) => {
|
associated_data,
|
||||||
self.handle_bootstrap_encrypted_key(bootstrap_encrypted_key)
|
} => {
|
||||||
|
self.handle_unseal_encrypted_key(nonce, ciphertext, associated_data)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
UserAgentRequestPayload::QueryVaultState(_) => self.handle_query_vault_state().await,
|
Request::BootstrapEncryptedKey {
|
||||||
UserAgentRequestPayload::EvmWalletCreate(_) => self.handle_evm_wallet_create().await,
|
nonce,
|
||||||
UserAgentRequestPayload::EvmWalletList(_) => self.handle_evm_wallet_list().await,
|
ciphertext,
|
||||||
_ => Err(TransportResponseError::UnexpectedRequestPayload),
|
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>;
|
type Output = Result<Response, TransportResponseError>;
|
||||||
|
|
||||||
fn response(payload: UserAgentResponsePayload) -> UserAgentResponse {
|
|
||||||
UserAgentResponse {
|
|
||||||
payload: Some(payload),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
fn take_unseal_secret(
|
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 secret = EphemeralSecret::random();
|
||||||
let public_key = PublicKey::from(&secret);
|
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 {
|
self.transition(UserAgentEvents::UnsealRequest(UnsealContext {
|
||||||
secret: Mutex::new(Some(secret)),
|
secret: Mutex::new(Some(secret)),
|
||||||
client_public_key,
|
client_public_key: client_pubkey
|
||||||
}))?;
|
}))?;
|
||||||
|
|
||||||
Ok(response(UserAgentResponsePayload::UnsealStartResponse(
|
Ok(Response::UnsealStartResponse {
|
||||||
UnsealStartResponse {
|
server_pubkey: public_key,
|
||||||
server_pubkey: public_key.as_bytes().to_vec(),
|
})
|
||||||
},
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
|
||||||
Ok(values) => values,
|
Ok(values) => values,
|
||||||
Err(TransportResponseError::StateTransitionFailed) => {
|
Err(TransportResponseError::StateTransitionFailed) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
return Ok(response(UserAgentResponsePayload::UnsealResult(
|
return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)));
|
||||||
UnsealResult::InvalidKey.into(),
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
@@ -280,16 +264,14 @@ impl UserAgentSession {
|
|||||||
let seal_key_buffer = match Self::decrypt_client_key_material(
|
let seal_key_buffer = match Self::decrypt_client_key_material(
|
||||||
ephemeral_secret,
|
ephemeral_secret,
|
||||||
client_public_key,
|
client_public_key,
|
||||||
&req.nonce,
|
&nonce,
|
||||||
&req.ciphertext,
|
&ciphertext,
|
||||||
&req.associated_data,
|
&associated_data,
|
||||||
) {
|
) {
|
||||||
Ok(buffer) => buffer,
|
Ok(buffer) => buffer,
|
||||||
Err(()) => {
|
Err(()) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
return Ok(response(UserAgentResponsePayload::UnsealResult(
|
return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)));
|
||||||
UnsealResult::InvalidKey.into(),
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -305,22 +287,16 @@ impl UserAgentSession {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!("Successfully unsealed key with client-provided key");
|
info!("Successfully unsealed key with client-provided key");
|
||||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
Ok(response(UserAgentResponsePayload::UnsealResult(
|
Ok(Response::UnsealResult(Ok(())))
|
||||||
UnsealResult::Success.into(),
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => {
|
Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
Ok(response(UserAgentResponsePayload::UnsealResult(
|
Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)))
|
||||||
UnsealResult::InvalidKey.into(),
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
Err(SendError::HandlerError(err)) => {
|
Err(SendError::HandlerError(err)) => {
|
||||||
error!(?err, "Keyholder failed to unseal key");
|
error!(?err, "Keyholder failed to unseal key");
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
Ok(response(UserAgentResponsePayload::UnsealResult(
|
Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)))
|
||||||
UnsealResult::InvalidKey.into(),
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "Failed to send unseal request to keyholder");
|
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() {
|
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
|
||||||
Ok(values) => values,
|
Ok(values) => values,
|
||||||
Err(TransportResponseError::StateTransitionFailed) => {
|
Err(TransportResponseError::StateTransitionFailed) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
return Ok(response(UserAgentResponsePayload::BootstrapResult(
|
return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)));
|
||||||
BootstrapResult::InvalidKey.into(),
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
@@ -345,16 +324,14 @@ impl UserAgentSession {
|
|||||||
let seal_key_buffer = match Self::decrypt_client_key_material(
|
let seal_key_buffer = match Self::decrypt_client_key_material(
|
||||||
ephemeral_secret,
|
ephemeral_secret,
|
||||||
client_public_key,
|
client_public_key,
|
||||||
&req.nonce,
|
&nonce,
|
||||||
&req.ciphertext,
|
&ciphertext,
|
||||||
&req.associated_data,
|
&associated_data,
|
||||||
) {
|
) {
|
||||||
Ok(buffer) => buffer,
|
Ok(buffer) => buffer,
|
||||||
Err(()) => {
|
Err(()) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
return Ok(response(UserAgentResponsePayload::BootstrapResult(
|
return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)));
|
||||||
BootstrapResult::InvalidKey.into(),
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -370,22 +347,18 @@ impl UserAgentSession {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!("Successfully bootstrapped vault with client-provided key");
|
info!("Successfully bootstrapped vault with client-provided key");
|
||||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
Ok(response(UserAgentResponsePayload::BootstrapResult(
|
Ok(Response::BootstrapResult(Ok(())))
|
||||||
BootstrapResult::Success.into(),
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => {
|
Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
Ok(response(UserAgentResponsePayload::BootstrapResult(
|
Ok(Response::BootstrapResult(Err(
|
||||||
BootstrapResult::AlreadyBootstrapped.into(),
|
BootstrapError::AlreadyBootstrapped,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
Err(SendError::HandlerError(err)) => {
|
Err(SendError::HandlerError(err)) => {
|
||||||
error!(?err, "Keyholder failed to bootstrap vault");
|
error!(?err, "Keyholder failed to bootstrap vault");
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
Ok(response(UserAgentResponsePayload::BootstrapResult(
|
Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)))
|
||||||
BootstrapResult::InvalidKey.into(),
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "Failed to send bootstrap request to keyholder");
|
error!(?err, "Failed to send bootstrap request to keyholder");
|
||||||
@@ -399,7 +372,6 @@ impl UserAgentSession {
|
|||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
async fn handle_query_vault_state(&mut self) -> Output {
|
async fn handle_query_vault_state(&mut self) -> Output {
|
||||||
use crate::actors::keyholder::{GetState, StateDiscriminants};
|
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 {
|
let vault_state = match self.props.actors.key_holder.ask(GetState {}).await {
|
||||||
Ok(StateDiscriminants::Unbootstrapped) => VaultState::Unbootstrapped,
|
Ok(StateDiscriminants::Unbootstrapped) => VaultState::Unbootstrapped,
|
||||||
@@ -407,70 +379,34 @@ impl UserAgentSession {
|
|||||||
Ok(StateDiscriminants::Unsealed) => VaultState::Unsealed,
|
Ok(StateDiscriminants::Unsealed) => VaultState::Unsealed,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, actor = "useragent", "keyholder.query.failed");
|
error!(?err, actor = "useragent", "keyholder.query.failed");
|
||||||
VaultState::Error
|
return Err(TransportResponseError::KeyHolderActorUnreachable);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(response(UserAgentResponsePayload::VaultState(
|
Ok(Response::VaultState(vault_state))
|
||||||
vault_state.into(),
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
async fn handle_evm_wallet_create(&mut self) -> Output {
|
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 {
|
let result = match self.props.actors.evm.ask(Generate {}).await {
|
||||||
Ok(address) => CreateResult::Wallet(evm_proto::WalletEntry {
|
Ok(_address) => return Ok(Response::EvmWalletCreate(Ok(()))),
|
||||||
address: address.as_slice().to_vec(),
|
Err(SendError::HandlerError(err)) => Err(err),
|
||||||
}),
|
Err(err) => {
|
||||||
Err(err) => CreateResult::Error(map_evm_error("wallet create", err).into()),
|
error!(?err, "EVM actor unreachable during wallet create");
|
||||||
|
return Err(TransportResponseError::KeyHolderActorUnreachable);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
Ok(Response::EvmWalletCreate(result))
|
||||||
Ok(response(UserAgentResponsePayload::EvmWalletCreate(
|
|
||||||
evm_proto::WalletCreateResponse {
|
|
||||||
result: Some(result),
|
|
||||||
},
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_evm_wallet_list(&mut self) -> Output {
|
async fn handle_evm_wallet_list(&mut self) -> Output {
|
||||||
use evm_proto::wallet_list_response::Result as ListResult;
|
match self.props.actors.evm.ask(ListWallets {}).await {
|
||||||
|
Ok(wallets) => Ok(Response::EvmWalletList(wallets)),
|
||||||
let result = match self.props.actors.evm.ask(ListWallets {}).await {
|
Err(err) => {
|
||||||
Ok(wallets) => ListResult::Wallets(evm_proto::WalletList {
|
error!(?err, "EVM wallet list failed");
|
||||||
wallets: wallets
|
Err(TransportResponseError::KeyHolderActorUnreachable)
|
||||||
.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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use rcgen::{
|
|||||||
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
|
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
|
||||||
IsCa, Issuer, KeyPair, KeyUsagePurpose,
|
IsCa, Issuer, KeyPair, KeyUsagePurpose,
|
||||||
};
|
};
|
||||||
use rustls::pki_types::{pem::PemObject};
|
use rustls::pki_types::pem::PemObject;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tonic::transport::CertificateDer;
|
use tonic::transport::CertificateDer;
|
||||||
|
|
||||||
@@ -59,10 +59,7 @@ pub enum InitError {
|
|||||||
pub type PemCert = String;
|
pub type PemCert = String;
|
||||||
|
|
||||||
pub fn encode_cert_to_pem(cert: &CertificateDer) -> PemCert {
|
pub fn encode_cert_to_pem(cert: &CertificateDer) -> PemCert {
|
||||||
pem::encode_config(
|
pem::encode_config(&Pem::new("CERTIFICATE", cert.to_vec()), ENCODE_CONFIG)
|
||||||
&Pem::new("CERTIFICATE", cert.to_vec()),
|
|
||||||
ENCODE_CONFIG,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|||||||
@@ -117,9 +117,7 @@ async fn check_shared_constraints(
|
|||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
// Validity window
|
// Validity window
|
||||||
if shared.valid_from.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) {
|
||||||
|| shared.valid_until.is_some_and(|t| now > t)
|
|
||||||
{
|
|
||||||
violations.push(EvalViolation::InvalidTime);
|
violations.push(EvalViolation::InvalidTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +125,9 @@ async fn check_shared_constraints(
|
|||||||
let fee_exceeded = shared
|
let fee_exceeded = shared
|
||||||
.max_gas_fee_per_gas
|
.max_gas_fee_per_gas
|
||||||
.is_some_and(|cap| U256::from(context.max_fee_per_gas) > cap);
|
.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| {
|
let priority_exceeded = shared
|
||||||
U256::from(context.max_priority_fee_per_gas) > cap
|
.max_priority_fee_per_gas
|
||||||
});
|
.is_some_and(|cap| U256::from(context.max_priority_fee_per_gas) > cap);
|
||||||
if fee_exceeded || priority_exceeded {
|
if fee_exceeded || priority_exceeded {
|
||||||
violations.push(EvalViolation::GasLimitExceeded {
|
violations.push(EvalViolation::GasLimitExceeded {
|
||||||
max_gas_fee_per_gas: shared.max_gas_fee_per_gas,
|
max_gas_fee_per_gas: shared.max_gas_fee_per_gas,
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ pub struct Grant<PolicySettings> {
|
|||||||
pub settings: PolicySettings,
|
pub settings: PolicySettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub trait Policy: Sized {
|
pub trait Policy: Sized {
|
||||||
type Settings: Send + Sync + 'static + Into<SpecificGrant>;
|
type Settings: Send + Sync + 'static + Into<SpecificGrant>;
|
||||||
type Meaning: Display + std::fmt::Debug + Send + Sync + 'static + Into<SpecificMeaning>;
|
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},
|
schema::{evm_basic_grant, evm_transaction_log},
|
||||||
};
|
};
|
||||||
use crate::evm::{
|
use crate::evm::{
|
||||||
policies::{
|
policies::{EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, VolumeRateLimit},
|
||||||
EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, VolumeRateLimit,
|
|
||||||
},
|
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -140,10 +140,18 @@ async fn evaluate_rejects_nonzero_eth_value() {
|
|||||||
let mut context = ctx(DAI, calldata);
|
let mut context = ctx(DAI, calldata);
|
||||||
context.value = U256::from(1u64); // ETH attached to an ERC-20 call
|
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();
|
.unwrap();
|
||||||
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
|
assert!(
|
||||||
assert!(v.iter().any(|e| matches!(e, EvalViolation::InvalidTransactionType)));
|
v.iter()
|
||||||
|
.any(|e| matches!(e, EvalViolation::InvalidTransactionType))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -160,7 +168,9 @@ async fn evaluate_passes_any_recipient_when_no_restriction() {
|
|||||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||||
let context = ctx(DAI, calldata);
|
let context = ctx(DAI, calldata);
|
||||||
let m = TokenTransfer::analyze(&context).unwrap();
|
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());
|
assert!(v.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +188,9 @@ async fn evaluate_passes_matching_restricted_recipient() {
|
|||||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||||
let context = ctx(DAI, calldata);
|
let context = ctx(DAI, calldata);
|
||||||
let m = TokenTransfer::analyze(&context).unwrap();
|
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());
|
assert!(v.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,8 +208,13 @@ async fn evaluate_rejects_wrong_restricted_recipient() {
|
|||||||
let calldata = transfer_calldata(OTHER, U256::from(100u64));
|
let calldata = transfer_calldata(OTHER, U256::from(100u64));
|
||||||
let context = ctx(DAI, calldata);
|
let context = ctx(DAI, calldata);
|
||||||
let m = TokenTransfer::analyze(&context).unwrap();
|
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)
|
||||||
assert!(v.iter().any(|e| matches!(e, EvalViolation::InvalidTarget { .. })));
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
v.iter()
|
||||||
|
.any(|e| matches!(e, EvalViolation::InvalidTarget { .. }))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -207,7 +224,9 @@ async fn evaluate_passes_volume_within_limit() {
|
|||||||
|
|
||||||
let basic = insert_basic(&mut conn, false).await;
|
let basic = insert_basic(&mut conn, false).await;
|
||||||
let settings = make_settings(None, Some(1_000));
|
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)
|
// Record a past transfer of 500 (within 1000 limit)
|
||||||
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
|
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
|
||||||
@@ -224,12 +243,22 @@ async fn evaluate_passes_volume_within_limit() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.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 calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||||
let context = ctx(DAI, calldata);
|
let context = ctx(DAI, calldata);
|
||||||
let m = TokenTransfer::analyze(&context).unwrap();
|
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)
|
||||||
assert!(!v.iter().any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded)));
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
!v.iter()
|
||||||
|
.any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -239,7 +268,9 @@ async fn evaluate_rejects_volume_over_limit() {
|
|||||||
|
|
||||||
let basic = insert_basic(&mut conn, false).await;
|
let basic = insert_basic(&mut conn, false).await;
|
||||||
let settings = make_settings(None, Some(1_000));
|
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};
|
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
|
||||||
insert_into(evm_token_transfer_log::table)
|
insert_into(evm_token_transfer_log::table)
|
||||||
@@ -255,12 +286,22 @@ async fn evaluate_rejects_volume_over_limit() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.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 calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||||
let context = ctx(DAI, calldata);
|
let context = ctx(DAI, calldata);
|
||||||
let m = TokenTransfer::analyze(&context).unwrap();
|
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)
|
||||||
assert!(v.iter().any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded)));
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
v.iter()
|
||||||
|
.any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -277,8 +318,13 @@ async fn evaluate_no_volume_limits_always_passes() {
|
|||||||
let calldata = transfer_calldata(RECIPIENT, U256::from(u64::MAX));
|
let calldata = transfer_calldata(RECIPIENT, U256::from(u64::MAX));
|
||||||
let context = ctx(DAI, calldata);
|
let context = ctx(DAI, calldata);
|
||||||
let m = TokenTransfer::analyze(&context).unwrap();
|
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)
|
||||||
assert!(!v.iter().any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded)));
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
!v.iter()
|
||||||
|
.any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── try_find_grant ───────────────────────────────────────────────────────
|
// ── try_find_grant ───────────────────────────────────────────────────────
|
||||||
@@ -290,7 +336,9 @@ async fn try_find_grant_roundtrip() {
|
|||||||
|
|
||||||
let basic = insert_basic(&mut conn, false).await;
|
let basic = insert_basic(&mut conn, false).await;
|
||||||
let settings = make_settings(Some(RECIPIENT), Some(5_000));
|
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 calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||||
let found = TokenTransfer::try_find_grant(&ctx(DAI, calldata), &mut *conn)
|
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 basic = insert_basic(&mut conn, true).await;
|
||||||
let settings = make_settings(None, None);
|
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 calldata = transfer_calldata(RECIPIENT, U256::from(1u64));
|
||||||
let found = TokenTransfer::try_find_grant(&ctx(DAI, calldata), &mut *conn)
|
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 basic = insert_basic(&mut conn, false).await;
|
||||||
let settings = make_settings(None, None);
|
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
|
// Query with a different token contract
|
||||||
let calldata = transfer_calldata(RECIPIENT, U256::from(1u64));
|
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 settings = make_settings(None, Some(1_000));
|
||||||
let active = insert_basic(&mut conn, false).await;
|
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;
|
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();
|
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
|
||||||
assert_eq!(all.len(), 1);
|
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 basic = insert_basic(&mut conn, false).await;
|
||||||
let settings = make_settings(None, Some(9_999));
|
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();
|
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
|
||||||
assert_eq!(all.len(), 1);
|
assert_eq!(all.len(), 1);
|
||||||
assert_eq!(all[0].settings.volume_limits.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]
|
#[tokio::test]
|
||||||
@@ -388,9 +449,13 @@ async fn find_all_grants_multiple_grants_batch_loaded() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let b2 = insert_basic(&mut conn, false).await;
|
let b2 = insert_basic(&mut conn, false).await;
|
||||||
TokenTransfer::create_grant(&b2, &make_settings(Some(RECIPIENT), Some(2_000)), &mut *conn)
|
TokenTransfer::create_grant(
|
||||||
.await
|
&b2,
|
||||||
.unwrap();
|
&make_settings(Some(RECIPIENT), Some(2_000)),
|
||||||
|
&mut *conn,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
|
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
|
||||||
assert_eq!(all.len(), 2);
|
assert_eq!(all.len(), 2);
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ use std::sync::Mutex;
|
|||||||
use alloy::{
|
use alloy::{
|
||||||
consensus::SignableTransaction,
|
consensus::SignableTransaction,
|
||||||
network::{TxSigner, TxSignerSync},
|
network::{TxSigner, TxSignerSync},
|
||||||
primitives::{Address, ChainId, Signature, B256},
|
primitives::{Address, B256, ChainId, Signature},
|
||||||
signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address},
|
signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
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;
|
use memsafe::MemSafe;
|
||||||
|
|
||||||
/// An Ethereum signer that stores its secp256k1 secret key inside a
|
/// An Ethereum signer that stores its secp256k1 secret key inside a
|
||||||
@@ -90,10 +90,7 @@ impl SafeSigner {
|
|||||||
Ok(sig.into())
|
Ok(sig.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_tx_inner(
|
fn sign_tx_inner(&self, tx: &mut dyn SignableTransaction<Signature>) -> Result<Signature> {
|
||||||
&self,
|
|
||||||
tx: &mut dyn SignableTransaction<Signature>,
|
|
||||||
) -> Result<Signature> {
|
|
||||||
if let Some(chain_id) = self.chain_id
|
if let Some(chain_id) = self.chain_id
|
||||||
&& !tx.set_chain_id_checked(chain_id)
|
&& !tx.set_chain_id_checked(chain_id)
|
||||||
{
|
{
|
||||||
@@ -102,7 +99,8 @@ impl SafeSigner {
|
|||||||
tx: tx.chain_id().unwrap(),
|
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)]
|
#![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::{
|
use crate::context::ServerContext;
|
||||||
actors::{
|
|
||||||
client::{self, ClientConnection as ClientConnectionProps, ClientError, connect_client},
|
|
||||||
user_agent::{self, TransportResponseError, UserAgentConnection, connect_user_agent},
|
|
||||||
},
|
|
||||||
context::ServerContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod actors;
|
pub mod actors;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod evm;
|
pub mod evm;
|
||||||
|
pub mod grpc;
|
||||||
|
|
||||||
const DEFAULT_CHANNEL_SIZE: usize = 1000;
|
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 {
|
pub struct Server {
|
||||||
context: ServerContext,
|
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)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
use arbiter_proto::proto::client::{
|
|
||||||
AuthChallengeRequest, AuthChallengeSolution, ClientRequest,
|
|
||||||
client_request::Payload as ClientRequestPayload,
|
|
||||||
client_response::Payload as ClientResponsePayload,
|
|
||||||
};
|
|
||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::transport::Bi;
|
||||||
use arbiter_server::actors::GlobalActors;
|
use arbiter_server::actors::GlobalActors;
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::client::{ClientConnection, connect_client},
|
actors::client::{ClientConnection, Request, Response, connect_client},
|
||||||
db::{self, schema},
|
db::{self, schema},
|
||||||
};
|
};
|
||||||
use diesel::{ExpressionMethods as _, insert_into};
|
use diesel::{ExpressionMethods as _, insert_into};
|
||||||
@@ -29,12 +24,8 @@ pub async fn test_unregistered_pubkey_rejected() {
|
|||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(ClientRequest {
|
.send(Request::AuthChallengeRequest {
|
||||||
payload: Some(ClientRequestPayload::AuthChallengeRequest(
|
pubkey: pubkey_bytes,
|
||||||
AuthChallengeRequest {
|
|
||||||
pubkey: pubkey_bytes,
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -68,12 +59,8 @@ pub async fn test_challenge_auth() {
|
|||||||
|
|
||||||
// Send challenge request
|
// Send challenge request
|
||||||
test_transport
|
test_transport
|
||||||
.send(ClientRequest {
|
.send(Request::AuthChallengeRequest {
|
||||||
payload: Some(ClientRequestPayload::AuthChallengeRequest(
|
pubkey: pubkey_bytes,
|
||||||
AuthChallengeRequest {
|
|
||||||
pubkey: pubkey_bytes,
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -84,24 +71,20 @@ pub async fn test_challenge_auth() {
|
|||||||
.await
|
.await
|
||||||
.expect("should receive challenge");
|
.expect("should receive challenge");
|
||||||
let challenge = match response {
|
let challenge = match response {
|
||||||
Ok(resp) => match resp.payload {
|
Ok(resp) => match resp {
|
||||||
Some(ClientResponsePayload::AuthChallenge(c)) => c,
|
Response::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
|
||||||
other => panic!("Expected AuthChallenge, got {other:?}"),
|
other => panic!("Expected AuthChallenge, got {other:?}"),
|
||||||
},
|
},
|
||||||
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sign the challenge and send solution
|
// Sign the challenge and send solution
|
||||||
let formatted_challenge = arbiter_proto::format_challenge(challenge.nonce, &challenge.pubkey);
|
let formatted_challenge = arbiter_proto::format_challenge(challenge.1, &challenge.0);
|
||||||
let signature = new_key.sign(&formatted_challenge);
|
let signature = new_key.sign(&formatted_challenge);
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(ClientRequest {
|
.send(Request::AuthChallengeSolution {
|
||||||
payload: Some(ClientRequestPayload::AuthChallengeSolution(
|
signature: signature.to_bytes().to_vec(),
|
||||||
AuthChallengeSolution {
|
|
||||||
signature: signature.to_bytes().to_vec(),
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
use arbiter_proto::proto::user_agent::{
|
|
||||||
AuthChallengeRequest, AuthChallengeSolution, KeyType as ProtoKeyType, UserAgentRequest,
|
|
||||||
user_agent_request::Payload as UserAgentRequestPayload,
|
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
|
||||||
};
|
|
||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::transport::Bi;
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
bootstrap::GetToken,
|
bootstrap::GetToken,
|
||||||
user_agent::{UserAgentConnection, connect_user_agent},
|
user_agent::{AuthPublicKey, Request, Response, UserAgentConnection, connect_user_agent},
|
||||||
},
|
},
|
||||||
db::{self, schema},
|
db::{self, schema},
|
||||||
};
|
};
|
||||||
@@ -30,17 +25,10 @@ pub async fn test_bootstrap_token_auth() {
|
|||||||
let task = tokio::spawn(connect_user_agent(props));
|
let task = tokio::spawn(connect_user_agent(props));
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(UserAgentRequest {
|
.send(Request::AuthChallengeRequest {
|
||||||
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
AuthChallengeRequest {
|
bootstrap_token: Some(token),
|
||||||
pubkey: pubkey_bytes,
|
|
||||||
bootstrap_token: Some(token),
|
|
||||||
key_type: ProtoKeyType::Ed25519.into(),
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -67,17 +55,10 @@ pub async fn test_bootstrap_invalid_token_auth() {
|
|||||||
let task = tokio::spawn(connect_user_agent(props));
|
let task = tokio::spawn(connect_user_agent(props));
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(UserAgentRequest {
|
.send(Request::AuthChallengeRequest {
|
||||||
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
AuthChallengeRequest {
|
bootstrap_token: Some("invalid_token".to_string()),
|
||||||
pubkey: pubkey_bytes,
|
|
||||||
bootstrap_token: Some("invalid_token".to_string()),
|
|
||||||
key_type: ProtoKeyType::Ed25519.into(),
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -123,14 +104,9 @@ pub async fn test_challenge_auth() {
|
|||||||
|
|
||||||
// Send challenge request
|
// Send challenge request
|
||||||
test_transport
|
test_transport
|
||||||
.send(UserAgentRequest {
|
.send(Request::AuthChallengeRequest {
|
||||||
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
AuthChallengeRequest {
|
bootstrap_token: None,
|
||||||
pubkey: pubkey_bytes,
|
|
||||||
bootstrap_token: None,
|
|
||||||
key_type: ProtoKeyType::Ed25519.into(),
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -141,24 +117,19 @@ pub async fn test_challenge_auth() {
|
|||||||
.await
|
.await
|
||||||
.expect("should receive challenge");
|
.expect("should receive challenge");
|
||||||
let challenge = match response {
|
let challenge = match response {
|
||||||
Ok(resp) => match resp.payload {
|
Ok(resp) => match resp {
|
||||||
Some(UserAgentResponsePayload::AuthChallenge(c)) => c,
|
Response::AuthChallenge { nonce } => nonce,
|
||||||
other => panic!("Expected AuthChallenge, got {other:?}"),
|
other => panic!("Expected AuthChallenge, got {other:?}"),
|
||||||
},
|
},
|
||||||
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sign the challenge and send solution
|
let formatted_challenge = arbiter_proto::format_challenge(challenge, &pubkey_bytes);
|
||||||
let formatted_challenge = arbiter_proto::format_challenge(challenge.nonce, &challenge.pubkey);
|
|
||||||
let signature = new_key.sign(&formatted_challenge);
|
let signature = new_key.sign(&formatted_challenge);
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(UserAgentRequest {
|
.send(Request::AuthChallengeSolution {
|
||||||
payload: Some(UserAgentRequestPayload::AuthChallengeSolution(
|
signature: signature.to_bytes().to_vec(),
|
||||||
AuthChallengeSolution {
|
|
||||||
signature: signature.to_bytes().to_vec(),
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
use arbiter_proto::proto::user_agent::{
|
|
||||||
UnsealEncryptedKey, UnsealResult, UnsealStart, UserAgentRequest,
|
|
||||||
user_agent_request::Payload as UserAgentRequestPayload,
|
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
|
||||||
};
|
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
keyholder::{Bootstrap, Seal},
|
keyholder::{Bootstrap, Seal},
|
||||||
user_agent::session::UserAgentSession,
|
user_agent::{Request, Response, UnsealError, session::UserAgentSession},
|
||||||
},
|
},
|
||||||
db,
|
db,
|
||||||
};
|
};
|
||||||
@@ -15,9 +10,7 @@ use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
|||||||
use memsafe::MemSafe;
|
use memsafe::MemSafe;
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
async fn setup_sealed_user_agent(
|
async fn setup_sealed_user_agent(seal_key: &[u8]) -> (db::DatabasePool, UserAgentSession) {
|
||||||
seal_key: &[u8],
|
|
||||||
) -> (db::DatabasePool, UserAgentSession) {
|
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
|
||||||
@@ -35,29 +28,23 @@ async fn setup_sealed_user_agent(
|
|||||||
(db, session)
|
(db, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn client_dh_encrypt(
|
async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8]) -> Request {
|
||||||
user_agent: &mut UserAgentSession,
|
|
||||||
key_to_send: &[u8],
|
|
||||||
) -> UnsealEncryptedKey {
|
|
||||||
let client_secret = EphemeralSecret::random();
|
let client_secret = EphemeralSecret::random();
|
||||||
let client_public = PublicKey::from(&client_secret);
|
let client_public = PublicKey::from(&client_secret);
|
||||||
|
|
||||||
let response = user_agent
|
let response = user_agent
|
||||||
.process_transport_inbound(UserAgentRequest {
|
.process_transport_inbound(Request::UnsealStart {
|
||||||
payload: Some(UserAgentRequestPayload::UnsealStart(UnsealStart {
|
client_pubkey: client_public,
|
||||||
client_pubkey: client_public.as_bytes().to_vec(),
|
|
||||||
})),
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let server_pubkey = match response.payload.unwrap() {
|
let server_pubkey = match response {
|
||||||
UserAgentResponsePayload::UnsealStartResponse(resp) => resp.server_pubkey,
|
Response::UnsealStartResponse { server_pubkey } => server_pubkey,
|
||||||
other => panic!("Expected UnsealStartResponse, got {other:?}"),
|
other => panic!("Expected UnsealStartResponse, got {other:?}"),
|
||||||
};
|
};
|
||||||
let server_public = PublicKey::from(<[u8; 32]>::try_from(server_pubkey.as_slice()).unwrap());
|
|
||||||
|
|
||||||
let shared_secret = client_secret.diffie_hellman(&server_public);
|
let shared_secret = client_secret.diffie_hellman(&server_pubkey);
|
||||||
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
|
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
|
||||||
let nonce = XNonce::from([0u8; 24]);
|
let nonce = XNonce::from([0u8; 24]);
|
||||||
let associated_data = b"unseal";
|
let associated_data = b"unseal";
|
||||||
@@ -66,19 +53,13 @@ async fn client_dh_encrypt(
|
|||||||
.encrypt_in_place(&nonce, associated_data, &mut ciphertext)
|
.encrypt_in_place(&nonce, associated_data, &mut ciphertext)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
UnsealEncryptedKey {
|
Request::UnsealEncryptedKey {
|
||||||
nonce: nonce.to_vec(),
|
nonce: nonce.to_vec(),
|
||||||
ciphertext,
|
ciphertext,
|
||||||
associated_data: associated_data.to_vec(),
|
associated_data: associated_data.to_vec(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unseal_key_request(req: UnsealEncryptedKey) -> UserAgentRequest {
|
|
||||||
UserAgentRequest {
|
|
||||||
payload: Some(UserAgentRequestPayload::UnsealEncryptedKey(req)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn test_unseal_success() {
|
pub async fn test_unseal_success() {
|
||||||
@@ -88,14 +69,11 @@ pub async fn test_unseal_success() {
|
|||||||
let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await;
|
let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await;
|
||||||
|
|
||||||
let response = user_agent
|
let response = user_agent
|
||||||
.process_transport_inbound(unseal_key_request(encrypted_key))
|
.process_transport_inbound(encrypted_key)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert!(matches!(response, Response::UnsealResult(Ok(()))));
|
||||||
response.payload.unwrap(),
|
|
||||||
UserAgentResponsePayload::UnsealResult(UnsealResult::Success.into()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -106,14 +84,14 @@ pub async fn test_unseal_wrong_seal_key() {
|
|||||||
let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await;
|
let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await;
|
||||||
|
|
||||||
let response = user_agent
|
let response = user_agent
|
||||||
.process_transport_inbound(unseal_key_request(encrypted_key))
|
.process_transport_inbound(encrypted_key)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
response.payload.unwrap(),
|
response,
|
||||||
UserAgentResponsePayload::UnsealResult(UnsealResult::InvalidKey.into()),
|
Response::UnsealResult(Err(UnsealError::InvalidKey))
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -125,27 +103,25 @@ pub async fn test_unseal_corrupted_ciphertext() {
|
|||||||
let client_public = PublicKey::from(&client_secret);
|
let client_public = PublicKey::from(&client_secret);
|
||||||
|
|
||||||
user_agent
|
user_agent
|
||||||
.process_transport_inbound(UserAgentRequest {
|
.process_transport_inbound(Request::UnsealStart {
|
||||||
payload: Some(UserAgentRequestPayload::UnsealStart(UnsealStart {
|
client_pubkey: client_public,
|
||||||
client_pubkey: client_public.as_bytes().to_vec(),
|
|
||||||
})),
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let response = user_agent
|
let response = user_agent
|
||||||
.process_transport_inbound(unseal_key_request(UnsealEncryptedKey {
|
.process_transport_inbound(Request::UnsealEncryptedKey {
|
||||||
nonce: vec![0u8; 24],
|
nonce: vec![0u8; 24],
|
||||||
ciphertext: vec![0u8; 32],
|
ciphertext: vec![0u8; 32],
|
||||||
associated_data: vec![],
|
associated_data: vec![],
|
||||||
}))
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
response.payload.unwrap(),
|
response,
|
||||||
UserAgentResponsePayload::UnsealResult(UnsealResult::InvalidKey.into()),
|
Response::UnsealResult(Err(UnsealError::InvalidKey))
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -158,27 +134,24 @@ pub async fn test_unseal_retry_after_invalid_key() {
|
|||||||
let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await;
|
let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await;
|
||||||
|
|
||||||
let response = user_agent
|
let response = user_agent
|
||||||
.process_transport_inbound(unseal_key_request(encrypted_key))
|
.process_transport_inbound(encrypted_key)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
response.payload.unwrap(),
|
response,
|
||||||
UserAgentResponsePayload::UnsealResult(UnsealResult::InvalidKey.into()),
|
Response::UnsealResult(Err(UnsealError::InvalidKey))
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await;
|
let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await;
|
||||||
|
|
||||||
let response = user_agent
|
let response = user_agent
|
||||||
.process_transport_inbound(unseal_key_request(encrypted_key))
|
.process_transport_inbound(encrypted_key)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert!(matches!(response, Response::UnsealResult(Ok(()))));
|
||||||
response.payload.unwrap(),
|
|
||||||
UserAgentResponsePayload::UnsealResult(UnsealResult::Success.into()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "arbiter-useragent"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
license = "Apache-2.0"
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
arbiter-proto.path = "../arbiter-proto"
|
|
||||||
kameo.workspace = true
|
|
||||||
tokio = {workspace = true, features = ["net"]}
|
|
||||||
tonic.workspace = true
|
|
||||||
tonic.features = ["tls-aws-lc"]
|
|
||||||
tracing.workspace = true
|
|
||||||
ed25519-dalek.workspace = true
|
|
||||||
smlang.workspace = true
|
|
||||||
x25519-dalek.workspace = true
|
|
||||||
k256.workspace = true
|
|
||||||
rsa.workspace = true
|
|
||||||
sha2.workspace = true
|
|
||||||
spki.workspace = true
|
|
||||||
rand.workspace = true
|
|
||||||
thiserror.workspace = true
|
|
||||||
tokio-stream.workspace = true
|
|
||||||
http = "1.4.0"
|
|
||||||
rustls-webpki = { version = "0.103.9", features = ["aws-lc-rs"] }
|
|
||||||
async-trait.workspace = true
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
use arbiter_proto::{
|
|
||||||
proto::{
|
|
||||||
arbiter_service_client::ArbiterServiceClient,
|
|
||||||
user_agent::{UserAgentRequest, UserAgentResponse},
|
|
||||||
},
|
|
||||||
transport::{IdentityRecvConverter, IdentitySendConverter, grpc},
|
|
||||||
url::ArbiterUrl,
|
|
||||||
};
|
|
||||||
use kameo::actor::{ActorRef, Spawn};
|
|
||||||
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
|
||||||
|
|
||||||
use tonic::transport::ClientTlsConfig;
|
|
||||||
|
|
||||||
use super::{SigningKeyEnum, UserAgentActor};
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ConnectError {
|
|
||||||
#[error("Could 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),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type UserAgentGrpc = ActorRef<
|
|
||||||
UserAgentActor<
|
|
||||||
grpc::GrpcAdapter<
|
|
||||||
IdentityRecvConverter<UserAgentResponse>,
|
|
||||||
IdentitySendConverter<UserAgentRequest>,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
>;
|
|
||||||
pub async fn connect_grpc(
|
|
||||||
url: ArbiterUrl,
|
|
||||||
key: SigningKeyEnum,
|
|
||||||
) -> Result<UserAgentGrpc, ConnectError> {
|
|
||||||
let bootstrap_token = url.bootstrap_token.clone();
|
|
||||||
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
|
|
||||||
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
|
||||||
|
|
||||||
// TODO: if `host` is localhost, we need to verify server's process authenticity
|
|
||||||
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 bistream = client.user_agent(ReceiverStream::new(rx)).await?;
|
|
||||||
let bistream = bistream.into_inner();
|
|
||||||
|
|
||||||
let adapter = grpc::GrpcAdapter::new(
|
|
||||||
tx,
|
|
||||||
bistream,
|
|
||||||
IdentityRecvConverter::new(),
|
|
||||||
IdentitySendConverter::new(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let actor = UserAgentActor::spawn(UserAgentActor::new(key, bootstrap_token, adapter));
|
|
||||||
|
|
||||||
Ok(actor)
|
|
||||||
}
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
use arbiter_proto::{
|
|
||||||
format_challenge,
|
|
||||||
proto::user_agent::{
|
|
||||||
AuthChallengeRequest, AuthChallengeSolution, AuthOk, KeyType as ProtoKeyType,
|
|
||||||
UserAgentRequest, UserAgentResponse,
|
|
||||||
user_agent_request::Payload as UserAgentRequestPayload,
|
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
|
||||||
},
|
|
||||||
transport::Bi,
|
|
||||||
};
|
|
||||||
use kameo::{Actor, actor::ActorRef};
|
|
||||||
use smlang::statemachine;
|
|
||||||
use tokio::select;
|
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
/// Signing key variants supported by the user-agent auth protocol.
|
|
||||||
pub enum SigningKeyEnum {
|
|
||||||
Ed25519(ed25519_dalek::SigningKey),
|
|
||||||
/// secp256k1 ECDSA; public key is sent as SEC1 compressed 33 bytes; signature is raw 64-byte (r||s).
|
|
||||||
EcdsaSecp256k1(k256::ecdsa::SigningKey),
|
|
||||||
/// RSA for Windows Hello (KeyCredentialManager); public key is DER SPKI; signature is PSS+SHA-256.
|
|
||||||
Rsa(rsa::RsaPrivateKey),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SigningKeyEnum {
|
|
||||||
/// Returns the canonical public key bytes to include in `AuthChallengeRequest.pubkey`.
|
|
||||||
pub fn pubkey_bytes(&self) -> Vec<u8> {
|
|
||||||
match self {
|
|
||||||
SigningKeyEnum::Ed25519(k) => k.verifying_key().to_bytes().to_vec(),
|
|
||||||
// 33-byte SEC1 compressed point — compact and natively supported by secp256k1 tooling
|
|
||||||
SigningKeyEnum::EcdsaSecp256k1(k) => {
|
|
||||||
k.verifying_key().to_encoded_point(true).as_bytes().to_vec()
|
|
||||||
}
|
|
||||||
SigningKeyEnum::Rsa(k) => {
|
|
||||||
use rsa::pkcs8::EncodePublicKey as _;
|
|
||||||
k.to_public_key()
|
|
||||||
.to_public_key_der()
|
|
||||||
.expect("rsa SPKI encoding is infallible")
|
|
||||||
.to_vec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the proto `KeyType` discriminant to send in `AuthChallengeRequest.key_type`.
|
|
||||||
pub fn proto_key_type(&self) -> ProtoKeyType {
|
|
||||||
match self {
|
|
||||||
SigningKeyEnum::Ed25519(_) => ProtoKeyType::Ed25519,
|
|
||||||
SigningKeyEnum::EcdsaSecp256k1(_) => ProtoKeyType::EcdsaSecp256k1,
|
|
||||||
SigningKeyEnum::Rsa(_) => ProtoKeyType::Rsa,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signs `msg` and returns raw signature bytes matching the server-side verification.
|
|
||||||
pub fn sign(&self, msg: &[u8]) -> Vec<u8> {
|
|
||||||
match self {
|
|
||||||
SigningKeyEnum::Ed25519(k) => {
|
|
||||||
use ed25519_dalek::Signer as _;
|
|
||||||
k.sign(msg).to_bytes().to_vec()
|
|
||||||
}
|
|
||||||
SigningKeyEnum::EcdsaSecp256k1(k) => {
|
|
||||||
use k256::ecdsa::signature::Signer as _;
|
|
||||||
let sig: k256::ecdsa::Signature = k.sign(msg);
|
|
||||||
sig.to_bytes().to_vec()
|
|
||||||
}
|
|
||||||
SigningKeyEnum::Rsa(k) => {
|
|
||||||
use rsa::signature::RandomizedSigner as _;
|
|
||||||
let signing_key = rsa::pss::BlindedSigningKey::<sha2::Sha256>::new(k.clone());
|
|
||||||
// Use rand_core OsRng from the rsa crate's re-exported rand_core (0.6.x),
|
|
||||||
// which is the version rsa's signature API expects.
|
|
||||||
let sig = signing_key.sign_with_rng(&mut rsa::rand_core::OsRng, msg);
|
|
||||||
use rsa::signature::SignatureEncoding as _;
|
|
||||||
sig.to_vec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
statemachine! {
|
|
||||||
name: UserAgent,
|
|
||||||
custom_error: false,
|
|
||||||
transitions: {
|
|
||||||
*Init + SentAuthChallengeRequest = WaitingForServerAuth,
|
|
||||||
WaitingForServerAuth + ReceivedAuthChallenge = WaitingForAuthOk,
|
|
||||||
WaitingForServerAuth + ReceivedAuthOk = Authenticated,
|
|
||||||
WaitingForAuthOk + ReceivedAuthOk = Authenticated,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DummyContext;
|
|
||||||
impl UserAgentStateMachineContext for DummyContext {}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum InboundError {
|
|
||||||
#[error("Invalid user agent response")]
|
|
||||||
InvalidResponse,
|
|
||||||
#[error("Expected response payload")]
|
|
||||||
MissingResponsePayload,
|
|
||||||
#[error("Unexpected response payload")]
|
|
||||||
UnexpectedResponsePayload,
|
|
||||||
#[error("Invalid state for auth challenge")]
|
|
||||||
InvalidStateForAuthChallenge,
|
|
||||||
#[error("Invalid state for auth ok")]
|
|
||||||
InvalidStateForAuthOk,
|
|
||||||
#[error("State machine error")]
|
|
||||||
StateTransitionFailed,
|
|
||||||
#[error("Transport send failed")]
|
|
||||||
TransportSendFailed,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UserAgentActor<Transport>
|
|
||||||
where
|
|
||||||
Transport: Bi<UserAgentResponse, UserAgentRequest>,
|
|
||||||
{
|
|
||||||
key: SigningKeyEnum,
|
|
||||||
bootstrap_token: Option<String>,
|
|
||||||
state: UserAgentStateMachine<DummyContext>,
|
|
||||||
transport: Transport,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Transport> UserAgentActor<Transport>
|
|
||||||
where
|
|
||||||
Transport: Bi<UserAgentResponse, UserAgentRequest>,
|
|
||||||
{
|
|
||||||
pub fn new(key: SigningKeyEnum, bootstrap_token: Option<String>, transport: Transport) -> Self {
|
|
||||||
Self {
|
|
||||||
key,
|
|
||||||
bootstrap_token,
|
|
||||||
state: UserAgentStateMachine::new(DummyContext),
|
|
||||||
transport,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transition(&mut self, event: UserAgentEvents) -> Result<(), InboundError> {
|
|
||||||
self.state.process_event(event).map_err(|e| {
|
|
||||||
error!(?e, "useragent state transition failed");
|
|
||||||
InboundError::StateTransitionFailed
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_auth_challenge_request(&mut self) -> Result<(), InboundError> {
|
|
||||||
let req = AuthChallengeRequest {
|
|
||||||
pubkey: self.key.pubkey_bytes(),
|
|
||||||
bootstrap_token: self.bootstrap_token.take(),
|
|
||||||
key_type: self.key.proto_key_type().into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.transition(UserAgentEvents::SentAuthChallengeRequest)?;
|
|
||||||
|
|
||||||
self.transport
|
|
||||||
.send(UserAgentRequest {
|
|
||||||
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(req)),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| InboundError::TransportSendFailed)?;
|
|
||||||
|
|
||||||
info!(actor = "useragent", "auth.request.sent");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_auth_challenge(
|
|
||||||
&mut self,
|
|
||||||
challenge: arbiter_proto::proto::user_agent::AuthChallenge,
|
|
||||||
) -> Result<(), InboundError> {
|
|
||||||
self.transition(UserAgentEvents::ReceivedAuthChallenge)?;
|
|
||||||
|
|
||||||
let formatted = format_challenge(challenge.nonce, &challenge.pubkey);
|
|
||||||
let signature_bytes = self.key.sign(&formatted);
|
|
||||||
let solution = AuthChallengeSolution {
|
|
||||||
signature: signature_bytes,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.transport
|
|
||||||
.send(UserAgentRequest {
|
|
||||||
payload: Some(UserAgentRequestPayload::AuthChallengeSolution(solution)),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| InboundError::TransportSendFailed)?;
|
|
||||||
|
|
||||||
info!(actor = "useragent", "auth.solution.sent");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_auth_ok(&mut self, _ok: AuthOk) -> Result<(), InboundError> {
|
|
||||||
self.transition(UserAgentEvents::ReceivedAuthOk)?;
|
|
||||||
info!(actor = "useragent", "auth.ok");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn process_inbound_transport(
|
|
||||||
&mut self,
|
|
||||||
inbound: UserAgentResponse,
|
|
||||||
) -> Result<(), InboundError> {
|
|
||||||
let payload = inbound
|
|
||||||
.payload
|
|
||||||
.ok_or(InboundError::MissingResponsePayload)?;
|
|
||||||
|
|
||||||
match payload {
|
|
||||||
UserAgentResponsePayload::AuthChallenge(challenge) => {
|
|
||||||
self.handle_auth_challenge(challenge).await
|
|
||||||
}
|
|
||||||
UserAgentResponsePayload::AuthOk(ok) => self.handle_auth_ok(ok),
|
|
||||||
_ => Err(InboundError::UnexpectedResponsePayload),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Transport> Actor for UserAgentActor<Transport>
|
|
||||||
where
|
|
||||||
Transport: Bi<UserAgentResponse, UserAgentRequest>,
|
|
||||||
{
|
|
||||||
type Args = Self;
|
|
||||||
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
async fn on_start(
|
|
||||||
mut args: Self::Args,
|
|
||||||
_actor_ref: ActorRef<Self>,
|
|
||||||
) -> Result<Self, Self::Error> {
|
|
||||||
if let Err(err) = args.send_auth_challenge_request().await {
|
|
||||||
error!(?err, actor = "useragent", "auth.start.failed");
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
Ok(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn next(
|
|
||||||
&mut self,
|
|
||||||
_actor_ref: kameo::prelude::WeakActorRef<Self>,
|
|
||||||
mailbox_rx: &mut kameo::prelude::MailboxReceiver<Self>,
|
|
||||||
) -> Option<kameo::mailbox::Signal<Self>> {
|
|
||||||
loop {
|
|
||||||
select! {
|
|
||||||
signal = mailbox_rx.recv() => {
|
|
||||||
return signal;
|
|
||||||
}
|
|
||||||
inbound = self.transport.recv() => {
|
|
||||||
match inbound {
|
|
||||||
Some(inbound) => {
|
|
||||||
if let Err(err) = self.process_inbound_transport(inbound).await {
|
|
||||||
error!(?err, actor = "useragent", "transport.inbound.failed");
|
|
||||||
return Some(kameo::mailbox::Signal::Stop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
info!(actor = "useragent", "transport.closed");
|
|
||||||
return Some(kameo::mailbox::Signal::Stop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod grpc;
|
|
||||||
pub use grpc::{ConnectError, UserAgentGrpc, connect_grpc};
|
|
||||||
|
|
||||||
use arbiter_proto::proto::user_agent::{
|
|
||||||
BootstrapEncryptedKey, UnsealEncryptedKey, UnsealStart,
|
|
||||||
user_agent_request::Payload as RequestPayload, user_agent_response::Payload as ResponsePayload,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Send an `UnsealStart` request and await the server's `UnsealStartResponse`.
|
|
||||||
pub struct SendUnsealStart {
|
|
||||||
pub client_pubkey: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send an `UnsealEncryptedKey` request and await the server's `UnsealResult`.
|
|
||||||
pub struct SendUnsealEncryptedKey {
|
|
||||||
pub nonce: Vec<u8>,
|
|
||||||
pub ciphertext: Vec<u8>,
|
|
||||||
pub associated_data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a `BootstrapEncryptedKey` request and await the server's `BootstrapResult`.
|
|
||||||
pub struct SendBootstrapEncryptedKey {
|
|
||||||
pub nonce: Vec<u8>,
|
|
||||||
pub ciphertext: Vec<u8>,
|
|
||||||
pub associated_data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Query the server for the current `VaultState`.
|
|
||||||
pub struct QueryVaultState;
|
|
||||||
|
|
||||||
/// Errors that can occur during post-authentication session operations.
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum SessionError {
|
|
||||||
#[error("Transport send failed")]
|
|
||||||
TransportSendFailed,
|
|
||||||
#[error("Transport closed unexpectedly")]
|
|
||||||
TransportClosed,
|
|
||||||
#[error("Server sent an unexpected response payload")]
|
|
||||||
UnexpectedResponse,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Transport> kameo::message::Message<SendUnsealStart> for UserAgentActor<Transport>
|
|
||||||
where
|
|
||||||
Transport: Bi<UserAgentResponse, UserAgentRequest>,
|
|
||||||
{
|
|
||||||
type Reply = Result<arbiter_proto::proto::user_agent::UnsealStartResponse, SessionError>;
|
|
||||||
|
|
||||||
async fn handle(
|
|
||||||
&mut self,
|
|
||||||
msg: SendUnsealStart,
|
|
||||||
_ctx: &mut kameo::message::Context<Self, Self::Reply>,
|
|
||||||
) -> Self::Reply {
|
|
||||||
self.transport
|
|
||||||
.send(UserAgentRequest {
|
|
||||||
payload: Some(RequestPayload::UnsealStart(UnsealStart {
|
|
||||||
client_pubkey: msg.client_pubkey,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| SessionError::TransportSendFailed)?;
|
|
||||||
|
|
||||||
match self.transport.recv().await {
|
|
||||||
Some(resp) => match resp.payload {
|
|
||||||
Some(ResponsePayload::UnsealStartResponse(r)) => Ok(r),
|
|
||||||
_ => Err(SessionError::UnexpectedResponse),
|
|
||||||
},
|
|
||||||
None => Err(SessionError::TransportClosed),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Transport> kameo::message::Message<SendUnsealEncryptedKey> for UserAgentActor<Transport>
|
|
||||||
where
|
|
||||||
Transport: Bi<UserAgentResponse, UserAgentRequest>,
|
|
||||||
{
|
|
||||||
type Reply = Result<i32, SessionError>;
|
|
||||||
|
|
||||||
async fn handle(
|
|
||||||
&mut self,
|
|
||||||
msg: SendUnsealEncryptedKey,
|
|
||||||
_ctx: &mut kameo::message::Context<Self, Self::Reply>,
|
|
||||||
) -> Self::Reply {
|
|
||||||
self.transport
|
|
||||||
.send(UserAgentRequest {
|
|
||||||
payload: Some(RequestPayload::UnsealEncryptedKey(UnsealEncryptedKey {
|
|
||||||
nonce: msg.nonce,
|
|
||||||
ciphertext: msg.ciphertext,
|
|
||||||
associated_data: msg.associated_data,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| SessionError::TransportSendFailed)?;
|
|
||||||
|
|
||||||
match self.transport.recv().await {
|
|
||||||
Some(resp) => match resp.payload {
|
|
||||||
Some(ResponsePayload::UnsealResult(r)) => Ok(r),
|
|
||||||
_ => Err(SessionError::UnexpectedResponse),
|
|
||||||
},
|
|
||||||
None => Err(SessionError::TransportClosed),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Transport> kameo::message::Message<SendBootstrapEncryptedKey> for UserAgentActor<Transport>
|
|
||||||
where
|
|
||||||
Transport: Bi<UserAgentResponse, UserAgentRequest>,
|
|
||||||
{
|
|
||||||
type Reply = Result<i32, SessionError>;
|
|
||||||
|
|
||||||
async fn handle(
|
|
||||||
&mut self,
|
|
||||||
msg: SendBootstrapEncryptedKey,
|
|
||||||
_ctx: &mut kameo::message::Context<Self, Self::Reply>,
|
|
||||||
) -> Self::Reply {
|
|
||||||
self.transport
|
|
||||||
.send(UserAgentRequest {
|
|
||||||
payload: Some(RequestPayload::BootstrapEncryptedKey(
|
|
||||||
BootstrapEncryptedKey {
|
|
||||||
nonce: msg.nonce,
|
|
||||||
ciphertext: msg.ciphertext,
|
|
||||||
associated_data: msg.associated_data,
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| SessionError::TransportSendFailed)?;
|
|
||||||
|
|
||||||
match self.transport.recv().await {
|
|
||||||
Some(resp) => match resp.payload {
|
|
||||||
Some(ResponsePayload::BootstrapResult(r)) => Ok(r),
|
|
||||||
_ => Err(SessionError::UnexpectedResponse),
|
|
||||||
},
|
|
||||||
None => Err(SessionError::TransportClosed),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Transport> kameo::message::Message<QueryVaultState> for UserAgentActor<Transport>
|
|
||||||
where
|
|
||||||
Transport: Bi<UserAgentResponse, UserAgentRequest>,
|
|
||||||
{
|
|
||||||
type Reply = Result<i32, SessionError>;
|
|
||||||
|
|
||||||
async fn handle(
|
|
||||||
&mut self,
|
|
||||||
_msg: QueryVaultState,
|
|
||||||
_ctx: &mut kameo::message::Context<Self, Self::Reply>,
|
|
||||||
) -> Self::Reply {
|
|
||||||
self.transport
|
|
||||||
.send(UserAgentRequest {
|
|
||||||
payload: Some(RequestPayload::QueryVaultState(())),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| SessionError::TransportSendFailed)?;
|
|
||||||
|
|
||||||
match self.transport.recv().await {
|
|
||||||
Some(resp) => match resp.payload {
|
|
||||||
Some(ResponsePayload::VaultState(v)) => Ok(v),
|
|
||||||
_ => Err(SessionError::UnexpectedResponse),
|
|
||||||
},
|
|
||||||
None => Err(SessionError::TransportClosed),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
use arbiter_proto::{
|
|
||||||
format_challenge,
|
|
||||||
proto::user_agent::{
|
|
||||||
AuthChallenge, AuthOk, UserAgentRequest, UserAgentResponse,
|
|
||||||
user_agent_request::Payload as UserAgentRequestPayload,
|
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
|
||||||
},
|
|
||||||
transport::Bi,
|
|
||||||
};
|
|
||||||
use arbiter_useragent::{SigningKeyEnum, UserAgentActor};
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use ed25519_dalek::SigningKey;
|
|
||||||
use kameo::actor::Spawn;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio::time::{Duration, timeout};
|
|
||||||
|
|
||||||
struct TestTransport {
|
|
||||||
inbound_rx: mpsc::Receiver<UserAgentResponse>,
|
|
||||||
outbound_tx: mpsc::Sender<UserAgentRequest>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Bi<UserAgentResponse, UserAgentRequest> for TestTransport {
|
|
||||||
async fn send(
|
|
||||||
&mut self,
|
|
||||||
item: UserAgentRequest,
|
|
||||||
) -> Result<(), arbiter_proto::transport::Error> {
|
|
||||||
self.outbound_tx
|
|
||||||
.send(item)
|
|
||||||
.await
|
|
||||||
.map_err(|_| arbiter_proto::transport::Error::ChannelClosed)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn recv(&mut self) -> Option<UserAgentResponse> {
|
|
||||||
self.inbound_rx.recv().await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_transport() -> (
|
|
||||||
TestTransport,
|
|
||||||
mpsc::Sender<UserAgentResponse>,
|
|
||||||
mpsc::Receiver<UserAgentRequest>,
|
|
||||||
) {
|
|
||||||
let (inbound_tx, inbound_rx) = mpsc::channel(8);
|
|
||||||
let (outbound_tx, outbound_rx) = mpsc::channel(8);
|
|
||||||
(
|
|
||||||
TestTransport {
|
|
||||||
inbound_rx,
|
|
||||||
outbound_tx,
|
|
||||||
},
|
|
||||||
inbound_tx,
|
|
||||||
outbound_rx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_key() -> SigningKeyEnum {
|
|
||||||
SigningKeyEnum::Ed25519(SigningKey::from_bytes(&[7u8; 32]))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn sends_auth_request_on_start_with_bootstrap_token() {
|
|
||||||
let key = test_key();
|
|
||||||
let pubkey = key.pubkey_bytes();
|
|
||||||
let bootstrap_token = Some("bootstrap-123".to_string());
|
|
||||||
let (transport, inbound_tx, mut outbound_rx) = make_transport();
|
|
||||||
|
|
||||||
let actor = UserAgentActor::spawn(UserAgentActor::new(key, bootstrap_token.clone(), transport));
|
|
||||||
|
|
||||||
let outbound = timeout(Duration::from_secs(1), outbound_rx.recv())
|
|
||||||
.await
|
|
||||||
.expect("timed out waiting for auth request")
|
|
||||||
.expect("channel closed before auth request");
|
|
||||||
|
|
||||||
let UserAgentRequest {
|
|
||||||
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(req)),
|
|
||||||
} = outbound
|
|
||||||
else {
|
|
||||||
panic!("expected auth challenge request");
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(req.pubkey, pubkey);
|
|
||||||
assert_eq!(req.bootstrap_token, bootstrap_token);
|
|
||||||
|
|
||||||
drop(inbound_tx);
|
|
||||||
drop(actor);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn challenge_flow_sends_solution_from_transport_inbound() {
|
|
||||||
let key = test_key();
|
|
||||||
let pubkey_bytes = key.pubkey_bytes();
|
|
||||||
let (transport, inbound_tx, mut outbound_rx) = make_transport();
|
|
||||||
|
|
||||||
let actor = UserAgentActor::spawn(UserAgentActor::new(key, None, transport));
|
|
||||||
|
|
||||||
let _initial_auth_request = timeout(Duration::from_secs(1), outbound_rx.recv())
|
|
||||||
.await
|
|
||||||
.expect("timed out waiting for initial auth request")
|
|
||||||
.expect("missing initial auth request");
|
|
||||||
|
|
||||||
let challenge = AuthChallenge {
|
|
||||||
pubkey: pubkey_bytes.clone(),
|
|
||||||
nonce: 42,
|
|
||||||
};
|
|
||||||
inbound_tx
|
|
||||||
.send(UserAgentResponse {
|
|
||||||
payload: Some(UserAgentResponsePayload::AuthChallenge(challenge.clone())),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let outbound = timeout(Duration::from_secs(1), outbound_rx.recv())
|
|
||||||
.await
|
|
||||||
.expect("timed out waiting for challenge solution")
|
|
||||||
.expect("missing challenge solution");
|
|
||||||
|
|
||||||
let UserAgentRequest {
|
|
||||||
payload: Some(UserAgentRequestPayload::AuthChallengeSolution(solution)),
|
|
||||||
} = outbound
|
|
||||||
else {
|
|
||||||
panic!("expected auth challenge solution");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Verify the signature using the Ed25519 verifying key
|
|
||||||
let formatted = format_challenge(challenge.nonce, &challenge.pubkey);
|
|
||||||
let raw_key = SigningKey::from_bytes(&[7u8; 32]);
|
|
||||||
let sig: ed25519_dalek::Signature = solution
|
|
||||||
.signature
|
|
||||||
.as_slice()
|
|
||||||
.try_into()
|
|
||||||
.expect("signature bytes length");
|
|
||||||
raw_key
|
|
||||||
.verifying_key()
|
|
||||||
.verify_strict(&formatted, &sig)
|
|
||||||
.expect("solution signature should verify");
|
|
||||||
|
|
||||||
inbound_tx
|
|
||||||
.send(UserAgentResponse {
|
|
||||||
payload: Some(UserAgentResponsePayload::AuthOk(AuthOk {})),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
drop(inbound_tx);
|
|
||||||
drop(actor);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user