merge: feat-lints into main
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
ci/woodpecker/push/useragent-analyze Pipeline failed

This commit was merged in pull request #87.
This commit is contained in:
Skipper
2026-04-18 13:55:45 +02:00
204 changed files with 14668 additions and 3007 deletions

View File

@@ -1,24 +1,23 @@
use super::common::ChannelTransport;
use arbiter_crypto::{
authn::{self, CLIENT_CONTEXT, format_challenge},
authn::{self, AuthChallenge, CLIENT_CONTEXT},
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_proto::ClientMetadata;
use arbiter_proto::transport::{Receiver, Sender};
use arbiter_proto::{
ClientMetadata,
transport::{Receiver, Sender},
};
use arbiter_server::{
actors::{
GlobalActors,
client::{ClientConnection, ClientCredentials, auth, connect_client},
keyholder::Bootstrap,
},
actors::{GlobalActors, vault::Bootstrap},
crypto::integrity,
db::{self, schema},
peers::client::{ClientConnection, ClientCredentials, auth, connect_client},
};
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
use diesel_async::RunQueryDsl;
use ml_dsa::{KeyGen, MlDsa87, SigningKey, VerifyingKey, signature::Keypair};
use super::common::ChannelTransport;
fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> ClientMetadata {
ClientMetadata {
name: name.to_owned(),
@@ -62,10 +61,9 @@ async fn insert_registered_client(
integrity::sign_entity(
&mut conn,
&actors.key_holder,
&actors.vault,
&ClientCredentials {
pubkey: pubkey.into(),
nonce: 1,
},
client_id,
)
@@ -73,12 +71,8 @@ async fn insert_registered_client(
.unwrap();
}
fn sign_client_challenge(
key: &SigningKey<MlDsa87>,
nonce: i32,
pubkey: &authn::PublicKey,
) -> authn::Signature {
let challenge = format_challenge(nonce, &pubkey.to_bytes());
fn sign_client_challenge(key: &SigningKey<MlDsa87>, challenge: &AuthChallenge) -> authn::Signature {
let challenge = challenge.format();
key.signing_key()
.sign_deterministic(&challenge, CLIENT_CONTEXT)
.unwrap()
@@ -93,10 +87,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();
@@ -107,7 +98,7 @@ async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors {
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.key_holder
.vault
.ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
})
@@ -182,14 +173,14 @@ pub async fn 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 @ auth::Outbound::AuthSuccess => 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 })
@@ -243,11 +234,11 @@ pub async fn 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,
auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got AuthSuccess"),
};
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
@@ -305,11 +296,11 @@ pub async fn 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,
auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got AuthSuccess"),
};
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
@@ -411,10 +402,7 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch() {
.recv()
.await
.expect("should receive auth rejection");
assert!(matches!(
response,
Err(auth::ClientAuthError::IntegrityCheckFailed)
));
assert!(matches!(response, Err(auth::Error::IntegrityCheckFailed)));
task.await.unwrap();
}

View File

@@ -1,8 +1,11 @@
#![allow(dead_code, reason = "Common test utilities that may not be used in every test")]
#![allow(
dead_code,
reason = "Common test utilities that may not be used in every test"
)]
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_proto::transport::{Bi, Error, Receiver, Sender};
use arbiter_server::{
actors::keyholder::KeyHolder,
actors::{GlobalActors, vault::Vault},
db::{self, schema},
};
@@ -11,8 +14,10 @@ use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use tokio::sync::mpsc;
pub async fn bootstrapped_keyholder(db: &db::DatabasePool) -> KeyHolder {
let mut actor = KeyHolder::new(db.clone()).await.unwrap();
pub(crate) async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault {
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
actor
.bootstrap(SafeCell::new(b"test-seal-key".to_vec()))
.await
@@ -20,7 +25,7 @@ pub async fn bootstrapped_keyholder(db: &db::DatabasePool) -> KeyHolder {
actor
}
pub async fn root_key_history_id(db: &db::DatabasePool) -> i32 {
pub(crate) async fn root_key_history_id(db: &db::DatabasePool) -> i32 {
let mut conn = db.get().await.unwrap();
let id = schema::arbiter_settings::table
.select(schema::arbiter_settings::root_key_id)
@@ -30,13 +35,13 @@ pub async fn root_key_history_id(db: &db::DatabasePool) -> i32 {
id.expect("root_key_id should be set after bootstrap")
}
pub struct ChannelTransport<T, Y> {
pub(crate) struct ChannelTransport<T, Y> {
receiver: mpsc::Receiver<T>,
sender: mpsc::Sender<Y>,
}
impl<T, Y> ChannelTransport<T, Y> {
pub fn new() -> (Self, ChannelTransport<Y, T>) {
pub(crate) fn new() -> (Self, ChannelTransport<Y, T>) {
let (tx1, rx1) = mpsc::channel(10);
let (tx2, rx2) = mpsc::channel(10);
(

View File

@@ -1,8 +0,0 @@
mod common;
#[path = "keyholder/concurrency.rs"]
mod concurrency;
#[path = "keyholder/lifecycle.rs"]
mod lifecycle;
#[path = "keyholder/storage.rs"]
mod storage;

View File

@@ -1,24 +1,21 @@
use super::common::ChannelTransport;
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,
keyholder::Bootstrap,
user_agent::{UserAgentConnection, UserAgentCredentials, auth},
},
actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap},
crypto::integrity,
db::{self, schema},
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, VerifyingKey, signature::Keypair};
use super::common::ChannelTransport;
use tokio::sync::mpsc;
fn verifying_key(key: &SigningKey<MlDsa87>) -> VerifyingKey<MlDsa87> {
<SigningKey<MlDsa87> as Keypair>::verifying_key(key)
@@ -26,23 +23,139 @@ fn verifying_key(key: &SigningKey<MlDsa87>) -> VerifyingKey<MlDsa87> {
fn sign_useragent_challenge(
key: &SigningKey<MlDsa87>,
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<user_agent::OutOfBand> for NullOobSender {
async fn send(&mut self, _item: user_agent::OutOfBand) -> Result<(), TransportError> {
Ok(())
}
}
struct StartServerTransport {
auth_rx: mpsc::Receiver<auth::Inbound>,
auth_tx: mpsc::Sender<Result<auth::Outbound, auth::Error>>,
vault_rx: mpsc::Receiver<vault_gate::Inbound>,
vault_tx: mpsc::Sender<Result<vault_gate::Outbound, vault_gate::Error>>,
}
struct StartTestTransport {
auth_rx: mpsc::Receiver<Result<auth::Outbound, auth::Error>>,
auth_tx: mpsc::Sender<auth::Inbound>,
}
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<auth::Inbound> for StartServerTransport {
async fn recv(&mut self) -> Option<auth::Inbound> {
self.auth_rx.recv().await
}
}
#[async_trait]
impl Sender<Result<auth::Outbound, auth::Error>> for StartServerTransport {
async fn send(
&mut self,
item: Result<auth::Outbound, auth::Error>,
) -> Result<(), TransportError> {
self.auth_tx
.send(item)
.await
.map_err(|_| TransportError::ChannelClosed)
}
}
impl arbiter_proto::transport::Bi<auth::Inbound, Result<auth::Outbound, auth::Error>>
for StartServerTransport
{
}
#[async_trait]
impl Receiver<vault_gate::Inbound> for StartServerTransport {
async fn recv(&mut self) -> Option<vault_gate::Inbound> {
self.vault_rx.recv().await
}
}
#[async_trait]
impl Sender<Result<vault_gate::Outbound, vault_gate::Error>> for StartServerTransport {
async fn send(
&mut self,
item: Result<vault_gate::Outbound, vault_gate::Error>,
) -> Result<(), TransportError> {
self.vault_tx
.send(item)
.await
.map_err(|_| TransportError::ChannelClosed)
}
}
impl
arbiter_proto::transport::Bi<
vault_gate::Inbound,
Result<vault_gate::Outbound, vault_gate::Error>,
> for StartServerTransport
{
}
#[async_trait]
impl Receiver<Result<auth::Outbound, auth::Error>> for StartTestTransport {
async fn recv(&mut self) -> Option<Result<auth::Outbound, auth::Error>> {
self.auth_rx.recv().await
}
}
#[async_trait]
impl Sender<auth::Inbound> 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 bootstrap_token_auth() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.key_holder
.vault
.ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
})
@@ -50,11 +163,11 @@ pub async fn bootstrap_token_auth() {
.unwrap();
let token = actors.bootstrapper.ask(GetToken).await.unwrap().unwrap();
let (server_transport, mut test_transport) = ChannelTransport::new();
let (mut server_transport, mut test_transport) = ChannelTransport::new();
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, server_transport).await
auth::authenticate(&mut props, &mut server_transport).await
});
let new_key = MlDsa87::key_gen(&mut rand::rng());
@@ -66,14 +179,29 @@ pub async fn 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();
@@ -92,11 +220,11 @@ pub async fn bootstrap_invalid_token_auth() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
let (server_transport, mut test_transport) = ChannelTransport::new();
let (mut server_transport, mut test_transport) = ChannelTransport::new();
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, server_transport).await
auth::authenticate(&mut props, &mut server_transport).await
});
let new_key = MlDsa87::key_gen(&mut rand::rng());
@@ -108,6 +236,23 @@ pub async fn 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)
@@ -128,7 +273,7 @@ pub async fn challenge_auth() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.key_holder
.vault
.ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
})
@@ -141,20 +286,17 @@ pub async fn 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
.unwrap();
integrity::sign_entity(
&mut conn,
&actors.key_holder,
&UserAgentCredentials {
&actors.vault,
&Credentials {
id,
pubkey: verifying_key(&new_key).into(),
nonce: 1,
},
id,
)
@@ -162,11 +304,11 @@ pub async fn challenge_auth() {
.unwrap();
}
let (server_transport, mut test_transport) = ChannelTransport::new();
let (mut server_transport, mut test_transport) = ChannelTransport::new();
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, server_transport).await
auth::authenticate(&mut props, &mut server_transport).await
});
test_transport
@@ -183,13 +325,13 @@ pub async fn challenge_auth() {
.expect("should receive challenge");
let challenge = match response {
Ok(resp) => match resp {
auth::Outbound::AuthChallenge { nonce } => nonce,
auth::Outbound::AuthChallenge { challenge } => challenge,
auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got AuthSuccess"),
},
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 {
@@ -217,7 +359,7 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.key_holder
.vault
.ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
})
@@ -230,81 +372,17 @@ pub async fn 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 (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, server_transport).await
});
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(),
bootstrap_token: None,
})
.await
.unwrap();
assert!(matches!(
task.await.unwrap(),
Err(auth::Error::Internal { .. })
));
}
#[tokio::test]
#[test_log::test]
pub async fn challenge_auth_rejects_invalid_signature() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.key_holder
.ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
})
.await
.unwrap();
let new_key = MlDsa87::key_gen(&mut rand::rng());
let pubkey_bytes = authn::PublicKey::from(verifying_key(&new_key)).to_bytes();
{
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),
))
.returning(schema::useragent_client::id)
.get_result(&mut conn)
.await
.unwrap();
integrity::sign_entity(
&mut conn,
&actors.key_holder,
&UserAgentCredentials {
pubkey: verifying_key(&new_key).into(),
nonce: 1,
},
id,
)
.await
.unwrap();
}
let (server_transport, mut test_transport) = ChannelTransport::new();
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, server_transport).await
user_agent::start(&mut props, server_transport, Box::new(NullOobSender)).await
});
test_transport
@@ -321,13 +399,98 @@ pub async fn 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);
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(user_agent::Error::Internal(_))
));
}
#[tokio::test]
#[test_log::test]
pub async fn challenge_auth_rejects_invalid_signature() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.vault
.ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
})
.await
.unwrap();
let new_key = MlDsa87::key_gen(&mut rand::rng());
let pubkey_bytes = authn::PublicKey::from(verifying_key(&new_key)).to_bytes();
{
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()),))
.returning(schema::useragent_client::id)
.get_result(&mut conn)
.await
.unwrap();
integrity::sign_entity(
&mut conn,
&actors.vault,
&Credentials {
id,
pubkey: verifying_key(&new_key).into(),
},
id,
)
.await
.unwrap();
}
let (mut server_transport, mut test_transport) = ChannelTransport::new();
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
});
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(),
bootstrap_token: None,
})
.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,
auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got AuthSuccess"),
},
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 {

View File

@@ -1,49 +1,62 @@
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_crypto::{
authn,
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_server::{
actors::{
GlobalActors,
keyholder::{Bootstrap, Seal},
user_agent::{
UserAgentSession,
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
},
vault::{Bootstrap, Seal},
},
db,
peers::user_agent::{
Credentials,
vault_gate::{
Error as VaultGateError, HandleHandshake, HandleUnsealEncryptedKey, VaultGate,
},
},
};
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use kameo::actor::Spawn as _;
use tokio::sync::oneshot;
use x25519_dalek::{EphemeralSecret, PublicKey};
async fn setup_sealed_user_agent(
async fn setup_sealed_gate(
seal_key: &[u8],
) -> (db::DatabasePool, kameo::actor::ActorRef<UserAgentSession>) {
) -> (
db::DatabasePool,
kameo::actor::ActorRef<VaultGate>,
oneshot::Receiver<Result<(), VaultGateError>>,
) {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.key_holder
.vault
.ask(Bootstrap {
seal_key_raw: SafeCell::new(seal_key.to_vec()),
})
.await
.unwrap();
actors.key_holder.ask(Seal).await.unwrap();
actors.vault.ask(Seal).await.unwrap();
let session = UserAgentSession::spawn(UserAgentSession::new_test(db.clone(), actors));
let (promotion_tx, promotion_rx) = oneshot::channel();
let pubkey = authn::SigningKey::generate().public_key();
let auth_creds = Credentials { id: 1, pubkey };
let gate = VaultGate::spawn(VaultGate::new(auth_creds, actors, db.clone(), promotion_tx));
(db, session)
(db, gate, promotion_rx)
}
async fn client_dh_encrypt(
user_agent: &kameo::actor::ActorRef<UserAgentSession>,
gate: &kameo::actor::ActorRef<VaultGate>,
key_to_send: &[u8],
) -> HandleUnsealEncryptedKey {
let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret);
let response = user_agent
.ask(HandleUnsealRequest {
let response = gate
.ask(HandleHandshake {
client_pubkey: client_public,
})
.await
@@ -71,26 +84,27 @@ async fn client_dh_encrypt(
#[test_log::test]
pub async fn unseal_success() {
let seal_key = b"test-seal-key";
let (_db, user_agent) = setup_sealed_user_agent(seal_key).await;
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await;
let encrypted_key = client_dh_encrypt(&gate, seal_key).await;
let response = user_agent.ask(encrypted_key).await;
let response = gate.ask(encrypted_key).await;
assert!(matches!(response, Ok(())));
}
#[tokio::test]
#[test_log::test]
pub async fn unseal_wrong_seal_key() {
let (_db, user_agent) = setup_sealed_user_agent(b"correct-key").await;
let seal_key = b"test-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let encrypted_key = client_dh_encrypt(&user_agent, b"wrong-key").await;
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await;
let response = user_agent.ask(encrypted_key).await;
let response = gate.ask(encrypted_key).await;
assert!(matches!(
response,
Err(kameo::error::SendError::HandlerError(
UnsealError::InvalidKey
VaultGateError::InvalidKey
))
));
}
@@ -98,19 +112,19 @@ pub async fn unseal_wrong_seal_key() {
#[tokio::test]
#[test_log::test]
pub async fn unseal_corrupted_ciphertext() {
let (_db, user_agent) = setup_sealed_user_agent(b"test-key").await;
let seal_key = b"test-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret);
user_agent
.ask(HandleUnsealRequest {
client_pubkey: client_public,
})
.await
.unwrap();
gate.ask(HandleHandshake {
client_pubkey: client_public,
})
.await
.unwrap();
let response = user_agent
let response = gate
.ask(HandleUnsealEncryptedKey {
nonce: vec![0u8; 24],
ciphertext: vec![0u8; 32],
@@ -121,7 +135,7 @@ pub async fn unseal_corrupted_ciphertext() {
assert!(matches!(
response,
Err(kameo::error::SendError::HandlerError(
UnsealError::InvalidKey
VaultGateError::InvalidKey
))
));
}
@@ -130,24 +144,24 @@ pub async fn unseal_corrupted_ciphertext() {
#[test_log::test]
pub async fn unseal_retry_after_invalid_key() {
let seal_key = b"real-seal-key";
let (_db, user_agent) = setup_sealed_user_agent(seal_key).await;
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
{
let encrypted_key = client_dh_encrypt(&user_agent, b"wrong-key").await;
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await;
let response = user_agent.ask(encrypted_key).await;
let response = gate.ask(encrypted_key).await;
assert!(matches!(
response,
Err(kameo::error::SendError::HandlerError(
UnsealError::InvalidKey
VaultGateError::InvalidKey
))
));
}
{
let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await;
let encrypted_key = client_dh_encrypt(&gate, seal_key).await;
let response = user_agent.ask(encrypted_key).await;
let response = gate.ask(encrypted_key).await;
assert!(matches!(response, Ok(())));
}
}

View File

@@ -0,0 +1,8 @@
mod common;
#[path = "vault/concurrency.rs"]
mod concurrency;
#[path = "vault/lifecycle.rs"]
mod lifecycle;
#[path = "vault/storage.rs"]
mod storage;

View File

@@ -1,20 +1,21 @@
use std::collections::{HashMap, HashSet};
use crate::common;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::keyholder::{CreateNew, KeyHolder, KeyHolderError},
actors::{
GlobalActors,
vault::{CreateNew, Error, Vault},
},
db::{self, models, schema},
};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::sql_query};
use diesel_async::RunQueryDsl;
use kameo::actor::{ActorRef, Spawn as _};
use std::collections::{HashMap, HashSet};
use tokio::task::JoinSet;
use crate::common;
async fn write_concurrently(
actor: ActorRef<KeyHolder>,
actor: ActorRef<Vault>,
prefix: &'static str,
count: usize,
) -> Vec<(i32, Vec<u8>)> {
@@ -44,7 +45,7 @@ async fn write_concurrently(
#[test_log::test]
async fn concurrent_create_new_no_duplicate_nonces_() {
let db = db::create_test_pool().await;
let actor = KeyHolder::spawn(common::bootstrapped_keyholder(&db).await);
let actor = Vault::spawn(common::bootstrapped_vault(&db).await);
let writes = write_concurrently(actor, "nonce-unique", 32).await;
assert_eq!(writes.len(), 32);
@@ -66,7 +67,7 @@ async fn concurrent_create_new_no_duplicate_nonces_() {
#[test_log::test]
async fn concurrent_create_new_root_nonce_never_moves_backward() {
let db = db::create_test_pool().await;
let actor = KeyHolder::spawn(common::bootstrapped_keyholder(&db).await);
let actor = Vault::spawn(common::bootstrapped_vault(&db).await);
write_concurrently(actor, "root-max", 24).await;
@@ -94,7 +95,7 @@ async fn concurrent_create_new_root_nonce_never_moves_backward() {
#[test_log::test]
async fn insert_failure_does_not_create_partial_row() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_keyholder(&db).await;
let mut actor = common::bootstrapped_vault(&db).await;
let root_key_history_id = common::root_key_history_id(&db).await;
let mut conn = db.get().await.unwrap();
@@ -122,7 +123,7 @@ async fn insert_failure_does_not_create_partial_row() {
.create_new(SafeCell::new(b"should fail".to_vec()))
.await
.unwrap_err();
assert!(matches!(err, KeyHolderError::DatabaseTransaction(_)));
assert!(matches!(err, Error::DatabaseTransaction(_)));
let mut conn = db.get().await.unwrap();
sql_query("DROP TRIGGER fail_aead_insert;")
@@ -156,12 +157,14 @@ async fn insert_failure_does_not_create_partial_row() {
#[test_log::test]
async fn decrypt_roundtrip_after_high_concurrency() {
let db = db::create_test_pool().await;
let actor = KeyHolder::spawn(common::bootstrapped_keyholder(&db).await);
let actor = Vault::spawn(common::bootstrapped_vault(&db).await);
let writes = write_concurrently(actor, "roundtrip", 40).await;
let expected: HashMap<i32, Vec<u8>> = writes.into_iter().collect();
let mut decryptor = KeyHolder::new(db.clone()).await.unwrap();
let mut decryptor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
decryptor
.try_unseal(SafeCell::new(b"test-seal-key".to_vec()))
.await

View File

@@ -1,6 +1,10 @@
use crate::common;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::keyholder::{KeyHolder, KeyHolderError},
actors::{
GlobalActors,
vault::{Error, Vault},
},
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG},
db::{self, models, schema},
};
@@ -8,13 +12,13 @@ use arbiter_server::{
use diesel::{QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;
use crate::common;
#[tokio::test]
#[test_log::test]
async fn bootstrap() {
async fn test_bootstrap() {
let db = db::create_test_pool().await;
let mut actor = KeyHolder::new(db.clone()).await.unwrap();
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
actor.bootstrap(seal_key).await.unwrap();
@@ -35,55 +39,61 @@ async fn bootstrap() {
#[tokio::test]
#[test_log::test]
async fn bootstrap_rejects_double() {
async fn test_bootstrap_rejects_double() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_keyholder(&db).await;
let mut actor = common::bootstrapped_vault(&db).await;
let seal_key2 = SafeCell::new(b"test-seal-key".to_vec());
let err = actor.bootstrap(seal_key2).await.unwrap_err();
assert!(matches!(err, KeyHolderError::AlreadyBootstrapped));
assert!(matches!(err, Error::AlreadyBootstrapped));
}
#[tokio::test]
#[test_log::test]
async fn create_new_before_bootstrap_fails() {
async fn test_create_new_before_bootstrap_fails() {
let db = db::create_test_pool().await;
let mut actor = KeyHolder::new(db).await.unwrap();
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus())
.await
.unwrap();
let err = actor
.create_new(SafeCell::new(b"data".to_vec()))
.await
.unwrap_err();
assert!(matches!(err, KeyHolderError::NotBootstrapped));
assert!(matches!(err, Error::NotBootstrapped));
}
#[tokio::test]
#[test_log::test]
async fn decrypt_before_bootstrap_fails() {
async fn test_decrypt_before_bootstrap_fails() {
let db = db::create_test_pool().await;
let mut actor = KeyHolder::new(db).await.unwrap();
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus())
.await
.unwrap();
let err = actor.decrypt(1).await.unwrap_err();
assert!(matches!(err, KeyHolderError::NotBootstrapped));
assert!(matches!(err, Error::NotBootstrapped));
}
#[tokio::test]
#[test_log::test]
async fn new_restores_sealed_state() {
async fn test_new_restores_sealed_state() {
let db = db::create_test_pool().await;
let actor = common::bootstrapped_keyholder(&db).await;
let actor = common::bootstrapped_vault(&db).await;
drop(actor);
let mut actor2 = KeyHolder::new(db).await.unwrap();
let mut actor2 = Vault::new(db, GlobalActors::spawn_message_bus())
.await
.unwrap();
let err = actor2.decrypt(1).await.unwrap_err();
assert!(matches!(err, KeyHolderError::NotBootstrapped));
assert!(matches!(err, Error::Sealed));
}
#[tokio::test]
#[test_log::test]
async fn unseal_correct_password() {
async fn test_unseal_correct_password() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_keyholder(&db).await;
let mut actor = common::bootstrapped_vault(&db).await;
let plaintext = b"survive a restart";
let aead_id = actor
@@ -92,7 +102,9 @@ async fn unseal_correct_password() {
.unwrap();
drop(actor);
let mut actor = KeyHolder::new(db.clone()).await.unwrap();
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
actor.try_unseal(seal_key).await.unwrap();
@@ -102,9 +114,9 @@ async fn unseal_correct_password() {
#[tokio::test]
#[test_log::test]
async fn unseal_wrong_then_correct_password() {
async fn test_unseal_wrong_then_correct_password() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_keyholder(&db).await;
let mut actor = common::bootstrapped_vault(&db).await;
let plaintext = b"important data";
let aead_id = actor
@@ -113,11 +125,13 @@ async fn unseal_wrong_then_correct_password() {
.unwrap();
drop(actor);
let mut actor = KeyHolder::new(db.clone()).await.unwrap();
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
let bad_key = SafeCell::new(b"wrong-password".to_vec());
let err = actor.try_unseal(bad_key).await.unwrap_err();
assert!(matches!(err, KeyHolderError::InvalidKey));
assert!(matches!(err, Error::InvalidKey));
let good_key = SafeCell::new(b"test-seal-key".to_vec());
actor.try_unseal(good_key).await.unwrap();

View File

@@ -1,22 +1,20 @@
use std::collections::HashSet;
use crate::common;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::keyholder::KeyHolderError,
actors::vault::Error,
crypto::encryption::v1::Nonce,
db::{self, models, schema},
};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update};
use diesel_async::RunQueryDsl;
use crate::common;
use std::collections::HashSet;
#[tokio::test]
#[test_log::test]
async fn create_decrypt_roundtrip() {
async fn test_create_decrypt_roundtrip() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_keyholder(&db).await;
let mut actor = common::bootstrapped_vault(&db).await;
let plaintext = b"hello arbiter";
let aead_id = actor
@@ -30,19 +28,19 @@ async fn create_decrypt_roundtrip() {
#[tokio::test]
#[test_log::test]
async fn decrypt_nonexistent_returns_not_found() {
async fn test_decrypt_nonexistent_returns_not_found() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_keyholder(&db).await;
let mut actor = common::bootstrapped_vault(&db).await;
let err = actor.decrypt(9999).await.unwrap_err();
assert!(matches!(err, KeyHolderError::NotFound));
assert!(matches!(err, Error::NotFound));
}
#[tokio::test]
#[test_log::test]
async fn ciphertext_differs_across_entries() {
async fn test_ciphertext_differs_across_entries() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_keyholder(&db).await;
let mut actor = common::bootstrapped_vault(&db).await;
let plaintext = b"same content";
let id1 = actor
@@ -78,9 +76,9 @@ async fn ciphertext_differs_across_entries() {
#[tokio::test]
#[test_log::test]
async fn nonce_never_reused() {
async fn test_nonce_never_reused() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_keyholder(&db).await;
let mut actor = common::bootstrapped_vault(&db).await;
let n = 5;
for i in 0..n {
@@ -124,7 +122,7 @@ async fn nonce_never_reused() {
#[test_log::test]
async fn broken_db_nonce_format_fails_closed() {
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_keyholder(&db).await;
let mut actor = common::bootstrapped_vault(&db).await;
let root_key_history_id = common::root_key_history_id(&db).await;
let mut conn = db.get().await.unwrap();
@@ -142,10 +140,10 @@ async fn broken_db_nonce_format_fails_closed() {
.create_new(SafeCell::new(b"must fail".to_vec()))
.await
.unwrap_err();
assert!(matches!(err, KeyHolderError::BrokenDatabase));
assert!(matches!(err, Error::BrokenDatabase));
let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_keyholder(&db).await;
let mut actor = common::bootstrapped_vault(&db).await;
let id = actor
.create_new(SafeCell::new(b"decrypt target".to_vec()))
.await
@@ -159,5 +157,5 @@ async fn broken_db_nonce_format_fails_closed() {
drop(conn);
let err = actor.decrypt(id).await.unwrap_err();
assert!(matches!(err, KeyHolderError::BrokenDatabase));
assert!(matches!(err, Error::BrokenDatabase));
}