From 790026e93b1bf5dd0dc3856b95094326fb5e3bbd Mon Sep 17 00:00:00 2001 From: Skipper Date: Fri, 17 Apr 2026 17:49:06 +0200 Subject: [PATCH] fix(server::tests): api surface of auth challenge changed --- .../2026-02-14-171124-0000_init/up.sql | 2 +- server/crates/arbiter-server/src/db/schema.rs | 1 - .../arbiter-server/tests/client/auth.rs | 22 +- .../arbiter-server/tests/user_agent/auth.rs | 234 +++++++++++++++--- .../arbiter-server/tests/user_agent/unseal.rs | 2 +- .../arbiter-server/tests/vault/lifecycle.rs | 2 +- 6 files changed, 207 insertions(+), 56 deletions(-) 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 dfc64d3..4913a2d 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 @@ -49,7 +49,7 @@ create table if not exists useragent_client ( created_at integer not null default(unixepoch ('now')), updated_at integer not null default(unixepoch ('now')) ) STRICT; -create unique index if not exists uniq_useragent_client_public_key on useragent_client (public_key, key_type); +create unique index if not exists uniq_useragent_client_public_key on useragent_client (public_key); create table if not exists client_metadata ( id integer not null primary key, diff --git a/server/crates/arbiter-server/src/db/schema.rs b/server/crates/arbiter-server/src/db/schema.rs index f02c036..6d1c6b2 100644 --- a/server/crates/arbiter-server/src/db/schema.rs +++ b/server/crates/arbiter-server/src/db/schema.rs @@ -189,7 +189,6 @@ diesel::table! { useragent_client (id) { id -> Integer, public_key -> Binary, - key_type -> Integer, created_at -> Integer, updated_at -> Integer, } diff --git a/server/crates/arbiter-server/tests/client/auth.rs b/server/crates/arbiter-server/tests/client/auth.rs index a6b0773..b106d34 100644 --- a/server/crates/arbiter-server/tests/client/auth.rs +++ b/server/crates/arbiter-server/tests/client/auth.rs @@ -58,7 +58,6 @@ async fn insert_registered_client( &actors.vault, &ClientCredentials { pubkey: pubkey.into(), - nonce: 1, }, client_id, ) @@ -82,10 +81,7 @@ async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) { .to_vec(); insert_into(schema::useragent_client::table) - .values(( - schema::useragent_client::public_key.eq(sentinel_key), - schema::useragent_client::key_type.eq(1i32), - )) + .values((schema::useragent_client::public_key.eq(sentinel_key),)) .execute(&mut conn) .await .unwrap(); @@ -171,14 +167,14 @@ pub async fn test_challenge_auth() { .expect("should receive challenge"); let challenge = match response { Ok(resp) => match resp { - auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce), + auth::Outbound::AuthChallenge { challenge } => challenge, other => panic!("Expected AuthChallenge, got {other:?}"), }, Err(err) => panic!("Expected Ok response, got Err({err:?})"), }; // Sign the challenge and send solution - let signature = sign_client_challenge(&new_key, challenge.1, &challenge.0); + let signature = sign_client_challenge(&new_key, &challenge); test_transport .send(auth::Inbound::AuthChallengeSolution { signature }) @@ -226,11 +222,11 @@ pub async fn test_metadata_unchanged_does_not_append_history() { .unwrap(); let response = test_transport.recv().await.unwrap().unwrap(); - let (pubkey, nonce) = match response { - auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce), + let challenge = match response { + auth::Outbound::AuthChallenge { challenge } => challenge, other => panic!("Expected AuthChallenge, got {other:?}"), }; - let signature = sign_client_challenge(&new_key, nonce, &pubkey); + let signature = sign_client_challenge(&new_key, &challenge); test_transport .send(auth::Inbound::AuthChallengeSolution { signature }) .await @@ -288,11 +284,11 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() { .unwrap(); let response = test_transport.recv().await.unwrap().unwrap(); - let (pubkey, nonce) = match response { - auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce), + let challenge = match response { + auth::Outbound::AuthChallenge { challenge } => challenge, other => panic!("Expected AuthChallenge, got {other:?}"), }; - let signature = sign_client_challenge(&new_key, nonce, &pubkey); + let signature = sign_client_challenge(&new_key, &challenge); test_transport .send(auth::Inbound::AuthChallengeSolution { signature }) .await diff --git a/server/crates/arbiter-server/tests/user_agent/auth.rs b/server/crates/arbiter-server/tests/user_agent/auth.rs index d461aa3..5c9c247 100644 --- a/server/crates/arbiter-server/tests/user_agent/auth.rs +++ b/server/crates/arbiter-server/tests/user_agent/auth.rs @@ -1,33 +1,145 @@ use arbiter_crypto::{ - authn::{self, USERAGENT_CONTEXT, format_challenge}, + authn::{self, AuthChallenge, USERAGENT_CONTEXT}, safecell::{SafeCell, SafeCellHandle as _}, }; -use arbiter_proto::transport::{Receiver, Sender}; +use arbiter_proto::transport::{Error as TransportError, Receiver, Sender}; use arbiter_server::{ actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap}, crypto::integrity, db::{self, schema}, - peers::user_agent::{Credentials, Credentials, UserAgentConnection, auth}, + peers::user_agent::{self, Credentials, UserAgentConnection, auth, vault_gate}, }; +use async_trait::async_trait; use diesel::{ExpressionMethods as _, QueryDsl, insert_into}; use diesel_async::RunQueryDsl; use ml_dsa::{KeyGen, MlDsa87, SigningKey, signature::Keypair as _}; +use tokio::sync::mpsc; use super::common::ChannelTransport; fn sign_useragent_challenge( key: &SigningKey, - nonce: i32, - pubkey_bytes: &[u8], + challenge: &AuthChallenge, ) -> authn::Signature { - let challenge = format_challenge(nonce, pubkey_bytes); + let challenge = challenge.format(); key.signing_key() .sign_deterministic(&challenge, USERAGENT_CONTEXT) .unwrap() .into() } +fn tamper_challenge(challenge: &AuthChallenge) -> AuthChallenge { + let mut challenge = challenge.clone(); + challenge.nonce[0] ^= 1; + challenge +} + +struct NullOobSender; + +#[async_trait] +impl Sender for NullOobSender { + async fn send(&mut self, _item: user_agent::OutOfBand) -> Result<(), TransportError> { + Ok(()) + } +} + +struct StartServerTransport { + auth_rx: mpsc::Receiver, + auth_tx: mpsc::Sender>, + vault_rx: mpsc::Receiver, + vault_tx: mpsc::Sender>, +} + +struct StartTestTransport { + auth_rx: mpsc::Receiver>, + auth_tx: mpsc::Sender, +} + +fn start_transport_pair() -> (StartServerTransport, StartTestTransport) { + let (auth_in_tx, auth_in_rx) = mpsc::channel(10); + let (auth_out_tx, auth_out_rx) = mpsc::channel(10); + let (_vault_in_tx, vault_in_rx) = mpsc::channel(10); + let (vault_out_tx, _vault_out_rx) = mpsc::channel(10); + + ( + StartServerTransport { + auth_rx: auth_in_rx, + auth_tx: auth_out_tx, + vault_rx: vault_in_rx, + vault_tx: vault_out_tx, + }, + StartTestTransport { + auth_rx: auth_out_rx, + auth_tx: auth_in_tx, + }, + ) +} + +#[async_trait] +impl Receiver for StartServerTransport { + async fn recv(&mut self) -> Option { + self.auth_rx.recv().await + } +} + +#[async_trait] +impl Sender> for StartServerTransport { + async fn send(&mut self, item: Result) -> Result<(), TransportError> { + self.auth_tx + .send(item) + .await + .map_err(|_| TransportError::ChannelClosed) + } +} + +impl arbiter_proto::transport::Bi> + for StartServerTransport +{ +} + +#[async_trait] +impl Receiver for StartServerTransport { + async fn recv(&mut self) -> Option { + self.vault_rx.recv().await + } +} + +#[async_trait] +impl Sender> for StartServerTransport { + async fn send( + &mut self, + item: Result, + ) -> Result<(), TransportError> { + self.vault_tx + .send(item) + .await + .map_err(|_| TransportError::ChannelClosed) + } +} + +impl arbiter_proto::transport::Bi> + for StartServerTransport +{ +} + +#[async_trait] +impl Receiver> for StartTestTransport { + async fn recv(&mut self) -> Option> { + self.auth_rx.recv().await + } +} + +#[async_trait] +impl Sender for StartTestTransport { + async fn send(&mut self, item: auth::Inbound) -> Result<(), TransportError> { + self.auth_tx + .send(item) + .await + .map_err(|_| TransportError::ChannelClosed) + } +} + #[tokio::test] #[test_log::test] pub async fn test_bootstrap_token_auth() { @@ -58,14 +170,29 @@ pub async fn test_bootstrap_token_auth() { .await .unwrap(); + let response = test_transport + .recv() + .await + .expect("should receive challenge"); + let challenge = match response { + Ok(auth::Outbound::AuthChallenge { challenge }) => challenge, + other => panic!("Expected AuthChallenge, got {other:?}"), + }; + + let signature = sign_useragent_challenge(&new_key, &challenge); + + test_transport + .send(auth::Inbound::AuthChallengeSolution { + signature: signature.to_bytes(), + }) + .await + .unwrap(); + let response = test_transport .recv() .await .expect("should receive auth result"); - match response { - Ok(auth::Outbound::AuthSuccess) => {} - other => panic!("Expected AuthSuccess, got {other:?}"), - } + assert!(matches!(response, Ok(auth::Outbound::AuthSuccess))); task.await.unwrap().unwrap(); @@ -100,6 +227,23 @@ pub async fn test_bootstrap_invalid_token_auth() { .await .unwrap(); + let response = test_transport + .recv() + .await + .expect("should receive challenge"); + let challenge = match response { + Ok(auth::Outbound::AuthChallenge { challenge }) => challenge, + other => panic!("Expected AuthChallenge, got {other:?}"), + }; + + let signature = sign_useragent_challenge(&new_key, &challenge); + test_transport + .send(auth::Inbound::AuthChallengeSolution { + signature: signature.to_bytes(), + }) + .await + .unwrap(); + assert!(matches!( task.await.unwrap(), Err(auth::Error::InvalidBootstrapToken) @@ -133,10 +277,7 @@ pub async fn test_challenge_auth() { { let mut conn = db.get().await.unwrap(); let id: i32 = insert_into(schema::useragent_client::table) - .values(( - schema::useragent_client::public_key.eq(pubkey_bytes.clone()), - schema::useragent_client::key_type.eq(1i32), - )) + .values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),)) .returning(schema::useragent_client::id) .get_result(&mut conn) .await @@ -145,11 +286,8 @@ pub async fn test_challenge_auth() { &mut conn, &actors.vault, &Credentials { - creds: Credentials { - id, - pubkey: new_key.verifying_key().into(), - }, - new_nonce: 1, + id, + pubkey: new_key.verifying_key().into(), }, id, ) @@ -178,13 +316,13 @@ pub async fn test_challenge_auth() { .expect("should receive challenge"); let challenge = match response { Ok(resp) => match resp { - auth::Outbound::AuthChallenge { nonce } => nonce, + auth::Outbound::AuthChallenge { challenge } => challenge, other => panic!("Expected AuthChallenge, got {other:?}"), }, Err(err) => panic!("Expected Ok response, got Err({err:?})"), }; - let signature = sign_useragent_challenge(&new_key, challenge, &pubkey_bytes); + let signature = sign_useragent_challenge(&new_key, &challenge); test_transport .send(auth::Inbound::AuthChallengeSolution { @@ -225,20 +363,17 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() { let mut conn = db.get().await.unwrap(); insert_into(schema::useragent_client::table) - .values(( - schema::useragent_client::public_key.eq(pubkey_bytes.clone()), - schema::useragent_client::key_type.eq(1i32), - )) + .values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),)) .execute(&mut conn) .await .unwrap(); } - let (mut server_transport, mut test_transport) = ChannelTransport::new(); + let (server_transport, mut test_transport) = start_transport_pair(); let db_for_task = db.clone(); let task = tokio::spawn(async move { let mut props = UserAgentConnection::new(db_for_task, actors); - auth::authenticate(&mut props, &mut server_transport).await + user_agent::start(&mut props, server_transport, Box::new(NullOobSender)).await }); test_transport @@ -249,9 +384,36 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() .await .unwrap(); + let response = test_transport + .recv() + .await + .expect("should receive challenge"); + let challenge = match response { + Ok(resp) => match resp { + auth::Outbound::AuthChallenge { challenge } => challenge, + other => panic!("Expected AuthChallenge, got {other:?}"), + }, + Err(err) => panic!("Expected Ok response, got Err({err:?})"), + }; + + let signature = sign_useragent_challenge(&new_key, &challenge); + + test_transport + .send(auth::Inbound::AuthChallengeSolution { + signature: signature.to_bytes(), + }) + .await + .unwrap(); + + let response = test_transport + .recv() + .await + .expect("should receive auth result"); + assert!(matches!(response, Ok(auth::Outbound::AuthSuccess))); + assert!(matches!( task.await.unwrap(), - Err(auth::Error::Internal { .. }) + Err(user_agent::Error::Internal(_)) )); } @@ -274,10 +436,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() { { let mut conn = db.get().await.unwrap(); let id: i32 = insert_into(schema::useragent_client::table) - .values(( - schema::useragent_client::public_key.eq(pubkey_bytes.clone()), - schema::useragent_client::key_type.eq(1i32), - )) + .values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),)) .returning(schema::useragent_client::id) .get_result(&mut conn) .await @@ -286,11 +445,8 @@ pub async fn test_challenge_auth_rejects_invalid_signature() { &mut conn, &actors.vault, &Credentials { - creds: Credentials { - id, - pubkey: new_key.verifying_key().into(), - }, - new_nonce: 1, + id, + pubkey: new_key.verifying_key().into(), }, id, ) @@ -319,13 +475,13 @@ pub async fn test_challenge_auth_rejects_invalid_signature() { .expect("should receive challenge"); let challenge = match response { Ok(resp) => match resp { - auth::Outbound::AuthChallenge { nonce } => nonce, + auth::Outbound::AuthChallenge { challenge } => challenge, other => panic!("Expected AuthChallenge, got {other:?}"), }, Err(err) => panic!("Expected Ok response, got Err({err:?})"), }; - let signature = sign_useragent_challenge(&new_key, challenge + 1, &pubkey_bytes); + let signature = sign_useragent_challenge(&new_key, &tamper_challenge(&challenge)); test_transport .send(auth::Inbound::AuthChallengeSolution { diff --git a/server/crates/arbiter-server/tests/user_agent/unseal.rs b/server/crates/arbiter-server/tests/user_agent/unseal.rs index ae7ee7a..6772b79 100644 --- a/server/crates/arbiter-server/tests/user_agent/unseal.rs +++ b/server/crates/arbiter-server/tests/user_agent/unseal.rs @@ -26,7 +26,7 @@ async fn setup_sealed_gate( ) -> ( db::DatabasePool, kameo::actor::ActorRef, - oneshot::Receiver>, + oneshot::Receiver>, ) { let db = db::create_test_pool().await; let actors = GlobalActors::spawn(db.clone()).await.unwrap(); diff --git a/server/crates/arbiter-server/tests/vault/lifecycle.rs b/server/crates/arbiter-server/tests/vault/lifecycle.rs index ddfb602..77238c2 100644 --- a/server/crates/arbiter-server/tests/vault/lifecycle.rs +++ b/server/crates/arbiter-server/tests/vault/lifecycle.rs @@ -87,7 +87,7 @@ async fn test_new_restores_sealed_state() { .await .unwrap(); let err = actor2.decrypt(1).await.unwrap_err(); - assert!(matches!(err, Error::NotBootstrapped)); + assert!(matches!(err, Error::Sealed)); } #[tokio::test]