From 056ff3470b1c174a7359ac98b38344caa84f521f Mon Sep 17 00:00:00 2001 From: hdbg Date: Tue, 24 Mar 2026 20:08:42 +0100 Subject: [PATCH] fix(tls, client): added proper errors to client & schema to connect url; added localhost wildcard for self-signed setup --- server/crates/arbiter-client/src/auth.rs | 48 +++++++------------ .../arbiter-client/src/bin/test_connect.rs | 7 ++- server/crates/arbiter-client/src/client.rs | 33 ++++++++----- server/crates/arbiter-client/src/lib.rs | 4 +- server/crates/arbiter-proto/src/url.rs | 2 + .../crates/arbiter-server/src/context/tls.rs | 9 +++- 6 files changed, 57 insertions(+), 46 deletions(-) diff --git a/server/crates/arbiter-client/src/auth.rs b/server/crates/arbiter-client/src/auth.rs index be1a608..a0e2b5c 100644 --- a/server/crates/arbiter-client/src/auth.rs +++ b/server/crates/arbiter-client/src/auth.rs @@ -14,19 +14,7 @@ use crate::{ }; #[derive(Debug, thiserror::Error)] -pub enum ConnectError { - #[error("Could not establish connection")] - Connection(#[from] tonic::transport::Error), - - #[error("Invalid server URI")] - InvalidUri(#[from] http::uri::InvalidUri), - - #[error("Invalid CA certificate")] - InvalidCaCert(#[from] webpki::Error), - - #[error("gRPC error")] - Grpc(#[from] tonic::Status), - +pub enum AuthError { #[error("Auth challenge was not returned by server")] MissingAuthChallenge, @@ -43,15 +31,15 @@ pub enum ConnectError { Storage(#[from] StorageError), } -fn map_auth_result(code: i32) -> ConnectError { +fn map_auth_result(code: i32) -> AuthError { match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) { - AuthResult::ApprovalDenied => ConnectError::ApprovalDenied, - AuthResult::NoUserAgentsOnline => ConnectError::NoUserAgentsOnline, + AuthResult::ApprovalDenied => AuthError::ApprovalDenied, + AuthResult::NoUserAgentsOnline => AuthError::NoUserAgentsOnline, AuthResult::Unspecified | AuthResult::Success | AuthResult::InvalidKey | AuthResult::InvalidSignature - | AuthResult::Internal => ConnectError::UnexpectedAuthResponse, + | AuthResult::Internal => AuthError::UnexpectedAuthResponse, } } @@ -59,7 +47,7 @@ async fn send_auth_challenge_request( transport: &mut ClientTransport, metadata: ClientMetadata, key: &ed25519_dalek::SigningKey, -) -> std::result::Result<(), ConnectError> { +) -> std::result::Result<(), AuthError> { transport .send(ClientRequest { request_id: next_request_id(), @@ -75,22 +63,22 @@ async fn send_auth_challenge_request( )), }) .await - .map_err(|_| ConnectError::UnexpectedAuthResponse) + .map_err(|_| AuthError::UnexpectedAuthResponse) } async fn receive_auth_challenge( transport: &mut ClientTransport, -) -> std::result::Result { +) -> std::result::Result { let response = transport .recv() .await - .map_err(|_| ConnectError::MissingAuthChallenge)?; + .map_err(|_| AuthError::MissingAuthChallenge)?; - let payload = response.payload.ok_or(ConnectError::MissingAuthChallenge)?; + let payload = response.payload.ok_or(AuthError::MissingAuthChallenge)?; match payload { ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge), ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)), - _ => Err(ConnectError::UnexpectedAuthResponse), + _ => Err(AuthError::UnexpectedAuthResponse), } } @@ -98,7 +86,7 @@ async fn send_auth_challenge_solution( transport: &mut ClientTransport, key: &ed25519_dalek::SigningKey, challenge: arbiter_proto::proto::client::AuthChallenge, -) -> std::result::Result<(), ConnectError> { +) -> std::result::Result<(), AuthError> { let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey); let signature = key.sign(&challenge_payload).to_bytes().to_vec(); @@ -110,20 +98,20 @@ async fn send_auth_challenge_solution( )), }) .await - .map_err(|_| ConnectError::UnexpectedAuthResponse) + .map_err(|_| AuthError::UnexpectedAuthResponse) } async fn receive_auth_confirmation( transport: &mut ClientTransport, -) -> std::result::Result<(), ConnectError> { +) -> std::result::Result<(), AuthError> { let response = transport .recv() .await - .map_err(|_| ConnectError::UnexpectedAuthResponse)?; + .map_err(|_| AuthError::UnexpectedAuthResponse)?; let payload = response .payload - .ok_or(ConnectError::UnexpectedAuthResponse)?; + .ok_or(AuthError::UnexpectedAuthResponse)?; match payload { ClientResponsePayload::AuthResult(result) if AuthResult::try_from(result).ok() == Some(AuthResult::Success) => @@ -131,7 +119,7 @@ async fn receive_auth_confirmation( Ok(()) } ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)), - _ => Err(ConnectError::UnexpectedAuthResponse), + _ => Err(AuthError::UnexpectedAuthResponse), } } @@ -139,7 +127,7 @@ pub(crate) async fn authenticate( transport: &mut ClientTransport, metadata: ClientMetadata, key: &ed25519_dalek::SigningKey, -) -> std::result::Result<(), ConnectError> { +) -> std::result::Result<(), AuthError> { send_auth_challenge_request(transport, metadata, key).await?; let challenge = receive_auth_challenge(transport).await?; send_auth_challenge_solution(transport, key, challenge).await?; diff --git a/server/crates/arbiter-client/src/bin/test_connect.rs b/server/crates/arbiter-client/src/bin/test_connect.rs index b6f2885..e078e3d 100644 --- a/server/crates/arbiter-client/src/bin/test_connect.rs +++ b/server/crates/arbiter-client/src/bin/test_connect.rs @@ -3,6 +3,7 @@ use std::io::{self, Write}; use arbiter_client::ArbiterClient; use arbiter_proto::{ClientMetadata, url::ArbiterUrl}; +use tonic::ConnectError; #[tokio::main] async fn main() { @@ -22,6 +23,8 @@ async fn main() { return; } + + let url = match ArbiterUrl::try_from(input) { Ok(url) => url, Err(err) => { @@ -30,6 +33,8 @@ async fn main() { } }; + println!("{:#?}", url); + let metadata = ClientMetadata { name: "arbiter-client test_connect".to_string(), description: Some("Manual connection smoke test".to_string()), @@ -38,6 +43,6 @@ async fn main() { match ArbiterClient::connect(url, metadata).await { Ok(_) => println!("Connected and authenticated successfully."), - Err(err) => eprintln!("Failed to connect: {err}"), + Err(err) => eprintln!("Failed to connect: {:#?}", err), } } \ No newline at end of file diff --git a/server/crates/arbiter-client/src/client.rs b/server/crates/arbiter-client/src/client.rs index 927a484..a9e9391 100644 --- a/server/crates/arbiter-client/src/client.rs +++ b/server/crates/arbiter-client/src/client.rs @@ -5,21 +5,32 @@ use tokio_stream::wrappers::ReceiverStream; use tonic::transport::ClientTlsConfig; use crate::{ - auth::{ConnectError, authenticate}, - storage::{FileSigningKeyStorage, SigningKeyStorage}, - transport::{BUFFER_LENGTH, ClientTransport}, + StorageError, auth::{AuthError, authenticate}, storage::{FileSigningKeyStorage, SigningKeyStorage}, transport::{BUFFER_LENGTH, ClientTransport} }; #[cfg(feature = "evm")] use crate::wallets::evm::ArbiterEvmWallet; #[derive(Debug, thiserror::Error)] -pub enum ClientError { +pub enum Error { #[error("gRPC error")] Grpc(#[from] tonic::Status), - #[error("Connection closed by server")] - ConnectionClosed, + #[error("Could not establish connection")] + Connection(#[from] tonic::transport::Error), + + #[error("Invalid server URI")] + InvalidUri(#[from] http::uri::InvalidUri), + + #[error("Invalid CA certificate")] + InvalidCaCert(#[from] webpki::Error), + + #[error("Authentication error")] + Authentication(#[from] AuthError), + + #[error("Storage error")] + Storage(#[from] StorageError), + } pub struct ArbiterClient { @@ -28,7 +39,7 @@ pub struct ArbiterClient { } impl ArbiterClient { - pub async fn connect(url: ArbiterUrl, metadata: ClientMetadata) -> Result { + pub async fn connect(url: ArbiterUrl, metadata: ClientMetadata) -> Result { let storage = FileSigningKeyStorage::from_default_location()?; Self::connect_with_storage(url, metadata, &storage).await } @@ -37,7 +48,7 @@ impl ArbiterClient { url: ArbiterUrl, metadata: ClientMetadata, storage: &S, - ) -> Result { + ) -> Result { let key = storage.load_or_create()?; Self::connect_with_key(url, metadata, key).await } @@ -46,11 +57,11 @@ impl ArbiterClient { url: ArbiterUrl, metadata: ClientMetadata, key: ed25519_dalek::SigningKey, - ) -> Result { + ) -> Result { let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned(); let tls = ClientTlsConfig::new().trust_anchor(anchor); - let channel = tonic::transport::Channel::from_shared(format!("{}:{}", url.host, url.port))? + let channel = tonic::transport::Channel::from_shared(format!("https://{}:{}", url.host, url.port))? .tls_config(tls)? .connect() .await?; @@ -72,7 +83,7 @@ impl ArbiterClient { } #[cfg(feature = "evm")] - pub async fn evm_wallets(&self) -> Result, ClientError> { + pub async fn evm_wallets(&self) -> Result, Error> { todo!("fetch EVM wallet list from server") } } diff --git a/server/crates/arbiter-client/src/lib.rs b/server/crates/arbiter-client/src/lib.rs index 1be4c38..83fdf48 100644 --- a/server/crates/arbiter-client/src/lib.rs +++ b/server/crates/arbiter-client/src/lib.rs @@ -4,8 +4,8 @@ mod storage; mod transport; pub mod wallets; -pub use auth::ConnectError; -pub use client::{ArbiterClient, ClientError}; +pub use auth::AuthError; +pub use client::{ArbiterClient, Error}; pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError}; #[cfg(feature = "evm")] diff --git a/server/crates/arbiter-proto/src/url.rs b/server/crates/arbiter-proto/src/url.rs index c961680..7459a4b 100644 --- a/server/crates/arbiter-proto/src/url.rs +++ b/server/crates/arbiter-proto/src/url.rs @@ -7,6 +7,8 @@ const ARBITER_URL_SCHEME: &str = "arbiter"; const CERT_QUERY_KEY: &str = "cert"; const BOOTSTRAP_TOKEN_QUERY_KEY: &str = "bootstrap_token"; + +#[derive(Debug, Clone)] pub struct ArbiterUrl { pub host: String, pub port: u16, diff --git a/server/crates/arbiter-server/src/context/tls.rs b/server/crates/arbiter-server/src/context/tls.rs index 0798dc8..eca7b3f 100644 --- a/server/crates/arbiter-server/src/context/tls.rs +++ b/server/crates/arbiter-server/src/context/tls.rs @@ -1,4 +1,4 @@ -use std::string::FromUtf8Error; +use std::{net::IpAddr, string::FromUtf8Error}; use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _}; use diesel_async::{AsyncConnection, RunQueryDsl}; @@ -6,7 +6,7 @@ use miette::Diagnostic; use pem::Pem; use rcgen::{ BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType, - IsCa, Issuer, KeyPair, KeyUsagePurpose, + IsCa, Issuer, KeyPair, KeyUsagePurpose, SanType, }; use rustls::pki_types::pem::PemObject; use thiserror::Error; @@ -114,6 +114,11 @@ impl TlsCa { KeyUsagePurpose::DigitalSignature, KeyUsagePurpose::KeyEncipherment, ]; + params + .subject_alt_names + .push(SanType::IpAddress(IpAddr::from([ + 127, 0, 0, 1, + ]))); let mut dn = DistinguishedName::new(); dn.push(DnType::CommonName, "Arbiter Instance Leaf");