fix(tls, client): added proper errors to client & schema to connect url; added localhost wildcard for self-signed setup

This commit is contained in:
hdbg
2026-03-24 20:08:42 +01:00
parent c0b08e84cc
commit 056ff3470b
6 changed files with 57 additions and 46 deletions

View File

@@ -14,19 +14,7 @@ use crate::{
}; };
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ConnectError { pub enum AuthError {
#[error("Could not establish connection")]
Connection(#[from] tonic::transport::Error),
#[error("Invalid server URI")]
InvalidUri(#[from] http::uri::InvalidUri),
#[error("Invalid CA certificate")]
InvalidCaCert(#[from] webpki::Error),
#[error("gRPC error")]
Grpc(#[from] tonic::Status),
#[error("Auth challenge was not returned by server")] #[error("Auth challenge was not returned by server")]
MissingAuthChallenge, MissingAuthChallenge,
@@ -43,15 +31,15 @@ pub enum ConnectError {
Storage(#[from] StorageError), 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) { match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) {
AuthResult::ApprovalDenied => ConnectError::ApprovalDenied, AuthResult::ApprovalDenied => AuthError::ApprovalDenied,
AuthResult::NoUserAgentsOnline => ConnectError::NoUserAgentsOnline, AuthResult::NoUserAgentsOnline => AuthError::NoUserAgentsOnline,
AuthResult::Unspecified AuthResult::Unspecified
| AuthResult::Success | AuthResult::Success
| AuthResult::InvalidKey | AuthResult::InvalidKey
| AuthResult::InvalidSignature | AuthResult::InvalidSignature
| AuthResult::Internal => ConnectError::UnexpectedAuthResponse, | AuthResult::Internal => AuthError::UnexpectedAuthResponse,
} }
} }
@@ -59,7 +47,7 @@ async fn send_auth_challenge_request(
transport: &mut ClientTransport, transport: &mut ClientTransport,
metadata: ClientMetadata, metadata: ClientMetadata,
key: &ed25519_dalek::SigningKey, key: &ed25519_dalek::SigningKey,
) -> std::result::Result<(), ConnectError> { ) -> std::result::Result<(), AuthError> {
transport transport
.send(ClientRequest { .send(ClientRequest {
request_id: next_request_id(), request_id: next_request_id(),
@@ -75,22 +63,22 @@ async fn send_auth_challenge_request(
)), )),
}) })
.await .await
.map_err(|_| ConnectError::UnexpectedAuthResponse) .map_err(|_| AuthError::UnexpectedAuthResponse)
} }
async fn receive_auth_challenge( async fn receive_auth_challenge(
transport: &mut ClientTransport, transport: &mut ClientTransport,
) -> std::result::Result<arbiter_proto::proto::client::AuthChallenge, ConnectError> { ) -> std::result::Result<arbiter_proto::proto::client::AuthChallenge, AuthError> {
let response = transport let response = transport
.recv() .recv()
.await .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 { match payload {
ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge), ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge),
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)), 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, transport: &mut ClientTransport,
key: &ed25519_dalek::SigningKey, key: &ed25519_dalek::SigningKey,
challenge: arbiter_proto::proto::client::AuthChallenge, challenge: arbiter_proto::proto::client::AuthChallenge,
) -> std::result::Result<(), ConnectError> { ) -> std::result::Result<(), AuthError> {
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey); let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
let signature = key.sign(&challenge_payload).to_bytes().to_vec(); let signature = key.sign(&challenge_payload).to_bytes().to_vec();
@@ -110,20 +98,20 @@ async fn send_auth_challenge_solution(
)), )),
}) })
.await .await
.map_err(|_| ConnectError::UnexpectedAuthResponse) .map_err(|_| AuthError::UnexpectedAuthResponse)
} }
async fn receive_auth_confirmation( async fn receive_auth_confirmation(
transport: &mut ClientTransport, transport: &mut ClientTransport,
) -> std::result::Result<(), ConnectError> { ) -> std::result::Result<(), AuthError> {
let response = transport let response = transport
.recv() .recv()
.await .await
.map_err(|_| ConnectError::UnexpectedAuthResponse)?; .map_err(|_| AuthError::UnexpectedAuthResponse)?;
let payload = response let payload = response
.payload .payload
.ok_or(ConnectError::UnexpectedAuthResponse)?; .ok_or(AuthError::UnexpectedAuthResponse)?;
match payload { match payload {
ClientResponsePayload::AuthResult(result) ClientResponsePayload::AuthResult(result)
if AuthResult::try_from(result).ok() == Some(AuthResult::Success) => if AuthResult::try_from(result).ok() == Some(AuthResult::Success) =>
@@ -131,7 +119,7 @@ async fn receive_auth_confirmation(
Ok(()) Ok(())
} }
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)), 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, transport: &mut ClientTransport,
metadata: ClientMetadata, metadata: ClientMetadata,
key: &ed25519_dalek::SigningKey, key: &ed25519_dalek::SigningKey,
) -> std::result::Result<(), ConnectError> { ) -> std::result::Result<(), AuthError> {
send_auth_challenge_request(transport, metadata, key).await?; send_auth_challenge_request(transport, metadata, key).await?;
let challenge = receive_auth_challenge(transport).await?; let challenge = receive_auth_challenge(transport).await?;
send_auth_challenge_solution(transport, key, challenge).await?; send_auth_challenge_solution(transport, key, challenge).await?;

View File

@@ -3,6 +3,7 @@ use std::io::{self, Write};
use arbiter_client::ArbiterClient; use arbiter_client::ArbiterClient;
use arbiter_proto::{ClientMetadata, url::ArbiterUrl}; use arbiter_proto::{ClientMetadata, url::ArbiterUrl};
use tonic::ConnectError;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@@ -22,6 +23,8 @@ async fn main() {
return; return;
} }
let url = match ArbiterUrl::try_from(input) { let url = match ArbiterUrl::try_from(input) {
Ok(url) => url, Ok(url) => url,
Err(err) => { Err(err) => {
@@ -30,6 +33,8 @@ async fn main() {
} }
}; };
println!("{:#?}", url);
let metadata = ClientMetadata { let metadata = ClientMetadata {
name: "arbiter-client test_connect".to_string(), name: "arbiter-client test_connect".to_string(),
description: Some("Manual connection smoke test".to_string()), description: Some("Manual connection smoke test".to_string()),
@@ -38,6 +43,6 @@ async fn main() {
match ArbiterClient::connect(url, metadata).await { match ArbiterClient::connect(url, metadata).await {
Ok(_) => println!("Connected and authenticated successfully."), Ok(_) => println!("Connected and authenticated successfully."),
Err(err) => eprintln!("Failed to connect: {err}"), Err(err) => eprintln!("Failed to connect: {:#?}", err),
} }
} }

View File

@@ -5,21 +5,32 @@ use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::ClientTlsConfig; use tonic::transport::ClientTlsConfig;
use crate::{ use crate::{
auth::{ConnectError, authenticate}, StorageError, auth::{AuthError, authenticate}, storage::{FileSigningKeyStorage, SigningKeyStorage}, transport::{BUFFER_LENGTH, ClientTransport}
storage::{FileSigningKeyStorage, SigningKeyStorage},
transport::{BUFFER_LENGTH, ClientTransport},
}; };
#[cfg(feature = "evm")] #[cfg(feature = "evm")]
use crate::wallets::evm::ArbiterEvmWallet; use crate::wallets::evm::ArbiterEvmWallet;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ClientError { pub enum Error {
#[error("gRPC error")] #[error("gRPC error")]
Grpc(#[from] tonic::Status), Grpc(#[from] tonic::Status),
#[error("Connection closed by server")] #[error("Could not establish connection")]
ConnectionClosed, 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 { pub struct ArbiterClient {
@@ -28,7 +39,7 @@ pub struct ArbiterClient {
} }
impl ArbiterClient { impl ArbiterClient {
pub async fn connect(url: ArbiterUrl, metadata: ClientMetadata) -> Result<Self, ConnectError> { pub async fn connect(url: ArbiterUrl, metadata: ClientMetadata) -> Result<Self, Error> {
let storage = FileSigningKeyStorage::from_default_location()?; let storage = FileSigningKeyStorage::from_default_location()?;
Self::connect_with_storage(url, metadata, &storage).await Self::connect_with_storage(url, metadata, &storage).await
} }
@@ -37,7 +48,7 @@ impl ArbiterClient {
url: ArbiterUrl, url: ArbiterUrl,
metadata: ClientMetadata, metadata: ClientMetadata,
storage: &S, storage: &S,
) -> Result<Self, ConnectError> { ) -> Result<Self, Error> {
let key = storage.load_or_create()?; let key = storage.load_or_create()?;
Self::connect_with_key(url, metadata, key).await Self::connect_with_key(url, metadata, key).await
} }
@@ -46,11 +57,11 @@ impl ArbiterClient {
url: ArbiterUrl, url: ArbiterUrl,
metadata: ClientMetadata, metadata: ClientMetadata,
key: ed25519_dalek::SigningKey, key: ed25519_dalek::SigningKey,
) -> Result<Self, ConnectError> { ) -> Result<Self, Error> {
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned(); let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
let tls = ClientTlsConfig::new().trust_anchor(anchor); 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)? .tls_config(tls)?
.connect() .connect()
.await?; .await?;
@@ -72,7 +83,7 @@ impl ArbiterClient {
} }
#[cfg(feature = "evm")] #[cfg(feature = "evm")]
pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, ClientError> { pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, Error> {
todo!("fetch EVM wallet list from server") todo!("fetch EVM wallet list from server")
} }
} }

View File

@@ -4,8 +4,8 @@ mod storage;
mod transport; mod transport;
pub mod wallets; pub mod wallets;
pub use auth::ConnectError; pub use auth::AuthError;
pub use client::{ArbiterClient, ClientError}; pub use client::{ArbiterClient, Error};
pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError}; pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
#[cfg(feature = "evm")] #[cfg(feature = "evm")]

View File

@@ -7,6 +7,8 @@ const ARBITER_URL_SCHEME: &str = "arbiter";
const CERT_QUERY_KEY: &str = "cert"; const CERT_QUERY_KEY: &str = "cert";
const BOOTSTRAP_TOKEN_QUERY_KEY: &str = "bootstrap_token"; const BOOTSTRAP_TOKEN_QUERY_KEY: &str = "bootstrap_token";
#[derive(Debug, Clone)]
pub struct ArbiterUrl { pub struct ArbiterUrl {
pub host: String, pub host: String,
pub port: u16, pub port: u16,

View File

@@ -1,4 +1,4 @@
use std::string::FromUtf8Error; use std::{net::IpAddr, string::FromUtf8Error};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _}; use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _};
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
@@ -6,7 +6,7 @@ use miette::Diagnostic;
use pem::Pem; use pem::Pem;
use rcgen::{ use rcgen::{
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType, BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
IsCa, Issuer, KeyPair, KeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose, SanType,
}; };
use rustls::pki_types::pem::PemObject; use rustls::pki_types::pem::PemObject;
use thiserror::Error; use thiserror::Error;
@@ -114,6 +114,11 @@ impl TlsCa {
KeyUsagePurpose::DigitalSignature, KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::KeyEncipherment, KeyUsagePurpose::KeyEncipherment,
]; ];
params
.subject_alt_names
.push(SanType::IpAddress(IpAddr::from([
127, 0, 0, 1,
])));
let mut dn = DistinguishedName::new(); let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "Arbiter Instance Leaf"); dn.push(DnType::CommonName, "Arbiter Instance Leaf");