Compare commits
6 Commits
33456a644d
...
enforcing-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bec82e036e | ||
|
|
763058b014 | ||
|
|
1497884ce6 | ||
|
|
b3464cf8a6 | ||
|
|
46d1318b6f | ||
| 9c80d51d45 |
19
server/Cargo.lock
generated
19
server/Cargo.lock
generated
@@ -742,8 +742,10 @@ dependencies = [
|
||||
"insta",
|
||||
"k256",
|
||||
"kameo",
|
||||
"macro_rules_attribute",
|
||||
"memsafe",
|
||||
"mutants",
|
||||
"paste",
|
||||
"pem",
|
||||
"proptest",
|
||||
"prost",
|
||||
@@ -760,6 +762,7 @@ dependencies = [
|
||||
"smlang",
|
||||
"spki",
|
||||
"strum 0.28.0",
|
||||
"subtle",
|
||||
"test-log",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
@@ -3056,6 +3059,22 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macro_rules_attribute"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520"
|
||||
dependencies = [
|
||||
"macro_rules_attribute-proc_macro",
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macro_rules_attribute-proc_macro"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
|
||||
@@ -59,6 +59,10 @@ pub struct ArbiterEvmWallet {
|
||||
}
|
||||
|
||||
impl ArbiterEvmWallet {
|
||||
#[expect(
|
||||
dead_code,
|
||||
reason = "constructor may be used in future extensions, e.g. to support wallet listing"
|
||||
)]
|
||||
pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
|
||||
Self {
|
||||
transport,
|
||||
|
||||
@@ -60,6 +60,9 @@ arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
||||
anyhow = "1.0.102"
|
||||
serde_with = "3.18.0"
|
||||
mutants.workspace = true
|
||||
subtle = "2.6.1"
|
||||
macro_rules_attribute = "0.2.2"
|
||||
paste = "1.0.15"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.46.3"
|
||||
|
||||
@@ -4,6 +4,7 @@ use diesel_async::RunQueryDsl;
|
||||
use kameo::{Actor, messages};
|
||||
|
||||
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
|
||||
use subtle::ConstantTimeEq as _;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::db::{self, DatabasePool, schema};
|
||||
@@ -44,14 +45,14 @@ pub struct Bootstrapper {
|
||||
|
||||
impl Bootstrapper {
|
||||
pub async fn new(db: &DatabasePool) -> Result<Self, Error> {
|
||||
let row_count: i64 = {
|
||||
let mut conn = db.get().await?;
|
||||
|
||||
let row_count: i64 = schema::useragent_client::table
|
||||
schema::useragent_client::table
|
||||
.count()
|
||||
.get_result(&mut conn)
|
||||
.await?;
|
||||
|
||||
drop(conn);
|
||||
.await?
|
||||
};
|
||||
|
||||
let token = if row_count == 0 {
|
||||
let token = generate_token().await?;
|
||||
@@ -69,7 +70,13 @@ impl Bootstrapper {
|
||||
#[message]
|
||||
pub fn is_correct_token(&self, token: String) -> bool {
|
||||
match &self.token {
|
||||
Some(expected) => *expected == token,
|
||||
Some(expected) => {
|
||||
let expected_bytes = expected.as_bytes();
|
||||
let token_bytes = token.as_bytes();
|
||||
|
||||
let choice = expected_bytes.ct_eq(token_bytes);
|
||||
bool::from(choice)
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,16 @@ use diesel::{
|
||||
};
|
||||
use diesel_async::RunQueryDsl as _;
|
||||
use ed25519_dalek::{Signature, VerifyingKey};
|
||||
use kameo::error::SendError;
|
||||
use kameo::{actor::ActorRef, error::SendError};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
actors::{
|
||||
client::{ClientConnection, ClientProfile},
|
||||
client::{ClientConnection, ClientCredentials, ClientProfile},
|
||||
flow_coordinator::{self, RequestClientApproval},
|
||||
keyholder::KeyHolder,
|
||||
},
|
||||
crypto::integrity::{self, Verified, verified::VerifiedFieldsAccessor},
|
||||
db::{
|
||||
self,
|
||||
models::{ProgramClientMetadata, SqliteTimestamp},
|
||||
@@ -30,6 +32,8 @@ pub enum Error {
|
||||
DatabasePoolUnavailable,
|
||||
#[error("Database operation failed")]
|
||||
DatabaseOperationFailed,
|
||||
#[error("Integrity check failed")]
|
||||
IntegrityCheckFailed,
|
||||
#[error("Invalid challenge solution")]
|
||||
InvalidChallengeSolution,
|
||||
#[error("Client approval request failed")]
|
||||
@@ -38,6 +42,13 @@ pub enum Error {
|
||||
Transport,
|
||||
}
|
||||
|
||||
impl From<diesel::result::Error> for Error {
|
||||
fn from(e: diesel::result::Error) -> Self {
|
||||
error!(?e, "Database error");
|
||||
Self::DatabaseOperationFailed
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ApproveError {
|
||||
#[error("Internal error")]
|
||||
@@ -65,17 +76,36 @@ pub enum Outbound {
|
||||
AuthSuccess,
|
||||
}
|
||||
|
||||
pub struct ClientInfo {
|
||||
pub id: i32,
|
||||
pub current_nonce: i32,
|
||||
}
|
||||
|
||||
/// Atomically reads and increments the nonce for a known client.
|
||||
/// Returns the current nonce and client ID for a registered client.
|
||||
/// Returns `None` if the pubkey is not registered.
|
||||
async fn get_client_and_nonce(
|
||||
async fn get_current_nonce_and_id(
|
||||
db: &db::DatabasePool,
|
||||
pubkey: &VerifyingKey,
|
||||
) -> Result<Option<ClientInfo>, Error> {
|
||||
) -> Result<Option<(i32, i32)>, Error> {
|
||||
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
||||
let mut conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::DatabasePoolUnavailable
|
||||
})?;
|
||||
program_client::table
|
||||
.filter(program_client::public_key.eq(&pubkey_bytes))
|
||||
.select((program_client::id, program_client::nonce))
|
||||
.first::<(i32, i32)>(&mut conn)
|
||||
.await
|
||||
.optional()
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::DatabaseOperationFailed
|
||||
})
|
||||
}
|
||||
|
||||
/// Atomically increments the nonce and re-signs the integrity envelope.
|
||||
/// Returns the new nonce, which is used as the challenge nonce.
|
||||
async fn create_nonce(
|
||||
db: &db::DatabasePool,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
pubkey: &VerifyingKey,
|
||||
) -> Result<i32, Error> {
|
||||
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
||||
|
||||
let mut conn = db.get().await.map_err(|e| {
|
||||
@@ -84,34 +114,35 @@ async fn get_client_and_nonce(
|
||||
})?;
|
||||
|
||||
conn.exclusive_transaction(|conn| {
|
||||
let keyholder = keyholder.clone();
|
||||
Box::pin(async move {
|
||||
let Some((client_id, current_nonce)) = program_client::table
|
||||
let (id, new_nonce): (i32, i32) = update(program_client::table)
|
||||
.filter(program_client::public_key.eq(&pubkey_bytes))
|
||||
.select((program_client::id, program_client::nonce))
|
||||
.first::<(i32, i32)>(conn)
|
||||
.await
|
||||
.optional()?
|
||||
else {
|
||||
return Result::<_, diesel::result::Error>::Ok(None);
|
||||
};
|
||||
|
||||
update(program_client::table)
|
||||
.filter(program_client::public_key.eq(&pubkey_bytes))
|
||||
.set(program_client::nonce.eq(current_nonce + 1))
|
||||
.execute(conn)
|
||||
.set(program_client::nonce.eq(program_client::nonce + 1))
|
||||
.returning((program_client::id, program_client::nonce))
|
||||
.get_result(conn)
|
||||
.await?;
|
||||
|
||||
Ok(Some(ClientInfo {
|
||||
id: client_id,
|
||||
current_nonce,
|
||||
}))
|
||||
integrity::sign_entity(
|
||||
conn,
|
||||
&keyholder,
|
||||
&ClientCredentials {
|
||||
pubkey: *pubkey,
|
||||
nonce: new_nonce,
|
||||
},
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Integrity sign failed after nonce update");
|
||||
Error::DatabaseOperationFailed
|
||||
})?
|
||||
.drop_verification_provenance();
|
||||
|
||||
Ok(new_nonce)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::DatabaseOperationFailed
|
||||
})
|
||||
}
|
||||
|
||||
async fn approve_new_client(
|
||||
@@ -139,15 +170,23 @@ async fn approve_new_client(
|
||||
|
||||
async fn insert_client(
|
||||
db: &db::DatabasePool,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
pubkey: &VerifyingKey,
|
||||
metadata: &ClientMetadata,
|
||||
) -> Result<i32, Error> {
|
||||
) -> Result<Verified<i32>, Error> {
|
||||
use crate::db::schema::{client_metadata, program_client};
|
||||
let metadata = metadata.clone();
|
||||
|
||||
let mut conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::DatabasePoolUnavailable
|
||||
})?;
|
||||
|
||||
conn.exclusive_transaction(|conn| {
|
||||
let keyholder = keyholder.clone();
|
||||
Box::pin(async move {
|
||||
const NONCE_START: i32 = 1;
|
||||
|
||||
let metadata_id = insert_into(client_metadata::table)
|
||||
.values((
|
||||
client_metadata::name.eq(&metadata.name),
|
||||
@@ -155,29 +194,39 @@ async fn insert_client(
|
||||
client_metadata::version.eq(&metadata.version),
|
||||
))
|
||||
.returning(client_metadata::id)
|
||||
.get_result::<i32>(&mut conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Failed to insert client metadata");
|
||||
Error::DatabaseOperationFailed
|
||||
})?;
|
||||
.get_result::<i32>(conn)
|
||||
.await?;
|
||||
|
||||
let client_id = insert_into(program_client::table)
|
||||
.values((
|
||||
program_client::public_key.eq(pubkey.as_bytes().to_vec()),
|
||||
program_client::metadata_id.eq(metadata_id),
|
||||
program_client::nonce.eq(1), // pre-incremented; challenge uses 0
|
||||
program_client::nonce.eq(NONCE_START),
|
||||
))
|
||||
.on_conflict_do_nothing()
|
||||
.returning(program_client::id)
|
||||
.get_result::<i32>(&mut conn)
|
||||
.get_result::<i32>(conn)
|
||||
.await?;
|
||||
|
||||
let verified_id = integrity::sign_entity(
|
||||
conn,
|
||||
&keyholder,
|
||||
&ClientCredentials {
|
||||
pubkey: *pubkey,
|
||||
nonce: NONCE_START,
|
||||
},
|
||||
client_id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Failed to insert client metadata");
|
||||
error!(error = ?e, "Failed to sign integrity tag for new client key");
|
||||
Error::DatabaseOperationFailed
|
||||
})?;
|
||||
|
||||
Ok(client_id)
|
||||
Ok(verified_id)
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn sync_client_metadata(
|
||||
@@ -287,7 +336,10 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn authenticate<T>(props: &mut ClientConnection, transport: &mut T) -> Result<i32, Error>
|
||||
pub async fn authenticate<T>(
|
||||
props: &mut ClientConnection,
|
||||
transport: &mut T,
|
||||
) -> Result<Verified<i32>, Error>
|
||||
where
|
||||
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
|
||||
{
|
||||
@@ -295,8 +347,28 @@ where
|
||||
return Err(Error::Transport);
|
||||
};
|
||||
|
||||
let info = match get_client_and_nonce(&props.db, &pubkey).await? {
|
||||
Some(nonce) => nonce,
|
||||
// fixme! triage needed: probable regretion since in match->Some get_current_nonce_and_id called only once instead of twice
|
||||
let client_id = match get_current_nonce_and_id(&props.db, &pubkey).await? {
|
||||
Some((nonce, id)) => {
|
||||
let mut db_conn = props.db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::DatabasePoolUnavailable
|
||||
})?;
|
||||
|
||||
integrity::verify_entity(
|
||||
&mut db_conn,
|
||||
&props.actors.key_holder,
|
||||
ClientCredentials { pubkey, nonce },
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Integrity verification failed");
|
||||
Error::IntegrityCheckFailed
|
||||
})?
|
||||
.inherit()
|
||||
.entity_id
|
||||
}
|
||||
None => {
|
||||
approve_new_client(
|
||||
&props.actors,
|
||||
@@ -306,16 +378,13 @@ where
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let client_id = insert_client(&props.db, &pubkey, &metadata).await?;
|
||||
ClientInfo {
|
||||
id: client_id,
|
||||
current_nonce: 0,
|
||||
}
|
||||
insert_client(&props.db, &props.actors.key_holder, &pubkey, &metadata).await?
|
||||
}
|
||||
};
|
||||
|
||||
sync_client_metadata(&props.db, info.id, &metadata).await?;
|
||||
challenge_client(transport, pubkey, info.current_nonce).await?;
|
||||
sync_client_metadata(&props.db, *client_id, &metadata).await?;
|
||||
let challenge_nonce = create_nonce(&props.db, &props.actors.key_holder, &pubkey).await?;
|
||||
challenge_client(transport, pubkey, challenge_nonce).await?;
|
||||
|
||||
transport
|
||||
.send(Ok(Outbound::AuthSuccess))
|
||||
@@ -325,5 +394,5 @@ where
|
||||
Error::Transport
|
||||
})?;
|
||||
|
||||
Ok(info.id)
|
||||
Ok(client_id)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
actors::{GlobalActors, client::session::ClientSession},
|
||||
crypto::integrity::{Integrable, hashing::Hashable},
|
||||
db,
|
||||
};
|
||||
|
||||
@@ -13,6 +14,22 @@ pub struct ClientProfile {
|
||||
pub metadata: ClientMetadata,
|
||||
}
|
||||
|
||||
pub struct ClientCredentials {
|
||||
pub pubkey: ed25519_dalek::VerifyingKey,
|
||||
pub nonce: i32,
|
||||
}
|
||||
|
||||
impl Integrable for ClientCredentials {
|
||||
const KIND: &'static str = "client_credentials";
|
||||
}
|
||||
|
||||
impl Hashable for ClientCredentials {
|
||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||
hasher.update(self.pubkey.as_bytes());
|
||||
self.nonce.hash(hasher);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientConnection {
|
||||
pub(crate) db: db::DatabasePool,
|
||||
pub(crate) actors: GlobalActors,
|
||||
|
||||
@@ -5,23 +5,25 @@ use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
|
||||
|
||||
use crate::{
|
||||
actors::{
|
||||
GlobalActors,
|
||||
client::ClientConnection,
|
||||
evm::{ClientSignTransaction, SignTransactionError},
|
||||
flow_coordinator::RegisterClient,
|
||||
keyholder::KeyHolderState,
|
||||
},
|
||||
db,
|
||||
crypto::integrity::Verified,
|
||||
evm::VetError,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::{actors::GlobalActors, db};
|
||||
|
||||
pub struct ClientSession {
|
||||
props: ClientConnection,
|
||||
client_id: i32,
|
||||
client_id: Verified<i32>,
|
||||
}
|
||||
|
||||
impl ClientSession {
|
||||
pub(crate) fn new(props: ClientConnection, client_id: i32) -> Self {
|
||||
pub(crate) fn new(props: ClientConnection, client_id: Verified<i32>) -> Self {
|
||||
Self { props, client_id }
|
||||
}
|
||||
}
|
||||
@@ -54,7 +56,7 @@ impl ClientSession {
|
||||
.actors
|
||||
.evm
|
||||
.ask(ClientSignTransaction {
|
||||
client_id: self.client_id,
|
||||
client_id: *self.client_id,
|
||||
wallet_address,
|
||||
transaction,
|
||||
})
|
||||
@@ -92,11 +94,12 @@ impl Actor for ClientSession {
|
||||
}
|
||||
|
||||
impl ClientSession {
|
||||
#[cfg(test)]
|
||||
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
||||
let props = ClientConnection::new(db, actors);
|
||||
Self {
|
||||
props,
|
||||
client_id: 0,
|
||||
client_id: Verified::new_unchecked(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ use kameo::{Actor, actor::ActorRef, messages};
|
||||
use rand::{SeedableRng, rng, rngs::StdRng};
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::{CreateNew, Decrypt, GetState, KeyHolder, KeyHolderState},
|
||||
crypto::integrity,
|
||||
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
||||
crypto::integrity::{self, Integrable, Verified, hashing::Hashable},
|
||||
db::{
|
||||
DatabaseError, DatabasePool,
|
||||
models::{self, SqliteTimestamp},
|
||||
models::{self},
|
||||
schema,
|
||||
},
|
||||
evm::{
|
||||
@@ -26,11 +26,37 @@ use crate::{
|
||||
|
||||
pub use crate::evm::safe_signer;
|
||||
|
||||
/// Hashable structure for wallet integrity protection.
|
||||
/// Binds the encrypted private key to the wallet address using HMAC.
|
||||
pub struct EvmWalletIntegrity {
|
||||
pub address: Vec<u8>, // 20-byte Ethereum address
|
||||
pub aead_encrypted_id: i32, // Reference to encrypted key material
|
||||
}
|
||||
|
||||
impl Hashable for EvmWalletIntegrity {
|
||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||
hasher.update(&self.address);
|
||||
hasher.update(self.aead_encrypted_id.to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl Integrable for EvmWalletIntegrity {
|
||||
const KIND: &'static str = "evm_wallet";
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SignTransactionError {
|
||||
#[error("Wallet not found")]
|
||||
WalletNotFound,
|
||||
|
||||
#[error("Wallet integrity check failed")]
|
||||
WalletIntegrityCheckFailed,
|
||||
|
||||
#[error(
|
||||
"Decrypted key does not correspond to wallet address (CRITICAL: possible key substitution attack)"
|
||||
)]
|
||||
KeyAddressMismatch,
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] DatabaseError),
|
||||
|
||||
@@ -45,6 +71,9 @@ pub enum SignTransactionError {
|
||||
|
||||
#[error("Policy error: {0}")]
|
||||
Vet(#[from] evm::VetError),
|
||||
|
||||
#[error("Integrity error: {0}")]
|
||||
Integrity(#[from] integrity::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -88,7 +117,7 @@ impl EvmActor {
|
||||
#[messages]
|
||||
impl EvmActor {
|
||||
#[message]
|
||||
pub async fn generate(&mut self) -> Result<(i32, Address), Error> {
|
||||
pub async fn generate(&mut self) -> Result<(Verified<i32>, Address), Error> {
|
||||
let (mut key_cell, address) = safe_signer::generate(&mut self.rng);
|
||||
|
||||
let plaintext = key_cell.read_inline(|reader| SafeCell::new(reader.to_vec()));
|
||||
@@ -110,7 +139,16 @@ impl EvmActor {
|
||||
.await
|
||||
.map_err(DatabaseError::from)?;
|
||||
|
||||
Ok((wallet_id, address))
|
||||
// Sign integrity envelope to bind encrypted key to wallet address
|
||||
let wallet_integrity = EvmWalletIntegrity {
|
||||
address: address.as_slice().to_vec(),
|
||||
aead_encrypted_id: aead_id,
|
||||
};
|
||||
let verified_wallet_id =
|
||||
integrity::sign_entity(&mut conn, &self.keyholder, &wallet_integrity, wallet_id)
|
||||
.await?;
|
||||
|
||||
Ok((verified_wallet_id, address))
|
||||
}
|
||||
|
||||
#[message]
|
||||
@@ -136,7 +174,7 @@ impl EvmActor {
|
||||
&mut self,
|
||||
basic: SharedGrantSettings,
|
||||
grant: SpecificGrant,
|
||||
) -> Result<i32, Error> {
|
||||
) -> Result<integrity::Verified<i32>, Error> {
|
||||
match grant {
|
||||
SpecificGrant::EtherTransfer(settings) => self
|
||||
.engine
|
||||
@@ -158,7 +196,7 @@ impl EvmActor {
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
|
||||
pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> {
|
||||
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||
// let keyholder = self.keyholder.clone();
|
||||
|
||||
@@ -207,9 +245,23 @@ impl EvmActor {
|
||||
.optional()
|
||||
.map_err(DatabaseError::from)?
|
||||
.ok_or(SignTransactionError::WalletNotFound)?;
|
||||
|
||||
// Verify wallet integrity envelope
|
||||
let wallet = integrity::verify_entity(
|
||||
&mut conn,
|
||||
&self.keyholder,
|
||||
EvmWalletIntegrity {
|
||||
address: wallet.address.clone(),
|
||||
aead_encrypted_id: wallet.aead_encrypted_id,
|
||||
},
|
||||
wallet.id,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| SignTransactionError::WalletIntegrityCheckFailed)?;
|
||||
|
||||
let wallet_access = schema::evm_wallet_access::table
|
||||
.select(models::EvmWalletAccess::as_select())
|
||||
.filter(schema::evm_wallet_access::wallet_id.eq(wallet.id))
|
||||
.filter(schema::evm_wallet_access::wallet_id.eq(wallet.entity_id))
|
||||
.filter(schema::evm_wallet_access::client_id.eq(client_id))
|
||||
.first(&mut conn)
|
||||
.await
|
||||
@@ -242,9 +294,23 @@ impl EvmActor {
|
||||
.optional()
|
||||
.map_err(DatabaseError::from)?
|
||||
.ok_or(SignTransactionError::WalletNotFound)?;
|
||||
|
||||
// Verify wallet integrity envelope to ensure encrypted key is bound to address
|
||||
let wallet = integrity::verify_entity(
|
||||
&mut conn,
|
||||
&self.keyholder,
|
||||
EvmWalletIntegrity {
|
||||
address: wallet.address.clone(),
|
||||
aead_encrypted_id: wallet.aead_encrypted_id,
|
||||
},
|
||||
wallet.id,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| SignTransactionError::WalletIntegrityCheckFailed)?;
|
||||
|
||||
let wallet_access = schema::evm_wallet_access::table
|
||||
.select(models::EvmWalletAccess::as_select())
|
||||
.filter(schema::evm_wallet_access::wallet_id.eq(wallet.id))
|
||||
.filter(schema::evm_wallet_access::wallet_id.eq(wallet.entity_id))
|
||||
.filter(schema::evm_wallet_access::client_id.eq(client_id))
|
||||
.first(&mut conn)
|
||||
.await
|
||||
@@ -263,6 +329,12 @@ impl EvmActor {
|
||||
|
||||
let signer = safe_signer::SafeSigner::from_cell(raw_key)?;
|
||||
|
||||
// Verify that the decrypted key's derived address matches the wallet address
|
||||
// This prevents an attacker from substituting one wallet's key with another's even if they compromised the DB
|
||||
if signer.address() != wallet_address {
|
||||
return Err(SignTransactionError::KeyAddressMismatch);
|
||||
}
|
||||
|
||||
self.engine
|
||||
.evaluate_transaction(wallet_access, transaction.clone(), RunKind::Execution)
|
||||
.await?;
|
||||
|
||||
@@ -30,17 +30,26 @@ pub enum Error {
|
||||
}
|
||||
|
||||
impl Error {
|
||||
fn internal(details: impl Into<String>) -> Self {
|
||||
Self::Internal {
|
||||
details: details.into(),
|
||||
}
|
||||
#[track_caller]
|
||||
pub(super) fn internal(details: impl Into<String>, err: &impl std::fmt::Debug) -> Self {
|
||||
let details = details.into();
|
||||
let caller = std::panic::Location::caller();
|
||||
error!(
|
||||
caller_file = %caller.file(),
|
||||
caller_line = caller.line(),
|
||||
caller_column = caller.column(),
|
||||
details = %details,
|
||||
error = ?err,
|
||||
"Internal error"
|
||||
);
|
||||
|
||||
Self::Internal { details }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diesel::result::Error> for Error {
|
||||
fn from(e: diesel::result::Error) -> Self {
|
||||
error!(?e, "Database error");
|
||||
Self::internal("Database error")
|
||||
Self::internal("Database error", &e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use arbiter_proto::transport::Bi;
|
||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
use kameo::{actor::ActorRef, error::SendError};
|
||||
use kameo::actor::ActorRef;
|
||||
use tracing::error;
|
||||
|
||||
use super::Error;
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
keyholder::KeyHolder,
|
||||
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth::Outbound},
|
||||
},
|
||||
crypto::integrity::{self, AttestationStatus},
|
||||
crypto::integrity,
|
||||
db::{DatabasePool, schema::useragent_client},
|
||||
};
|
||||
|
||||
@@ -48,10 +48,10 @@ async fn get_current_nonce_and_id(
|
||||
db: &DatabasePool,
|
||||
key: &AuthPublicKey,
|
||||
) -> Result<(i32, i32), Error> {
|
||||
let mut db_conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
let mut db_conn = db
|
||||
.get()
|
||||
.await
|
||||
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||
db_conn
|
||||
.exclusive_transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
@@ -65,10 +65,7 @@ async fn get_current_nonce_and_id(
|
||||
})
|
||||
.await
|
||||
.optional()
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::internal("Database operation failed")
|
||||
})?
|
||||
.map_err(|e| Error::internal("Database operation failed", &e))?
|
||||
.ok_or_else(|| {
|
||||
error!(?key, "Public key not found in database");
|
||||
Error::UnregisteredPublicKey
|
||||
@@ -80,14 +77,14 @@ async fn verify_integrity(
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
pubkey: &AuthPublicKey,
|
||||
) -> Result<(), Error> {
|
||||
let mut db_conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
let mut db_conn = db
|
||||
.get()
|
||||
.await
|
||||
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||
|
||||
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?;
|
||||
|
||||
let result = integrity::verify_entity(
|
||||
let attestation_status = integrity::check_entity_attestation(
|
||||
&mut db_conn,
|
||||
keyholder,
|
||||
&UserAgentCredentials {
|
||||
@@ -97,12 +94,17 @@ async fn verify_integrity(
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Integrity verification failed");
|
||||
Error::internal("Integrity verification failed")
|
||||
})?;
|
||||
.map_err(|e| Error::internal("Integrity verification failed", &e))?;
|
||||
|
||||
Ok(())
|
||||
use integrity::AttestationStatus as AS;
|
||||
// SAFETY (policy): challenge auth must work in both vault states.
|
||||
// While sealed, integrity checks can only report `Unavailable` because key material is not
|
||||
// accessible. While unsealed, the same check can report `Attested`.
|
||||
// This path intentionally accepts both outcomes to keep challenge auth available across state
|
||||
// transitions; stricter verification is enforced in sensitive post-auth flows.
|
||||
match attestation_status {
|
||||
AS::Attested | AS::Unavailable => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_nonce(
|
||||
@@ -110,10 +112,10 @@ async fn create_nonce(
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
pubkey: &AuthPublicKey,
|
||||
) -> Result<i32, Error> {
|
||||
let mut db_conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
let mut db_conn = db
|
||||
.get()
|
||||
.await
|
||||
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||
let new_nonce = db_conn
|
||||
.exclusive_transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
@@ -124,10 +126,7 @@ async fn create_nonce(
|
||||
.returning((useragent_client::id, useragent_client::nonce))
|
||||
.get_result(conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::internal("Database operation failed")
|
||||
})?;
|
||||
.map_err(|e| Error::internal("Database operation failed", &e))?;
|
||||
|
||||
integrity::sign_entity(
|
||||
conn,
|
||||
@@ -139,10 +138,8 @@ async fn create_nonce(
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Integrity signature update failed");
|
||||
Error::internal("Database error")
|
||||
})?;
|
||||
.map_err(|e| Error::internal("Database error", &e))?
|
||||
.drop_verification_provenance();
|
||||
|
||||
Result::<_, Error>::Ok(new_nonce)
|
||||
})
|
||||
@@ -158,10 +155,10 @@ async fn register_key(
|
||||
) -> Result<(), Error> {
|
||||
let pubkey_bytes = pubkey.to_stored_bytes();
|
||||
let key_type = pubkey.key_type();
|
||||
let mut conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
let mut conn = db
|
||||
.get()
|
||||
.await
|
||||
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||
|
||||
conn.transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
@@ -176,22 +173,32 @@ async fn register_key(
|
||||
.returning(useragent_client::id)
|
||||
.get_result(conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::internal("Database operation failed")
|
||||
})?;
|
||||
.map_err(|e| Error::internal("Database operation failed", &e))?;
|
||||
|
||||
let entity = UserAgentCredentials {
|
||||
if let Err(e) = integrity::sign_entity(
|
||||
conn,
|
||||
keyholder,
|
||||
&UserAgentCredentials {
|
||||
pubkey: pubkey.clone(),
|
||||
nonce: NONCE_START,
|
||||
};
|
||||
|
||||
integrity::sign_entity(conn, &keyholder, &entity, id)
|
||||
},
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Failed to sign integrity tag for new user-agent key");
|
||||
Error::internal("Failed to register public key")
|
||||
})?;
|
||||
{
|
||||
match e {
|
||||
integrity::Error::Keyholder(
|
||||
crate::actors::keyholder::Error::NotBootstrapped,
|
||||
) => {
|
||||
// IMPORTANT: bootstrap-token auth must work before the vault has a root key.
|
||||
// We intentionally allow creating the DB row first and backfill envelopes
|
||||
// after bootstrap/unseal to keep the bootstrap flow possible.
|
||||
}
|
||||
other => {
|
||||
return Err(Error::internal("Failed to register public key", &other));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result::<_, Error>::Ok(())
|
||||
})
|
||||
@@ -254,10 +261,7 @@ where
|
||||
token: token.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Failed to consume bootstrap token");
|
||||
Error::internal("Failed to consume bootstrap token")
|
||||
})?;
|
||||
.map_err(|e| Error::internal("Failed to consume bootstrap token", &e))?;
|
||||
|
||||
if !token_ok {
|
||||
error!("Invalid bootstrap token provided");
|
||||
|
||||
@@ -108,7 +108,7 @@ use crate::crypto::integrity::hashing::Hashable;
|
||||
|
||||
impl Hashable for AuthPublicKey {
|
||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||
hasher.update(&self.to_stored_bytes());
|
||||
hasher.update(self.to_stored_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ 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::db::models::{
|
||||
@@ -18,6 +17,10 @@ use crate::db::models::{
|
||||
};
|
||||
use crate::evm::policies::{Grant, SpecificGrant};
|
||||
use crate::safe_cell::SafeCell;
|
||||
use crate::{
|
||||
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer,
|
||||
crypto::integrity::{self, Verified},
|
||||
};
|
||||
use crate::{
|
||||
actors::{
|
||||
evm::{
|
||||
@@ -29,11 +32,67 @@ use crate::{
|
||||
UserAgentSession,
|
||||
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
||||
},
|
||||
user_agent::{AuthPublicKey, UserAgentCredentials},
|
||||
},
|
||||
db::schema::useragent_client,
|
||||
safe_cell::SafeCellHandle as _,
|
||||
};
|
||||
|
||||
fn is_vault_sealed_from_evm<M>(err: &SendError<M, crate::actors::evm::Error>) -> bool {
|
||||
matches!(
|
||||
err,
|
||||
SendError::HandlerError(crate::actors::evm::Error::Keyholder(
|
||||
keyholder::Error::NotBootstrapped
|
||||
)) | SendError::HandlerError(crate::actors::evm::Error::Integrity(
|
||||
crate::crypto::integrity::Error::Keyholder(keyholder::Error::NotBootstrapped)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
impl UserAgentSession {
|
||||
async fn backfill_useragent_integrity(&self) -> Result<(), Error> {
|
||||
let mut conn = self.props.db.get().await?;
|
||||
let keyholder = self.props.actors.key_holder.clone();
|
||||
|
||||
conn.transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
let rows: Vec<(i32, i32, Vec<u8>, crate::db::models::KeyType)> =
|
||||
useragent_client::table
|
||||
.select((
|
||||
useragent_client::id,
|
||||
useragent_client::nonce,
|
||||
useragent_client::public_key,
|
||||
useragent_client::key_type,
|
||||
))
|
||||
.load(conn)
|
||||
.await?;
|
||||
|
||||
for (id, nonce, public_key, key_type) in rows {
|
||||
let pubkey = AuthPublicKey::try_from((key_type, public_key)).map_err(|e| {
|
||||
Error::internal(format!("Invalid user-agent key in db: {e}"))
|
||||
})?;
|
||||
|
||||
integrity::sign_entity(
|
||||
conn,
|
||||
&keyholder,
|
||||
&UserAgentCredentials { pubkey, nonce },
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::internal(format!("Failed to backfill user-agent integrity: {e}"))
|
||||
})?
|
||||
.drop_verification_provenance();
|
||||
}
|
||||
|
||||
Result::<_, Error>::Ok(())
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
|
||||
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
||||
error!("Received encrypted key in invalid state");
|
||||
@@ -191,6 +250,7 @@ impl UserAgentSession {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
self.backfill_useragent_integrity().await?;
|
||||
info!("Successfully unsealed key with client-provided key");
|
||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||
Ok(())
|
||||
@@ -252,6 +312,7 @@ impl UserAgentSession {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
self.backfill_useragent_integrity().await?;
|
||||
info!("Successfully bootstrapped vault with client-provided key");
|
||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||
Ok(())
|
||||
@@ -297,7 +358,9 @@ impl UserAgentSession {
|
||||
#[messages]
|
||||
impl UserAgentSession {
|
||||
#[message]
|
||||
pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<(i32, Address), Error> {
|
||||
pub(crate) async fn handle_evm_wallet_create(
|
||||
&mut self,
|
||||
) -> Result<(Verified<i32>, Address), Error> {
|
||||
match self.props.actors.evm.ask(Generate {}).await {
|
||||
Ok(address) => Ok(address),
|
||||
Err(SendError::HandlerError(err)) => Err(Error::internal(format!(
|
||||
@@ -325,12 +388,15 @@ impl UserAgentSession {
|
||||
#[messages]
|
||||
impl UserAgentSession {
|
||||
#[message]
|
||||
pub(crate) async fn handle_grant_list(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
||||
pub(crate) async fn handle_grant_list(
|
||||
&mut self,
|
||||
) -> Result<Vec<Grant<SpecificGrant>>, GrantMutationError> {
|
||||
match self.props.actors.evm.ask(UseragentListGrants {}).await {
|
||||
Ok(grants) => Ok(grants),
|
||||
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||
Err(err) => {
|
||||
error!(?err, "EVM grant list failed");
|
||||
Err(Error::internal("Failed to list EVM grants"))
|
||||
Err(GrantMutationError::Internal)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,7 +406,7 @@ impl UserAgentSession {
|
||||
&mut self,
|
||||
basic: crate::evm::policies::SharedGrantSettings,
|
||||
grant: crate::evm::policies::SpecificGrant,
|
||||
) -> Result<i32, GrantMutationError> {
|
||||
) -> Result<Verified<i32>, GrantMutationError> {
|
||||
match self
|
||||
.props
|
||||
.actors
|
||||
@@ -349,6 +415,7 @@ impl UserAgentSession {
|
||||
.await
|
||||
{
|
||||
Ok(grant_id) => Ok(grant_id),
|
||||
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||
Err(err) => {
|
||||
error!(?err, "EVM grant create failed");
|
||||
Err(GrantMutationError::Internal)
|
||||
@@ -365,10 +432,13 @@ impl UserAgentSession {
|
||||
.props
|
||||
.actors
|
||||
.evm
|
||||
.ask(UseragentDeleteGrant { grant_id })
|
||||
.ask(UseragentDeleteGrant {
|
||||
_grant_id: grant_id,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||
Err(err) => {
|
||||
error!(?err, "EVM grant delete failed");
|
||||
Err(GrantMutationError::Internal)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::{
|
||||
actors::keyholder, crypto::integrity::hashing::Hashable, safe_cell::SafeCellHandle as _,
|
||||
};
|
||||
use hmac::{Hmac, Mac as _};
|
||||
use crate::actors::keyholder;
|
||||
use hmac::Hmac;
|
||||
use sha2::Sha256;
|
||||
use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
|
||||
use diesel::{ExpressionMethods as _, QueryDsl, dsl::insert_into, sqlite::Sqlite};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
@@ -10,16 +11,24 @@ use kameo::{actor::ActorRef, error::SendError};
|
||||
use sha2::Digest as _;
|
||||
|
||||
pub mod hashing;
|
||||
pub mod verified;
|
||||
use self::hashing::Hashable;
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::{KeyHolder, SignIntegrity, VerifyIntegrity},
|
||||
db::{
|
||||
self,
|
||||
models::{IntegrityEnvelope, NewIntegrityEnvelope},
|
||||
models::{IntegrityEnvelope as IntegrityEnvelopeRow, NewIntegrityEnvelope},
|
||||
schema::integrity_envelope,
|
||||
},
|
||||
};
|
||||
|
||||
pub const CURRENT_PAYLOAD_VERSION: i32 = 1;
|
||||
pub const INTEGRITY_SUBKEY_TAG: &[u8] = b"arbiter/db-integrity-key/v1";
|
||||
|
||||
pub type HmacSha256 = Hmac<Sha256>;
|
||||
pub use self::verified::Verified;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Database error: {0}")]
|
||||
@@ -48,71 +57,90 @@ pub enum Error {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[must_use]
|
||||
pub enum AttestationStatus {
|
||||
Attested,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
pub const CURRENT_PAYLOAD_VERSION: i32 = 1;
|
||||
pub const INTEGRITY_SUBKEY_TAG: &[u8] = b"arbiter/db-integrity-key/v1";
|
||||
|
||||
pub type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
pub trait Integrable: Hashable {
|
||||
const KIND: &'static str;
|
||||
const VERSION: i32 = 1;
|
||||
}
|
||||
|
||||
fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
|
||||
let mut hasher = Sha256::new();
|
||||
payload.hash(&mut hasher);
|
||||
hasher.finalize().into()
|
||||
impl<T: Integrable> Integrable for &T {
|
||||
const KIND: &'static str = T::KIND;
|
||||
const VERSION: i32 = T::VERSION;
|
||||
}
|
||||
|
||||
fn push_len_prefixed(out: &mut Vec<u8>, bytes: &[u8]) {
|
||||
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EntityId(Vec<u8>);
|
||||
|
||||
fn build_mac_input(
|
||||
entity_kind: &str,
|
||||
entity_id: &[u8],
|
||||
payload_version: i32,
|
||||
payload_hash: &[u8; 32],
|
||||
) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(8 + entity_kind.len() + entity_id.len() + 32);
|
||||
push_len_prefixed(&mut out, entity_kind.as_bytes());
|
||||
push_len_prefixed(&mut out, entity_id);
|
||||
out.extend_from_slice(&payload_version.to_be_bytes());
|
||||
out.extend_from_slice(payload_hash);
|
||||
out
|
||||
}
|
||||
impl Deref for EntityId {
|
||||
type Target = [u8];
|
||||
|
||||
pub trait IntoId {
|
||||
fn into_id(self) -> Vec<u8>;
|
||||
}
|
||||
|
||||
impl IntoId for i32 {
|
||||
fn into_id(self) -> Vec<u8> {
|
||||
self.to_be_bytes().to_vec()
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoId for &'_ [u8] {
|
||||
fn into_id(self) -> Vec<u8> {
|
||||
self.to_vec()
|
||||
impl From<i32> for EntityId {
|
||||
fn from(value: i32) -> Self {
|
||||
Self(value.to_be_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sign_entity<E: Integrable>(
|
||||
impl From<&'_ [u8]> for EntityId {
|
||||
fn from(bytes: &'_ [u8]) -> Self {
|
||||
Self(bytes.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lookup_verified<E, Id, C, F, Fut>(
|
||||
conn: &mut C,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
entity_id: Id,
|
||||
load: F,
|
||||
) -> Result<Verified<Entity<E, Id>>, Error>
|
||||
where
|
||||
C: AsyncConnection<Backend = Sqlite>,
|
||||
E: Integrable,
|
||||
Id: Into<EntityId> + Clone,
|
||||
F: FnOnce(&mut C) -> Fut,
|
||||
Fut: Future<Output = Result<E, db::DatabaseError>>,
|
||||
{
|
||||
let entity = load(conn).await?;
|
||||
verify_entity(conn, keyholder, entity, entity_id).await
|
||||
}
|
||||
|
||||
pub async fn lookup_verified_from_query<E, Id, C, F>(
|
||||
conn: &mut C,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
load: F,
|
||||
) -> Result<Verified<Entity<E, Id>>, Error>
|
||||
where
|
||||
C: AsyncConnection<Backend = Sqlite> + Send,
|
||||
E: Integrable,
|
||||
Id: Into<EntityId> + Clone,
|
||||
F: for<'a> FnOnce(
|
||||
&'a mut C,
|
||||
) -> Pin<
|
||||
Box<dyn Future<Output = Result<(Id, E), db::DatabaseError>> + Send + 'a>,
|
||||
>,
|
||||
{
|
||||
let (entity_id, entity) = load(conn).await?;
|
||||
verify_entity(conn, keyholder, entity, entity_id).await
|
||||
}
|
||||
|
||||
pub async fn sign_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
entity: &E,
|
||||
entity_id: impl IntoId,
|
||||
) -> Result<(), Error> {
|
||||
let payload_hash = payload_hash(&entity);
|
||||
as_entity_id: Id,
|
||||
) -> Result<Verified<Id>, Error> {
|
||||
let payload_hash = payload_hash(entity);
|
||||
|
||||
let entity_id = entity_id.into_id();
|
||||
let entity_id = as_entity_id.clone().into();
|
||||
|
||||
let mac_input = build_mac_input(E::KIND, &entity_id, E::VERSION, &payload_hash);
|
||||
|
||||
@@ -127,7 +155,7 @@ pub async fn sign_entity<E: Integrable>(
|
||||
insert_into(integrity_envelope::table)
|
||||
.values(NewIntegrityEnvelope {
|
||||
entity_kind: E::KIND.to_owned(),
|
||||
entity_id: entity_id,
|
||||
entity_id: entity_id.to_vec(),
|
||||
payload_version: E::VERSION,
|
||||
key_version,
|
||||
mac: mac.to_vec(),
|
||||
@@ -146,19 +174,19 @@ pub async fn sign_entity<E: Integrable>(
|
||||
.await
|
||||
.map_err(db::DatabaseError::from)?;
|
||||
|
||||
Ok(())
|
||||
Ok(Verified::new(as_entity_id))
|
||||
}
|
||||
|
||||
pub async fn verify_entity<E: Integrable>(
|
||||
pub async fn check_entity_attestation<E: Integrable>(
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
entity: &E,
|
||||
entity_id: impl IntoId,
|
||||
entity_id: impl Into<EntityId>,
|
||||
) -> Result<AttestationStatus, Error> {
|
||||
let entity_id = entity_id.into_id();
|
||||
let envelope: IntegrityEnvelope = integrity_envelope::table
|
||||
let entity_id = entity_id.into();
|
||||
let envelope: IntegrityEnvelopeRow = integrity_envelope::table
|
||||
.filter(integrity_envelope::entity_kind.eq(E::KIND))
|
||||
.filter(integrity_envelope::entity_id.eq(&entity_id))
|
||||
.filter(integrity_envelope::entity_id.eq(&*entity_id))
|
||||
.first(conn)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
@@ -176,7 +204,7 @@ pub async fn verify_entity<E: Integrable>(
|
||||
});
|
||||
}
|
||||
|
||||
let payload_hash = payload_hash(&entity);
|
||||
let payload_hash = payload_hash(entity);
|
||||
let mac_input = build_mac_input(E::KIND, &entity_id, envelope.payload_version, &payload_hash);
|
||||
|
||||
let result = keyholder
|
||||
@@ -199,139 +227,87 @@ pub async fn verify_entity<E: Integrable>(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use diesel::{ExpressionMethods as _, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::{actor::ActorRef, prelude::Spawn};
|
||||
use rand::seq::SliceRandom;
|
||||
use sha2::Digest;
|
||||
#[derive(Debug, Clone, crate::VerifiedFields!)]
|
||||
#[repr(C)]
|
||||
pub struct Entity<E, Id> {
|
||||
pub entity: E,
|
||||
pub entity_id: Id,
|
||||
}
|
||||
|
||||
use proptest::prelude::*;
|
||||
impl<E, Id> Deref for Entity<E, Id> {
|
||||
type Target = E;
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::{Bootstrap, KeyHolder},
|
||||
db::{self, schema},
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
|
||||
use super::{Error, Integrable, sign_entity, verify_entity};
|
||||
use super::{hashing::Hashable, payload_hash};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DummyEntity {
|
||||
payload_version: i32,
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Hashable for DummyEntity {
|
||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||
self.payload_version.hash(hasher);
|
||||
self.payload.hash(hasher);
|
||||
}
|
||||
}
|
||||
impl Integrable for DummyEntity {
|
||||
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());
|
||||
actor
|
||||
.ask(Bootstrap {
|
||||
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
actor
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sign_writes_envelope_and_verify_passes() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: &[u8] = b"entity-id-7";
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let count: i64 = schema::integrity_envelope::table
|
||||
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
||||
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
||||
.count()
|
||||
.get_result(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(count, 1, "envelope row must be created exactly once");
|
||||
verify_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tampered_mac_fails_verification() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: &[u8] = b"entity-id-11";
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
diesel::update(schema::integrity_envelope::table)
|
||||
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
||||
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
||||
.set(schema::integrity_envelope::mac.eq(vec![0u8; 32]))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let err = verify_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, Error::MacMismatch { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn changed_payload_fails_verification() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: &[u8] = b"entity-id-21";
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tampered = DummyEntity {
|
||||
payload: b"payload-v1-but-tampered".to_vec(),
|
||||
..entity
|
||||
};
|
||||
|
||||
let err = verify_entity(&mut conn, &keyholder, &tampered, ENTITY_ID)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, Error::MacMismatch { .. }));
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.entity
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
entity: E,
|
||||
entity_id: Id,
|
||||
) -> Result<Verified<Entity<E, Id>>, Error> {
|
||||
match check_entity_attestation(conn, keyholder, &entity, entity_id.clone()).await? {
|
||||
AttestationStatus::Attested => Ok(Verified::new(Entity { entity, entity_id })),
|
||||
AttestationStatus::Unavailable => Err(Error::Keyholder(keyholder::Error::NotBootstrapped)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify_entity_ref<'e, E: Integrable, Id: Into<EntityId> + Clone>(
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
entity: &'e E,
|
||||
entity_id: Id,
|
||||
) -> Result<Verified<Entity<&'e E, Id>>, Error> {
|
||||
match check_entity_attestation(conn, keyholder, entity, entity_id.clone()).await? {
|
||||
AttestationStatus::Attested => Ok(Verified::new(Entity { entity, entity_id })),
|
||||
AttestationStatus::Unavailable => Err(Error::Keyholder(keyholder::Error::NotBootstrapped)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_envelope<E: Integrable>(
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
entity_id: impl Into<EntityId>,
|
||||
) -> Result<usize, Error> {
|
||||
let entity_id = entity_id.into();
|
||||
|
||||
let affected = diesel::delete(
|
||||
integrity_envelope::table
|
||||
.filter(integrity_envelope::entity_kind.eq(E::KIND))
|
||||
.filter(integrity_envelope::entity_id.eq(&*entity_id)),
|
||||
)
|
||||
.execute(conn)
|
||||
.await
|
||||
.map_err(db::DatabaseError::from)?;
|
||||
|
||||
Ok(affected)
|
||||
}
|
||||
|
||||
fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
|
||||
let mut hasher = Sha256::new();
|
||||
payload.hash(&mut hasher);
|
||||
hasher.finalize().into()
|
||||
}
|
||||
|
||||
fn build_mac_input(
|
||||
entity_kind: &str,
|
||||
entity_id: &[u8],
|
||||
payload_version: i32,
|
||||
payload_hash: &[u8; 32],
|
||||
) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(8 + entity_kind.len() + entity_id.len() + 32);
|
||||
push_len_prefixed(&mut out, entity_kind.as_bytes());
|
||||
push_len_prefixed(&mut out, entity_id);
|
||||
out.extend_from_slice(&payload_version.to_be_bytes());
|
||||
out.extend_from_slice(payload_hash);
|
||||
out
|
||||
}
|
||||
|
||||
fn push_len_prefixed(out: &mut Vec<u8>, bytes: &[u8]) {
|
||||
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -62,10 +62,10 @@ impl<T: Hashable> Hashable for Option<T> {
|
||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||
match self {
|
||||
Some(value) => {
|
||||
hasher.update(&[1]);
|
||||
hasher.update([1]);
|
||||
value.hash(hasher);
|
||||
}
|
||||
None => hasher.update(&[0]),
|
||||
None => hasher.update([0]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,12 +96,12 @@ impl Hashable for alloy::primitives::U256 {
|
||||
|
||||
impl Hashable for chrono::Duration {
|
||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||
hasher.update(&self.num_seconds().to_be_bytes());
|
||||
hasher.update(self.num_seconds().to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl Hashable for chrono::DateTime<chrono::Utc> {
|
||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||
hasher.update(&self.timestamp_millis().to_be_bytes());
|
||||
hasher.update(self.timestamp_millis().to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
298
server/crates/arbiter-server/src/crypto/integrity/v1/tests.rs
Normal file
298
server/crates/arbiter-server/src/crypto/integrity/v1/tests.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
use diesel::{ExpressionMethods as _, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::{actor::ActorRef, prelude::Spawn};
|
||||
use sha2::Digest;
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::{Bootstrap, KeyHolder},
|
||||
db::{self, schema},
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
|
||||
use super::hashing::Hashable;
|
||||
use super::{
|
||||
Error, Integrable, check_entity_attestation, lookup_verified, lookup_verified_from_query,
|
||||
sign_entity, verify_entity,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct DummyEntity {
|
||||
payload_version: i32,
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Hashable for DummyEntity {
|
||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||
self.payload_version.hash(hasher);
|
||||
self.payload.hash(hasher);
|
||||
}
|
||||
}
|
||||
impl Integrable for DummyEntity {
|
||||
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());
|
||||
actor
|
||||
.ask(Bootstrap {
|
||||
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
actor
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sign_writes_envelope_and_verify_passes() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: &[u8] = b"entity-id-7";
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap()
|
||||
.drop_verification_provenance();
|
||||
|
||||
let count: i64 = schema::integrity_envelope::table
|
||||
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
||||
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
||||
.count()
|
||||
.get_result(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(count, 1, "envelope row must be created exactly once");
|
||||
let _ = check_entity_attestation(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tampered_mac_fails_verification() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: &[u8] = b"entity-id-11";
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap()
|
||||
.drop_verification_provenance();
|
||||
|
||||
diesel::update(schema::integrity_envelope::table)
|
||||
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
||||
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
||||
.set(schema::integrity_envelope::mac.eq(vec![0u8; 32]))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let err = check_entity_attestation(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, Error::MacMismatch { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn changed_payload_fails_verification() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: &[u8] = b"entity-id-21";
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap()
|
||||
.drop_verification_provenance();
|
||||
|
||||
let tampered = DummyEntity {
|
||||
payload: b"payload-v1-but-tampered".to_vec(),
|
||||
..entity
|
||||
};
|
||||
|
||||
let err = check_entity_attestation(&mut conn, &keyholder, &tampered, ENTITY_ID)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, Error::MacMismatch { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn strict_verify_fails_closed_while_sealed() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: &[u8] = b"entity-id-41";
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap()
|
||||
.drop_verification_provenance();
|
||||
drop(keyholder);
|
||||
|
||||
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||
|
||||
let err = verify_entity(&mut conn, &sealed_keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
||||
));
|
||||
|
||||
let err = lookup_verified(&mut conn, &sealed_keyholder, ENTITY_ID, |_| async {
|
||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lookup_verified_supports_loaded_aggregate() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: i32 = 77;
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap()
|
||||
.drop_verification_provenance();
|
||||
|
||||
let verified = lookup_verified(&mut conn, &keyholder, ENTITY_ID, |_| async {
|
||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(verified.entity.payload, b"payload-v1".to_vec());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extension_trait_lookup_verified_required_works() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: i32 = 79;
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap()
|
||||
.drop_verification_provenance();
|
||||
|
||||
let verified = lookup_verified(&mut conn, &keyholder, ENTITY_ID, |_| {
|
||||
Box::pin(async {
|
||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
})
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(verified.entity.payload, b"payload-v1".to_vec());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lookup_verified_from_query_helpers_work() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: i32 = 80;
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap()
|
||||
.drop_verification_provenance();
|
||||
|
||||
let verified = lookup_verified_from_query(&mut conn, &keyholder, |_| {
|
||||
Box::pin(async {
|
||||
Ok::<_, db::DatabaseError>((
|
||||
ENTITY_ID,
|
||||
DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
},
|
||||
))
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(verified.entity.payload, b"payload-v1".to_vec());
|
||||
|
||||
drop(keyholder);
|
||||
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||
|
||||
let err = lookup_verified_from_query(&mut conn, &sealed_keyholder, |_| {
|
||||
Box::pin(async {
|
||||
Ok::<_, db::DatabaseError>((
|
||||
ENTITY_ID,
|
||||
DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
},
|
||||
))
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
||||
));
|
||||
}
|
||||
463
server/crates/arbiter-server/src/crypto/integrity/v1/verified.rs
Normal file
463
server/crates/arbiter-server/src/crypto/integrity/v1/verified.rs
Normal file
@@ -0,0 +1,463 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
// todo! rewrite macro_rules to derive crate
|
||||
#[macro_export]
|
||||
macro_rules! VerifiedFields {
|
||||
// --- Entry point ---
|
||||
(
|
||||
$(#$attr:tt)*
|
||||
$vis:vis struct $name:ident $(<$($gen:tt),*>)?
|
||||
{
|
||||
$(
|
||||
$field_vis:vis $field_name:ident : $field_ty:ty
|
||||
),* $(,)?
|
||||
}
|
||||
) => {
|
||||
// Attribute-list checks run in isolation — they only receive the attrs,
|
||||
// not the struct body.
|
||||
$crate::VerifiedFields!(@require_repr [$(#$attr)*]);
|
||||
$crate::VerifiedFields!(@reject_packed [$(#$attr)*]);
|
||||
|
||||
paste::paste! {
|
||||
#[doc = concat!(
|
||||
"Field-wise verified counterpart of [`", stringify!($name), "`]."
|
||||
)]
|
||||
//
|
||||
// `#[repr(C)]` is required for the pointer casts in `inherit_ref`
|
||||
// and `inherit` to be sound. Both the source struct (enforced by
|
||||
// `@require_repr`) and this counterpart carry `#[repr(C)]`, which
|
||||
// guarantees matching field offsets. Combined with each
|
||||
// `Verified<F>` being `#[repr(transparent)]` over `F`, the two
|
||||
// structs have identical memory layout.
|
||||
//
|
||||
// `#[repr(transparent)]` is not usable here because it only permits
|
||||
// a single non-ZST field; multi-field structs would fail to compile.
|
||||
#[repr(C)]
|
||||
$vis struct [<Verified $name>] $(<$($gen),*>)?
|
||||
{
|
||||
$(
|
||||
$field_vis $field_name : $crate::crypto::integrity::Verified<$field_ty>
|
||||
),*
|
||||
}
|
||||
|
||||
impl $(<$($gen),*>)?
|
||||
$crate::crypto::integrity::v1::verified::VerifiedFieldsAccessor
|
||||
for $crate::crypto::integrity::Verified<$name $(<$($gen),*>)?>
|
||||
{
|
||||
type Counterpart = [<Verified $name>] $(<$($gen),*>)?;
|
||||
|
||||
fn inherit_ref(&self) -> &Self::Counterpart {
|
||||
// SAFETY: `Self` is `Verified<T>` (transparent over
|
||||
// `T #[repr(C)]`) and `Self::Counterpart` is `#[repr(C)]`
|
||||
// with the same fields in the same order, each wrapped in
|
||||
// a `#[repr(transparent)]` `Verified<F>`. The two types
|
||||
// therefore have identical memory layout, which
|
||||
// `reinterpret_layout_ref` re-checks as size/align
|
||||
// equality at monomorphization.
|
||||
unsafe {
|
||||
$crate::crypto::integrity::v1::verified::reinterpret_layout_ref::<
|
||||
Self,
|
||||
Self::Counterpart,
|
||||
>(self)
|
||||
}
|
||||
}
|
||||
|
||||
fn inherit(self) -> Self::Counterpart {
|
||||
// SAFETY: identical layout — see `inherit_ref`. The owned
|
||||
// helper additionally suppresses the source destructor so
|
||||
// the returned counterpart owns the original bytes (no
|
||||
// double-drop is possible).
|
||||
unsafe {
|
||||
$crate::crypto::integrity::v1::verified::reinterpret_layout::<
|
||||
Self,
|
||||
Self::Counterpart,
|
||||
>(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- @require_repr: ensure `#[repr(C)]` appears in the attribute list ---
|
||||
(@require_repr [#[repr(C)] $($rest:tt)*]) => {};
|
||||
(@require_repr [#$other:tt $($rest:tt)*]) => {
|
||||
$crate::VerifiedFields!(@require_repr [$($rest)*]);
|
||||
};
|
||||
(@require_repr []) => {
|
||||
::std::compile_error!(
|
||||
"VerifiedFields requires `#[repr(C)]` on the struct to guarantee field layout"
|
||||
);
|
||||
};
|
||||
|
||||
// --- @reject_packed: walk attrs and reject any `#[repr(..., packed, ...)]`.
|
||||
//
|
||||
// Without this, a packed struct would still fail at monomorphization via
|
||||
// the const assertions inside the `reinterpret_layout*` helpers, but the
|
||||
// diagnostic would be much harder to read. `align(N)` is *not* rejected
|
||||
// here because const assertions catch alignment mismatches cleanly, and
|
||||
// forbidding it would be unnecessarily restrictive.
|
||||
(@reject_packed [#[repr($($inner:tt)*)] $($rest:tt)*]) => {
|
||||
$crate::VerifiedFields!(@reject_packed_inner [$($inner)*]);
|
||||
$crate::VerifiedFields!(@reject_packed [$($rest)*]);
|
||||
};
|
||||
(@reject_packed [#$other:tt $($rest:tt)*]) => {
|
||||
$crate::VerifiedFields!(@reject_packed [$($rest)*]);
|
||||
};
|
||||
(@reject_packed []) => {};
|
||||
|
||||
(@reject_packed_inner [packed $($rest:tt)*]) => {
|
||||
::std::compile_error!(
|
||||
"VerifiedFields does not support packed layouts; the generated \
|
||||
counterpart would not share layout with the source struct"
|
||||
);
|
||||
};
|
||||
(@reject_packed_inner [$first:tt $($rest:tt)*]) => {
|
||||
$crate::VerifiedFields!(@reject_packed_inner [$($rest)*]);
|
||||
};
|
||||
(@reject_packed_inner []) => {};
|
||||
}
|
||||
|
||||
/// Implemented on `Verified<T>` by [`VerifiedFields!`], exposing the field-wise counterpart.
|
||||
///
|
||||
/// ## Disclaimer
|
||||
/// Do not implement this trait manually. It is intended to be implemented only
|
||||
/// by the `VerifiedFields!` macro, which generates the necessary layout
|
||||
/// guarantees for sound pointer casts.
|
||||
///
|
||||
/// ## Soundness
|
||||
/// When [`verify_entity`][crate::crypto::integrity::verify_entity] attests an
|
||||
/// entity, it returns `Verified<T>` — an aggregate proof over the whole value.
|
||||
/// This trait converts that wrapper into `Counterpart` (e.g.
|
||||
/// `VerifiedMyStruct`), where every field is individually wrapped in
|
||||
/// [`Verified`], allowing verified data to flow into functions that require
|
||||
/// `Verified<FieldType>` without re-verifying.
|
||||
///
|
||||
/// ## Safety
|
||||
/// The conversion is a zero-cost reinterpretation — no copying (beyond a
|
||||
/// bitwise move in the owned variant) or HMAC work occurs. Soundness rests on
|
||||
/// identical memory layout between `Verified<T>` and `Counterpart`:
|
||||
///
|
||||
/// - `T` carries `#[repr(C)]` (enforced by `@require_repr` in the macro).
|
||||
/// - `T` does **not** carry `packed` (enforced by `@reject_packed`).
|
||||
/// - `Counterpart` also carries `#[repr(C)]`, with the same fields in the same
|
||||
/// order.
|
||||
/// - Each `Verified<F>` field is `#[repr(transparent)]` over `F`, so its size
|
||||
/// and alignment match `F` exactly.
|
||||
/// - `Verified<T>` itself is `#[repr(transparent)]` over `T`.
|
||||
///
|
||||
/// As an additional machine-checked guard, [`reinterpret_layout`] and
|
||||
/// [`reinterpret_layout_ref`] assert size/align equality of the two types at
|
||||
/// monomorphization time.
|
||||
///
|
||||
/// The trait is implemented directly on `Verified<T>` (not on `T`), so no
|
||||
/// `Deref`-coercion or auto-ref stripping is needed at call sites — the impl
|
||||
/// is unambiguous.
|
||||
pub trait VerifiedFieldsAccessor {
|
||||
/// The field-wise verified counterpart, e.g. `VerifiedMyStruct`.
|
||||
type Counterpart;
|
||||
|
||||
/// Reinterprets `&self` as `&Counterpart` via a layout-preserving pointer cast.
|
||||
///
|
||||
/// No data is copied and no re-verification occurs. The returned reference
|
||||
/// borrows from `self` and has the same lifetime.
|
||||
fn inherit_ref(&self) -> &Self::Counterpart;
|
||||
|
||||
/// Consumes `self` and returns `Counterpart` via a layout-preserving
|
||||
/// bitwise move.
|
||||
///
|
||||
/// The original `Verified<T>` is moved without running its destructor
|
||||
/// (there is none — `Verified` is a transparent wrapper with no heap
|
||||
/// allocation), and the returned counterpart owns the original bytes. No
|
||||
/// re-verification occurs.
|
||||
fn inherit(self) -> Self::Counterpart;
|
||||
}
|
||||
|
||||
/// A value whose integrity has been verified against the HMAC envelope stored
|
||||
/// in the database.
|
||||
///
|
||||
/// `Verified<T>` is a zero-cost transparent wrapper produced exclusively by
|
||||
/// [`crate::crypto::integrity`](super) module's functions. Holding one is proof
|
||||
/// that the underlying value passed an HMAC check keyed with the vault's
|
||||
/// integrity subkey.
|
||||
///
|
||||
/// The wrapper is intentionally narrow: it does not expose a constructor and
|
||||
/// the inner value cannot be moved out without explicitly calling
|
||||
/// [`drop_verification_provenance`][Verified::drop_verification_provenance],
|
||||
/// making accidental provenance loss visible at the call site.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
#[must_use = "Verified<T> is a proof-bearing wrapper; use self.drop_verification_provenance() to explicitly discard integrity provenance when needed"]
|
||||
pub struct Verified<T>(T);
|
||||
|
||||
impl<T> AsRef<Verified<T>> for Verified<&T> {
|
||||
fn as_ref(&self) -> &Verified<T> {
|
||||
// SAFETY: `Verified<T>` is `#[repr(transparent)]` over `T`, so `&T`
|
||||
// and `&Verified<T>` have identical layout.
|
||||
unsafe { reinterpret_layout_ref::<T, Verified<T>>(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Verified<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Verified<T> {
|
||||
/// Unwraps the verified value, discarding the integrity provenance.
|
||||
///
|
||||
/// The name is intentionally verbose — call sites where provenance is
|
||||
/// dropped should be easy to find and audit.
|
||||
pub fn drop_verification_provenance(self) -> T {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Constructs a `Verified<T>` by wrapping a `T`.
|
||||
pub(super) fn new(value: T) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Constructs a `Verified<T>` from a raw value without performing any
|
||||
/// integrity check. Only available in test builds; use the integrity
|
||||
/// module's functions to obtain a `Verified<T>` in production code.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new_unchecked(value: T) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Reinterprets `&T` as `&Verified<T>`.
|
||||
#[allow(dead_code)]
|
||||
pub(super) fn from_ref(from: &T) -> &Self {
|
||||
// SAFETY: `Self` is `#[repr(transparent)]` over `T`.
|
||||
unsafe { reinterpret_layout_ref::<T, Self>(from) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Bit-copies `value: From` into a `To`, suppressing the source destructor so
|
||||
/// the destination owns the bytes.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must guarantee that `From` and `To` have identical in-memory
|
||||
/// layout — the raw bytes that encode a valid `From` must also encode a valid
|
||||
/// `To`.
|
||||
///
|
||||
/// A `union` is used instead of [`std::mem::transmute`] because `transmute`
|
||||
/// rejects generic source/destination types at the call site even when their
|
||||
/// sizes are provably equal at monomorphization time.
|
||||
#[allow(dead_code)]
|
||||
#[inline]
|
||||
pub const unsafe fn reinterpret_layout<From, To>(value: From) -> To {
|
||||
const {
|
||||
assert!(
|
||||
::std::mem::size_of::<From>() == ::std::mem::size_of::<To>(),
|
||||
"reinterpret_layout: source and destination must have identical size"
|
||||
);
|
||||
assert!(
|
||||
::std::mem::align_of::<From>() == ::std::mem::align_of::<To>(),
|
||||
"reinterpret_layout: source and destination must have identical alignment"
|
||||
);
|
||||
}
|
||||
union Reinterpret<A, B> {
|
||||
from: ::std::mem::ManuallyDrop<A>,
|
||||
to: ::std::mem::ManuallyDrop<B>,
|
||||
}
|
||||
// SAFETY: caller guarantees layout equivalence (see fn docs). The union
|
||||
// write-read copies the raw bytes of `value` into a `To` slot, and
|
||||
// `ManuallyDrop` on the source side suppresses its destructor so the
|
||||
// destination owns the bytes unambiguously — no double-drop is possible.
|
||||
unsafe {
|
||||
::std::mem::ManuallyDrop::into_inner(
|
||||
Reinterpret {
|
||||
from: ::std::mem::ManuallyDrop::new(value),
|
||||
}
|
||||
.to,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reinterprets `&From` as `&To` via a layout-preserving pointer cast.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Same invariants as [`reinterpret_layout`].
|
||||
#[inline]
|
||||
pub const unsafe fn reinterpret_layout_ref<From, To>(value: &From) -> &To {
|
||||
const {
|
||||
assert!(
|
||||
::std::mem::size_of::<From>() == ::std::mem::size_of::<To>(),
|
||||
"reinterpret_layout_ref: source and destination must have identical size"
|
||||
);
|
||||
assert!(
|
||||
::std::mem::align_of::<From>() == ::std::mem::align_of::<To>(),
|
||||
"reinterpret_layout_ref: source and destination must have identical alignment"
|
||||
);
|
||||
}
|
||||
// SAFETY: caller guarantees layout equivalence (see fn docs). A reference
|
||||
// cast between identically-laid-out types produces a reference with the
|
||||
// same address and lifetime, which is sound.
|
||||
unsafe { &*(value as *const From as *const To) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(VerifiedFields!)]
|
||||
#[repr(C)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct MyStruct<T> {
|
||||
pub field1: String,
|
||||
pub field2: T,
|
||||
}
|
||||
|
||||
fn verify<T>(t: T) -> Verified<T> {
|
||||
Verified(t)
|
||||
}
|
||||
|
||||
// --- inherit_ref ---
|
||||
|
||||
// Verifies that `inherit_ref` returns a reference to the same memory
|
||||
// address, confirming that no copy is made and the cast is purely a
|
||||
// reinterpretation.
|
||||
#[test]
|
||||
fn inherit_ref_is_same_address() {
|
||||
let v = verify(MyStruct {
|
||||
field1: "hello".into(),
|
||||
field2: 42u32,
|
||||
});
|
||||
let fields = v.inherit_ref();
|
||||
assert_eq!(
|
||||
&v as *const _ as *const u8, fields as *const _ as *const u8,
|
||||
"inherit_ref must return a pointer to the same memory, not a copy"
|
||||
);
|
||||
}
|
||||
|
||||
// Verifies that field values are correctly accessible after `inherit_ref`.
|
||||
#[test]
|
||||
fn inherit_ref_field_values() {
|
||||
let v = verify(MyStruct {
|
||||
field1: "hello".into(),
|
||||
field2: 99u32,
|
||||
});
|
||||
let fields = v.inherit_ref();
|
||||
assert_eq!(*fields.field1, "hello");
|
||||
assert_eq!(*fields.field2, 99u32);
|
||||
}
|
||||
|
||||
// Verifies that casting the counterpart back to `Verified<T>` via a raw
|
||||
// pointer lands on the original address — confirms the round-trip is a
|
||||
// pure reinterpretation.
|
||||
#[test]
|
||||
fn inherit_ref_cast_roundtrip() {
|
||||
let v = verify(MyStruct {
|
||||
field1: "x".into(),
|
||||
field2: 7u32,
|
||||
});
|
||||
let fields: &VerifiedMyStruct<u32> = v.inherit_ref();
|
||||
let back_ptr = fields as *const VerifiedMyStruct<u32> as *const Verified<MyStruct<u32>>;
|
||||
assert_eq!(
|
||||
back_ptr as *const u8, &v as *const _ as *const u8,
|
||||
"cast of counterpart must point back to the same Verified<T>"
|
||||
);
|
||||
}
|
||||
|
||||
// ZST fields must still produce a counterpart with identical layout — the
|
||||
// const asserts in `reinterpret_layout_ref` guard this at monomorphization.
|
||||
#[test]
|
||||
fn inherit_ref_with_zst_field() {
|
||||
#[derive(VerifiedFields!)]
|
||||
#[repr(C)]
|
||||
struct WithZst {
|
||||
pub unit: (),
|
||||
pub val: u64,
|
||||
}
|
||||
|
||||
let v = Verified(WithZst { unit: (), val: 777 });
|
||||
let fields = v.inherit_ref();
|
||||
assert_eq!(*fields.val, 777);
|
||||
assert_eq!(*fields.unit, ());
|
||||
}
|
||||
|
||||
// --- inherit ---
|
||||
|
||||
// Verifies that `inherit` preserves field values in the owned counterpart.
|
||||
#[test]
|
||||
fn inherit_field_values() {
|
||||
let v = verify(MyStruct {
|
||||
field1: "world".into(),
|
||||
field2: 1234u64,
|
||||
});
|
||||
let VerifiedMyStruct { field1, field2 } = v.inherit();
|
||||
assert_eq!(*field1, "world");
|
||||
assert_eq!(*field2, 1234u64);
|
||||
}
|
||||
|
||||
// Verifies that `inherit` does not double-drop the inner value.
|
||||
// If `ManuallyDrop` handling is wrong, running under Miri or with a drop
|
||||
// counter catches a double-free.
|
||||
#[test]
|
||||
fn inherit_no_double_drop() {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
struct DropCounter;
|
||||
impl Drop for DropCounter {
|
||||
fn drop(&mut self) {
|
||||
DROP_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(VerifiedFields!)]
|
||||
#[repr(C)]
|
||||
struct WithDrop {
|
||||
pub val: DropCounter,
|
||||
}
|
||||
|
||||
DROP_COUNT.store(0, Ordering::Relaxed);
|
||||
{
|
||||
let v = Verified(WithDrop { val: DropCounter });
|
||||
let _ = v.inherit();
|
||||
}
|
||||
assert_eq!(
|
||||
DROP_COUNT.load(Ordering::Relaxed),
|
||||
1,
|
||||
"DropCounter must be dropped exactly once"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Verified::from_ref ---
|
||||
|
||||
#[test]
|
||||
fn from_ref_is_same_address() {
|
||||
let val = 42u32;
|
||||
let verified: &Verified<u32> = Verified::from_ref(&val);
|
||||
assert_eq!(
|
||||
&val as *const u32 as *const u8, verified as *const _ as *const u8,
|
||||
"from_ref must alias the original reference, not copy the value"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ref_value_preserved() {
|
||||
let val = String::from("test");
|
||||
let verified: &Verified<String> = Verified::from_ref(&val);
|
||||
assert_eq!(**verified, "test");
|
||||
}
|
||||
|
||||
// --- AsRef<Verified<T>> for Verified<&T> ---
|
||||
|
||||
#[test]
|
||||
fn verified_ref_as_ref_is_same_address() {
|
||||
let val = 99u32;
|
||||
let vref: Verified<&u32> = Verified(&val);
|
||||
let v: &Verified<u32> = vref.as_ref();
|
||||
assert_eq!(
|
||||
&val as *const u32 as *const u8, v as *const _ as *const u8,
|
||||
"AsRef<Verified<T>> for Verified<&T> must alias the referent, not copy it"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use kameo::actor::ActorRef;
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::KeyHolder,
|
||||
crypto::integrity,
|
||||
crypto::integrity::{self, Verified, VerifiedEntity, verified::VerifiedFieldsAccessor},
|
||||
db::{
|
||||
self, DatabaseError,
|
||||
models::{
|
||||
@@ -153,12 +153,39 @@ impl Engine {
|
||||
{
|
||||
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||
|
||||
let grant = P::try_find_grant(&context, &mut conn)
|
||||
let verified_settings =
|
||||
match integrity::lookup_verified_from_query(&mut conn, &self.keyholder, |conn| {
|
||||
let context = context.clone();
|
||||
Box::pin(async move {
|
||||
let grant = P::try_find_grant(&context, conn)
|
||||
.await
|
||||
.map_err(DatabaseError::from)?
|
||||
.ok_or_else(|| DatabaseError::from(diesel::result::Error::NotFound))?;
|
||||
|
||||
Ok::<_, DatabaseError>((grant.common_settings_id, grant.settings))
|
||||
})
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(verified) => verified,
|
||||
Err(integrity::Error::Database(DatabaseError::Connection(
|
||||
diesel::result::Error::NotFound,
|
||||
))) => return Err(PolicyError::NoMatchingGrant),
|
||||
Err(err) => return Err(PolicyError::Integrity(err)),
|
||||
};
|
||||
|
||||
let mut grant = P::try_find_grant(&context, &mut conn)
|
||||
.await
|
||||
.map_err(DatabaseError::from)?
|
||||
.ok_or(PolicyError::NoMatchingGrant)?;
|
||||
|
||||
integrity::verify_entity(&mut conn, &self.keyholder, &grant.settings, grant.id).await?;
|
||||
// IMPORTANT: policy evaluation uses extra non-integrity fields from Grant
|
||||
// (e.g., per-policy ids), so we currently reload Grant after the query-native
|
||||
// integrity check over canonicalized settings.
|
||||
grant.settings = verified_settings
|
||||
.inherit()
|
||||
.entity
|
||||
.drop_verification_provenance();
|
||||
|
||||
let mut violations = check_shared_constraints(
|
||||
&context,
|
||||
@@ -214,7 +241,7 @@ impl Engine {
|
||||
pub async fn create_grant<P: Policy>(
|
||||
&self,
|
||||
full_grant: CombinedSettings<P::Settings>,
|
||||
) -> Result<i32, DatabaseError>
|
||||
) -> Result<Verified<i32>, DatabaseError>
|
||||
where
|
||||
P::Settings: Clone,
|
||||
{
|
||||
@@ -258,11 +285,12 @@ impl Engine {
|
||||
|
||||
P::create_grant(&basic_grant, &full_grant.specific, conn).await?;
|
||||
|
||||
let verified_entity_id =
|
||||
integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id)
|
||||
.await
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
|
||||
QueryResult::Ok(basic_grant.id)
|
||||
QueryResult::Ok(verified_entity_id)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
@@ -273,7 +301,7 @@ impl Engine {
|
||||
async fn list_one_kind<Kind: Policy, Y>(
|
||||
&self,
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
) -> Result<impl Iterator<Item = Grant<Y>>, ListError>
|
||||
) -> Result<Vec<Grant<Y>>, ListError>
|
||||
where
|
||||
Y: From<Kind::Settings>,
|
||||
{
|
||||
@@ -281,16 +309,32 @@ impl Engine {
|
||||
.await
|
||||
.map_err(DatabaseError::from)?;
|
||||
|
||||
// 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?;
|
||||
let mut verified_grants = Vec::with_capacity(all_grants.len());
|
||||
|
||||
// Verify integrity of all grants before returning any results.
|
||||
for grant in all_grants {
|
||||
let VerifiedEntity {
|
||||
entity: verified_settings,
|
||||
entity_id: _,
|
||||
} = integrity::verify_entity(
|
||||
conn,
|
||||
&self.keyholder,
|
||||
grant.settings,
|
||||
grant.common_settings_id,
|
||||
)
|
||||
.await?
|
||||
.inherit();
|
||||
|
||||
verified_grants.push(Grant {
|
||||
id: grant.id,
|
||||
common_settings_id: grant.common_settings_id,
|
||||
settings: verified_settings
|
||||
.drop_verification_provenance()
|
||||
.generalize(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(all_grants.into_iter().map(|g| Grant {
|
||||
id: g.id,
|
||||
common_settings_id: g.common_settings_id,
|
||||
settings: g.settings.generalize(),
|
||||
}))
|
||||
Ok(verified_grants)
|
||||
}
|
||||
|
||||
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListError> {
|
||||
|
||||
@@ -200,7 +200,7 @@ pub enum SpecificGrant {
|
||||
TokenTransfer(token_transfers::Settings),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CombinedSettings<PolicyGrant> {
|
||||
pub shared: SharedGrantSettings,
|
||||
pub specific: PolicyGrant,
|
||||
|
||||
@@ -110,7 +110,8 @@ async fn check_rate_limits(
|
||||
let mut violations = Vec::new();
|
||||
let window = grant.settings.specific.limit.window;
|
||||
|
||||
let past_transaction = query_relevant_past_transaction(grant.id, window, db).await?;
|
||||
let past_transaction =
|
||||
query_relevant_past_transaction(grant.common_settings_id, window, db).await?;
|
||||
|
||||
let window_start = chrono::Utc::now() - grant.settings.specific.limit.window;
|
||||
let prospective_cumulative_volume: U256 = past_transaction
|
||||
@@ -249,21 +250,20 @@ impl Policy for EtherTransfer {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let settings = Settings {
|
||||
target: targets,
|
||||
limit: VolumeRateLimit {
|
||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume)
|
||||
.map_err(|err| diesel::result::Error::DeserializationError(Box::new(err)))?,
|
||||
window: chrono::Duration::seconds(limit.window_secs as i64),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Some(Grant {
|
||||
id: grant.id,
|
||||
common_settings_id: grant.basic_grant_id,
|
||||
settings: CombinedSettings {
|
||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||
specific: settings,
|
||||
specific: Settings {
|
||||
target: targets,
|
||||
limit: VolumeRateLimit {
|
||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(|err| {
|
||||
diesel::result::Error::DeserializationError(Box::new(err))
|
||||
})?,
|
||||
window: chrono::Duration::seconds(limit.window_secs as i64),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -286,18 +286,16 @@ impl Policy for TokenTransfer {
|
||||
}
|
||||
};
|
||||
|
||||
let settings = Settings {
|
||||
token_contract: Address::from(token_contract),
|
||||
target,
|
||||
volume_limits,
|
||||
};
|
||||
|
||||
Ok(Some(Grant {
|
||||
id: token_grant.id,
|
||||
common_settings_id: token_grant.basic_grant_id,
|
||||
settings: CombinedSettings {
|
||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||
specific: settings,
|
||||
specific: Settings {
|
||||
token_contract: Address::from(token_contract),
|
||||
target,
|
||||
volume_limits,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
actors::client::{self, ClientConnection, auth},
|
||||
crypto::integrity::Verified,
|
||||
grpc::request_tracker::RequestTracker,
|
||||
};
|
||||
|
||||
@@ -68,6 +69,7 @@ impl<'a> AuthTransportAdapter<'a> {
|
||||
auth::Error::ApproveError(auth::ApproveError::Internal)
|
||||
| auth::Error::DatabasePoolUnavailable
|
||||
| auth::Error::DatabaseOperationFailed
|
||||
| auth::Error::IntegrityCheckFailed
|
||||
| auth::Error::Transport => ProtoAuthResult::Internal,
|
||||
}
|
||||
.into(),
|
||||
@@ -199,7 +201,7 @@ pub async fn start(
|
||||
conn: &mut ClientConnection,
|
||||
bi: &mut GrpcBi<ClientRequest, ClientResponse>,
|
||||
request_tracker: &mut RequestTracker,
|
||||
) -> Result<i32, auth::Error> {
|
||||
) -> Result<Verified<i32>, auth::Error> {
|
||||
let mut transport = AuthTransportAdapter::new(bi, request_tracker);
|
||||
client::auth::authenticate(conn, &mut transport).await
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ async fn handle_wallet_create(
|
||||
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||
let result = match actor.ask(HandleEvmWalletCreate {}).await {
|
||||
Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry {
|
||||
id: wallet_id,
|
||||
id: wallet_id.drop_verification_provenance(),
|
||||
address: address.to_vec(),
|
||||
}),
|
||||
Err(err) => {
|
||||
@@ -121,6 +121,9 @@ async fn handle_grant_list(
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
||||
EvmGrantListResult::Error(ProtoEvmError::VaultSealed.into())
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(error = ?err, "Failed to list EVM grants");
|
||||
EvmGrantListResult::Error(ProtoEvmError::Internal.into())
|
||||
@@ -147,7 +150,7 @@ async fn handle_grant_create(
|
||||
.try_convert()?;
|
||||
|
||||
let result = match actor.ask(HandleGrantCreate { basic, grant }).await {
|
||||
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id),
|
||||
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id.drop_verification_provenance()),
|
||||
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
||||
EvmGrantCreateResult::Error(ProtoEvmError::VaultSealed.into())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::context::ServerContext;
|
||||
|
||||
#[macro_use]
|
||||
extern crate macro_rules_attribute;
|
||||
|
||||
pub mod actors;
|
||||
pub mod context;
|
||||
pub mod crypto;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
struct DeferClosure<F: FnOnce()> {
|
||||
f: Option<F>,
|
||||
}
|
||||
@@ -14,3 +16,19 @@ impl<F: FnOnce()> Drop for DeferClosure<F> {
|
||||
pub fn defer<F: FnOnce()>(f: F) -> impl Drop + Sized {
|
||||
DeferClosure { f: Some(f) }
|
||||
}
|
||||
|
||||
/// A trait for casting between two transparently wrapped types with identical memory layouts.
|
||||
///
|
||||
/// [`ReinterpretWrapper`] enables zero-cost conversions between two types (`Self` and `Counterpart`)
|
||||
/// that wrap the same underlying data but differ in how that data is presented. Both types must
|
||||
/// transparently wrap the same "deref target" and provide bidirectional `AsRef` conversions.
|
||||
pub trait ReinterpretWrapper<Counterpart>
|
||||
where
|
||||
Self: Deref<Target = Self::Inner> + AsRef<Counterpart>,
|
||||
Counterpart: Deref<Target = Self::Inner> + AsRef<Self>,
|
||||
{
|
||||
/// The shared target type that both `Self` and `Counterpart` transparently wrap.
|
||||
type Inner;
|
||||
/// Reinterprets `Self` as `Counterpart`.
|
||||
fn reinterpret(self) -> Counterpart;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use arbiter_proto::ClientMetadata;
|
||||
use arbiter_proto::transport::{Receiver, Sender};
|
||||
use arbiter_server::actors::GlobalActors;
|
||||
use arbiter_server::{
|
||||
actors::client::{ClientConnection, auth, connect_client},
|
||||
db,
|
||||
actors::{
|
||||
GlobalActors,
|
||||
client::{ClientConnection, ClientCredentials, auth, connect_client},
|
||||
keyholder::Bootstrap,
|
||||
},
|
||||
crypto::integrity,
|
||||
db::{self, schema},
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
|
||||
use diesel_async::RunQueryDsl;
|
||||
@@ -21,7 +26,8 @@ fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> Cli
|
||||
|
||||
async fn insert_registered_client(
|
||||
db: &db::DatabasePool,
|
||||
pubkey: Vec<u8>,
|
||||
actors: &GlobalActors,
|
||||
pubkey: ed25519_dalek::VerifyingKey,
|
||||
metadata: &ClientMetadata,
|
||||
) {
|
||||
use arbiter_server::db::schema::{client_metadata, program_client};
|
||||
@@ -37,23 +43,64 @@ async fn insert_registered_client(
|
||||
.get_result(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_into(program_client::table)
|
||||
let client_id: i32 = insert_into(program_client::table)
|
||||
.values((
|
||||
program_client::public_key.eq(pubkey),
|
||||
program_client::public_key.eq(pubkey.to_bytes().to_vec()),
|
||||
program_client::metadata_id.eq(metadata_id),
|
||||
))
|
||||
.returning(program_client::id)
|
||||
.get_result(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _ = integrity::sign_entity(
|
||||
&mut conn,
|
||||
&actors.key_holder,
|
||||
&ClientCredentials { pubkey, nonce: 1 },
|
||||
client_id,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) {
|
||||
let mut conn = db.get().await.unwrap();
|
||||
let sentinel_key = ed25519_dalek::SigningKey::generate(&mut rand::rng())
|
||||
.verifying_key()
|
||||
.to_bytes()
|
||||
.to_vec();
|
||||
|
||||
insert_into(schema::useragent_client::table)
|
||||
.values((
|
||||
schema::useragent_client::public_key.eq(sentinel_key),
|
||||
schema::useragent_client::key_type.eq(1i32),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors {
|
||||
insert_bootstrap_sentinel_useragent(db).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();
|
||||
actors
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
pub async fn test_unregistered_pubkey_rejected() {
|
||||
let db = db::create_test_pool().await;
|
||||
|
||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||
let actors = spawn_test_actors(&db).await;
|
||||
let props = ClientConnection::new(db.clone(), actors);
|
||||
let task = tokio::spawn(async move {
|
||||
let mut server_transport = server_transport;
|
||||
@@ -78,20 +125,19 @@ pub async fn test_unregistered_pubkey_rejected() {
|
||||
#[test_log::test]
|
||||
pub async fn test_challenge_auth() {
|
||||
let db = db::create_test_pool().await;
|
||||
let actors = spawn_test_actors(&db).await;
|
||||
|
||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||
|
||||
insert_registered_client(
|
||||
&db,
|
||||
pubkey_bytes.clone(),
|
||||
&actors,
|
||||
new_key.verifying_key(),
|
||||
&metadata("client", Some("desc"), Some("1.0.0")),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||
|
||||
let props = ClientConnection::new(db.clone(), actors);
|
||||
let task = tokio::spawn(async move {
|
||||
let mut server_transport = server_transport;
|
||||
@@ -147,34 +193,13 @@ pub async fn test_challenge_auth() {
|
||||
#[test_log::test]
|
||||
pub async fn test_metadata_unchanged_does_not_append_history() {
|
||||
let db = db::create_test_pool().await;
|
||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||
let props = ClientConnection::new(db.clone(), actors);
|
||||
|
||||
let actors = spawn_test_actors(&db).await;
|
||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||
let requested = metadata("client", Some("desc"), Some("1.0.0"));
|
||||
|
||||
{
|
||||
use arbiter_server::db::schema::{client_metadata, program_client};
|
||||
let mut conn = db.get().await.unwrap();
|
||||
let metadata_id: i32 = insert_into(client_metadata::table)
|
||||
.values((
|
||||
client_metadata::name.eq(&requested.name),
|
||||
client_metadata::description.eq(&requested.description),
|
||||
client_metadata::version.eq(&requested.version),
|
||||
))
|
||||
.returning(client_metadata::id)
|
||||
.get_result(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_into(program_client::table)
|
||||
.values((
|
||||
program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()),
|
||||
program_client::metadata_id.eq(metadata_id),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
insert_registered_client(&db, &actors, new_key.verifying_key(), &requested).await;
|
||||
|
||||
let props = ClientConnection::new(db.clone(), actors);
|
||||
|
||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||
let task = tokio::spawn(async move {
|
||||
@@ -225,33 +250,18 @@ pub async fn test_metadata_unchanged_does_not_append_history() {
|
||||
#[test_log::test]
|
||||
pub async fn test_metadata_change_appends_history_and_repoints_binding() {
|
||||
let db = db::create_test_pool().await;
|
||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||
let props = ClientConnection::new(db.clone(), actors);
|
||||
|
||||
let actors = spawn_test_actors(&db).await;
|
||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||
|
||||
{
|
||||
use arbiter_server::db::schema::{client_metadata, program_client};
|
||||
let mut conn = db.get().await.unwrap();
|
||||
let metadata_id: i32 = insert_into(client_metadata::table)
|
||||
.values((
|
||||
client_metadata::name.eq("client"),
|
||||
client_metadata::description.eq(Some("old")),
|
||||
client_metadata::version.eq(Some("1.0.0")),
|
||||
))
|
||||
.returning(client_metadata::id)
|
||||
.get_result(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_into(program_client::table)
|
||||
.values((
|
||||
program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()),
|
||||
program_client::metadata_id.eq(metadata_id),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
insert_registered_client(
|
||||
&db,
|
||||
&actors,
|
||||
new_key.verifying_key(),
|
||||
&metadata("client", Some("old"), Some("1.0.0")),
|
||||
)
|
||||
.await;
|
||||
|
||||
let props = ClientConnection::new(db.clone(), actors);
|
||||
|
||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||
let task = tokio::spawn(async move {
|
||||
@@ -322,3 +332,59 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() {
|
||||
let db = db::create_test_pool().await;
|
||||
let actors = spawn_test_actors(&db).await;
|
||||
|
||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||
let requested = metadata("client", Some("desc"), Some("1.0.0"));
|
||||
|
||||
{
|
||||
use arbiter_server::db::schema::{client_metadata, program_client};
|
||||
let mut conn = db.get().await.unwrap();
|
||||
let metadata_id: i32 = insert_into(client_metadata::table)
|
||||
.values((
|
||||
client_metadata::name.eq(&requested.name),
|
||||
client_metadata::description.eq(&requested.description),
|
||||
client_metadata::version.eq(&requested.version),
|
||||
))
|
||||
.returning(client_metadata::id)
|
||||
.get_result(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_into(program_client::table)
|
||||
.values((
|
||||
program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()),
|
||||
program_client::metadata_id.eq(metadata_id),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||
let props = ClientConnection::new(db.clone(), actors);
|
||||
let task = tokio::spawn(async move {
|
||||
let mut server_transport = server_transport;
|
||||
connect_client(props, &mut server_transport).await;
|
||||
});
|
||||
|
||||
test_transport
|
||||
.send(auth::Inbound::AuthChallengeRequest {
|
||||
pubkey: new_key.verifying_key(),
|
||||
metadata: requested,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let response = test_transport
|
||||
.recv()
|
||||
.await
|
||||
.expect("should receive auth rejection");
|
||||
assert!(matches!(response, Err(auth::Error::IntegrityCheckFailed)));
|
||||
|
||||
task.await.unwrap();
|
||||
}
|
||||
|
||||
@@ -139,7 +139,8 @@ pub async fn test_challenge_auth() {
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
.drop_verification_provenance();
|
||||
}
|
||||
|
||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||
@@ -278,7 +279,8 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
.drop_verification_provenance();
|
||||
}
|
||||
|
||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||
|
||||
@@ -11,8 +11,6 @@ use arbiter_server::{
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||
use diesel::{ExpressionMethods as _, QueryDsl as _, insert_into};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::actor::Spawn as _;
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user