SDK-client-UA-registration #34

Merged
Skipper merged 21 commits from SDK-client-UA-registration into main 2026-03-22 11:11:11 +00:00
2 changed files with 155 additions and 3 deletions
Showing only changes of commit c87456ae2f - Show all commits

View File

@@ -20,3 +20,4 @@ thiserror.workspace = true
http = "1.4.0"
rustls-webpki = { version = "0.103.9", features = ["aws-lc-rs"] }
async-trait.workspace = true
rand.workspace = true

View File

@@ -5,7 +5,7 @@ use alloy::{
signers::{Error, Result, Signer},
};
use arbiter_proto::{
format_challenge,
format_challenge, home_path,
proto::{
arbiter_service_client::ArbiterServiceClient,
client::{
@@ -21,6 +21,7 @@ use arbiter_proto::{
};
use async_trait::async_trait;
use ed25519_dalek::Signer as _;
use std::path::{Path, PathBuf};
use tokio::sync::{Mutex, mpsc};
use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::ClientTlsConfig;
@@ -52,6 +53,83 @@ pub enum ConnectError {
#[error("Unexpected auth response payload")]
UnexpectedAuthResponse,
#[error("Signing key storage error")]
Storage(#[from] StorageError),
}
#[derive(Debug, thiserror::Error)]
pub enum StorageError {
#[error("I/O error")]
Io(#[from] std::io::Error),
#[error("Invalid signing key length in storage: expected {expected} bytes, got {actual} bytes")]
InvalidKeyLength { expected: usize, actual: usize },
}
pub trait SigningKeyStorage {
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError>;
}
#[derive(Debug, Clone)]
pub struct FileSigningKeyStorage {
path: PathBuf,
}
impl FileSigningKeyStorage {
pub const DEFAULT_FILE_NAME: &str = "sdk_client_ed25519.key";
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn from_default_location() -> std::result::Result<Self, StorageError> {
Ok(Self::new(home_path()?.join(Self::DEFAULT_FILE_NAME)))
}
fn read_key(path: &Path) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
let bytes = std::fs::read(path)?;
let raw: [u8; 32] =
bytes
.try_into()
.map_err(|v: Vec<u8>| StorageError::InvalidKeyLength {
expected: 32,
actual: v.len(),
})?;
Ok(ed25519_dalek::SigningKey::from_bytes(&raw))
}
}
CleverWild marked this conversation as resolved Outdated

introduce Storage abstraction to store private key as file.
On first start, it should be transparently created.

This aligns with online approval flow.

introduce `Storage` abstraction to store private key as file. On first start, it should be transparently created. This aligns with online approval flow.
CleverWild marked this conversation as resolved Outdated

Address wrong. How could consumer know address beforehand?
What if it doesn't exist? Same goes for chain_id

Address wrong. How could consumer know address beforehand? What if it doesn't exist? Same goes for chain_id
impl SigningKeyStorage for FileSigningKeyStorage {
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?;
}
CleverWild marked this conversation as resolved Outdated

obsolete comment

obsolete comment
if self.path.exists() {
return Self::read_key(&self.path);
}
let key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let raw_key = key.to_bytes();
CleverWild marked this conversation as resolved Outdated

move 16 to const. Put it in arbiter-proto.

move `16` to const. Put it in `arbiter-proto`.
// Use create_new to prevent accidental overwrite if another process creates the key first.
match std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&self.path)
{
Ok(mut file) => {
use std::io::Write as _;
file.write_all(&raw_key)?;
Ok(key)
}
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
Self::read_key(&self.path)
}
Err(err) => Err(StorageError::Io(err)),
}
}
CleverWild marked this conversation as resolved Outdated

Put this code directly in TxSigner impl?

Put this code directly in `TxSigner` impl?
}
#[derive(Debug, thiserror::Error)]
@@ -99,6 +177,23 @@ pub struct ArbiterSigner {
impl ArbiterSigner {
pub async fn connect_grpc(
url: ArbiterUrl,
address: Address,
) -> std::result::Result<Self, ConnectError> {
let storage = FileSigningKeyStorage::from_default_location()?;
Self::connect_grpc_with_storage(url, address, &storage).await
}
pub async fn connect_grpc_with_storage<S: SigningKeyStorage>(
url: ArbiterUrl,
address: Address,
storage: &S,
) -> std::result::Result<Self, ConnectError> {
let key = storage.load_or_create()?;
Self::connect_grpc_with_key(url, key, address).await
}
pub async fn connect_grpc_with_key(
url: ArbiterUrl,
key: ed25519_dalek::SigningKey,
address: Address,
@@ -122,7 +217,7 @@ impl ArbiterSigner {
receiver: response_stream,
};
CleverWild marked this conversation as resolved Outdated

lol, this should be fixed on server-side. Had same problem with user agent auth

lol, this should be fixed on server-side. Had same problem with user agent auth
authenticate(&mut transport, key).await?;
authenticate(&mut transport, &key).await?;
Ok(Self {
transport: Mutex::new(transport),
@@ -185,7 +280,7 @@ impl ArbiterSigner {
async fn authenticate(
transport: &mut ClientTransport,
key: ed25519_dalek::SigningKey,
key: &ed25519_dalek::SigningKey,
) -> std::result::Result<(), ConnectError> {
transport
.send(ClientRequest {
@@ -272,3 +367,59 @@ impl TxSigner<Signature> for ArbiterSigner {
self.sign_transaction_via_arbiter(tx).await
}
}
#[cfg(test)]
mod tests {
use super::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
fn unique_temp_key_path() -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be after unix epoch")
.as_nanos();
std::env::temp_dir().join(format!(
"arbiter-client-key-{}-{}.bin",
std::process::id(),
nanos
))
}
#[test]
fn file_storage_creates_and_reuses_key() {
let path = unique_temp_key_path();
let storage = FileSigningKeyStorage::new(path.clone());
let key_a = storage
.load_or_create()
.expect("first load_or_create should create key");
let key_b = storage
.load_or_create()
.expect("second load_or_create should read same key");
assert_eq!(key_a.to_bytes(), key_b.to_bytes());
assert!(path.exists());
std::fs::remove_file(path).expect("temp key file should be removable");
}
#[test]
fn file_storage_rejects_invalid_key_length() {
let path = unique_temp_key_path();
std::fs::write(&path, [42u8; 31]).expect("should write invalid key file");
let storage = FileSigningKeyStorage::new(path.clone());
let err = storage
.load_or_create()
.expect_err("storage should reject non-32-byte key file");
match err {
StorageError::InvalidKeyLength { expected, actual } => {
assert_eq!(expected, 32);
assert_eq!(actual, 31);
}
other => panic!("unexpected error: {other:?}"),
}
std::fs::remove_file(path).expect("temp key file should be removable");
}
}