diff --git a/server/Cargo.lock b/server/Cargo.lock index 406c2b2..5b89b7e 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -680,10 +680,10 @@ name = "arbiter-client" version = "0.1.0" dependencies = [ "alloy", + "arbiter-crypto", "arbiter-proto", "async-trait", "http", - "ml-dsa", "rand 0.10.0", "rustls-webpki", "thiserror 2.0.18", @@ -692,6 +692,16 @@ dependencies = [ "tonic", ] +[[package]] +name = "arbiter-crypto" +version = "0.1.0" +dependencies = [ + "base64", + "memsafe", + "ml-dsa", + "rand 0.10.0", +] + [[package]] name = "arbiter-proto" version = "0.1.0" @@ -725,6 +735,7 @@ version = "0.1.0" dependencies = [ "alloy", "anyhow", + "arbiter-crypto", "arbiter-proto", "arbiter-tokens-registry", "argon2", @@ -742,7 +753,6 @@ dependencies = [ "insta", "k256", "kameo", - "memsafe", "ml-dsa", "mutants", "pem", diff --git a/server/Cargo.toml b/server/Cargo.toml index 0ba6a6d..98018a0 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -46,3 +46,4 @@ prost = "0.14.3" miette = { version = "7.6.0", features = ["fancy", "serde"] } mutants = "0.0.4" ml-dsa = { version = "0.1.0-rc.8", features = ["zeroize"] } +base64 = "0.22.1" diff --git a/server/crates/arbiter-client/Cargo.toml b/server/crates/arbiter-client/Cargo.toml index 3a4bbf9..30f5d14 100644 --- a/server/crates/arbiter-client/Cargo.toml +++ b/server/crates/arbiter-client/Cargo.toml @@ -13,12 +13,12 @@ evm = ["dep:alloy"] [dependencies] arbiter-proto.path = "../arbiter-proto" +arbiter-crypto.path = "../arbiter-crypto" alloy = { workspace = true, optional = true } tonic.workspace = true tonic.features = ["tls-aws-lc"] tokio.workspace = true tokio-stream.workspace = true -ml-dsa.workspace = true thiserror.workspace = true http = "1.4.0" rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] } diff --git a/server/crates/arbiter-client/src/auth.rs b/server/crates/arbiter-client/src/auth.rs index a45a9a0..7f7378f 100644 --- a/server/crates/arbiter-client/src/auth.rs +++ b/server/crates/arbiter-client/src/auth.rs @@ -1,5 +1,5 @@ use arbiter_proto::{ - CLIENT_CONTEXT, ClientMetadata, format_challenge, + ClientMetadata, proto::{ client::{ ClientRequest, @@ -14,7 +14,7 @@ use arbiter_proto::{ shared::ClientInfo as ProtoClientInfo, }, }; -use ml_dsa::{MlDsa87, SigningKey, signature::Keypair as _}; +use arbiter_crypto::authn::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, format_challenge}; use crate::{ storage::StorageError, @@ -54,14 +54,14 @@ fn map_auth_result(code: i32) -> AuthError { async fn send_auth_challenge_request( transport: &mut ClientTransport, metadata: ClientMetadata, - key: &SigningKey, + key: &SigningKey, ) -> std::result::Result<(), AuthError> { transport .send(ClientRequest { request_id: next_request_id(), payload: Some(ClientRequestPayload::Auth(proto_auth::Request { payload: Some(AuthRequestPayload::ChallengeRequest(AuthChallengeRequest { - pubkey: key.verifying_key().encode().to_vec(), + pubkey: key.public_key().to_bytes(), client_info: Some(ProtoClientInfo { name: metadata.name, description: metadata.description, @@ -95,16 +95,14 @@ async fn receive_auth_challenge( async fn send_auth_challenge_solution( transport: &mut ClientTransport, - key: &SigningKey, + key: &SigningKey, challenge: AuthChallenge, ) -> std::result::Result<(), AuthError> { let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey); let signature = key - .signing_key() - .sign_deterministic(&challenge_payload, CLIENT_CONTEXT) + .sign_message(&challenge_payload, CLIENT_CONTEXT) .map_err(|_| AuthError::UnexpectedAuthResponse)? - .encode() - .to_vec(); + .to_bytes(); transport .send(ClientRequest { @@ -145,7 +143,7 @@ async fn receive_auth_confirmation( pub(crate) async fn authenticate( transport: &mut ClientTransport, metadata: ClientMetadata, - key: &SigningKey, + key: &SigningKey, ) -> std::result::Result<(), AuthError> { send_auth_challenge_request(transport, metadata, key).await?; let challenge = receive_auth_challenge(transport).await?; diff --git a/server/crates/arbiter-client/src/client.rs b/server/crates/arbiter-client/src/client.rs index 0b31f50..b540647 100644 --- a/server/crates/arbiter-client/src/client.rs +++ b/server/crates/arbiter-client/src/client.rs @@ -1,7 +1,7 @@ +use arbiter_crypto::authn::SigningKey; use arbiter_proto::{ ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl, }; -use ml_dsa::{MlDsa87, SigningKey}; use std::sync::Arc; use tokio::sync::{Mutex, mpsc}; use tokio_stream::wrappers::ReceiverStream; @@ -61,7 +61,7 @@ impl ArbiterClient { pub async fn connect_with_key( url: ArbiterUrl, metadata: ClientMetadata, - key: SigningKey, + key: SigningKey, ) -> Result { let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned(); let tls = ClientTlsConfig::new().trust_anchor(anchor); diff --git a/server/crates/arbiter-client/src/storage.rs b/server/crates/arbiter-client/src/storage.rs index 4d11f60..55d4a46 100644 --- a/server/crates/arbiter-client/src/storage.rs +++ b/server/crates/arbiter-client/src/storage.rs @@ -1,5 +1,5 @@ +use arbiter_crypto::authn::SigningKey; use arbiter_proto::home_path; -use ml_dsa::{KeyGen, MlDsa87, Seed, SigningKey}; use std::path::{Path, PathBuf}; #[derive(Debug, thiserror::Error)] @@ -12,7 +12,7 @@ pub enum StorageError { } pub trait SigningKeyStorage { - fn load_or_create(&self) -> std::result::Result, StorageError>; + fn load_or_create(&self) -> std::result::Result; } #[derive(Debug, Clone)] @@ -31,20 +31,21 @@ impl FileSigningKeyStorage { Ok(Self::new(home_path()?.join(Self::DEFAULT_FILE_NAME))) } - fn read_key(path: &Path) -> std::result::Result, StorageError> { + fn read_key(path: &Path) -> std::result::Result { let bytes = std::fs::read(path)?; - let raw: [u8; 32] = bytes - .try_into() - .map_err(|v: Vec| StorageError::InvalidKeyLength { - expected: 32, - actual: v.len(), - })?; - Ok(MlDsa87::from_seed(&Seed::from(raw))) + let raw: [u8; 32] = + bytes + .try_into() + .map_err(|v: Vec| StorageError::InvalidKeyLength { + expected: 32, + actual: v.len(), + })?; + Ok(SigningKey::from_seed(raw)) } } impl SigningKeyStorage for FileSigningKeyStorage { - fn load_or_create(&self) -> std::result::Result, StorageError> { + fn load_or_create(&self) -> std::result::Result { if let Some(parent) = self.path.parent() { std::fs::create_dir_all(parent)?; } @@ -53,7 +54,7 @@ impl SigningKeyStorage for FileSigningKeyStorage { return Self::read_key(&self.path); } - let key = MlDsa87::key_gen(&mut rand::rng()); + let key = SigningKey::generate(); let raw_key = key.to_seed(); // Use create_new to prevent accidental overwrite if another process creates the key first. diff --git a/server/crates/arbiter-crypto/.gitignore b/server/crates/arbiter-crypto/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/server/crates/arbiter-crypto/.gitignore @@ -0,0 +1 @@ +/target diff --git a/server/crates/arbiter-crypto/Cargo.toml b/server/crates/arbiter-crypto/Cargo.toml new file mode 100644 index 0000000..e39c7eb --- /dev/null +++ b/server/crates/arbiter-crypto/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "arbiter-crypto" +version = "0.1.0" +edition = "2024" + +[dependencies] +ml-dsa = {workspace = true, optional = true } +rand = {workspace = true, optional = true} +base64 = {workspace = true, optional = true } +memsafe = {version = "0.4.0", optional = true} + +[lints] +workspace = true + +[features] +default = ["authn", "safecell"] +authn = ["dep:ml-dsa", "dep:rand", "dep:base64"] +safecell = ["dep:memsafe"] diff --git a/server/crates/arbiter-server/src/crypto/authn/mod.rs b/server/crates/arbiter-crypto/src/authn/mod.rs similarity index 96% rename from server/crates/arbiter-server/src/crypto/authn/mod.rs rename to server/crates/arbiter-crypto/src/authn/mod.rs index 44c6ed4..3789969 100644 --- a/server/crates/arbiter-server/src/crypto/authn/mod.rs +++ b/server/crates/arbiter-crypto/src/authn/mod.rs @@ -1,3 +1,2 @@ pub mod v1; - pub use v1::*; diff --git a/server/crates/arbiter-crypto/src/authn/v1.rs b/server/crates/arbiter-crypto/src/authn/v1.rs new file mode 100644 index 0000000..6536383 --- /dev/null +++ b/server/crates/arbiter-crypto/src/authn/v1.rs @@ -0,0 +1,186 @@ +use base64::{Engine as _, prelude::BASE64_STANDARD}; +use ml_dsa::{ + EncodedVerifyingKey, Error, KeyGen, MlDsa87, Seed, Signature as MlDsaSignature, + SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, signature::Keypair as _, +}; + +pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client"; +pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent"; + +pub fn format_challenge(nonce: i32, pubkey: &[u8]) -> Vec { + let concat_form = format!("{}:{}", nonce, BASE64_STANDARD.encode(pubkey)); + concat_form.into_bytes() +} + +pub type KeyParams = MlDsa87; + +#[derive(Clone, Debug, PartialEq)] +pub struct PublicKey(Box>); + +#[derive(Clone, Debug, PartialEq)] +pub struct Signature(Box>); + +#[derive(Debug)] +pub struct SigningKey(Box>); + +impl PublicKey { + pub fn to_bytes(&self) -> Vec { + self.0.encode().to_vec() + } + + pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool { + self.0.verify_with_context( + &format_challenge(nonce, &self.to_bytes()), + context, + &signature.0, + ) + } +} + +impl Signature { + pub fn to_bytes(&self) -> Vec { + self.0.encode().to_vec() + } +} + +impl SigningKey { + pub fn generate() -> Self { + Self(Box::new(KeyParams::key_gen(&mut rand::rng()))) + } + + pub fn from_seed(seed: [u8; 32]) -> Self { + Self(Box::new(KeyParams::from_seed(&Seed::from(seed)))) + } + + pub fn to_seed(&self) -> [u8; 32] { + self.0.to_seed().into() + } + + pub fn public_key(&self) -> PublicKey { + self.0.verifying_key().into() + } + + pub fn sign_message(&self, message: &[u8], context: &[u8]) -> Result { + self.0 + .signing_key() + .sign_deterministic(message, context) + .map(Into::into) + } + + pub fn sign_challenge(&self, nonce: i32, context: &[u8]) -> Result { + self.sign_message( + &format_challenge(nonce, &self.public_key().to_bytes()), + context, + ) + } +} + +impl From> for PublicKey { + fn from(value: MlDsaVerifyingKey) -> Self { + Self(Box::new(value)) + } +} + +impl From> for Signature { + fn from(value: MlDsaSignature) -> Self { + Self(Box::new(value)) + } +} + +impl From> for SigningKey { + fn from(value: MlDsaSigningKey) -> Self { + Self(Box::new(value)) + } +} + +impl TryFrom> for PublicKey { + type Error = (); + + fn try_from(value: Vec) -> Result { + Self::try_from(value.as_slice()) + } +} + +impl TryFrom<&'_ [u8]> for PublicKey { + type Error = (); + + fn try_from(value: &[u8]) -> Result { + let encoded = EncodedVerifyingKey::::try_from(value).map_err(|_| ())?; + Ok(Self(Box::new(MlDsaVerifyingKey::decode(&encoded)))) + } +} + +impl TryFrom> for Signature { + type Error = (); + + fn try_from(value: Vec) -> Result { + Self::try_from(value.as_slice()) + } +} + +impl TryFrom<&'_ [u8]> for Signature { + type Error = (); + + fn try_from(value: &[u8]) -> Result { + MlDsaSignature::try_from(value) + .map(|sig| Self(Box::new(sig))) + .map_err(|_| ()) + } +} + +#[cfg(test)] +mod tests { + use ml_dsa::{KeyGen, MlDsa87, signature::Keypair as _}; + + use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, USERAGENT_CONTEXT}; + + #[test] + fn public_key_round_trip_decodes() { + let key = MlDsa87::key_gen(&mut rand::rng()); + let encoded = PublicKey::from(key.verifying_key()).to_bytes(); + + let decoded = PublicKey::try_from(encoded.as_slice()).expect("public key should decode"); + + assert_eq!(decoded, PublicKey::from(key.verifying_key())); + } + + #[test] + fn signature_round_trip_decodes() { + let key = SigningKey::generate(); + let signature = key + .sign_message(b"challenge", CLIENT_CONTEXT) + .expect("signature should be created"); + + let decoded = + Signature::try_from(signature.to_bytes().as_slice()).expect("signature should decode"); + + assert_eq!(decoded, signature); + } + + #[test] + fn challenge_verification_uses_context_and_canonical_key_bytes() { + let key = SigningKey::generate(); + let public_key = key.public_key(); + let nonce = 17; + let signature = key + .sign_challenge(nonce, CLIENT_CONTEXT) + .expect("signature should be created"); + + assert!(public_key.verify(nonce, CLIENT_CONTEXT, &signature)); + assert!(!public_key.verify(nonce, USERAGENT_CONTEXT, &signature)); + } + + #[test] + fn signing_key_round_trip_seed_preserves_public_key_and_signing() { + let original = SigningKey::generate(); + let restored = SigningKey::from_seed(original.to_seed()); + + assert_eq!(restored.public_key(), original.public_key()); + + let signature = restored + .sign_challenge(9, CLIENT_CONTEXT) + .expect("signature should be created"); + + assert!(restored.public_key().verify(9, CLIENT_CONTEXT, &signature)); + } +} diff --git a/server/crates/arbiter-crypto/src/lib.rs b/server/crates/arbiter-crypto/src/lib.rs new file mode 100644 index 0000000..feac900 --- /dev/null +++ b/server/crates/arbiter-crypto/src/lib.rs @@ -0,0 +1,7 @@ + +#[cfg(feature = "authn")] +pub mod authn; + + +#[cfg(feature = "safecell")] +pub mod safecell; \ No newline at end of file diff --git a/server/crates/arbiter-server/src/safe_cell.rs b/server/crates/arbiter-crypto/src/safecell.rs similarity index 92% rename from server/crates/arbiter-server/src/safe_cell.rs rename to server/crates/arbiter-crypto/src/safecell.rs index dc44065..6249325 100644 --- a/server/crates/arbiter-server/src/safe_cell.rs +++ b/server/crates/arbiter-crypto/src/safecell.rs @@ -105,6 +105,11 @@ impl SafeCellHandle for MemSafeCell { fn abort_memory_breach(action: &str, err: &memsafe::error::MemoryError) -> ! { eprintln!("fatal {action}: {err}"); + // SAFETY: Intentionally cause a segmentation fault to prevent further execution in a compromised state. + unsafe { + let unsafe_pointer = 0x0 as *mut u8; + std::ptr::write_volatile(unsafe_pointer, 0); + } std::process::abort(); } diff --git a/server/crates/arbiter-proto/Cargo.toml b/server/crates/arbiter-proto/Cargo.toml index e1651a7..56cc61a 100644 --- a/server/crates/arbiter-proto/Cargo.toml +++ b/server/crates/arbiter-proto/Cargo.toml @@ -17,7 +17,7 @@ url = "2.5.8" miette.workspace = true thiserror.workspace = true rustls-pki-types.workspace = true -base64 = "0.22.1" +base64.workspace = true prost-types.workspace = true tracing.workspace = true async-trait.workspace = true diff --git a/server/crates/arbiter-proto/src/lib.rs b/server/crates/arbiter-proto/src/lib.rs index a26f152..3d68b06 100644 --- a/server/crates/arbiter-proto/src/lib.rs +++ b/server/crates/arbiter-proto/src/lib.rs @@ -63,9 +63,6 @@ pub mod proto { } } -pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client"; -pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent"; - #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientMetadata { pub name: String, @@ -87,8 +84,3 @@ pub fn home_path() -> Result { Ok(arbiter_home) } - -pub fn format_challenge(nonce: i32, pubkey: &[u8]) -> Vec { - let concat_form = format!("{}:{}", nonce, BASE64_STANDARD.encode(pubkey)); - concat_form.into_bytes() -} diff --git a/server/crates/arbiter-server/Cargo.toml b/server/crates/arbiter-server/Cargo.toml index 13154fb..c563ee0 100644 --- a/server/crates/arbiter-server/Cargo.toml +++ b/server/crates/arbiter-server/Cargo.toml @@ -17,6 +17,7 @@ diesel-async = { version = "0.8.0", features = [ "tokio", ] } arbiter-proto.path = "../arbiter-proto" +arbiter-crypto.path = "../arbiter-crypto" tracing.workspace = true tracing-subscriber = { version = "0.3", features = ["env-filter"] } tonic.workspace = true @@ -35,7 +36,6 @@ dashmap = "6.1.0" rand.workspace = true rcgen.workspace = true chrono.workspace = true -memsafe = "0.4.0" zeroize = { version = "1.8.2", features = ["std", "simd"] } kameo.workspace = true chacha20poly1305 = { version = "0.10.1", features = ["std"] } diff --git a/server/crates/arbiter-server/src/actors/client/auth.rs b/server/crates/arbiter-server/src/actors/client/auth.rs index 0abd806..a9ff7c2 100644 --- a/server/crates/arbiter-server/src/actors/client/auth.rs +++ b/server/crates/arbiter-server/src/actors/client/auth.rs @@ -1,5 +1,6 @@ +use arbiter_crypto::authn::{self, CLIENT_CONTEXT}; use arbiter_proto::{ - CLIENT_CONTEXT, ClientMetadata, + ClientMetadata, transport::{Bi, expect_message}, }; use chrono::Utc; @@ -17,7 +18,6 @@ use crate::{ flow_coordinator::{self, RequestClientApproval}, keyholder::KeyHolder, }, - crypto::authn, crypto::integrity::{self, AttestationStatus}, db::{ self, @@ -26,8 +26,6 @@ use crate::{ }, }; - - #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] pub enum Error { #[error("Database pool unavailable")] @@ -355,7 +353,10 @@ where T: Bi> + ?Sized, { transport - .send(Ok(Outbound::AuthChallenge { pubkey: pubkey.clone(), nonce })) + .send(Ok(Outbound::AuthChallenge { + pubkey: pubkey.clone(), + nonce, + })) .await .map_err(|e| { error!(error = ?e, "Failed to send auth challenge"); diff --git a/server/crates/arbiter-server/src/actors/client/mod.rs b/server/crates/arbiter-server/src/actors/client/mod.rs index 73b76dd..03b8861 100644 --- a/server/crates/arbiter-server/src/actors/client/mod.rs +++ b/server/crates/arbiter-server/src/actors/client/mod.rs @@ -1,10 +1,10 @@ +use arbiter_crypto::authn; use arbiter_proto::{ClientMetadata, transport::Bi}; use kameo::actor::Spawn; use tracing::{error, info}; use crate::{ actors::{GlobalActors, client::session::ClientSession}, - crypto::authn, crypto::integrity::{Integrable, hashing::Hashable}, db, }; @@ -50,10 +50,7 @@ where T: Bi> + Send + ?Sized, { let fut = auth::authenticate(&mut props, transport); - println!( - "authenticate future size: {}", - std::mem::size_of_val(&fut) - ); + println!("authenticate future size: {}", std::mem::size_of_val(&fut)); match fut.await { Ok(client_id) => { ClientSession::spawn(ClientSession::new(props, client_id)); diff --git a/server/crates/arbiter-server/src/actors/evm/mod.rs b/server/crates/arbiter-server/src/actors/evm/mod.rs index 84326eb..a31de61 100644 --- a/server/crates/arbiter-server/src/actors/evm/mod.rs +++ b/server/crates/arbiter-server/src/actors/evm/mod.rs @@ -21,8 +21,9 @@ use crate::{ ether_transfer::EtherTransfer, token_transfers::TokenTransfer, }, }, - safe_cell::{SafeCell, SafeCellHandle as _}, + }; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; pub use crate::evm::safe_signer; diff --git a/server/crates/arbiter-server/src/actors/keyholder/mod.rs b/server/crates/arbiter-server/src/actors/keyholder/mod.rs index 8e43129..d139f0d 100644 --- a/server/crates/arbiter-server/src/actors/keyholder/mod.rs +++ b/server/crates/arbiter-server/src/actors/keyholder/mod.rs @@ -15,7 +15,6 @@ use crate::{ encryption::v1::{self, Nonce}, integrity::v1::HmacSha256, }, - safe_cell::SafeCell, }; use crate::{ db::{ @@ -23,8 +22,8 @@ use crate::{ models::{self, RootKeyHistory}, schema::{self}, }, - safe_cell::SafeCellHandle as _, }; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; #[derive(Default, EnumDiscriminants)] #[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))] @@ -402,8 +401,8 @@ mod tests { use crate::{ db::{self}, - safe_cell::SafeCell, }; + use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use super::*; diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth.rs b/server/crates/arbiter-server/src/actors/user_agent/auth.rs index 2ab29a7..00d2d55 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth.rs @@ -1,3 +1,4 @@ +use arbiter_crypto::authn; use arbiter_proto::transport::Bi; use tracing::error; @@ -5,8 +6,6 @@ use crate::actors::user_agent::{ UserAgentConnection, auth::state::{AuthContext, AuthStateMachine}, }; -use crate::crypto::authn; - mod state; use state::*; diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs index c017b1d..76dc079 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs @@ -1,4 +1,5 @@ -use arbiter_proto::{USERAGENT_CONTEXT, transport::Bi}; +use arbiter_crypto::authn::{self, USERAGENT_CONTEXT}; +use arbiter_proto::{transport::Bi}; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update}; use diesel_async::{AsyncConnection, RunQueryDsl}; use kameo::actor::ActorRef; @@ -11,7 +12,6 @@ use crate::{ keyholder::KeyHolder, user_agent::{UserAgentConnection, UserAgentCredentials, auth::Outbound}, }, - crypto::authn, crypto::integrity, db::{DatabasePool, schema::useragent_client}, }; diff --git a/server/crates/arbiter-server/src/actors/user_agent/mod.rs b/server/crates/arbiter-server/src/actors/user_agent/mod.rs index 006bf9d..de1a8c9 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/mod.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/mod.rs @@ -1,9 +1,10 @@ use crate::{ actors::{GlobalActors, client::ClientProfile}, - crypto::authn, crypto::integrity::Integrable, db, }; +use arbiter_crypto::authn; + #[derive(Debug)] pub struct UserAgentCredentials { diff --git a/server/crates/arbiter-server/src/actors/user_agent/session.rs b/server/crates/arbiter-server/src/actors/user_agent/session.rs index 67e5fc3..d3410bd 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session.rs @@ -1,3 +1,5 @@ +use arbiter_crypto::authn; + use std::{borrow::Cow, collections::HashMap}; use arbiter_proto::transport::Sender; @@ -11,8 +13,6 @@ use crate::actors::{ flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController}, user_agent::{OutOfBand, UserAgentConnection}, }; -use crate::crypto::authn; - mod state; use state::{DummyContext, UserAgentEvents, UserAgentStateMachine}; @@ -119,14 +119,13 @@ impl UserAgentSession { return; } - self.pending_client_approvals - .insert( - client.pubkey.to_bytes(), - PendingClientApproval { - pubkey: client.pubkey, - controller, - }, - ); + self.pending_client_approvals.insert( + client.pubkey.to_bytes(), + PendingClientApproval { + pubkey: client.pubkey, + controller, + }, + ); } } diff --git a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs index ffb6b4d..a71b894 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs @@ -1,6 +1,7 @@ use std::sync::Mutex; use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; +use arbiter_crypto::{authn, safecell::{SafeCell, SafeCellHandle as _}}; use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper}; use diesel_async::{AsyncConnection, RunQueryDsl}; @@ -13,12 +14,10 @@ use x25519_dalek::{EphemeralSecret, PublicKey}; use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer; use crate::actors::keyholder::KeyHolderState; use crate::actors::user_agent::session::Error; -use crate::crypto::authn; use crate::db::models::{ EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata, }; use crate::evm::policies::{Grant, SpecificGrant}; -use crate::safe_cell::SafeCell; use crate::{ actors::{ evm::{ @@ -31,7 +30,6 @@ use crate::{ state::{UnsealContext, UserAgentEvents, UserAgentStates}, }, }, - safe_cell::SafeCellHandle as _, }; impl UserAgentSession { @@ -477,10 +475,7 @@ impl UserAgentSession { pubkey: authn::PublicKey, ctx: &mut Context>, ) -> Result<(), Error> { - let pending_approval = match self - .pending_client_approvals - .remove(&pubkey.to_bytes()) - { + let pending_approval = match self.pending_client_approvals.remove(&pubkey.to_bytes()) { Some(approval) => approval, None => { error!("Received client connection response for unknown client"); diff --git a/server/crates/arbiter-server/src/crypto/authn/v1/mod.rs b/server/crates/arbiter-server/src/crypto/authn/v1/mod.rs deleted file mode 100644 index 37ec3b7..0000000 --- a/server/crates/arbiter-server/src/crypto/authn/v1/mod.rs +++ /dev/null @@ -1,110 +0,0 @@ -use ml_dsa::{ - EncodedVerifyingKey, MlDsa87, Signature as MlDsaSignature, VerifyingKey as MlDsaVerifyingKey, -}; - -pub type KeyParams = MlDsa87; - -#[derive(Clone, Debug, PartialEq)] -pub struct PublicKey(Box>); - -#[derive(Clone, Debug, PartialEq)] -pub struct Signature(Box>); - -impl PublicKey { - pub fn to_bytes(&self) -> Vec { - self.0.encode().to_vec() - } - - pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool { - self.0.verify_with_context(&format_challenge(nonce, self), context, &signature.0) - } -} - -impl Signature { - pub fn to_bytes(&self) -> Vec { - self.0.encode().to_vec() - } -} - -impl From> for PublicKey { - fn from(value: MlDsaVerifyingKey) -> Self { - Self(Box::new(value)) - } -} - -impl From> for Signature { - fn from(value: MlDsaSignature) -> Self { - Self(Box::new(value)) - } -} - -impl TryFrom<&'_ [u8]> for PublicKey { - type Error = (); - - fn try_from(value: &[u8]) -> Result { - let encoded = EncodedVerifyingKey::::try_from(value).map_err(|_| ())?; - Ok(Self(Box::new(MlDsaVerifyingKey::decode(&encoded)))) - } -} - -impl TryFrom<&'_ [u8]> for Signature { - type Error = (); - - fn try_from(value: &[u8]) -> Result { - MlDsaSignature::try_from(value) - .map(|sig| Self(Box::new(sig))) - .map_err(|_| ()) - } -} - -pub fn format_challenge(nonce: i32, pubkey: &PublicKey) -> Vec { - arbiter_proto::format_challenge(nonce, &pubkey.to_bytes()) -} - -#[cfg(test)] -mod tests { - use ml_dsa::{KeyGen, MlDsa87, signature::Keypair as _}; - - use super::{PublicKey, Signature}; - - #[test] - fn public_key_round_trip_decodes() { - let key = MlDsa87::key_gen(&mut rand::rng()); - let encoded = PublicKey::from(key.verifying_key()).to_bytes(); - - let decoded = PublicKey::try_from(encoded.as_slice()).expect("public key should decode"); - - assert_eq!(decoded, PublicKey::from(key.verifying_key())); - } - - #[test] - fn signature_round_trip_decodes() { - let key = MlDsa87::key_gen(&mut rand::rng()); - let challenge = b"challenge"; - let signature = key - .signing_key() - .sign_deterministic(challenge, arbiter_proto::CLIENT_CONTEXT) - .expect("signature should be created"); - - let decoded = Signature::try_from(signature.encode().to_vec().as_slice()).expect("signature should decode"); - - assert_eq!(decoded, Signature::from(signature)); - } - - #[test] - fn challenge_verification_uses_context_and_canonical_key_bytes() { - let key = MlDsa87::key_gen(&mut rand::rng()); - let public_key = PublicKey::from(key.verifying_key()); - let nonce = 17; - let challenge = - arbiter_proto::format_challenge(nonce, &public_key.to_bytes()); - let signature = key - .signing_key() - .sign_deterministic(&challenge, arbiter_proto::CLIENT_CONTEXT) - .expect("signature should be created") - .into(); - - assert!(public_key.verify(nonce, arbiter_proto::CLIENT_CONTEXT, &signature)); - assert!(!public_key.verify(nonce, arbiter_proto::USERAGENT_CONTEXT, &signature)); - } -} diff --git a/server/crates/arbiter-server/src/crypto/encryption/v1.rs b/server/crates/arbiter-server/src/crypto/encryption/v1.rs index a6cca33..6f1a6a2 100644 --- a/server/crates/arbiter-server/src/crypto/encryption/v1.rs +++ b/server/crates/arbiter-server/src/crypto/encryption/v1.rs @@ -60,9 +60,9 @@ mod tests { use super::*; use crate::{ - crypto::derive_key, - safe_cell::{SafeCell, SafeCellHandle as _}, + crypto::derive_key }; + use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; #[test] pub fn derive_seal_key_deterministic() { diff --git a/server/crates/arbiter-server/src/crypto/integrity/v1.rs b/server/crates/arbiter-server/src/crypto/integrity/v1.rs index afd8358..2114cc2 100644 --- a/server/crates/arbiter-server/src/crypto/integrity/v1.rs +++ b/server/crates/arbiter-server/src/crypto/integrity/v1.rs @@ -1,6 +1,7 @@ use crate::{ - actors::keyholder, crypto::integrity::hashing::Hashable, safe_cell::SafeCellHandle as _, + actors::keyholder, crypto::integrity::hashing::Hashable }; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use hmac::{Hmac, Mac as _}; use sha2::Sha256; @@ -212,8 +213,9 @@ mod tests { use crate::{ actors::keyholder::{Bootstrap, KeyHolder}, db::{self, schema}, - safe_cell::{SafeCell, SafeCellHandle as _}, + }; + use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use super::{Error, Integrable, sign_entity, verify_entity}; use super::{hashing::Hashable, payload_hash}; diff --git a/server/crates/arbiter-server/src/crypto/mod.rs b/server/crates/arbiter-server/src/crypto/mod.rs index f558b8a..5a11898 100644 --- a/server/crates/arbiter-server/src/crypto/mod.rs +++ b/server/crates/arbiter-server/src/crypto/mod.rs @@ -10,9 +10,8 @@ use rand::{ rngs::{StdRng, SysRng}, }; -use crate::safe_cell::{SafeCell, SafeCellHandle as _}; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; -pub mod authn; pub mod encryption; pub mod integrity; @@ -142,7 +141,7 @@ mod tests { derive_key, encryption::v1::{Nonce, generate_salt}, }; - use crate::safe_cell::{SafeCell, SafeCellHandle as _}; + use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; #[test] pub fn encrypt_decrypt() { diff --git a/server/crates/arbiter-server/src/evm/safe_signer.rs b/server/crates/arbiter-server/src/evm/safe_signer.rs index 3d15a05..8604b57 100644 --- a/server/crates/arbiter-server/src/evm/safe_signer.rs +++ b/server/crates/arbiter-server/src/evm/safe_signer.rs @@ -1,6 +1,6 @@ use std::sync::Mutex; -use crate::safe_cell::{SafeCell, SafeCellHandle as _}; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use alloy::{ consensus::SignableTransaction, network::{TxSigner, TxSignerSync}, diff --git a/server/crates/arbiter-server/src/grpc/client/auth.rs b/server/crates/arbiter-server/src/grpc/client/auth.rs index e7b8b9a..4a7b944 100644 --- a/server/crates/arbiter-server/src/grpc/client/auth.rs +++ b/server/crates/arbiter-server/src/grpc/client/auth.rs @@ -1,3 +1,4 @@ +use arbiter_crypto::authn; use arbiter_proto::{ ClientMetadata, proto::{ @@ -22,7 +23,6 @@ use tracing::warn; use crate::{ actors::client::{self, ClientConnection, auth}, - crypto::authn, grpc::request_tracker::RequestTracker, }; diff --git a/server/crates/arbiter-server/src/grpc/user_agent/auth.rs b/server/crates/arbiter-server/src/grpc/user_agent/auth.rs index 2ca5f97..e2625e0 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/auth.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/auth.rs @@ -1,3 +1,4 @@ +use arbiter_crypto::authn; use arbiter_proto::{ proto::user_agent::{ UserAgentRequest, UserAgentResponse, @@ -18,7 +19,6 @@ use tracing::warn; use crate::{ actors::user_agent::{UserAgentConnection, auth}, - crypto::authn, grpc::request_tracker::RequestTracker, }; diff --git a/server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs b/server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs index 5e7984d..b0d832f 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/sdk_client.rs @@ -1,3 +1,4 @@ +use arbiter_crypto::authn; use arbiter_proto::proto::{ shared::ClientInfo as ProtoClientMetadata, user_agent::{ @@ -27,7 +28,6 @@ use crate::{ HandleRevokeEvmWalletAccess, HandleSdkClientList, }, }, - crypto::authn, db::models::NewEvmWalletAccess, grpc::Convert, }; diff --git a/server/crates/arbiter-server/src/lib.rs b/server/crates/arbiter-server/src/lib.rs index e551182..8bb07c8 100644 --- a/server/crates/arbiter-server/src/lib.rs +++ b/server/crates/arbiter-server/src/lib.rs @@ -7,7 +7,6 @@ pub mod crypto; pub mod db; pub mod evm; pub mod grpc; -pub mod safe_cell; pub mod utils; pub struct Server { diff --git a/server/crates/arbiter-server/tests/client/auth.rs b/server/crates/arbiter-server/tests/client/auth.rs index 21b5966..a7137e3 100644 --- a/server/crates/arbiter-server/tests/client/auth.rs +++ b/server/crates/arbiter-server/tests/client/auth.rs @@ -1,3 +1,7 @@ +use arbiter_crypto::{ + authn::{self, CLIENT_CONTEXT, format_challenge}, + safecell::{SafeCell, SafeCellHandle as _}, +}; use arbiter_proto::ClientMetadata; use arbiter_proto::transport::{Receiver, Sender}; use arbiter_server::{ @@ -6,10 +10,8 @@ use arbiter_server::{ client::{ClientConnection, ClientCredentials, auth, connect_client}, keyholder::Bootstrap, }, - crypto::authn, crypto::integrity, db::{self, schema}, - safe_cell::{SafeCell, SafeCellHandle as _}, }; use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into}; use diesel_async::RunQueryDsl; @@ -72,9 +74,9 @@ fn sign_client_challenge( nonce: i32, pubkey: &authn::PublicKey, ) -> authn::Signature { - let challenge = arbiter_proto::format_challenge(nonce, &pubkey.to_bytes()); + let challenge = format_challenge(nonce, &pubkey.to_bytes()); key.signing_key() - .sign_deterministic(&challenge, arbiter_proto::CLIENT_CONTEXT) + .sign_deterministic(&challenge, CLIENT_CONTEXT) .unwrap() .into() } diff --git a/server/crates/arbiter-server/tests/common/mod.rs b/server/crates/arbiter-server/tests/common/mod.rs index 13ccd32..cf6bee6 100644 --- a/server/crates/arbiter-server/tests/common/mod.rs +++ b/server/crates/arbiter-server/tests/common/mod.rs @@ -2,8 +2,9 @@ use arbiter_proto::transport::{Bi, Error, Receiver, Sender}; use arbiter_server::{ actors::keyholder::KeyHolder, db::{self, schema}, - safe_cell::{SafeCell, SafeCellHandle as _}, }; +use arbiter_crypto::{authn::{self, format_challenge, CLIENT_CONTEXT}, safecell::{SafeCell, SafeCellHandle as _}}; + use async_trait::async_trait; use diesel::QueryDsl; use diesel_async::RunQueryDsl; diff --git a/server/crates/arbiter-server/tests/keyholder/concurrency.rs b/server/crates/arbiter-server/tests/keyholder/concurrency.rs index 7dbe669..51ddda0 100644 --- a/server/crates/arbiter-server/tests/keyholder/concurrency.rs +++ b/server/crates/arbiter-server/tests/keyholder/concurrency.rs @@ -3,8 +3,10 @@ use std::collections::{HashMap, HashSet}; use arbiter_server::{ actors::keyholder::{CreateNew, Error, KeyHolder}, db::{self, models, schema}, - safe_cell::{SafeCell, SafeCellHandle as _}, + }; +use arbiter_crypto::{authn::{self, format_challenge, CLIENT_CONTEXT}, safecell::{SafeCell, SafeCellHandle as _}}; + use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::sql_query}; use diesel_async::RunQueryDsl; use kameo::actor::{ActorRef, Spawn as _}; diff --git a/server/crates/arbiter-server/tests/keyholder/lifecycle.rs b/server/crates/arbiter-server/tests/keyholder/lifecycle.rs index 88863a6..0228e6f 100644 --- a/server/crates/arbiter-server/tests/keyholder/lifecycle.rs +++ b/server/crates/arbiter-server/tests/keyholder/lifecycle.rs @@ -2,8 +2,9 @@ use arbiter_server::{ actors::keyholder::{Error, KeyHolder}, crypto::encryption::v1::{Nonce, ROOT_KEY_TAG}, db::{self, models, schema}, - safe_cell::{SafeCell, SafeCellHandle as _}, }; +use arbiter_crypto::{authn::{self, format_challenge, CLIENT_CONTEXT}, safecell::{SafeCell, SafeCellHandle as _}}; + use diesel::{QueryDsl, SelectableHelper}; use diesel_async::RunQueryDsl; diff --git a/server/crates/arbiter-server/tests/keyholder/storage.rs b/server/crates/arbiter-server/tests/keyholder/storage.rs index a66aa2e..4bb7351 100644 --- a/server/crates/arbiter-server/tests/keyholder/storage.rs +++ b/server/crates/arbiter-server/tests/keyholder/storage.rs @@ -4,8 +4,10 @@ use arbiter_server::{ actors::keyholder::Error, crypto::encryption::v1::Nonce, db::{self, models, schema}, - safe_cell::{SafeCell, SafeCellHandle as _}, + }; +use arbiter_crypto::{authn::{self, format_challenge, CLIENT_CONTEXT}, safecell::{SafeCell, SafeCellHandle as _}}; + use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update}; use diesel_async::RunQueryDsl; diff --git a/server/crates/arbiter-server/tests/user_agent/auth.rs b/server/crates/arbiter-server/tests/user_agent/auth.rs index f6598b7..00abf17 100644 --- a/server/crates/arbiter-server/tests/user_agent/auth.rs +++ b/server/crates/arbiter-server/tests/user_agent/auth.rs @@ -1,3 +1,5 @@ +use arbiter_crypto::{authn::{self, format_challenge, USERAGENT_CONTEXT}, safecell::{SafeCell, SafeCellHandle as _}}; + use arbiter_proto::transport::{Receiver, Sender}; use arbiter_server::{ actors::{ @@ -6,10 +8,8 @@ use arbiter_server::{ keyholder::Bootstrap, user_agent::{UserAgentConnection, UserAgentCredentials, auth}, }, - crypto::authn, crypto::integrity, db::{self, schema}, - safe_cell::{SafeCell, SafeCellHandle as _}, }; use diesel::{ExpressionMethods as _, QueryDsl, insert_into}; use diesel_async::RunQueryDsl; @@ -22,9 +22,9 @@ fn sign_useragent_challenge( nonce: i32, pubkey_bytes: &[u8], ) -> authn::Signature { - let challenge = arbiter_proto::format_challenge(nonce, pubkey_bytes); + let challenge = format_challenge(nonce, pubkey_bytes); key.signing_key() - .sign_deterministic(&challenge, arbiter_proto::USERAGENT_CONTEXT) + .sign_deterministic(&challenge, USERAGENT_CONTEXT) .unwrap() .into() } diff --git a/server/crates/arbiter-server/tests/user_agent/unseal.rs b/server/crates/arbiter-server/tests/user_agent/unseal.rs index 232b2e9..ce15095 100644 --- a/server/crates/arbiter-server/tests/user_agent/unseal.rs +++ b/server/crates/arbiter-server/tests/user_agent/unseal.rs @@ -8,8 +8,10 @@ use arbiter_server::{ }, }, db, - safe_cell::{SafeCell, SafeCellHandle as _}, + }; +use arbiter_crypto::{authn::{self, format_challenge, CLIENT_CONTEXT}, safecell::{SafeCell, SafeCellHandle as _}}; + use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; use diesel::{ExpressionMethods as _, QueryDsl as _, insert_into}; use diesel_async::RunQueryDsl;