diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index b410614..a1816b0 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -67,18 +67,14 @@ The `program_client.nonce` column stores the **next usable nonce** — i.e. it i ## Cryptography ### Authentication -- **Client protocol:** ed25519 +- **Client protocol:** ML-DSA ### User-Agent Authentication User-agent authentication supports multiple signature schemes because platform-provided "hardware-bound" keys do not expose a uniform algorithm across operating systems and hardware. -- **Supported schemes:** RSA, Ed25519, ECDSA (secp256k1) -- **Why:** the user agent authenticates with keys backed by platform facilities, and those facilities differ by platform -- **Apple Silicon Secure Enclave / Secure Element:** ECDSA-only in practice -- **Windows Hello / TPM 2.0:** currently RSA-backed in our integration - -This is why the user-agent auth protocol carries an explicit `KeyType`, while the SDK client protocol remains fixed to ed25519. +- **Supported schemes:** ML-DSA +- **Why:** Secure Enclave (MacOS) support them natively, on other platforms we could emulate while they roll-out ### Encryption at Rest - **Scheme:** Symmetric AEAD — currently **XChaCha20-Poly1305** diff --git a/server/Cargo.lock b/server/Cargo.lock index f3138b8..5b89b7e 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -347,7 +347,7 @@ dependencies = [ "ruint", "rustc-hash", "serde", - "sha3", + "sha3 0.10.8", ] [[package]] @@ -548,7 +548,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "sha3", + "sha3 0.10.8", "syn 2.0.117", "syn-solidity", ] @@ -680,9 +680,9 @@ name = "arbiter-client" version = "0.1.0" dependencies = [ "alloy", + "arbiter-crypto", "arbiter-proto", "async-trait", - "ed25519-dalek", "http", "rand 0.10.0", "rustls-webpki", @@ -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,7 @@ dependencies = [ "insta", "k256", "kameo", - "memsafe", + "ml-dsa", "mutants", "pem", "proptest", @@ -751,14 +762,13 @@ dependencies = [ "rand 0.10.0", "rcgen", "restructed", - "rsa", "rstest", "rustls", "secrecy", "serde_with", "sha2 0.10.9", "smlang", - "spki", + "spki 0.7.3", "strum 0.28.0", "subtle", "test-log", @@ -1449,6 +1459,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "console" version = "0.15.11" @@ -1479,6 +1495,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const_format" version = "0.2.35" @@ -1600,6 +1622,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1738,8 +1769,17 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", - "pem-rfc7468", + "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", "zeroize", ] @@ -1882,7 +1922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid", + "const-oid 0.9.6", "crypto-common 0.1.7", "subtle", ] @@ -1946,13 +1986,13 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.10", "digest 0.10.7", "elliptic-curve", "rfc6979", "serdect", "signature 2.2.0", - "spki", + "spki 0.7.3", ] [[package]] @@ -1961,7 +2001,6 @@ version = "3.0.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" dependencies = [ - "serde", "signature 3.0.0-rc.10", ] @@ -1974,7 +2013,6 @@ dependencies = [ "curve25519-dalek 5.0.0-pre.6", "ed25519", "rand_core 0.10.0", - "serde", "sha2 0.11.0-rc.5", "subtle", "zeroize", @@ -2013,7 +2051,7 @@ dependencies = [ "ff", "generic-array", "group", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", "serdect", @@ -2560,6 +2598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" dependencies = [ "typenum", + "zeroize", ] [[package]] @@ -2957,6 +2996,16 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + [[package]] name = "keccak-asm" version = "0.1.5" @@ -2972,9 +3021,6 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] [[package]] name = "leb128fmt" @@ -3173,6 +3219,34 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-dsa" +version = "0.1.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5b2bb0ad6fa2b40396775bd56f51345171490fef993f46f91a876ecdbdaea55" +dependencies = [ + "const-oid 0.10.2", + "ctutils", + "hybrid-array", + "module-lattice", + "pkcs8 0.11.0-rc.11", + "rand_core 0.10.0", + "sha3 0.11.0", + "signature 3.0.0-rc.10", + "zeroize", +] + +[[package]] +name = "module-lattice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164eb3faeaecbd14b0b2a917c1b4d0c035097a9c559b0bed85c2cdd032bc8faa" +dependencies = [ + "hybrid-array", + "num-traits", + "zeroize", +] + [[package]] name = "multimap" version = "0.10.1" @@ -3214,23 +3288,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "serde", - "smallvec", - "zeroize", -] - [[package]] name = "num-conv" version = "0.2.0" @@ -3246,17 +3303,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -3426,15 +3472,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -3494,25 +3531,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" +dependencies = [ + "der 0.8.0", + "spki 0.8.0", ] [[package]] @@ -4152,28 +4188,6 @@ dependencies = [ "rustc-hex", ] -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid", - "digest 0.10.7", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "serde", - "sha2 0.10.9", - "signature 2.2.0", - "spki", - "subtle", - "zeroize", -] - [[package]] name = "rsqlite-vfs" version = "0.1.0" @@ -4413,9 +4427,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der", + "der 0.7.10", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "serdect", "subtle", "zeroize", @@ -4609,7 +4623,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ "digest 0.10.7", - "keccak", + "keccak 0.1.6", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.2", + "keccak 0.2.0", ] [[package]] @@ -4662,6 +4686,10 @@ name = "signature" version = "3.0.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" +dependencies = [ + "digest 0.11.2", + "rand_core 0.10.0", +] [[package]] name = "simd-adler32" @@ -4721,12 +4749,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "spki" version = "0.7.3" @@ -4734,7 +4756,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.0", ] [[package]] diff --git a/server/Cargo.toml b/server/Cargo.toml index 980cff1..98018a0 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -20,7 +20,7 @@ tokio = { version = "1.50.0", features = ["full"] } ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] } chrono = { version = "0.4.44", features = ["serde"] } rand = "0.10.0" -rustls = { version = "0.23.37", features = ["aws-lc-rs"] } +rustls = { version = "0.23.37", features = ["aws-lc-rs", "logging", "prefer-post-quantum", "std"], default-features = false } smlang = "0.8.0" thiserror = "2.0.18" async-trait = "0.1.89" @@ -45,3 +45,5 @@ spki = "0.7" 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 f5e353b..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 -ed25519-dalek.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 ff26e09..e6068e5 100644 --- a/server/crates/arbiter-client/src/auth.rs +++ b/server/crates/arbiter-client/src/auth.rs @@ -1,5 +1,6 @@ +use arbiter_crypto::authn::{CLIENT_CONTEXT, SigningKey, format_challenge}; use arbiter_proto::{ - ClientMetadata, format_challenge, + ClientMetadata, proto::{ client::{ ClientRequest, @@ -14,7 +15,6 @@ use arbiter_proto::{ shared::ClientInfo as ProtoClientInfo, }, }; -use ed25519_dalek::Signer as _; 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: &ed25519_dalek::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().to_bytes().to_vec(), + pubkey: key.public_key().to_bytes(), client_info: Some(ProtoClientInfo { name: metadata.name, description: metadata.description, @@ -95,11 +95,14 @@ async fn receive_auth_challenge( async fn send_auth_challenge_solution( transport: &mut ClientTransport, - key: &ed25519_dalek::SigningKey, + key: &SigningKey, challenge: AuthChallenge, ) -> std::result::Result<(), AuthError> { let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey); - let signature = key.sign(&challenge_payload).to_bytes().to_vec(); + let signature = key + .sign_message(&challenge_payload, CLIENT_CONTEXT) + .map_err(|_| AuthError::UnexpectedAuthResponse)? + .to_bytes(); transport .send(ClientRequest { @@ -140,7 +143,7 @@ async fn receive_auth_confirmation( pub(crate) async fn authenticate( transport: &mut ClientTransport, metadata: ClientMetadata, - key: &ed25519_dalek::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 8a77441..b540647 100644 --- a/server/crates/arbiter-client/src/client.rs +++ b/server/crates/arbiter-client/src/client.rs @@ -1,3 +1,4 @@ +use arbiter_crypto::authn::SigningKey; use arbiter_proto::{ ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl, }; @@ -60,7 +61,7 @@ impl ArbiterClient { pub async fn connect_with_key( url: ArbiterUrl, metadata: ClientMetadata, - key: ed25519_dalek::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 17d0bf2..55d4a46 100644 --- a/server/crates/arbiter-client/src/storage.rs +++ b/server/crates/arbiter-client/src/storage.rs @@ -1,3 +1,4 @@ +use arbiter_crypto::authn::SigningKey; use arbiter_proto::home_path; use std::path::{Path, PathBuf}; @@ -11,7 +12,7 @@ pub enum StorageError { } pub trait SigningKeyStorage { - fn load_or_create(&self) -> std::result::Result; + fn load_or_create(&self) -> std::result::Result; } #[derive(Debug, Clone)] @@ -20,7 +21,7 @@ pub struct FileSigningKeyStorage { } impl FileSigningKeyStorage { - pub const DEFAULT_FILE_NAME: &str = "sdk_client_ed25519.key"; + pub const DEFAULT_FILE_NAME: &str = "sdk_client_ml_dsa.key"; pub fn new(path: impl Into) -> Self { Self { path: path.into() } @@ -30,7 +31,7 @@ impl FileSigningKeyStorage { Ok(Self::new(home_path()?.join(Self::DEFAULT_FILE_NAME))) } - fn read_key(path: &Path) -> std::result::Result { + fn read_key(path: &Path) -> std::result::Result { let bytes = std::fs::read(path)?; let raw: [u8; 32] = bytes @@ -39,12 +40,12 @@ impl FileSigningKeyStorage { expected: 32, actual: v.len(), })?; - Ok(ed25519_dalek::SigningKey::from_bytes(&raw)) + Ok(SigningKey::from_seed(raw)) } } impl SigningKeyStorage for FileSigningKeyStorage { - fn load_or_create(&self) -> std::result::Result { + fn load_or_create(&self) -> std::result::Result { if let Some(parent) = self.path.parent() { std::fs::create_dir_all(parent)?; } @@ -53,8 +54,8 @@ impl SigningKeyStorage for FileSigningKeyStorage { return Self::read_key(&self.path); } - let key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); - let raw_key = key.to_bytes(); + let key = SigningKey::generate(); + let raw_key = key.to_seed(); // Use create_new to prevent accidental overwrite if another process creates the key first. match std::fs::OpenOptions::new() @@ -103,7 +104,7 @@ mod tests { .load_or_create() .expect("second load_or_create should read same key"); - assert_eq!(key_a.to_bytes(), key_b.to_bytes()); + assert_eq!(key_a.to_seed(), key_b.to_seed()); assert!(path.exists()); std::fs::remove_file(path).expect("temp key file should be removable"); 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-crypto/src/authn/mod.rs b/server/crates/arbiter-crypto/src/authn/mod.rs new file mode 100644 index 0000000..3789969 --- /dev/null +++ b/server/crates/arbiter-crypto/src/authn/mod.rs @@ -0,0 +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..5015af2 --- /dev/null +++ b/server/crates/arbiter-crypto/src/lib.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "authn")] +pub mod authn; + +#[cfg(feature = "safecell")] +pub mod safecell; diff --git a/server/crates/arbiter-server/src/safe_cell.rs b/server/crates/arbiter-crypto/src/safecell.rs similarity index 91% rename from server/crates/arbiter-server/src/safe_cell.rs rename to server/crates/arbiter-crypto/src/safecell.rs index dc44065..80dc57e 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 = std::ptr::null_mut::(); + 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 141b231..5f63aa1 100644 --- a/server/crates/arbiter-proto/src/lib.rs +++ b/server/crates/arbiter-proto/src/lib.rs @@ -1,8 +1,6 @@ pub mod transport; pub mod url; -use base64::{Engine, prelude::BASE64_STANDARD}; - pub mod proto { tonic::include_proto!("arbiter"); @@ -84,8 +82,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 20f3f00..c563ee0 100644 --- a/server/crates/arbiter-server/Cargo.toml +++ b/server/crates/arbiter-server/Cargo.toml @@ -16,9 +16,8 @@ diesel-async = { version = "0.8.0", features = [ "sqlite", "tokio", ] } -ed25519-dalek.workspace = true -ed25519-dalek.features = ["serde"] arbiter-proto.path = "../arbiter-proto" +arbiter-crypto.path = "../arbiter-crypto" tracing.workspace = true tracing-subscriber = { version = "0.3", features = ["env-filter"] } tonic.workspace = true @@ -37,19 +36,13 @@ 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 -x25519-dalek.workspace = true chacha20poly1305 = { version = "0.10.1", features = ["std"] } argon2 = { version = "0.5.3", features = ["zeroize"] } restructed = "0.2.2" strum = { version = "0.28.0", features = ["derive"] } pem = "3.0.6" -k256.workspace = true -k256.features = ["serde"] -rsa.workspace = true -rsa.features = ["serde"] sha2.workspace = true hmac = "0.12" spki.workspace = true @@ -61,6 +54,10 @@ anyhow = "1.0.102" serde_with = "3.18.0" mutants.workspace = true subtle = "2.6.1" +ml-dsa.workspace = true +ed25519-dalek.workspace = true +x25519-dalek.workspace = true +k256.workspace = true [dev-dependencies] insta = "1.46.3" diff --git a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql index 7ec6729..bb33278 100644 --- a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql +++ b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql @@ -47,7 +47,7 @@ create table if not exists useragent_client ( id integer not null primary key, nonce integer not null default(1), -- used for auth challenge public_key blob not null, - key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1) + key_type integer not null default(1), created_at integer not null default(unixepoch ('now')), updated_at integer not null default(unixepoch ('now')) ) STRICT; diff --git a/server/crates/arbiter-server/src/actors/client/auth.rs b/server/crates/arbiter-server/src/actors/client/auth.rs index 034efd3..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::{ - ClientMetadata, format_challenge, + ClientMetadata, transport::{Bi, expect_message}, }; use chrono::Utc; @@ -8,7 +9,6 @@ use diesel::{ dsl::insert_into, update, }; use diesel_async::RunQueryDsl as _; -use ed25519_dalek::{Signature, VerifyingKey}; use kameo::{actor::ActorRef, error::SendError}; use tracing::error; @@ -62,17 +62,20 @@ pub enum ApproveError { #[derive(Debug, Clone)] pub enum Inbound { AuthChallengeRequest { - pubkey: VerifyingKey, + pubkey: authn::PublicKey, metadata: ClientMetadata, }, AuthChallengeSolution { - signature: Signature, + signature: authn::Signature, }, } #[derive(Debug, Clone)] pub enum Outbound { - AuthChallenge { pubkey: VerifyingKey, nonce: i32 }, + AuthChallenge { + pubkey: authn::PublicKey, + nonce: i32, + }, AuthSuccess, } @@ -80,9 +83,9 @@ pub enum Outbound { /// Returns `None` if the pubkey is not registered. async fn get_current_nonce_and_id( db: &db::DatabasePool, - pubkey: &VerifyingKey, + pubkey: &authn::PublicKey, ) -> Result, Error> { - let pubkey_bytes = pubkey.as_bytes().to_vec(); + let pubkey_bytes = pubkey.to_bytes(); let mut conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); Error::DatabasePoolUnavailable @@ -102,19 +105,17 @@ async fn get_current_nonce_and_id( async fn verify_integrity( db: &db::DatabasePool, keyholder: &ActorRef, - pubkey: &VerifyingKey, + pubkey: &authn::PublicKey, ) -> Result<(), Error> { let mut db_conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); Error::DatabasePoolUnavailable })?; - let (id, nonce) = get_current_nonce_and_id(db, pubkey) - .await? - .ok_or_else(|| { - error!("Client not found during integrity verification"); - Error::DatabaseOperationFailed - })?; + let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?.ok_or_else(|| { + error!("Client not found during integrity verification"); + Error::DatabaseOperationFailed + })?; let attestation = integrity::verify_entity( &mut db_conn, @@ -144,9 +145,9 @@ async fn verify_integrity( async fn create_nonce( db: &db::DatabasePool, keyholder: &ActorRef, - pubkey: &VerifyingKey, + pubkey: &authn::PublicKey, ) -> Result { - let pubkey_bytes = pubkey.as_bytes().to_vec(); + let pubkey_bytes = pubkey.to_bytes(); let pubkey = pubkey.clone(); let mut conn = db.get().await.map_err(|e| { @@ -212,7 +213,7 @@ async fn approve_new_client( async fn insert_client( db: &db::DatabasePool, keyholder: &ActorRef, - pubkey: &VerifyingKey, + pubkey: &authn::PublicKey, metadata: &ClientMetadata, ) -> Result { use crate::db::schema::{client_metadata, program_client}; @@ -242,7 +243,7 @@ async fn insert_client( let client_id = insert_into(program_client::table) .values(( - program_client::public_key.eq(pubkey.as_bytes().to_vec()), + program_client::public_key.eq(pubkey.to_bytes()), program_client::metadata_id.eq(metadata_id), program_client::nonce.eq(NONCE_START), )) @@ -345,14 +346,17 @@ async fn sync_client_metadata( async fn challenge_client( transport: &mut T, - pubkey: VerifyingKey, + pubkey: authn::PublicKey, nonce: i32, ) -> Result<(), Error> where T: Bi> + ?Sized, { transport - .send(Ok(Outbound::AuthChallenge { pubkey, nonce })) + .send(Ok(Outbound::AuthChallenge { + pubkey: pubkey.clone(), + nonce, + })) .await .map_err(|e| { error!(error = ?e, "Failed to send auth challenge"); @@ -369,12 +373,10 @@ where Error::Transport })?; - let formatted = format_challenge(nonce, pubkey.as_bytes()); - - pubkey.verify_strict(&formatted, &signature).map_err(|_| { + if !pubkey.verify(nonce, CLIENT_CONTEXT, &signature) { error!("Challenge solution verification failed"); - Error::InvalidChallengeSolution - })?; + return Err(Error::InvalidChallengeSolution); + } Ok(()) } @@ -396,7 +398,7 @@ where approve_new_client( &props.actors, ClientProfile { - pubkey, + pubkey: pubkey.clone(), metadata: metadata.clone(), }, ) diff --git a/server/crates/arbiter-server/src/actors/client/mod.rs b/server/crates/arbiter-server/src/actors/client/mod.rs index b97a1e6..03b8861 100644 --- a/server/crates/arbiter-server/src/actors/client/mod.rs +++ b/server/crates/arbiter-server/src/actors/client/mod.rs @@ -1,3 +1,4 @@ +use arbiter_crypto::authn; use arbiter_proto::{ClientMetadata, transport::Bi}; use kameo::actor::Spawn; use tracing::{error, info}; @@ -10,12 +11,12 @@ use crate::{ #[derive(Debug, Clone)] pub struct ClientProfile { - pub pubkey: ed25519_dalek::VerifyingKey, + pub pubkey: authn::PublicKey, pub metadata: ClientMetadata, } pub struct ClientCredentials { - pub pubkey: ed25519_dalek::VerifyingKey, + pub pubkey: authn::PublicKey, pub nonce: i32, } @@ -25,7 +26,7 @@ impl Integrable for ClientCredentials { impl Hashable for ClientCredentials { fn hash(&self, hasher: &mut H) { - hasher.update(self.pubkey.as_bytes()); + hasher.update(self.pubkey.to_bytes()); self.nonce.hash(hasher); } } @@ -48,7 +49,9 @@ pub async fn connect_client(mut props: ClientConnection, transport: &mut T) where T: Bi> + Send + ?Sized, { - match auth::authenticate(&mut props, transport).await { + let fut = auth::authenticate(&mut props, transport); + println!("authenticate future size: {}", std::mem::size_of_val(&fut)); + match fut.await { Ok(client_id) => { ClientSession::spawn(ClientSession::new(props, client_id)); info!("Client authenticated, session started"); diff --git a/server/crates/arbiter-server/src/actors/evm/mod.rs b/server/crates/arbiter-server/src/actors/evm/mod.rs index 84326eb..c31cdd0 100644 --- a/server/crates/arbiter-server/src/actors/evm/mod.rs +++ b/server/crates/arbiter-server/src/actors/evm/mod.rs @@ -7,11 +7,11 @@ use kameo::{Actor, actor::ActorRef, messages}; use rand::{SeedableRng, rng, rngs::StdRng}; use crate::{ - actors::keyholder::{CreateNew, Decrypt, GetState, KeyHolder, KeyHolderState}, + actors::keyholder::{CreateNew, Decrypt, KeyHolder}, crypto::integrity, db::{ DatabaseError, DatabasePool, - models::{self, SqliteTimestamp}, + models::{self}, schema, }, evm::{ @@ -21,8 +21,8 @@ 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; @@ -158,7 +158,7 @@ impl EvmActor { } #[message] - pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> { + pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> { // let mut conn = self.db.get().await.map_err(DatabaseError::from)?; // let keyholder = self.keyholder.clone(); diff --git a/server/crates/arbiter-server/src/actors/keyholder/mod.rs b/server/crates/arbiter-server/src/actors/keyholder/mod.rs index 8e43129..64387bc 100644 --- a/server/crates/arbiter-server/src/actors/keyholder/mod.rs +++ b/server/crates/arbiter-server/src/actors/keyholder/mod.rs @@ -9,22 +9,17 @@ use kameo::{Actor, Reply, messages}; use strum::{EnumDiscriminants, IntoDiscriminant}; use tracing::{error, info}; -use crate::{ - crypto::{ - KeyCell, derive_key, - encryption::v1::{self, Nonce}, - integrity::v1::HmacSha256, - }, - safe_cell::SafeCell, +use crate::crypto::{ + KeyCell, derive_key, + encryption::v1::{self, Nonce}, + integrity::v1::HmacSha256, }; -use crate::{ - db::{ - self, - models::{self, RootKeyHistory}, - schema::{self}, - }, - safe_cell::SafeCellHandle as _, +use crate::db::{ + self, + models::{self, RootKeyHistory}, + schema::{self}, }; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; #[derive(Default, EnumDiscriminants)] #[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))] @@ -400,10 +395,8 @@ mod tests { use diesel_async::RunQueryDsl; - use crate::{ - db::{self}, - safe_cell::SafeCell, - }; + use crate::db::{self}; + 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 83b0472..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,18 +1,18 @@ +use arbiter_crypto::authn; use arbiter_proto::transport::Bi; use tracing::error; use crate::actors::user_agent::{ - AuthPublicKey, UserAgentConnection, + UserAgentConnection, auth::state::{AuthContext, AuthStateMachine}, }; - mod state; use state::*; #[derive(Debug, Clone)] pub enum Inbound { AuthChallengeRequest { - pubkey: AuthPublicKey, + pubkey: authn::PublicKey, bootstrap_token: Option, }, AuthChallengeSolution { @@ -71,7 +71,7 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents { pub async fn authenticate( props: &mut UserAgentConnection, transport: T, -) -> Result +) -> Result where T: Bi> + Send, { 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 ccc4b31..60bcf6f 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,7 +1,8 @@ +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, error::SendError}; +use kameo::actor::ActorRef; use tracing::error; use super::Error; @@ -9,24 +10,24 @@ use crate::{ actors::{ bootstrap::ConsumeToken, keyholder::KeyHolder, - user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth::Outbound}, + user_agent::{UserAgentConnection, UserAgentCredentials, auth::Outbound}, }, - crypto::integrity::{self, AttestationStatus}, + crypto::integrity, db::{DatabasePool, schema::useragent_client}, }; pub struct ChallengeRequest { - pub pubkey: AuthPublicKey, + pub pubkey: authn::PublicKey, } pub struct BootstrapAuthRequest { - pub pubkey: AuthPublicKey, + pub pubkey: authn::PublicKey, pub token: String, } pub struct ChallengeContext { pub challenge_nonce: i32, - pub key: AuthPublicKey, + pub key: authn::PublicKey, } pub struct ChallengeSolution { @@ -38,15 +39,15 @@ smlang::statemachine!( custom_error: true, transitions: { *Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext), - Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthPublicKey), - SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthPublicKey), + Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(authn::PublicKey), + SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(authn::PublicKey), } ); /// Returns the current nonce, ready to use for the challenge nonce. async fn get_current_nonce_and_id( db: &DatabasePool, - key: &AuthPublicKey, + key: &authn::PublicKey, ) -> Result<(i32, i32), Error> { let mut db_conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); @@ -56,8 +57,7 @@ async fn get_current_nonce_and_id( .exclusive_transaction(|conn| { Box::pin(async move { useragent_client::table - .filter(useragent_client::public_key.eq(key.to_stored_bytes())) - .filter(useragent_client::key_type.eq(key.key_type())) + .filter(useragent_client::public_key.eq(key.to_bytes())) .select((useragent_client::id, useragent_client::nonce)) .first::<(i32, i32)>(conn) .await @@ -78,7 +78,7 @@ async fn get_current_nonce_and_id( async fn verify_integrity( db: &DatabasePool, keyholder: &ActorRef, - pubkey: &AuthPublicKey, + pubkey: &authn::PublicKey, ) -> Result<(), Error> { let mut db_conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); @@ -87,7 +87,7 @@ async fn verify_integrity( let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?; - let result = integrity::verify_entity( + let _result = integrity::verify_entity( &mut db_conn, keyholder, &UserAgentCredentials { @@ -108,7 +108,7 @@ async fn verify_integrity( async fn create_nonce( db: &DatabasePool, keyholder: &ActorRef, - pubkey: &AuthPublicKey, + pubkey: &authn::PublicKey, ) -> Result { let mut db_conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); @@ -118,8 +118,7 @@ async fn create_nonce( .exclusive_transaction(|conn| { Box::pin(async move { let (id, new_nonce): (i32, i32) = update(useragent_client::table) - .filter(useragent_client::public_key.eq(pubkey.to_stored_bytes())) - .filter(useragent_client::key_type.eq(pubkey.key_type())) + .filter(useragent_client::public_key.eq(pubkey.to_bytes())) .set(useragent_client::nonce.eq(useragent_client::nonce + 1)) .returning((useragent_client::id, useragent_client::nonce)) .get_result(conn) @@ -154,10 +153,9 @@ async fn create_nonce( async fn register_key( db: &DatabasePool, keyholder: &ActorRef, - pubkey: &AuthPublicKey, + pubkey: &authn::PublicKey, ) -> Result<(), Error> { - let pubkey_bytes = pubkey.to_stored_bytes(); - let key_type = pubkey.key_type(); + let pubkey_bytes = pubkey.to_bytes(); let mut conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); Error::internal("Database unavailable") @@ -171,7 +169,6 @@ async fn register_key( .values(( useragent_client::public_key.eq(pubkey_bytes), useragent_client::nonce.eq(NONCE_START), - useragent_client::key_type.eq(key_type), )) .returning(useragent_client::id) .get_result(conn) @@ -186,7 +183,7 @@ async fn register_key( nonce: NONCE_START, }; - integrity::sign_entity(conn, &keyholder, &entity, id) + integrity::sign_entity(conn, keyholder, &entity, id) .await .map_err(|e| { error!(error = ?e, "Failed to sign integrity tag for new user-agent key"); @@ -245,7 +242,7 @@ where async fn verify_bootstrap_token( &mut self, BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest, - ) -> Result { + ) -> Result { let token_ok: bool = self .conn .actors @@ -293,35 +290,13 @@ where key, }: &ChallengeContext, ChallengeSolution { solution }: ChallengeSolution, - ) -> Result { - let formatted = arbiter_proto::format_challenge(*challenge_nonce, &key.to_stored_bytes()); + ) -> Result { + let signature = authn::Signature::try_from(solution.as_slice()).map_err(|_| { + error!("Failed to decode signature in challenge solution"); + Error::InvalidChallengeSolution + })?; - let valid = match key { - AuthPublicKey::Ed25519(vk) => { - let sig = solution.as_slice().try_into().map_err(|_| { - error!(?solution, "Invalid Ed25519 signature length"); - Error::InvalidChallengeSolution - })?; - vk.verify_strict(&formatted, &sig).is_ok() - } - AuthPublicKey::EcdsaSecp256k1(vk) => { - use k256::ecdsa::signature::Verifier as _; - let sig = k256::ecdsa::Signature::try_from(solution.as_slice()).map_err(|_| { - error!(?solution, "Invalid ECDSA signature bytes"); - Error::InvalidChallengeSolution - })?; - vk.verify(&formatted, &sig).is_ok() - } - AuthPublicKey::Rsa(pk) => { - use rsa::signature::Verifier as _; - let verifying_key = rsa::pss::VerifyingKey::::new(pk.clone()); - let sig = rsa::pss::Signature::try_from(solution.as_slice()).map_err(|_| { - error!(?solution, "Invalid RSA signature bytes"); - Error::InvalidChallengeSolution - })?; - verifying_key.verify(&formatted, &sig).is_ok() - } - }; + let valid = key.verify(*challenge_nonce, USERAGENT_CONTEXT, &signature); match valid { true => { 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 2451e49..ac571d9 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/mod.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/mod.rs @@ -1,22 +1,13 @@ use crate::{ actors::{GlobalActors, client::ClientProfile}, crypto::integrity::Integrable, - db::{self, models::KeyType}, + db, }; - -/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake. -#[derive(Clone, Debug)] -pub enum AuthPublicKey { - Ed25519(ed25519_dalek::VerifyingKey), - /// Compressed SEC1 public key; signature bytes are raw 64-byte (r||s). - EcdsaSecp256k1(k256::ecdsa::VerifyingKey), - /// RSA-2048+ public key (Windows Hello / KeyCredentialManager); signature bytes are PSS+SHA-256. - Rsa(rsa::RsaPublicKey), -} +use arbiter_crypto::authn; #[derive(Debug)] pub struct UserAgentCredentials { - pub pubkey: AuthPublicKey, + pub pubkey: authn::PublicKey, pub nonce: i32, } @@ -24,67 +15,11 @@ impl Integrable for UserAgentCredentials { const KIND: &'static str = "useragent_credentials"; } -impl AuthPublicKey { - /// Canonical bytes stored in DB and echoed back in the challenge. - /// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI. - pub fn to_stored_bytes(&self) -> Vec { - match self { - AuthPublicKey::Ed25519(k) => k.to_bytes().to_vec(), - // SEC1 compressed (33 bytes) is the natural compact format for secp256k1 - AuthPublicKey::EcdsaSecp256k1(k) => k.to_encoded_point(true).as_bytes().to_vec(), - AuthPublicKey::Rsa(k) => { - use rsa::pkcs8::EncodePublicKey as _; - #[allow(clippy::expect_used)] - k.to_public_key_der() - .expect("rsa SPKI encoding is infallible") - .to_vec() - } - } - } - - pub fn key_type(&self) -> KeyType { - match self { - AuthPublicKey::Ed25519(_) => KeyType::Ed25519, - AuthPublicKey::EcdsaSecp256k1(_) => KeyType::EcdsaSecp256k1, - AuthPublicKey::Rsa(_) => KeyType::Rsa, - } - } -} - -impl TryFrom<(KeyType, Vec)> for AuthPublicKey { - type Error = &'static str; - - fn try_from(value: (KeyType, Vec)) -> Result { - let (key_type, bytes) = value; - match key_type { - KeyType::Ed25519 => { - let bytes: [u8; 32] = bytes.try_into().map_err(|_| "invalid Ed25519 key length")?; - let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes) - .map_err(|_e| "invalid Ed25519 key")?; - Ok(AuthPublicKey::Ed25519(key)) - } - KeyType::EcdsaSecp256k1 => { - let point = - k256::EncodedPoint::from_bytes(&bytes).map_err(|_e| "invalid ECDSA key")?; - let key = k256::ecdsa::VerifyingKey::from_encoded_point(&point) - .map_err(|_e| "invalid ECDSA key")?; - Ok(AuthPublicKey::EcdsaSecp256k1(key)) - } - KeyType::Rsa => { - use rsa::pkcs8::DecodePublicKey as _; - let key = rsa::RsaPublicKey::from_public_key_der(&bytes) - .map_err(|_e| "invalid RSA key")?; - Ok(AuthPublicKey::Rsa(key)) - } - } - } -} - // Messages, sent by user agent to connection client without having a request #[derive(Debug)] pub enum OutOfBand { ClientConnectionRequest { profile: ClientProfile }, - ClientConnectionCancel { pubkey: ed25519_dalek::VerifyingKey }, + ClientConnectionCancel { pubkey: authn::PublicKey }, } pub struct UserAgentConnection { @@ -106,9 +41,9 @@ pub use session::UserAgentSession; use crate::crypto::integrity::hashing::Hashable; -impl Hashable for AuthPublicKey { +impl Hashable for authn::PublicKey { fn hash(&self, hasher: &mut H) { - hasher.update(&self.to_stored_bytes()); + hasher.update(self.to_bytes()); } } 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 182db6e..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,8 +1,9 @@ +use arbiter_crypto::authn; + use std::{borrow::Cow, collections::HashMap}; use arbiter_proto::transport::Sender; use async_trait::async_trait; -use ed25519_dalek::VerifyingKey; use kameo::{Actor, actor::ActorRef, messages}; use thiserror::Error; use tracing::error; @@ -12,7 +13,6 @@ use crate::actors::{ flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController}, user_agent::{OutOfBand, UserAgentConnection}, }; - mod state; use state::{DummyContext, UserAgentEvents, UserAgentStateMachine}; @@ -47,6 +47,7 @@ impl Error { } pub struct PendingClientApproval { + pubkey: authn::PublicKey, controller: ActorRef, } @@ -55,7 +56,7 @@ pub struct UserAgentSession { state: UserAgentStateMachine, sender: Box>, - pending_client_approvals: HashMap, + pending_client_approvals: HashMap, PendingClientApproval>, } pub mod connection; @@ -118,8 +119,13 @@ impl UserAgentSession { return; } - self.pending_client_approvals - .insert(client.pubkey, PendingClientApproval { controller }); + self.pending_client_approvals.insert( + client.pubkey.to_bytes(), + PendingClientApproval { + pubkey: client.pubkey, + controller, + }, + ); } } @@ -158,14 +164,18 @@ impl Actor for UserAgentSession { let cancelled_pubkey = self .pending_client_approvals .iter() - .find_map(|(k, v)| (v.controller.id() == id).then_some(*k)); + .find_map(|(k, v)| (v.controller.id() == id).then_some(k.clone())); - if let Some(pubkey) = cancelled_pubkey { - self.pending_client_approvals.remove(&pubkey); + if let Some(pubkey_bytes) = cancelled_pubkey { + let Some(approval) = self.pending_client_approvals.remove(&pubkey_bytes) else { + return Ok(std::ops::ControlFlow::Continue(())); + }; if let Err(e) = self .sender - .send(OutOfBand::ClientConnectionCancel { pubkey }) + .send(OutOfBand::ClientConnectionCancel { + pubkey: approval.pubkey, + }) .await { error!( 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 3017819..71f4067 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,10 @@ 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,25 +17,21 @@ 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::actors::{ + evm::{ + ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError, + UseragentCreateGrant, UseragentListGrants, + }, + keyholder::{self, Bootstrap, TryUnseal}, + user_agent::session::{ + UserAgentSession, + state::{UnsealContext, UserAgentEvents, UserAgentStates}, + }, +}; use crate::db::models::{ EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata, }; use crate::evm::policies::{Grant, SpecificGrant}; -use crate::safe_cell::SafeCell; -use crate::{ - actors::{ - evm::{ - ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError, - UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants, - }, - keyholder::{self, Bootstrap, TryUnseal}, - user_agent::session::{ - UserAgentSession, - state::{UnsealContext, UserAgentEvents, UserAgentStates}, - }, - }, - safe_cell::SafeCellHandle as _, -}; impl UserAgentSession { fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> { @@ -361,19 +361,21 @@ impl UserAgentSession { &mut self, grant_id: i32, ) -> Result<(), GrantMutationError> { - match self - .props - .actors - .evm - .ask(UseragentDeleteGrant { grant_id }) - .await - { - Ok(()) => Ok(()), - Err(err) => { - error!(?err, "EVM grant delete failed"); - Err(GrantMutationError::Internal) - } - } + // match self + // .props + // .actors + // .evm + // .ask(UseragentDeleteGrant { grant_id }) + // .await + // { + // Ok(()) => Ok(()), + // Err(err) => { + // error!(?err, "EVM grant delete failed"); + // Err(GrantMutationError::Internal) + // } + // } + let _ = grant_id; + todo!() } #[message] @@ -473,10 +475,10 @@ impl UserAgentSession { pub(crate) async fn handle_new_client_approve( &mut self, approved: bool, - pubkey: ed25519_dalek::VerifyingKey, + pubkey: authn::PublicKey, ctx: &mut Context>, ) -> Result<(), Error> { - let pending_approval = match self.pending_client_approvals.remove(&pubkey) { + 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/encryption/v1.rs b/server/crates/arbiter-server/src/crypto/encryption/v1.rs index a6cca33..e2b7c04 100644 --- a/server/crates/arbiter-server/src/crypto/encryption/v1.rs +++ b/server/crates/arbiter-server/src/crypto/encryption/v1.rs @@ -59,10 +59,8 @@ mod tests { use std::ops::Deref as _; use super::*; - use crate::{ - crypto::derive_key, - safe_cell::{SafeCell, SafeCellHandle as _}, - }; + use crate::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..4b67217 100644 --- a/server/crates/arbiter-server/src/crypto/integrity/v1.rs +++ b/server/crates/arbiter-server/src/crypto/integrity/v1.rs @@ -1,6 +1,5 @@ -use crate::{ - actors::keyholder, crypto::integrity::hashing::Hashable, safe_cell::SafeCellHandle as _, -}; +use crate::{actors::keyholder, crypto::integrity::hashing::Hashable}; +use arbiter_crypto::safecell::SafeCellHandle as _; use hmac::{Hmac, Mac as _}; use sha2::Sha256; @@ -127,7 +126,7 @@ pub async fn sign_entity( insert_into(integrity_envelope::table) .values(NewIntegrityEnvelope { entity_kind: E::KIND.to_owned(), - entity_id: entity_id, + entity_id, payload_version: E::VERSION, key_version, mac: mac.to_vec(), @@ -204,19 +203,19 @@ mod tests { use diesel::{ExpressionMethods as _, QueryDsl}; use diesel_async::RunQueryDsl; use kameo::{actor::ActorRef, prelude::Spawn}; - use rand::seq::SliceRandom; + use sha2::Digest; - use proptest::prelude::*; + 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}; + use super::hashing::Hashable; #[derive(Clone)] struct DummyEntity { diff --git a/server/crates/arbiter-server/src/crypto/integrity/v1/hashing.rs b/server/crates/arbiter-server/src/crypto/integrity/v1/hashing.rs index d172359..ec1aa71 100644 --- a/server/crates/arbiter-server/src/crypto/integrity/v1/hashing.rs +++ b/server/crates/arbiter-server/src/crypto/integrity/v1/hashing.rs @@ -62,10 +62,10 @@ impl Hashable for Option { fn hash(&self, hasher: &mut H) { match self { Some(value) => { - hasher.update(&[1]); + hasher.update([1]); value.hash(hasher); } - None => hasher.update(&[0]), + None => hasher.update([0]), } } } @@ -96,12 +96,12 @@ impl Hashable for alloy::primitives::U256 { impl Hashable for chrono::Duration { fn hash(&self, hasher: &mut H) { - hasher.update(&self.num_seconds().to_be_bytes()); + hasher.update(self.num_seconds().to_be_bytes()); } } impl Hashable for chrono::DateTime { fn hash(&self, hasher: &mut H) { - hasher.update(&self.timestamp_millis().to_be_bytes()); + hasher.update(self.timestamp_millis().to_be_bytes()); } } diff --git a/server/crates/arbiter-server/src/crypto/mod.rs b/server/crates/arbiter-server/src/crypto/mod.rs index d26c41f..5a11898 100644 --- a/server/crates/arbiter-server/src/crypto/mod.rs +++ b/server/crates/arbiter-server/src/crypto/mod.rs @@ -10,7 +10,7 @@ use rand::{ rngs::{StdRng, SysRng}, }; -use crate::safe_cell::{SafeCell, SafeCellHandle as _}; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; pub mod encryption; pub mod integrity; @@ -141,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/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index f558072..dba14dc 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -72,40 +72,6 @@ pub mod types { Ok(SqliteTimestamp(datetime)) } } - - /// Key algorithm stored in the `useragent_client.key_type` column. - /// Values must stay stable — they are persisted in the database. - #[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression, strum::FromRepr)] - #[diesel(sql_type = Integer)] - #[repr(i32)] - pub enum KeyType { - Ed25519 = 1, - EcdsaSecp256k1 = 2, - Rsa = 3, - } - - impl ToSql for KeyType { - fn to_sql<'b>( - &'b self, - out: &mut diesel::serialize::Output<'b, '_, Sqlite>, - ) -> diesel::serialize::Result { - out.set_value(*self as i32); - Ok(IsNull::No) - } - } - - impl FromSql for KeyType { - fn from_sql( - mut bytes: ::RawValue<'_>, - ) -> diesel::deserialize::Result { - let Some(SqliteType::Long) = bytes.value_type() else { - return Err("Expected Integer for KeyType".into()); - }; - let discriminant = bytes.read_long(); - KeyType::from_repr(discriminant as i32) - .ok_or_else(|| format!("Unknown KeyType discriminant: {discriminant}").into()) - } - } } pub use types::*; @@ -244,7 +210,6 @@ pub struct UseragentClient { pub public_key: Vec, pub created_at: SqliteTimestamp, pub updated_at: SqliteTimestamp, - pub key_type: KeyType, } #[derive(Models, Queryable, Debug, Insertable, Selectable)] diff --git a/server/crates/arbiter-server/src/evm/safe_signer.rs b/server/crates/arbiter-server/src/evm/safe_signer.rs index 3d15a05..e2f8100 100644 --- a/server/crates/arbiter-server/src/evm/safe_signer.rs +++ b/server/crates/arbiter-server/src/evm/safe_signer.rs @@ -1,12 +1,12 @@ use std::sync::Mutex; -use crate::safe_cell::{SafeCell, SafeCellHandle as _}; use alloy::{ consensus::SignableTransaction, network::{TxSigner, TxSignerSync}, primitives::{Address, B256, ChainId, Signature}, signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address}, }; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use async_trait::async_trait; use k256::ecdsa::{self, RecoveryId, SigningKey, signature::hazmat::PrehashSigner}; diff --git a/server/crates/arbiter-server/src/grpc/client/auth.rs b/server/crates/arbiter-server/src/grpc/client/auth.rs index 3a6f69e..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::{ @@ -45,7 +46,7 @@ impl<'a> AuthTransportAdapter<'a> { match response { auth::Outbound::AuthChallenge { pubkey, nonce } => { AuthResponsePayload::Challenge(ProtoAuthChallenge { - pubkey: pubkey.to_bytes().to_vec(), + pubkey: pubkey.to_bytes(), nonce, }) } @@ -160,11 +161,7 @@ impl Receiver for AuthTransportAdapter<'_> { .await; return None; }; - let Ok(pubkey) = <[u8; 32]>::try_from(pubkey) else { - let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await; - return None; - }; - let Ok(pubkey) = ed25519_dalek::VerifyingKey::from_bytes(&pubkey) else { + let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else { let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await; return None; }; @@ -174,7 +171,7 @@ impl Receiver for AuthTransportAdapter<'_> { }) } AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => { - let Ok(signature) = ed25519_dalek::Signature::try_from(signature.as_slice()) else { + let Ok(signature) = authn::Signature::try_from(signature.as_slice()) else { let _ = self .send_auth_result(ProtoAuthResult::InvalidSignature) .await; 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 d7c89e6..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, @@ -5,8 +6,7 @@ use arbiter_proto::{ self as proto_auth, AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest, AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult, - KeyType as ProtoKeyType, request::Payload as AuthRequestPayload, - response::Payload as AuthResponsePayload, + request::Payload as AuthRequestPayload, response::Payload as AuthResponsePayload, }, user_agent_request::Payload as UserAgentRequestPayload, user_agent_response::Payload as UserAgentResponsePayload, @@ -18,8 +18,7 @@ use tonic::Status; use tracing::warn; use crate::{ - actors::user_agent::{AuthPublicKey, UserAgentConnection, auth}, - db::models::KeyType, + actors::user_agent::{UserAgentConnection, auth}, grpc::request_tracker::RequestTracker, }; @@ -141,28 +140,9 @@ impl Receiver for AuthTransportAdapter<'_> { AuthRequestPayload::ChallengeRequest(ProtoAuthChallengeRequest { pubkey, bootstrap_token, - key_type, + key_type: _, }) => { - let Ok(key_type) = ProtoKeyType::try_from(key_type) else { - warn!( - event = "received request with invalid key type", - "grpc.useragent.auth_adapter" - ); - return None; - }; - let key_type = match key_type { - ProtoKeyType::Ed25519 => KeyType::Ed25519, - ProtoKeyType::EcdsaSecp256k1 => KeyType::EcdsaSecp256k1, - ProtoKeyType::Rsa => KeyType::Rsa, - ProtoKeyType::Unspecified => { - warn!( - event = "received request with unspecified key type", - "grpc.useragent.auth_adapter" - ); - return None; - } - }; - let Ok(pubkey) = AuthPublicKey::try_from((key_type, pubkey)) else { + let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else { warn!( event = "received request with invalid public key", "grpc.useragent.auth_adapter" @@ -188,7 +168,7 @@ pub async fn start( conn: &mut UserAgentConnection, bi: &mut GrpcBi, request_tracker: &mut RequestTracker, -) -> Result { +) -> Result { let transport = AuthTransportAdapter::new(bi, request_tracker); auth::authenticate(conn, transport).await } 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 e06e4b1..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::{ @@ -41,7 +42,7 @@ pub(super) fn out_of_band_payload(oob: OutOfBand) -> UserAgentResponsePayload { match oob { OutOfBand::ClientConnectionRequest { profile } => wrap_sdk_client_response( SdkClientResponsePayload::ConnectionRequest(ProtoSdkClientConnectionRequest { - pubkey: profile.pubkey.to_bytes().to_vec(), + pubkey: profile.pubkey.to_bytes(), info: Some(ProtoClientMetadata { name: profile.metadata.name, description: profile.metadata.description, @@ -51,7 +52,7 @@ pub(super) fn out_of_band_payload(oob: OutOfBand) -> UserAgentResponsePayload { ), OutOfBand::ClientConnectionCancel { pubkey } => wrap_sdk_client_response( SdkClientResponsePayload::ConnectionCancel(ProtoSdkClientConnectionCancel { - pubkey: pubkey.to_bytes().to_vec(), + pubkey: pubkey.to_bytes(), }), ), } @@ -89,10 +90,8 @@ async fn handle_connection_response( actor: &ActorRef, resp: ProtoSdkClientConnectionResponse, ) -> Result, Status> { - let pubkey_bytes = <[u8; 32]>::try_from(resp.pubkey) - .map_err(|_| Status::invalid_argument("Invalid Ed25519 public key length"))?; - let pubkey = ed25519_dalek::VerifyingKey::from_bytes(&pubkey_bytes) - .map_err(|_| Status::invalid_argument("Invalid Ed25519 public key"))?; + let pubkey = authn::PublicKey::try_from(resp.pubkey.as_slice()) + .map_err(|_| Status::invalid_argument("Invalid ML-DSA public key"))?; actor .ask(HandleNewClientApprove { @@ -117,7 +116,7 @@ async fn handle_list( .into_iter() .map(|(client, metadata)| ProtoSdkClientEntry { id: client.id, - pubkey: client.public_key, + pubkey: client.public_key.to_vec(), info: Some(ProtoClientMetadata { name: metadata.name, description: metadata.description, 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 299a1be..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::{ @@ -8,11 +12,10 @@ use arbiter_server::{ }, crypto::integrity, db::{self, schema}, - safe_cell::{SafeCell, SafeCellHandle as _}, }; use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into}; use diesel_async::RunQueryDsl; -use ed25519_dalek::Signer as _; +use ml_dsa::{KeyGen, MlDsa87, SigningKey, VerifyingKey, signature::Keypair as _}; use super::common::ChannelTransport; @@ -27,7 +30,7 @@ fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> Cli async fn insert_registered_client( db: &db::DatabasePool, actors: &GlobalActors, - pubkey: ed25519_dalek::VerifyingKey, + pubkey: VerifyingKey, metadata: &ClientMetadata, ) { use arbiter_server::db::schema::{client_metadata, program_client}; @@ -45,7 +48,7 @@ async fn insert_registered_client( .unwrap(); let client_id: i32 = insert_into(program_client::table) .values(( - program_client::public_key.eq(pubkey.to_bytes().to_vec()), + program_client::public_key.eq(pubkey.encode().to_vec()), program_client::metadata_id.eq(metadata_id), )) .returning(program_client::id) @@ -56,18 +59,33 @@ async fn insert_registered_client( integrity::sign_entity( &mut conn, &actors.key_holder, - &ClientCredentials { pubkey, nonce: 1 }, + &ClientCredentials { + pubkey: pubkey.into(), + nonce: 1, + }, client_id, ) .await .unwrap(); } +fn sign_client_challenge( + key: &SigningKey, + nonce: i32, + pubkey: &authn::PublicKey, +) -> authn::Signature { + let challenge = format_challenge(nonce, &pubkey.to_bytes()); + key.signing_key() + .sign_deterministic(&challenge, CLIENT_CONTEXT) + .unwrap() + .into() +} + async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) { let mut conn = db.get().await.unwrap(); - let sentinel_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()) + let sentinel_key = MlDsa87::key_gen(&mut rand::rng()) .verifying_key() - .to_bytes() + .encode() .to_vec(); insert_into(schema::useragent_client::table) @@ -107,11 +125,11 @@ pub async fn test_unregistered_pubkey_rejected() { connect_client(props, &mut server_transport).await; }); - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let new_key = MlDsa87::key_gen(&mut rand::rng()); test_transport .send(auth::Inbound::AuthChallengeRequest { - pubkey: new_key.verifying_key(), + pubkey: new_key.verifying_key().into(), metadata: metadata("client", Some("desc"), Some("1.0.0")), }) .await @@ -127,7 +145,7 @@ pub async fn test_challenge_auth() { let db = db::create_test_pool().await; let actors = spawn_test_actors(&db).await; - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let new_key = MlDsa87::key_gen(&mut rand::rng()); insert_registered_client( &db, @@ -147,7 +165,7 @@ pub async fn test_challenge_auth() { // Send challenge request test_transport .send(auth::Inbound::AuthChallengeRequest { - pubkey: new_key.verifying_key(), + pubkey: new_key.verifying_key().into(), metadata: metadata("client", Some("desc"), Some("1.0.0")), }) .await @@ -167,8 +185,7 @@ pub async fn test_challenge_auth() { }; // Sign the challenge and send solution - let formatted_challenge = arbiter_proto::format_challenge(challenge.1, challenge.0.as_bytes()); - let signature = new_key.sign(&formatted_challenge); + let signature = sign_client_challenge(&new_key, challenge.1, &challenge.0); test_transport .send(auth::Inbound::AuthChallengeSolution { signature }) @@ -194,7 +211,7 @@ pub async fn test_challenge_auth() { pub async fn test_metadata_unchanged_does_not_append_history() { let db = db::create_test_pool().await; let actors = spawn_test_actors(&db).await; - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let new_key = MlDsa87::key_gen(&mut rand::rng()); let requested = metadata("client", Some("desc"), Some("1.0.0")); insert_registered_client(&db, &actors, new_key.verifying_key(), &requested).await; @@ -209,7 +226,7 @@ pub async fn test_metadata_unchanged_does_not_append_history() { test_transport .send(auth::Inbound::AuthChallengeRequest { - pubkey: new_key.verifying_key(), + pubkey: new_key.verifying_key().into(), metadata: requested, }) .await @@ -220,7 +237,7 @@ pub async fn test_metadata_unchanged_does_not_append_history() { auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce), other => panic!("Expected AuthChallenge, got {other:?}"), }; - let signature = new_key.sign(&arbiter_proto::format_challenge(nonce, pubkey.as_bytes())); + let signature = sign_client_challenge(&new_key, nonce, &pubkey); test_transport .send(auth::Inbound::AuthChallengeSolution { signature }) .await @@ -251,7 +268,7 @@ pub async fn test_metadata_unchanged_does_not_append_history() { pub async fn test_metadata_change_appends_history_and_repoints_binding() { let db = db::create_test_pool().await; let actors = spawn_test_actors(&db).await; - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let new_key = MlDsa87::key_gen(&mut rand::rng()); insert_registered_client( &db, @@ -271,7 +288,7 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() { test_transport .send(auth::Inbound::AuthChallengeRequest { - pubkey: new_key.verifying_key(), + pubkey: new_key.verifying_key().into(), metadata: metadata("client", Some("new"), Some("2.0.0")), }) .await @@ -282,7 +299,7 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() { auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce), other => panic!("Expected AuthChallenge, got {other:?}"), }; - let signature = new_key.sign(&arbiter_proto::format_challenge(nonce, pubkey.as_bytes())); + let signature = sign_client_challenge(&new_key, nonce, &pubkey); test_transport .send(auth::Inbound::AuthChallengeSolution { signature }) .await @@ -339,7 +356,7 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() { let db = db::create_test_pool().await; let actors = spawn_test_actors(&db).await; - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let new_key = MlDsa87::key_gen(&mut rand::rng()); let requested = metadata("client", Some("desc"), Some("1.0.0")); { @@ -357,7 +374,7 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() { .unwrap(); insert_into(program_client::table) .values(( - program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()), + program_client::public_key.eq(new_key.verifying_key().encode().to_vec()), program_client::metadata_id.eq(metadata_id), )) .execute(&mut conn) @@ -374,7 +391,7 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() { test_transport .send(auth::Inbound::AuthChallengeRequest { - pubkey: new_key.verifying_key(), + pubkey: new_key.verifying_key().into(), metadata: requested, }) .await diff --git a/server/crates/arbiter-server/tests/common/mod.rs b/server/crates/arbiter-server/tests/common/mod.rs index 13ccd32..c4e6878 100644 --- a/server/crates/arbiter-server/tests/common/mod.rs +++ b/server/crates/arbiter-server/tests/common/mod.rs @@ -1,9 +1,10 @@ +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_proto::transport::{Bi, Error, Receiver, Sender}; use arbiter_server::{ actors::keyholder::KeyHolder, db::{self, schema}, - safe_cell::{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..f128beb 100644 --- a/server/crates/arbiter-server/tests/keyholder/concurrency.rs +++ b/server/crates/arbiter-server/tests/keyholder/concurrency.rs @@ -1,10 +1,11 @@ use std::collections::{HashMap, HashSet}; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_server::{ actors::keyholder::{CreateNew, Error, KeyHolder}, db::{self, models, schema}, - safe_cell::{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..bd50b6f 100644 --- a/server/crates/arbiter-server/tests/keyholder/lifecycle.rs +++ b/server/crates/arbiter-server/tests/keyholder/lifecycle.rs @@ -1,9 +1,10 @@ +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_server::{ actors::keyholder::{Error, KeyHolder}, crypto::encryption::v1::{Nonce, ROOT_KEY_TAG}, db::{self, models, schema}, - safe_cell::{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..71ebccf 100644 --- a/server/crates/arbiter-server/tests/keyholder/storage.rs +++ b/server/crates/arbiter-server/tests/keyholder/storage.rs @@ -1,11 +1,12 @@ use std::collections::HashSet; +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_server::{ actors::keyholder::Error, crypto::encryption::v1::Nonce, db::{self, models, schema}, - safe_cell::{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 660fae4..aeccc8a 100644 --- a/server/crates/arbiter-server/tests/user_agent/auth.rs +++ b/server/crates/arbiter-server/tests/user_agent/auth.rs @@ -1,21 +1,37 @@ +use arbiter_crypto::{ + authn::{self, USERAGENT_CONTEXT, format_challenge}, + safecell::{SafeCell, SafeCellHandle as _}, +}; + use arbiter_proto::transport::{Receiver, Sender}; use arbiter_server::{ actors::{ GlobalActors, bootstrap::GetToken, keyholder::Bootstrap, - user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth}, + user_agent::{UserAgentConnection, UserAgentCredentials, auth}, }, crypto::integrity, db::{self, schema}, - safe_cell::{SafeCell, SafeCellHandle as _}, }; use diesel::{ExpressionMethods as _, QueryDsl, insert_into}; use diesel_async::RunQueryDsl; -use ed25519_dalek::Signer as _; +use ml_dsa::{KeyGen, MlDsa87, SigningKey, signature::Keypair as _}; use super::common::ChannelTransport; +fn sign_useragent_challenge( + key: &SigningKey, + nonce: i32, + pubkey_bytes: &[u8], +) -> authn::Signature { + let challenge = format_challenge(nonce, pubkey_bytes); + key.signing_key() + .sign_deterministic(&challenge, USERAGENT_CONTEXT) + .unwrap() + .into() +} + #[tokio::test] #[test_log::test] pub async fn test_bootstrap_token_auth() { @@ -37,10 +53,10 @@ pub async fn test_bootstrap_token_auth() { auth::authenticate(&mut props, server_transport).await }); - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let new_key = MlDsa87::key_gen(&mut rand::rng()); test_transport .send(auth::Inbound::AuthChallengeRequest { - pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), + pubkey: new_key.verifying_key().into(), bootstrap_token: Some(token), }) .await @@ -63,7 +79,7 @@ pub async fn test_bootstrap_token_auth() { .first::>(&mut conn) .await .unwrap(); - assert_eq!(stored_pubkey, new_key.verifying_key().to_bytes().to_vec()); + assert_eq!(stored_pubkey, new_key.verifying_key().encode().to_vec()); } #[tokio::test] @@ -79,10 +95,10 @@ pub async fn test_bootstrap_invalid_token_auth() { auth::authenticate(&mut props, server_transport).await }); - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let new_key = MlDsa87::key_gen(&mut rand::rng()); test_transport .send(auth::Inbound::AuthChallengeRequest { - pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), + pubkey: new_key.verifying_key().into(), bootstrap_token: Some("invalid_token".to_string()), }) .await @@ -115,8 +131,8 @@ pub async fn test_challenge_auth() { .await .unwrap(); - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); - let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + let new_key = MlDsa87::key_gen(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().encode().to_vec(); { let mut conn = db.get().await.unwrap(); @@ -133,7 +149,7 @@ pub async fn test_challenge_auth() { &mut conn, &actors.key_holder, &UserAgentCredentials { - pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), + pubkey: new_key.verifying_key().into(), nonce: 1, }, id, @@ -151,7 +167,7 @@ pub async fn test_challenge_auth() { test_transport .send(auth::Inbound::AuthChallengeRequest { - pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), + pubkey: new_key.verifying_key().into(), bootstrap_token: None, }) .await @@ -169,12 +185,11 @@ pub async fn test_challenge_auth() { Err(err) => panic!("Expected Ok response, got Err({err:?})"), }; - let formatted_challenge = arbiter_proto::format_challenge(challenge, &pubkey_bytes); - let signature = new_key.sign(&formatted_challenge); + let signature = sign_useragent_challenge(&new_key, challenge, &pubkey_bytes); test_transport .send(auth::Inbound::AuthChallengeSolution { - signature: signature.to_bytes().to_vec(), + signature: signature.to_bytes(), }) .await .unwrap(); @@ -205,8 +220,8 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() .await .unwrap(); - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); - let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + let new_key = MlDsa87::key_gen(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().encode().to_vec(); { let mut conn = db.get().await.unwrap(); @@ -229,7 +244,7 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() test_transport .send(auth::Inbound::AuthChallengeRequest { - pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), + pubkey: new_key.verifying_key().into(), bootstrap_token: None, }) .await @@ -254,8 +269,8 @@ pub async fn test_challenge_auth_rejects_invalid_signature() { .await .unwrap(); - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); - let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + let new_key = MlDsa87::key_gen(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().encode().to_vec(); { let mut conn = db.get().await.unwrap(); @@ -272,7 +287,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() { &mut conn, &actors.key_holder, &UserAgentCredentials { - pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), + pubkey: new_key.verifying_key().into(), nonce: 1, }, id, @@ -290,7 +305,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() { test_transport .send(auth::Inbound::AuthChallengeRequest { - pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), + pubkey: new_key.verifying_key().into(), bootstrap_token: None, }) .await @@ -308,12 +323,11 @@ pub async fn test_challenge_auth_rejects_invalid_signature() { Err(err) => panic!("Expected Ok response, got Err({err:?})"), }; - let wrong_challenge = arbiter_proto::format_challenge(challenge + 1, &pubkey_bytes); - let signature = new_key.sign(&wrong_challenge); + let signature = sign_useragent_challenge(&new_key, challenge + 1, &pubkey_bytes); test_transport .send(auth::Inbound::AuthChallengeSolution { - signature: signature.to_bytes().to_vec(), + signature: signature.to_bytes(), }) .await .unwrap(); diff --git a/server/crates/arbiter-server/tests/user_agent/unseal.rs b/server/crates/arbiter-server/tests/user_agent/unseal.rs index 232b2e9..15cf475 100644 --- a/server/crates/arbiter-server/tests/user_agent/unseal.rs +++ b/server/crates/arbiter-server/tests/user_agent/unseal.rs @@ -1,3 +1,4 @@ +use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_server::{ actors::{ GlobalActors, @@ -8,11 +9,9 @@ use arbiter_server::{ }, }, db, - safe_cell::{SafeCell, SafeCellHandle as _}, }; + use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; -use diesel::{ExpressionMethods as _, QueryDsl as _, insert_into}; -use diesel_async::RunQueryDsl; use kameo::actor::Spawn as _; use x25519_dalek::{EphemeralSecret, PublicKey};