feat(integrity): derive-like macro VerifiedFields that allows to inherit Verified<T> type's provenance to all fields of T
This commit is contained in:
@@ -61,6 +61,8 @@ 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"
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::{
|
||||
flow_coordinator::{self, RequestClientApproval},
|
||||
keyholder::KeyHolder,
|
||||
},
|
||||
crypto::integrity::{self},
|
||||
crypto::integrity::{self, Verified, verified::VerifiedFieldsAccessor},
|
||||
db::{
|
||||
self,
|
||||
models::{ProgramClientMetadata, SqliteTimestamp},
|
||||
@@ -99,39 +99,6 @@ async fn get_current_nonce_and_id(
|
||||
})
|
||||
}
|
||||
|
||||
async fn verify_integrity(
|
||||
db: &db::DatabasePool,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
pubkey: &VerifyingKey,
|
||||
) -> Result<(), Error> {
|
||||
let mut db_conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::DatabasePoolUnavailable
|
||||
})?;
|
||||
|
||||
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?.ok_or_else(|| {
|
||||
error!("Client not found during integrity verification");
|
||||
Error::DatabaseOperationFailed
|
||||
})?;
|
||||
|
||||
integrity::verify_entity(
|
||||
&mut db_conn,
|
||||
keyholder,
|
||||
&ClientCredentials {
|
||||
pubkey: *pubkey,
|
||||
nonce,
|
||||
},
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Integrity verification failed");
|
||||
Error::IntegrityCheckFailed
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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(
|
||||
@@ -169,7 +136,8 @@ async fn create_nonce(
|
||||
.map_err(|e| {
|
||||
error!(?e, "Integrity sign failed after nonce update");
|
||||
Error::DatabaseOperationFailed
|
||||
})?;
|
||||
})?
|
||||
.drop_verification_provenance();
|
||||
|
||||
Ok(new_nonce)
|
||||
})
|
||||
@@ -205,7 +173,7 @@ async fn insert_client(
|
||||
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();
|
||||
|
||||
@@ -240,7 +208,7 @@ async fn insert_client(
|
||||
.get_result::<i32>(conn)
|
||||
.await?;
|
||||
|
||||
integrity::sign_entity(
|
||||
let verified_id = integrity::sign_entity(
|
||||
conn,
|
||||
&keyholder,
|
||||
&ClientCredentials {
|
||||
@@ -255,7 +223,7 @@ async fn insert_client(
|
||||
Error::DatabaseOperationFailed
|
||||
})?;
|
||||
|
||||
Ok(client_id)
|
||||
Ok(verified_id)
|
||||
})
|
||||
})
|
||||
.await
|
||||
@@ -368,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,
|
||||
{
|
||||
@@ -376,10 +347,27 @@ where
|
||||
return Err(Error::Transport);
|
||||
};
|
||||
|
||||
// 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((id, _)) => {
|
||||
verify_integrity(&props.db, &props.actors.key_holder, &pubkey).await?;
|
||||
id
|
||||
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(
|
||||
@@ -394,7 +382,7 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
sync_client_metadata(&props.db, client_id, &metadata).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?;
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use rand::{SeedableRng, rng, rngs::StdRng};
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
||||
crypto::integrity,
|
||||
crypto::integrity::{self, Integrable, Verified, hashing::Hashable},
|
||||
db::{
|
||||
DatabaseError, DatabasePool,
|
||||
models::{self},
|
||||
@@ -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]
|
||||
@@ -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?;
|
||||
|
||||
@@ -138,7 +138,8 @@ async fn create_nonce(
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Error::internal("Database error", &e))?;
|
||||
.map_err(|e| Error::internal("Database error", &e))?
|
||||
.drop_verification_provenance();
|
||||
|
||||
Result::<_, Error>::Ok(new_nonce)
|
||||
})
|
||||
|
||||
@@ -81,7 +81,8 @@ impl UserAgentSession {
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::internal(format!("Failed to backfill user-agent integrity: {e}"))
|
||||
})?;
|
||||
})?
|
||||
.drop_verification_provenance();
|
||||
}
|
||||
|
||||
Result::<_, Error>::Ok(())
|
||||
@@ -357,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!(
|
||||
|
||||
@@ -11,6 +11,7 @@ use kameo::{actor::ActorRef, error::SendError};
|
||||
use sha2::Digest as _;
|
||||
|
||||
pub mod hashing;
|
||||
pub mod verified;
|
||||
use self::hashing::Hashable;
|
||||
|
||||
use crate::{
|
||||
@@ -22,6 +23,12 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
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}")]
|
||||
@@ -56,62 +63,14 @@ pub enum AttestationStatus {
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Verified<T>(T);
|
||||
|
||||
impl<T> AsRef<T> for Verified<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Verified<T> {
|
||||
pub fn into_inner(self) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Verified<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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<T: Integrable> Integrable for &T {
|
||||
const KIND: &'static str = T::KIND;
|
||||
const VERSION: i32 = T::VERSION;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -137,52 +96,32 @@ impl From<&'_ [u8]> for EntityId {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lookup_verified<E, C, F, Fut>(
|
||||
pub async fn lookup_verified<E, Id, C, F, Fut>(
|
||||
conn: &mut C,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
entity_id: impl Into<EntityId>,
|
||||
entity_id: Id,
|
||||
load: F,
|
||||
) -> Result<Verified<E>, Error>
|
||||
) -> 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?;
|
||||
Ok(Verified(entity))
|
||||
}
|
||||
|
||||
pub async fn lookup_verified_allow_unavailable<E, C, F, Fut>(
|
||||
conn: &mut C,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
entity_id: impl Into<EntityId>,
|
||||
load: F,
|
||||
) -> Result<Verified<E>, Error>
|
||||
where
|
||||
C: AsyncConnection<Backend = Sqlite>,
|
||||
E: Integrable+ 'static,
|
||||
F: FnOnce(&mut C) -> Fut,
|
||||
Fut: Future<Output = Result<E, db::DatabaseError>>,
|
||||
{
|
||||
let entity = load(conn).await?;
|
||||
match check_entity_attestation(conn, keyholder, &entity, entity_id.into()).await? {
|
||||
// IMPORTANT: allow_unavailable mode must succeed with an unattested result when vault key
|
||||
// material is unavailable, otherwise integrity checks can be silently bypassed while sealed.
|
||||
AttestationStatus::Attested | AttestationStatus::Unavailable => Ok(Verified(entity)),
|
||||
}
|
||||
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<E>, Error>
|
||||
) -> Result<Verified<Entity<E, Id>>, Error>
|
||||
where
|
||||
C: AsyncConnection<Backend = Sqlite> + Send,
|
||||
E: Integrable,
|
||||
Id: Into<EntityId>,
|
||||
Id: Into<EntityId> + Clone,
|
||||
F: for<'a> FnOnce(
|
||||
&'a mut C,
|
||||
) -> Pin<
|
||||
@@ -190,8 +129,7 @@ where
|
||||
>,
|
||||
{
|
||||
let (entity_id, entity) = load(conn).await?;
|
||||
verify_entity(conn, keyholder, &entity, entity_id).await?;
|
||||
Ok(Verified(entity))
|
||||
verify_entity(conn, keyholder, entity, entity_id).await
|
||||
}
|
||||
|
||||
pub async fn sign_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
||||
@@ -236,7 +174,7 @@ pub async fn sign_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
||||
.await
|
||||
.map_err(db::DatabaseError::from)?;
|
||||
|
||||
Ok(Verified(as_entity_id))
|
||||
Ok(Verified::new(as_entity_id))
|
||||
}
|
||||
|
||||
pub async fn check_entity_attestation<E: Integrable>(
|
||||
@@ -289,14 +227,41 @@ pub async fn check_entity_attestation<E: Integrable>(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify_entity<'a, E: Integrable>(
|
||||
#[derive(Debug, Clone, crate::VerifiedFields!)]
|
||||
#[repr(C)]
|
||||
pub struct Entity<E, Id> {
|
||||
pub entity: E,
|
||||
pub entity_id: Id,
|
||||
}
|
||||
|
||||
impl<E, Id> Deref for Entity<E, Id> {
|
||||
type Target = E;
|
||||
|
||||
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: &'a E,
|
||||
entity_id: impl Into<EntityId>,
|
||||
) -> Result<Verified<&'a E>, Error> {
|
||||
match check_entity_attestation::<E>(conn, keyholder, entity, entity_id).await? {
|
||||
AttestationStatus::Attested => Ok(Verified(entity)),
|
||||
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)),
|
||||
}
|
||||
}
|
||||
@@ -319,363 +284,30 @@ pub async fn delete_envelope<E: Integrable>(
|
||||
Ok(affected)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use diesel::{ExpressionMethods as _, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::{actor::ActorRef, prelude::Spawn};
|
||||
use sha2::Digest;
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::{Bootstrap, KeyHolder},
|
||||
db::{self, schema},
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
|
||||
use super::hashing::Hashable;
|
||||
use super::{
|
||||
check_entity_attestation, AttestationStatus, Error, Integrable, lookup_verified,
|
||||
lookup_verified_allow_unavailable, 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();
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
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 allow_unavailable_lookup_passes_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-31";
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
drop(keyholder);
|
||||
|
||||
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||
let status = check_entity_attestation(&mut conn, &sealed_keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(status, AttestationStatus::Unavailable);
|
||||
|
||||
#[expect(clippy::disallowed_methods, reason = "test only")]
|
||||
lookup_verified_allow_unavailable(&mut conn, &sealed_keyholder, ENTITY_ID, |_| async {
|
||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[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(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();
|
||||
|
||||
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.payload, b"payload-v1".to_vec());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lookup_verified_allow_unavailable_works_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: i32 = 78;
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
drop(keyholder);
|
||||
|
||||
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||
|
||||
#[expect(clippy::disallowed_methods, reason = "test only")]
|
||||
lookup_verified_allow_unavailable(&mut conn, &sealed_keyholder, ENTITY_ID, |_| async {
|
||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
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.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();
|
||||
|
||||
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.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)
|
||||
));
|
||||
}
|
||||
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;
|
||||
|
||||
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::{self, Verified},
|
||||
crypto::integrity::{self, Verified, VerifiedEntity, verified::VerifiedFieldsAccessor},
|
||||
db::{
|
||||
self, DatabaseError,
|
||||
models::{
|
||||
@@ -182,7 +182,10 @@ impl Engine {
|
||||
// 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.into_inner();
|
||||
grant.settings = verified_settings
|
||||
.inherit()
|
||||
.entity
|
||||
.drop_verification_provenance();
|
||||
|
||||
let mut violations = check_shared_constraints(
|
||||
&context,
|
||||
@@ -310,18 +313,24 @@ impl Engine {
|
||||
|
||||
// Verify integrity of all grants before returning any results.
|
||||
for grant in all_grants {
|
||||
integrity::verify_entity(
|
||||
let VerifiedEntity {
|
||||
entity: verified_settings,
|
||||
entity_id: _,
|
||||
} = integrity::verify_entity(
|
||||
conn,
|
||||
&self.keyholder,
|
||||
&grant.settings,
|
||||
grant.settings,
|
||||
grant.common_settings_id,
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
.inherit();
|
||||
|
||||
verified_grants.push(Grant {
|
||||
id: grant.id,
|
||||
common_settings_id: grant.common_settings_id,
|
||||
settings: grant.settings.generalize(),
|
||||
settings: verified_settings
|
||||
.drop_verification_provenance()
|
||||
.generalize(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
actors::client::{self, ClientConnection, auth},
|
||||
crypto::integrity::Verified,
|
||||
grpc::request_tracker::RequestTracker,
|
||||
};
|
||||
|
||||
@@ -200,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) => {
|
||||
@@ -150,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.into_inner()),
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ async fn insert_registered_client(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
integrity::sign_entity(
|
||||
let _ = integrity::sign_entity(
|
||||
&mut conn,
|
||||
&actors.key_holder,
|
||||
&ClientCredentials { pubkey, nonce: 1 },
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user