SDK-client-UA-registration #34
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
CleverWild marked this conversation as resolved
Outdated
Skipper
commented
Address wrong. How could consumer know address beforehand? 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
|
||||
|
||||
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
Skipper
commented
move 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
Skipper
commented
Put this code directly in 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
Skipper
commented
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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user
introduce
Storageabstraction to store private key as file.On first start, it should be transparently created.
This aligns with online approval flow.