refactor-integrity-check #90

Merged
Skipper merged 17 commits from refactor-integrity-check into main 2026-04-18 11:54:31 +00:00
42 changed files with 332 additions and 286 deletions
Showing only changes of commit 1585f90cae - Show all commits

View File

@@ -66,7 +66,7 @@ cargo insta review
The server is actor-based using the **kameo** crate. All long-lived state lives in `GlobalActors`:
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run.
- **`KeyHolder`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
- **`Vault`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
- **`FlowCoordinator`** — Coordinates cross-connection flow between user agents and SDK clients.
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing.

View File

@@ -66,7 +66,7 @@ cargo insta review
The server is actor-based using the **kameo** crate. All long-lived state lives in `GlobalActors`:
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run.
- **`KeyHolder`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
- **`Vault`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
- **`FlowCoordinator`** — Coordinates cross-connection flow between user agents and SDK clients.
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing.

24
server/Cargo.lock generated
View File

@@ -753,6 +753,7 @@ dependencies = [
"insta",
"k256",
"kameo",
"kameo_actors",
"ml-dsa",
"mutants",
"pem",
@@ -2962,9 +2963,9 @@ dependencies = [
[[package]]
name = "kameo"
version = "0.19.2"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c4af7638c67029fd6821d02813c3913c803784648725d4df4082c9b91d7cbb1"
checksum = "b1dfd134d7a2c6ec05ee696dcbf3f7a034bdb97ecc623e981014652dcd124d77"
dependencies = [
"downcast-rs",
"dyn-clone",
@@ -2976,10 +2977,23 @@ dependencies = [
]
[[package]]
name = "kameo_macros"
version = "0.19.0"
name = "kameo_actors"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13c324e2d8c8e126e63e66087448b4267e263e6cb8770c56d10a9d0d279d9e2"
checksum = "220bdd75769f0a9b752a91123e58bf42f595e3c36ff4b13818d7a87d962076e6"
dependencies = [
"futures",
"glob",
"kameo",
"thiserror 2.0.18",
"tokio",
]
[[package]]
name = "kameo_macros"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16c9002c9ecd16e1636f566c0bf62db48e70d86ed0d0a91b398955e883217a23"
dependencies = [
"heck",
"proc-macro2",

View File

@@ -26,7 +26,7 @@ thiserror = "2.0.18"
async-trait = "0.1.89"
futures = "0.3.32"
tokio-stream = { version = "0.1.18", features = ["full"] }
kameo = "0.19.2"
kameo = "0.20"
prost-types = { version = "0.14.3", features = ["chrono"] }
x25519-dalek = { version = "2.0.1", features = ["getrandom"] }
rstest = "0.26.1"

View File

@@ -58,6 +58,7 @@ ml-dsa.workspace = true
ed25519-dalek.workspace = true
x25519-dalek.workspace = true
k256.workspace = true
kameo_actors = "0.5.0"
[dev-dependencies]
insta = "1.46.3"

View File

@@ -7,7 +7,7 @@ use kameo::{Actor, actor::ActorRef, messages};
use rand::{SeedableRng, rng, rngs::StdRng};
use crate::{
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
actors::vault::{CreateNew, Decrypt, Vault},
crypto::integrity,
db::{
DatabaseError, DatabasePool,
@@ -34,11 +34,11 @@ pub enum SignTransactionError {
#[error("Database error: {0}")]
Database(#[from] DatabaseError),
#[error("Keyholder error: {0}")]
Keyholder(#[from] crate::actors::keyholder::Error),
#[error("Vault error: {0}")]
Vault(#[from] crate::actors::vault::Error),
#[error("Keyholder mailbox error")]
KeyholderSend,
#[error("Vault mailbox error")]
VaultSend,
#[error("Signing error: {0}")]
Signing(#[from] alloy::signers::Error),
@@ -49,11 +49,11 @@ pub enum SignTransactionError {
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Keyholder error: {0}")]
Keyholder(#[from] crate::actors::keyholder::Error),
#[error("Vault error: {0}")]
Vault(#[from] crate::actors::vault::Error),
#[error("Keyholder mailbox error")]
KeyholderSend,
#[error("Vault mailbox error")]
VaultSend,
#[error("Database error: {0}")]
Database(#[from] DatabaseError),
@@ -64,20 +64,20 @@ pub enum Error {
#[derive(Actor)]
pub struct EvmActor {
pub keyholder: ActorRef<KeyHolder>,
pub vault: ActorRef<Vault>,
pub db: DatabasePool,
pub rng: StdRng,
pub engine: evm::Engine,
}
impl EvmActor {
pub fn new(keyholder: ActorRef<KeyHolder>, db: DatabasePool) -> Self {
pub fn new(vault: ActorRef<Vault>, db: DatabasePool) -> Self {
// is it safe to seed rng from system once?
// todo: audit
let rng = StdRng::from_rng(&mut rng());
let engine = evm::Engine::new(db.clone(), keyholder.clone());
let engine = evm::Engine::new(db.clone(), vault.clone());
Self {
keyholder,
vault,
db,
rng,
engine,
@@ -94,10 +94,10 @@ impl EvmActor {
let plaintext = key_cell.read_inline(|reader| SafeCell::new(reader.to_vec()));
let aead_id: i32 = self
.keyholder
.vault
.ask(CreateNew { plaintext })
.await
.map_err(|_| Error::KeyholderSend)?;
.map_err(|_| Error::VaultSend)?;
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
let wallet_id = insert_into(schema::evm_wallet::table)
@@ -160,7 +160,7 @@ impl EvmActor {
#[message]
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();
// let vault = self.vault.clone();
// diesel_async::AsyncConnection::transaction(&mut conn, |conn| {
// Box::pin(async move {
@@ -254,12 +254,12 @@ impl EvmActor {
drop(conn);
let raw_key: SafeCell<Vec<u8>> = self
.keyholder
.vault
.ask(Decrypt {
aead_id: wallet.aead_encrypted_id,
})
.await
.map_err(|_| SignTransactionError::KeyholderSend)?;
.map_err(|_| SignTransactionError::VaultSend)?;
let signer = safe_signer::SafeSigner::from_cell(raw_key)?;

View File

@@ -6,10 +6,9 @@ use kameo::{
reply::ReplySender,
};
use crate::actors::{
client::ClientProfile,
flow_coordinator::ApprovalError,
user_agent::{UserAgentSession, session::BeginNewClientApproval},
use crate::{
actors::flow_coordinator::ApprovalError,
peers::{client::ClientProfile, user_agent::{UserAgentSession, session::BeginNewClientApproval}},
};
pub struct Args {

View File

@@ -9,11 +9,7 @@ use kameo::{
};
use tracing::info;
use crate::actors::{
client::{ClientProfile, session::ClientSession},
flow_coordinator::client_connect_approval::ClientApprovalController,
user_agent::session::UserAgentSession,
};
use crate::{actors::flow_coordinator::client_connect_approval::ClientApprovalController, peers::{client::{ClientProfile, session::ClientSession}, user_agent::UserAgentSession}};
pub mod client_connect_approval;

View File

@@ -1,47 +1,53 @@
use kameo::actor::{ActorRef, Spawn};
use kameo_actors::{DeliveryStrategy, message_bus::MessageBus};
use thiserror::Error;
use crate::{
actors::{
bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator,
keyholder::KeyHolder,
vault::Vault,
},
db,
};
pub mod bootstrap;
pub mod client;
mod evm;
pub mod evm;
pub mod flow_coordinator;
pub mod keyholder;
pub mod user_agent;
pub mod vault;
#[derive(Error, Debug)]
pub enum SpawnError {
#[error("Failed to spawn Bootstrapper actor")]
Bootstrapper(#[from] bootstrap::Error),
#[error("Failed to spawn KeyHolder actor")]
KeyHolder(#[from] keyholder::Error),
#[error("Failed to spawn Vault actor")]
Vault(#[from] vault::Error),
}
/// Long-lived actors that are shared across all connections and handle global state and operations
#[derive(Clone)]
pub struct GlobalActors {
pub key_holder: ActorRef<KeyHolder>,
pub vault: ActorRef<Vault>,
pub bootstrapper: ActorRef<Bootstrapper>,
pub flow_coordinator: ActorRef<FlowCoordinator>,
pub evm: ActorRef<EvmActor>,
pub events: ActorRef<MessageBus>,
}
impl GlobalActors {
pub fn spawn_message_bus() -> ActorRef<MessageBus> {
MessageBus::spawn(MessageBus::new(DeliveryStrategy::Guaranteed))
}
pub async fn spawn(db: db::DatabasePool) -> Result<Self, SpawnError> {
let key_holder = KeyHolder::spawn(KeyHolder::new(db.clone()).await?);
let message_bus = Self::spawn_message_bus();
let key_holder = Vault::spawn(Vault::new(db.clone(), message_bus.clone()).await?);
Ok(Self {
bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?),
evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db)),
key_holder,
vault: key_holder,
flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::default()),
events: message_bus,
})
}
}

View File

@@ -5,7 +5,8 @@ use diesel::{
};
use diesel_async::{AsyncConnection, RunQueryDsl};
use hmac::Mac as _;
use kameo::{Actor, Reply, messages};
use kameo::{Actor, Reply, actor::ActorRef, messages};
use kameo_actors::message_bus::{MessageBus, Publish};
use strum::{EnumDiscriminants, IntoDiscriminant};
use tracing::{error, info};
@@ -21,26 +22,26 @@ use crate::db::{
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
#[derive(Default, EnumDiscriminants)]
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
enum State {
#[default]
Unbootstrapped,
Sealed {
root_key_history_id: i32,
},
Unsealed {
root_key_history_id: i32,
root_key: KeyCell,
},
pub mod events {
#[derive(Clone, Copy)]
pub struct VaultBootstrapped;
#[derive(Clone, Copy)]
pub struct VaultUnsealed;
#[derive(Clone, Copy)]
pub struct VaultResealed;
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Keyholder is already bootstrapped")]
#[error("Vault is already bootstrapped")]
AlreadyBootstrapped,
#[error("Keyholder is not bootstrapped")]
#[error("Vault is not bootstrapped")]
NotBootstrapped,
#[error("Vault is sealed")]
Sealed,
#[error("Invalid key provided")]
InvalidKey,
@@ -60,18 +61,35 @@ pub enum Error {
BrokenDatabase,
}
struct Unsealed {
root_key_history_id: i32,
root_key: KeyCell,
}
#[derive(Default, EnumDiscriminants)]
#[strum_discriminants(derive(Reply), vis(pub), name(VaultState))]
enum State {
#[default]
Unbootstrapped,
Sealed {
root_key_history_id: i32,
},
Unsealed(Unsealed),
}
/// Manages vault root key and tracks current state of the vault (bootstrapped/unbootstrapped, sealed/unsealed).
/// Provides API for encrypting and decrypting data using the vault root key.
/// Abstraction over database to make sure nonces are never reused and encryption keys are never exposed in plaintext outside of this actor.
#[derive(Actor)]
pub struct KeyHolder {
pub struct Vault {
db: db::DatabasePool,
state: State,
events: ActorRef<MessageBus>,
}
#[messages]
impl KeyHolder {
pub async fn new(db: db::DatabasePool) -> Result<Self, Error> {
impl Vault {
pub async fn new(db: db::DatabasePool, events: ActorRef<MessageBus>) -> Result<Self, Error> {
let state = {
let mut conn = db.get().await?;
@@ -89,10 +107,10 @@ impl KeyHolder {
}
};
Ok(Self { db, state })
Ok(Self { db, state, events })
}
// Exclusive transaction to avoid race condtions if multiple keyholders write
// Exclusive transaction to avoid race condtions if multiple vaults write
// additional layer of protection against nonce-reuse
async fn get_new_nonce(pool: &db::DatabasePool, root_key_id: i32) -> Result<Nonce, Error> {
let mut conn = pool.get().await?;
@@ -129,6 +147,14 @@ impl KeyHolder {
Ok(nonce)
}
fn expect_unsealed<'a>(state: &'a mut State) -> Result<&'a mut Unsealed, Error> {
match state {
State::Unsealed(unsealed) => Ok(unsealed),
State::Unbootstrapped => Err(Error::NotBootstrapped),
State::Sealed { .. } => Err(Error::Sealed),
}
}
#[message]
pub async fn bootstrap(&mut self, seal_key_raw: SafeCell<Vec<u8>>) -> Result<(), Error> {
if !matches!(self.state, State::Unbootstrapped) {
@@ -181,12 +207,13 @@ impl KeyHolder {
})
.await?;
self.state = State::Unsealed {
self.state = State::Unsealed(Unsealed {
root_key,
root_key_history_id,
};
});
info!("Keyholder bootstrapped successfully");
info!("Vault bootstrapped successfully");
self.events.tell(Publish(events::VaultBootstrapped)).await;
Ok(())
}
@@ -233,24 +260,23 @@ impl KeyHolder {
Error::InvalidKey
})?;
self.state = State::Unsealed {
self.state = State::Unsealed(Unsealed {
root_key_history_id: current_key.id,
root_key: KeyCell::try_from(root_key).map_err(|err| {
error!(?err, "Broken database: invalid encryption key size");
Error::BrokenDatabase
})?,
};
});
info!("Keyholder unsealed successfully");
info!("Vault unsealed successfully");
self.events.tell(Publish(events::VaultUnsealed)).await;
Ok(())
}
#[message]
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
let State::Unsealed { root_key, .. } = &mut self.state else {
return Err(Error::NotBootstrapped);
};
let Unsealed { root_key, .. } = Self::expect_unsealed(&mut self.state)?;
let row: models::AeadEncrypted = {
let mut conn = self.db.get().await?;
@@ -278,14 +304,10 @@ impl KeyHolder {
// Creates new `aead_encrypted` entry in the database and returns it's ID
#[message]
pub async fn create_new(&mut self, mut plaintext: SafeCell<Vec<u8>>) -> Result<i32, Error> {
let State::Unsealed {
let Unsealed {
root_key,
root_key_history_id,
..
} = &mut self.state
else {
return Err(Error::NotBootstrapped);
};
} = Self::expect_unsealed(&mut self.state)?;
// Order matters here - `get_new_nonce` acquires connection, so we need to call it before next acquire
// Borrow checker note: &mut borrow a few lines above is disjoint from this field
@@ -315,19 +337,16 @@ impl KeyHolder {
}
#[message]
pub fn get_state(&self) -> KeyHolderState {
pub fn get_state(&self) -> VaultState {
self.state.discriminant()
}
#[message]
pub fn sign_integrity(&mut self, mac_input: Vec<u8>) -> Result<(i32, Vec<u8>), Error> {
let State::Unsealed {
let Unsealed {
root_key,
root_key_history_id,
} = &mut self.state
else {
return Err(Error::NotBootstrapped);
};
} = Self::expect_unsealed(&mut self.state)?;
let mut hmac = root_key
.0
@@ -349,13 +368,10 @@ impl KeyHolder {
expected_mac: Vec<u8>,
key_version: i32,
) -> Result<bool, Error> {
let State::Unsealed {
let Unsealed {
root_key,
root_key_history_id,
} = &mut self.state
else {
return Err(Error::NotBootstrapped);
};
} = Self::expect_unsealed(&mut self.state)?;
if *root_key_history_id != key_version {
return Ok(false);
@@ -374,17 +390,16 @@ impl KeyHolder {
}
#[message]
pub fn seal(&mut self) -> Result<(), Error> {
let State::Unsealed {
pub async fn seal(&mut self) -> Result<(), Error> {
let Unsealed {
root_key_history_id,
..
} = &self.state
else {
return Err(Error::NotBootstrapped);
};
} = Self::expect_unsealed(&mut self.state)?;
self.state = State::Sealed {
root_key_history_id: *root_key_history_id,
};
self.events.tell(Publish(events::VaultResealed)).await;
Ok(())
}
}
@@ -395,13 +410,18 @@ mod tests {
use diesel_async::RunQueryDsl;
use crate::db::{self};
use crate::{
actors::GlobalActors,
db::{self},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use super::*;
async fn bootstrapped_actor(db: &db::DatabasePool) -> KeyHolder {
let mut actor = KeyHolder::new(db.clone()).await.unwrap();
async fn bootstrapped_actor(db: &db::DatabasePool) -> Vault {
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();
actor
@@ -413,17 +433,17 @@ mod tests {
let db = db::create_test_pool().await;
let mut actor = bootstrapped_actor(&db).await;
let root_key_history_id = match actor.state {
State::Unsealed {
State::Unsealed(Unsealed {
root_key_history_id,
..
} => root_key_history_id,
}) => root_key_history_id,
_ => panic!("expected unsealed state"),
};
let n1 = KeyHolder::get_new_nonce(&db, root_key_history_id)
let n1 = Vault::get_new_nonce(&db, root_key_history_id)
.await
.unwrap();
let n2 = KeyHolder::get_new_nonce(&db, root_key_history_id)
let n2 = Vault::get_new_nonce(&db, root_key_history_id)
.await
.unwrap();
assert!(n2.to_vec() > n1.to_vec(), "nonce must increase");

View File

@@ -1,4 +1,4 @@
use crate::{actors::keyholder, crypto::integrity::hashing::Hashable};
use crate::{actors::vault, crypto::integrity::hashing::Hashable};
use arbiter_crypto::safecell::SafeCellHandle as _;
use hmac::{Hmac, Mac as _};
use sha2::Sha256;
@@ -11,7 +11,7 @@ use sha2::Digest as _;
pub mod hashing;
use crate::{
actors::keyholder::{KeyHolder, SignIntegrity, VerifyIntegrity},
actors::vault::{SignIntegrity, Vault, VerifyIntegrity},
db::{
self,
models::{IntegrityEnvelope, NewIntegrityEnvelope},
@@ -24,11 +24,11 @@ pub enum Error {
#[error("Database error: {0}")]
Database(#[from] db::DatabaseError),
#[error("KeyHolder error: {0}")]
Keyholder(#[from] keyholder::Error),
#[error("Vault error: {0}")]
Vault(#[from] vault::Error),
#[error("KeyHolder mailbox error")]
KeyholderSend,
#[error("Vault mailbox error")]
VaultSend,
#[error("Integrity envelope is missing for entity {entity_kind}")]
MissingEnvelope { entity_kind: &'static str },
@@ -105,7 +105,7 @@ impl IntoId for &'_ [u8] {
pub async fn sign_entity<E: Integrable>(
conn: &mut impl AsyncConnection<Backend = Sqlite>,
keyholder: &ActorRef<KeyHolder>,
vault: &ActorRef<Vault>,
entity: &E,
entity_id: impl IntoId,
) -> Result<(), Error> {
@@ -115,13 +115,14 @@ pub async fn sign_entity<E: Integrable>(
let mac_input = build_mac_input(E::KIND, &entity_id, E::VERSION, &payload_hash);
let (key_version, mac) = keyholder
.ask(SignIntegrity { mac_input })
.await
.map_err(|err| match err {
kameo::error::SendError::HandlerError(inner) => Error::Keyholder(inner),
_ => Error::KeyholderSend,
})?;
let (key_version, mac) =
vault
.ask(SignIntegrity { mac_input })
.await
.map_err(|err| match err {
kameo::error::SendError::HandlerError(inner) => Error::Vault(inner),
_ => Error::VaultSend,
})?;
insert_into(integrity_envelope::table)
.values(NewIntegrityEnvelope {
@@ -150,7 +151,7 @@ pub async fn sign_entity<E: Integrable>(
pub async fn verify_entity<E: Integrable>(
conn: &mut impl AsyncConnection<Backend = Sqlite>,
keyholder: &ActorRef<KeyHolder>,
vault: &ActorRef<Vault>,
entity: &E,
entity_id: impl IntoId,
) -> Result<AttestationStatus, Error> {
@@ -178,7 +179,7 @@ pub async fn verify_entity<E: Integrable>(
let payload_hash = payload_hash(&entity);
let mac_input = build_mac_input(E::KIND, &entity_id, envelope.payload_version, &payload_hash);
let result = keyholder
let result = vault
.ask(VerifyIntegrity {
mac_input,
expected_mac: envelope.mac,
@@ -191,10 +192,10 @@ pub async fn verify_entity<E: Integrable>(
Ok(false) => Err(Error::MacMismatch {
entity_kind: E::KIND,
}),
Err(SendError::HandlerError(keyholder::Error::NotBootstrapped)) => {
Err(SendError::HandlerError(vault::Error::NotBootstrapped)) => {
Ok(AttestationStatus::Unavailable)
}
Err(_) => Err(Error::KeyholderSend),
Err(_) => Err(Error::VaultSend),
}
}
@@ -203,19 +204,17 @@ mod tests {
use diesel::{ExpressionMethods as _, QueryDsl};
use diesel_async::RunQueryDsl;
use kameo::{actor::ActorRef, prelude::Spawn};
use sha2::Digest;
use crate::{
actors::keyholder::{Bootstrap, KeyHolder},
actors::{GlobalActors, vault::{Bootstrap, Vault}},
db::{self, schema},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use super::{Error, Integrable, sign_entity, verify_entity};
use super::hashing::Hashable;
use super::{Error, Integrable, sign_entity, verify_entity};
#[derive(Clone)]
struct DummyEntity {
@@ -233,8 +232,12 @@ mod tests {
const KIND: &'static str = "dummy_entity";
}
async fn bootstrapped_keyholder(db: &db::DatabasePool) -> ActorRef<KeyHolder> {
let actor = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
async fn bootstrapped_vault(db: &db::DatabasePool) -> ActorRef<Vault> {
let actor = Vault::spawn(
Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap(),
);
actor
.ask(Bootstrap {
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()),
@@ -247,7 +250,7 @@ mod tests {
#[tokio::test]
async fn sign_writes_envelope_and_verify_passes() {
let db = db::create_test_pool().await;
let keyholder = bootstrapped_keyholder(&db).await;
let vault = bootstrapped_vault(&db).await;
let mut conn = db.get().await.unwrap();
const ENTITY_ID: &[u8] = b"entity-id-7";
@@ -257,7 +260,7 @@ mod tests {
payload: b"payload-v1".to_vec(),
};
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
sign_entity(&mut conn, &vault, &entity, ENTITY_ID)
.await
.unwrap();
@@ -270,7 +273,7 @@ mod tests {
.unwrap();
assert_eq!(count, 1, "envelope row must be created exactly once");
verify_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
verify_entity(&mut conn, &vault, &entity, ENTITY_ID)
.await
.unwrap();
}
@@ -278,7 +281,7 @@ mod tests {
#[tokio::test]
async fn tampered_mac_fails_verification() {
let db = db::create_test_pool().await;
let keyholder = bootstrapped_keyholder(&db).await;
let vault = bootstrapped_vault(&db).await;
let mut conn = db.get().await.unwrap();
const ENTITY_ID: &[u8] = b"entity-id-11";
@@ -288,7 +291,7 @@ mod tests {
payload: b"payload-v1".to_vec(),
};
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
sign_entity(&mut conn, &vault, &entity, ENTITY_ID)
.await
.unwrap();
@@ -300,7 +303,7 @@ mod tests {
.await
.unwrap();
let err = verify_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
let err = verify_entity(&mut conn, &vault, &entity, ENTITY_ID)
.await
.unwrap_err();
assert!(matches!(err, Error::MacMismatch { .. }));
@@ -309,7 +312,7 @@ mod tests {
#[tokio::test]
async fn changed_payload_fails_verification() {
let db = db::create_test_pool().await;
let keyholder = bootstrapped_keyholder(&db).await;
let vault = bootstrapped_vault(&db).await;
let mut conn = db.get().await.unwrap();
const ENTITY_ID: &[u8] = b"entity-id-21";
@@ -319,7 +322,7 @@ mod tests {
payload: b"payload-v1".to_vec(),
};
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
sign_entity(&mut conn, &vault, &entity, ENTITY_ID)
.await
.unwrap();
@@ -328,7 +331,7 @@ mod tests {
..entity
};
let err = verify_entity(&mut conn, &keyholder, &tampered, ENTITY_ID)
let err = verify_entity(&mut conn, &vault, &tampered, ENTITY_ID)
.await
.unwrap_err();
assert!(matches!(err, Error::MacMismatch { .. }));

View File

@@ -11,7 +11,7 @@ use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::actor::ActorRef;
use crate::{
actors::keyholder::KeyHolder,
actors::vault::Vault,
crypto::integrity,
db::{
self, DatabaseError,
@@ -138,7 +138,7 @@ async fn check_shared_constraints(
// Supporting only EIP-1559 transactions for now, but we can easily extend this to support legacy transactions if needed
pub struct Engine {
db: db::DatabasePool,
keyholder: ActorRef<KeyHolder>,
vault: ActorRef<Vault>,
}
impl Engine {
@@ -158,7 +158,7 @@ impl Engine {
.map_err(DatabaseError::from)?
.ok_or(PolicyError::NoMatchingGrant)?;
integrity::verify_entity(&mut conn, &self.keyholder, &grant.settings, grant.id).await?;
integrity::verify_entity(&mut conn, &self.vault, &grant.settings, grant.id).await?;
let mut violations = check_shared_constraints(
&context,
@@ -207,8 +207,8 @@ impl Engine {
}
impl Engine {
pub fn new(db: db::DatabasePool, keyholder: ActorRef<KeyHolder>) -> Self {
Self { db, keyholder }
pub fn new(db: db::DatabasePool, vault: ActorRef<Vault>) -> Self {
Self { db, vault }
}
pub async fn create_grant<P: Policy>(
@@ -219,7 +219,7 @@ impl Engine {
P::Settings: Clone,
{
let mut conn = self.db.get().await?;
let keyholder = self.keyholder.clone();
let vault = self.vault.clone();
let id = conn
.transaction(|conn| {
@@ -258,7 +258,7 @@ impl Engine {
P::create_grant(&basic_grant, &full_grant.specific, conn).await?;
integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id)
integrity::sign_entity(conn, &vault, &full_grant, basic_grant.id)
.await
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
@@ -283,7 +283,7 @@ impl Engine {
// Verify integrity of all grants before returning any results
for grant in &all_grants {
integrity::verify_entity(conn, &self.keyholder, &grant.settings, grant.id).await?;
integrity::verify_entity(conn, &self.vault, &grant.settings, grant.id).await?;
}
Ok(all_grants.into_iter().map(|g| Grant {

View File

@@ -10,7 +10,7 @@ use tonic::Status;
use tracing::{info, warn};
use crate::{
actors::client::{ClientConnection, session::ClientSession},
peers::client::{ClientConnection, session::ClientSession},
grpc::request_tracker::RequestTracker,
};

View File

@@ -22,7 +22,7 @@ use tonic::Status;
use tracing::warn;
use crate::{
actors::client::{self, ClientConnection, auth},
peers::client::{self, ClientConnection, auth},
grpc::request_tracker::RequestTracker,
};

View File

@@ -16,7 +16,7 @@ use tonic::Status;
use tracing::warn;
use crate::{
actors::client::session::{ClientSession, HandleSignTransaction, SignTransactionRpcError},
peers::client::session::{ClientSession, HandleSignTransaction, SignTransactionRpcError},
grpc::{
Convert, TryConvert,
common::inbound::{RawEvmAddress, RawEvmTransaction},

View File

@@ -12,9 +12,9 @@ use kameo::{actor::ActorRef, error::SendError};
use tonic::Status;
use tracing::warn;
use crate::actors::{
client::session::{ClientSession, Error, HandleQueryVaultState},
keyholder::KeyHolderState,
use crate::{
peers::client::session::{ClientSession, Error, HandleQueryVaultState},
actors::vault::VaultState,
};
pub(super) async fn dispatch(
@@ -30,9 +30,9 @@ pub(super) async fn dispatch(
match payload {
VaultRequestPayload::QueryState(_) => {
let state = match actor.ask(HandleQueryVaultState {}).await {
Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed,
Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed,
Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
Ok(VaultState::Sealed) => ProtoVaultState::Sealed,
Ok(VaultState::Unsealed) => ProtoVaultState::Unsealed,
Err(SendError::HandlerError(Error::Internal)) => ProtoVaultState::Error,
Err(err) => {
warn!(error = ?err, "Failed to query vault state");

View File

@@ -10,7 +10,7 @@ use tonic::{Request, Response, Status, async_trait};
use tracing::info;
use crate::{
actors::{client::ClientConnection, user_agent::UserAgentConnection},
peers::{client::ClientConnection, user_agent::UserAgentConnection},
grpc::user_agent::start,
};

View File

@@ -14,7 +14,7 @@ use tonic::Status;
use tracing::{error, info, warn};
use crate::{
actors::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession},
peers::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession},
grpc::request_tracker::RequestTracker,
};

View File

@@ -18,7 +18,7 @@ use tonic::Status;
use tracing::warn;
use crate::{
actors::user_agent::{UserAgentConnection, auth},
peers::user_agent::{UserAgentConnection, auth},
grpc::request_tracker::RequestTracker,
};

View File

@@ -23,7 +23,7 @@ use tonic::Status;
use tracing::warn;
use crate::{
actors::user_agent::{
peers::user_agent::{
UserAgentSession,
session::connection::{
GrantMutationError, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate,

View File

@@ -21,7 +21,7 @@ use tonic::Status;
use tracing::{info, warn};
use crate::{
actors::user_agent::{
peers::user_agent::{
OutOfBand, UserAgentSession,
session::connection::{
HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove,

View File

@@ -20,9 +20,9 @@ use kameo::{actor::ActorRef, error::SendError};
use tonic::Status;
use tracing::warn;
use crate::actors::{
keyholder::KeyHolderState,
user_agent::{
use crate::{
actors::vault::VaultState,
peers::user_agent::{
UserAgentSession,
session::connection::{
BootstrapError, HandleBootstrapEncryptedKey, HandleQueryVaultState,
@@ -166,9 +166,9 @@ async fn handle_query_vault_state(
actor: &ActorRef<UserAgentSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> {
let state = match actor.ask(HandleQueryVaultState {}).await {
Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed,
Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed,
Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
Ok(VaultState::Sealed) => ProtoVaultState::Sealed,
Ok(VaultState::Unsealed) => ProtoVaultState::Unsealed,
Err(err) => {
warn!(error = ?err, "Failed to query vault state");
ProtoVaultState::Error

View File

@@ -2,6 +2,7 @@
use crate::context::ServerContext;
pub mod actors;
pub mod peers;
pub mod context;
pub mod crypto;
pub mod db;

View File

@@ -14,9 +14,7 @@ use tracing::error;
use crate::{
actors::{
client::{ClientConnection, ClientCredentials, ClientProfile},
flow_coordinator::{self, RequestClientApproval},
keyholder::KeyHolder,
GlobalActors, flow_coordinator::{self, RequestClientApproval}, vault::Vault
},
crypto::integrity::{self, AttestationStatus},
db::{
@@ -26,6 +24,8 @@ use crate::{
},
};
use super::{ClientConnection, ClientCredentials, ClientProfile};
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum Error {
#[error("Database pool unavailable")]
@@ -104,7 +104,7 @@ async fn get_current_nonce_and_id(
async fn verify_integrity(
db: &db::DatabasePool,
keyholder: &ActorRef<KeyHolder>,
vault: &ActorRef<Vault>,
pubkey: &authn::PublicKey,
) -> Result<(), Error> {
let mut db_conn = db.get().await.map_err(|e| {
@@ -119,7 +119,7 @@ async fn verify_integrity(
let attestation = integrity::verify_entity(
&mut db_conn,
keyholder,
vault,
&ClientCredentials {
pubkey: pubkey.clone(),
nonce,
@@ -144,7 +144,7 @@ async fn verify_integrity(
/// Returns the new nonce, which is used as the challenge nonce.
async fn create_nonce(
db: &db::DatabasePool,
keyholder: &ActorRef<KeyHolder>,
vault: &ActorRef<Vault>,
pubkey: &authn::PublicKey,
) -> Result<i32, Error> {
let pubkey_bytes = pubkey.to_bytes();
@@ -156,7 +156,7 @@ async fn create_nonce(
})?;
conn.exclusive_transaction(|conn| {
let keyholder = keyholder.clone();
let vault = vault.clone();
let pubkey = pubkey.clone();
Box::pin(async move {
let (id, new_nonce): (i32, i32) = update(program_client::table)
@@ -168,7 +168,7 @@ async fn create_nonce(
integrity::sign_entity(
conn,
&keyholder,
&vault,
&ClientCredentials {
pubkey: pubkey.clone(),
nonce: new_nonce,
@@ -188,7 +188,7 @@ async fn create_nonce(
}
async fn approve_new_client(
actors: &crate::actors::GlobalActors,
actors: &GlobalActors,
profile: ClientProfile,
) -> Result<(), Error> {
let result = actors
@@ -212,7 +212,7 @@ async fn approve_new_client(
async fn insert_client(
db: &db::DatabasePool,
keyholder: &ActorRef<KeyHolder>,
vault: &ActorRef<Vault>,
pubkey: &authn::PublicKey,
metadata: &ClientMetadata,
) -> Result<i32, Error> {
@@ -226,7 +226,7 @@ async fn insert_client(
})?;
conn.exclusive_transaction(|conn| {
let keyholder = keyholder.clone();
let vault = vault.clone();
let pubkey = pubkey.clone();
Box::pin(async move {
const NONCE_START: i32 = 1;
@@ -254,7 +254,7 @@ async fn insert_client(
integrity::sign_entity(
conn,
&keyholder,
&vault,
&ClientCredentials {
pubkey: pubkey.clone(),
nonce: NONCE_START,
@@ -391,7 +391,7 @@ where
let client_id = match get_current_nonce_and_id(&props.db, &pubkey).await? {
Some((id, _)) => {
verify_integrity(&props.db, &props.actors.key_holder, &pubkey).await?;
verify_integrity(&props.db, &props.actors.vault, &pubkey).await?;
id
}
None => {
@@ -403,12 +403,12 @@ where
},
)
.await?;
insert_client(&props.db, &props.actors.key_holder, &pubkey, &metadata).await?
insert_client(&props.db, &props.actors.vault, &pubkey, &metadata).await?
}
};
sync_client_metadata(&props.db, client_id, &metadata).await?;
let challenge_nonce = create_nonce(&props.db, &props.actors.key_holder, &pubkey).await?;
let challenge_nonce = create_nonce(&props.db, &props.actors.vault, &pubkey).await?;
challenge_client(transport, pubkey, challenge_nonce).await?;
transport

View File

@@ -4,9 +4,9 @@ use kameo::actor::Spawn;
use tracing::{error, info};
use crate::{
actors::{GlobalActors, client::session::ClientSession},
actors::GlobalActors,
crypto::integrity::{Integrable, hashing::Hashable},
db,
db, peers::client::session::ClientSession,
};
#[derive(Debug, Clone)]

View File

@@ -6,15 +6,16 @@ use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
use crate::{
actors::{
GlobalActors,
client::ClientConnection,
evm::{ClientSignTransaction, SignTransactionError},
flow_coordinator::RegisterClient,
keyholder::KeyHolderState,
vault::VaultState,
},
db,
evm::VetError,
};
use super::ClientConnection;
pub struct ClientSession {
props: ClientConnection,
client_id: i32,
@@ -29,13 +30,13 @@ impl ClientSession {
#[messages]
impl ClientSession {
#[message]
pub(crate) async fn handle_query_vault_state(&mut self) -> Result<KeyHolderState, Error> {
use crate::actors::keyholder::GetState;
pub(crate) async fn handle_query_vault_state(&mut self) -> Result<VaultState, Error> {
use crate::actors::vault::GetState;
let vault_state = match self.props.actors.key_holder.ask(GetState {}).await {
let vault_state = match self.props.actors.vault.ask(GetState {}).await {
Ok(state) => state,
Err(err) => {
error!(?err, actor = "client", "keyholder.query.failed");
error!(?err, actor = "client", "vault.query.failed");
return Err(Error::Internal);
}
};

View File

@@ -0,0 +1,2 @@
pub mod user_agent;
pub mod client;

View File

@@ -2,13 +2,11 @@ use arbiter_crypto::authn;
use arbiter_proto::transport::Bi;
use tracing::error;
use crate::actors::user_agent::{
UserAgentConnection,
auth::state::{AuthContext, AuthStateMachine},
};
mod state;
use state::*;
use super::UserAgentConnection;
#[derive(Debug, Clone)]
pub enum Inbound {
AuthChallengeRequest {

View File

@@ -4,13 +4,14 @@ use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::actor::ActorRef;
use tracing::error;
use super::super::{UserAgentCredentials, UserAgentConnection};
use super::Error;
use crate::peers::user_agent::auth::Outbound;
use crate::{
actors::{
bootstrap::ConsumeToken,
keyholder::KeyHolder,
user_agent::{UserAgentConnection, UserAgentCredentials, auth::Outbound},
vault::Vault,
},
crypto::integrity,
db::{DatabasePool, schema::useragent_client},
@@ -77,7 +78,7 @@ async fn get_current_nonce_and_id(
async fn verify_integrity(
db: &DatabasePool,
keyholder: &ActorRef<KeyHolder>,
vault: &ActorRef<Vault>,
pubkey: &authn::PublicKey,
) -> Result<(), Error> {
let mut db_conn = db.get().await.map_err(|e| {
@@ -89,7 +90,7 @@ async fn verify_integrity(
let _result = integrity::verify_entity(
&mut db_conn,
keyholder,
vault,
&UserAgentCredentials {
pubkey: pubkey.clone(),
nonce,
@@ -107,7 +108,7 @@ async fn verify_integrity(
async fn create_nonce(
db: &DatabasePool,
keyholder: &ActorRef<KeyHolder>,
vault: &ActorRef<Vault>,
pubkey: &authn::PublicKey,
) -> Result<i32, Error> {
let mut db_conn = db.get().await.map_err(|e| {
@@ -130,7 +131,7 @@ async fn create_nonce(
integrity::sign_entity(
conn,
keyholder,
vault,
&UserAgentCredentials {
pubkey: pubkey.clone(),
nonce: new_nonce,
@@ -152,7 +153,7 @@ async fn create_nonce(
async fn register_key(
db: &DatabasePool,
keyholder: &ActorRef<KeyHolder>,
vault: &ActorRef<Vault>,
pubkey: &authn::PublicKey,
) -> Result<(), Error> {
let pubkey_bytes = pubkey.to_bytes();
@@ -183,7 +184,7 @@ async fn register_key(
nonce: NONCE_START,
};
integrity::sign_entity(conn, keyholder, &entity, id)
integrity::sign_entity(conn, vault, &entity, id)
.await
.map_err(|e| {
error!(error = ?e, "Failed to sign integrity tag for new user-agent key");
@@ -219,9 +220,9 @@ where
&mut self,
ChallengeRequest { pubkey }: ChallengeRequest,
) -> Result<ChallengeContext, Self::Error> {
verify_integrity(&self.conn.db, &self.conn.actors.key_holder, &pubkey).await?;
verify_integrity(&self.conn.db, &self.conn.actors.vault, &pubkey).await?;
let nonce = create_nonce(&self.conn.db, &self.conn.actors.key_holder, &pubkey).await?;
let nonce = create_nonce(&self.conn.db, &self.conn.actors.vault, &pubkey).await?;
self.transport
.send(Ok(Outbound::AuthChallenge { nonce }))
@@ -263,7 +264,7 @@ where
match token_ok {
true => {
register_key(&self.conn.db, &self.conn.actors.key_holder, &pubkey).await?;
register_key(&self.conn.db, &self.conn.actors.vault, &pubkey).await?;
self.transport
.send(Ok(Outbound::AuthSuccess))
.await

View File

@@ -1,7 +1,7 @@
use crate::{
actors::{GlobalActors, client::ClientProfile},
actors::GlobalActors,
crypto::integrity::Integrable,
db,
db, peers::client::ClientProfile,
};
use arbiter_crypto::authn;

View File

@@ -8,14 +8,12 @@ use kameo::{Actor, actor::ActorRef, messages};
use thiserror::Error;
use tracing::error;
use crate::actors::{
client::ClientProfile,
flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController},
user_agent::{OutOfBand, UserAgentConnection},
};
use crate::{actors::flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController}, peers::client::ClientProfile};
mod state;
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
use super::{OutOfBand, UserAgentConnection};
#[derive(Debug, Error)]
pub enum Error {
#[error("State transition failed")]

View File

@@ -14,28 +14,27 @@ use kameo::prelude::Context;
use tracing::{error, info};
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::vault::VaultState, peers::user_agent::session::state::{UnsealContext, UserAgentEvents}};
use crate::actors::{
evm::{
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
UseragentCreateGrant, UseragentListGrants,
},
keyholder::{self, Bootstrap, TryUnseal},
user_agent::session::{
UserAgentSession,
state::{UnsealContext, UserAgentEvents, UserAgentStates},
},
vault::{self, Bootstrap, TryUnseal},
};
use crate::db::models::{
EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
};
use crate::evm::policies::{Grant, SpecificGrant};
use crate::{
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer,
};
use super::{UserAgentSession, state, Error};
impl UserAgentSession {
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
let state::UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
error!("Received encrypted key in invalid state");
return Err(Error::internal("Invalid state for unseal encrypted key"));
};
@@ -184,7 +183,7 @@ impl UserAgentSession {
match self
.props
.actors
.key_holder
.vault
.ask(TryUnseal {
seal_key_raw: seal_key_buffer,
})
@@ -195,17 +194,17 @@ impl UserAgentSession {
self.transition(UserAgentEvents::ReceivedValidKey)?;
Ok(())
}
Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => {
Err(SendError::HandlerError(vault::Error::InvalidKey)) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(UnsealError::InvalidKey)
}
Err(SendError::HandlerError(err)) => {
error!(?err, "Keyholder failed to unseal key");
error!(?err, "Vault failed to unseal key");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(UnsealError::InvalidKey)
}
Err(err) => {
error!(?err, "Failed to send unseal request to keyholder");
error!(?err, "Failed to send unseal request to vault");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(Error::internal("Vault actor error").into())
}
@@ -245,7 +244,7 @@ impl UserAgentSession {
match self
.props
.actors
.key_holder
.vault
.ask(Bootstrap {
seal_key_raw: seal_key_buffer,
})
@@ -256,17 +255,17 @@ impl UserAgentSession {
self.transition(UserAgentEvents::ReceivedValidKey)?;
Ok(())
}
Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => {
Err(SendError::HandlerError(vault::Error::AlreadyBootstrapped)) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(BootstrapError::AlreadyBootstrapped)
}
Err(SendError::HandlerError(err)) => {
error!(?err, "Keyholder failed to bootstrap vault");
error!(?err, "Vault failed to bootstrap vault");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(BootstrapError::InvalidKey)
}
Err(err) => {
error!(?err, "Failed to send bootstrap request to keyholder");
error!(?err, "Failed to send bootstrap request to vault");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(BootstrapError::General(Error::internal(
"Vault actor error",
@@ -279,13 +278,13 @@ impl UserAgentSession {
#[messages]
impl UserAgentSession {
#[message]
pub(crate) async fn handle_query_vault_state(&mut self) -> Result<KeyHolderState, Error> {
use crate::actors::keyholder::GetState;
pub(crate) async fn handle_query_vault_state(&mut self) -> Result<VaultState, Error> {
use crate::actors::vault::GetState;
let vault_state = match self.props.actors.key_holder.ask(GetState {}).await {
let vault_state = match self.props.actors.vault.ask(GetState {}).await {
Ok(state) => state,
Err(err) => {
error!(?err, actor = "useragent", "keyholder.query.failed");
error!(?err, actor = "useragent", "vault.query.failed");
return Err(Error::internal("Vault is in broken state"));
}
};
@@ -374,7 +373,7 @@ impl UserAgentSession {
// Err(GrantMutationError::Internal)
// }
// }
let _ = grant_id;
let _ = grant_id;
todo!()
}

View File

@@ -7,9 +7,10 @@ use arbiter_proto::transport::{Receiver, Sender};
use arbiter_server::{
actors::{
GlobalActors,
client::{ClientConnection, ClientCredentials, auth, connect_client},
keyholder::Bootstrap,
vault::Bootstrap,
},
peers::client::{ClientConnection, ClientCredentials, auth, connect_client},
crypto::integrity,
db::{self, schema},
};
@@ -58,7 +59,7 @@ async fn insert_registered_client(
integrity::sign_entity(
&mut conn,
&actors.key_holder,
&actors.vault,
&ClientCredentials {
pubkey: pubkey.into(),
nonce: 1,
@@ -103,7 +104,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()),
})

View File

@@ -1,7 +1,7 @@
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 +11,8 @@ use diesel_async::RunQueryDsl;
use tokio::sync::mpsc;
#[allow(dead_code)]
pub async fn bootstrapped_keyholder(db: &db::DatabasePool) -> KeyHolder {
let mut actor = KeyHolder::new(db.clone()).await.unwrap();
pub 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

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

@@ -8,9 +8,10 @@ use arbiter_server::{
actors::{
GlobalActors,
bootstrap::GetToken,
keyholder::Bootstrap,
user_agent::{UserAgentConnection, UserAgentCredentials, auth},
vault::Bootstrap,
},
peers::user_agent::{UserAgentConnection, UserAgentCredentials, auth},
crypto::integrity,
db::{self, schema},
};
@@ -38,7 +39,7 @@ pub async fn test_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()),
})
@@ -124,7 +125,7 @@ pub async fn test_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()),
})
@@ -147,7 +148,7 @@ pub async fn test_challenge_auth() {
.unwrap();
integrity::sign_entity(
&mut conn,
&actors.key_holder,
&actors.vault,
&UserAgentCredentials {
pubkey: new_key.verifying_key().into(),
nonce: 1,
@@ -213,7 +214,7 @@ pub async fn test_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()),
})
@@ -262,7 +263,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
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()),
})
@@ -285,7 +286,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
.unwrap();
integrity::sign_entity(
&mut conn,
&actors.key_holder,
&actors.vault,
&UserAgentCredentials {
pubkey: new_key.verifying_key().into(),
nonce: 1,

View File

@@ -2,12 +2,13 @@ use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::{
GlobalActors,
keyholder::{Bootstrap, Seal},
user_agent::{
vault::{Bootstrap, Seal},
},
peers::user_agent::{
UserAgentSession,
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
},
},
db,
};
@@ -22,13 +23,13 @@ async fn setup_sealed_user_agent(
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));

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

@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::keyholder::{CreateNew, Error, KeyHolder},
actors::{GlobalActors, vault::{CreateNew, Error, Vault}},
db::{self, models, schema},
};
@@ -14,7 +14,7 @@ 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 +44,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 +66,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 +94,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();
@@ -156,12 +156,12 @@ 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,8 +1,12 @@
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::keyholder::{Error, KeyHolder},
actors::{GlobalActors, vault::{Error, Vault}},
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG},
db::{self, models, schema},
peers::user_agent::{
UserAgentSession,
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
},
};
use diesel::{QueryDsl, SelectableHelper};
@@ -14,7 +18,7 @@ use crate::common;
#[test_log::test]
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();
@@ -37,7 +41,7 @@ async fn test_bootstrap() {
#[test_log::test]
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();
@@ -48,7 +52,7 @@ async fn test_bootstrap_rejects_double() {
#[test_log::test]
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()))
@@ -61,7 +65,7 @@ async fn test_create_new_before_bootstrap_fails() {
#[test_log::test]
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, Error::NotBootstrapped));
@@ -71,10 +75,10 @@ async fn test_decrypt_before_bootstrap_fails() {
#[test_log::test]
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, Error::NotBootstrapped));
}
@@ -83,7 +87,7 @@ async fn test_new_restores_sealed_state() {
#[test_log::test]
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 +96,7 @@ async fn test_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();
@@ -104,7 +108,7 @@ async fn test_unseal_correct_password() {
#[test_log::test]
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,7 +117,7 @@ async fn test_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();

View File

@@ -2,7 +2,7 @@ use std::collections::HashSet;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::keyholder::Error,
actors::vault::Error,
crypto::encryption::v1::Nonce,
db::{self, models, schema},
};
@@ -16,7 +16,7 @@ use crate::common;
#[test_log::test]
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
@@ -32,7 +32,7 @@ async fn test_create_decrypt_roundtrip() {
#[test_log::test]
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, Error::NotFound));
@@ -42,7 +42,7 @@ async fn test_decrypt_nonexistent_returns_not_found() {
#[test_log::test]
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
@@ -80,7 +80,7 @@ async fn test_ciphertext_differs_across_entries() {
#[test_log::test]
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 +124,7 @@ async fn test_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();
@@ -145,7 +145,7 @@ async fn broken_db_nonce_format_fails_closed() {
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