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:
18
server/Cargo.lock
generated
18
server/Cargo.lock
generated
@@ -742,8 +742,10 @@ dependencies = [
|
|||||||
"insta",
|
"insta",
|
||||||
"k256",
|
"k256",
|
||||||
"kameo",
|
"kameo",
|
||||||
|
"macro_rules_attribute",
|
||||||
"memsafe",
|
"memsafe",
|
||||||
"mutants",
|
"mutants",
|
||||||
|
"paste",
|
||||||
"pem",
|
"pem",
|
||||||
"proptest",
|
"proptest",
|
||||||
"prost",
|
"prost",
|
||||||
@@ -3057,6 +3059,22 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"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]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|||||||
@@ -6,6 +6,4 @@ disallowed-methods = [
|
|||||||
{ path = "rsa::RsaPrivateKey::decrypt_blinded", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted." },
|
{ path = "rsa::RsaPrivateKey::decrypt_blinded", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted." },
|
||||||
{ path = "rsa::traits::Decryptor::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). This blocks decrypt() on rsa::{pkcs1v15,oaep}::DecryptingKey." },
|
{ path = "rsa::traits::Decryptor::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). This blocks decrypt() on rsa::{pkcs1v15,oaep}::DecryptingKey." },
|
||||||
{ path = "rsa::traits::RandomizedDecryptor::decrypt_with_rng", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). This blocks decrypt_with_rng() on rsa::{pkcs1v15,oaep}::DecryptingKey." },
|
{ path = "rsa::traits::RandomizedDecryptor::decrypt_with_rng", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). This blocks decrypt_with_rng() on rsa::{pkcs1v15,oaep}::DecryptingKey." },
|
||||||
|
|
||||||
{ path = "arbiter_server::crypto::integrity::v1::lookup_verified_allow_unavailable", reason = "This function allows integrity checks to be bypassed when vault key material is unavailable, which can lead to silent security failures if used incorrectly. It should only be used in specific contexts where this behavior is acceptable, and its use should be carefully audited." },
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ anyhow = "1.0.102"
|
|||||||
serde_with = "3.18.0"
|
serde_with = "3.18.0"
|
||||||
mutants.workspace = true
|
mutants.workspace = true
|
||||||
subtle = "2.6.1"
|
subtle = "2.6.1"
|
||||||
|
macro_rules_attribute = "0.2.2"
|
||||||
|
paste = "1.0.15"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = "1.46.3"
|
insta = "1.46.3"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use crate::{
|
|||||||
flow_coordinator::{self, RequestClientApproval},
|
flow_coordinator::{self, RequestClientApproval},
|
||||||
keyholder::KeyHolder,
|
keyholder::KeyHolder,
|
||||||
},
|
},
|
||||||
crypto::integrity::{self},
|
crypto::integrity::{self, Verified, verified::VerifiedFieldsAccessor},
|
||||||
db::{
|
db::{
|
||||||
self,
|
self,
|
||||||
models::{ProgramClientMetadata, SqliteTimestamp},
|
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.
|
/// Atomically increments the nonce and re-signs the integrity envelope.
|
||||||
/// Returns the new nonce, which is used as the challenge nonce.
|
/// Returns the new nonce, which is used as the challenge nonce.
|
||||||
async fn create_nonce(
|
async fn create_nonce(
|
||||||
@@ -169,7 +136,8 @@ async fn create_nonce(
|
|||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(?e, "Integrity sign failed after nonce update");
|
error!(?e, "Integrity sign failed after nonce update");
|
||||||
Error::DatabaseOperationFailed
|
Error::DatabaseOperationFailed
|
||||||
})?;
|
})?
|
||||||
|
.drop_verification_provenance();
|
||||||
|
|
||||||
Ok(new_nonce)
|
Ok(new_nonce)
|
||||||
})
|
})
|
||||||
@@ -205,7 +173,7 @@ async fn insert_client(
|
|||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
pubkey: &VerifyingKey,
|
pubkey: &VerifyingKey,
|
||||||
metadata: &ClientMetadata,
|
metadata: &ClientMetadata,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<Verified<i32>, Error> {
|
||||||
use crate::db::schema::{client_metadata, program_client};
|
use crate::db::schema::{client_metadata, program_client};
|
||||||
let metadata = metadata.clone();
|
let metadata = metadata.clone();
|
||||||
|
|
||||||
@@ -240,7 +208,7 @@ async fn insert_client(
|
|||||||
.get_result::<i32>(conn)
|
.get_result::<i32>(conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
integrity::sign_entity(
|
let verified_id = integrity::sign_entity(
|
||||||
conn,
|
conn,
|
||||||
&keyholder,
|
&keyholder,
|
||||||
&ClientCredentials {
|
&ClientCredentials {
|
||||||
@@ -255,7 +223,7 @@ async fn insert_client(
|
|||||||
Error::DatabaseOperationFailed
|
Error::DatabaseOperationFailed
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(client_id)
|
Ok(verified_id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -368,7 +336,10 @@ where
|
|||||||
Ok(())
|
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
|
where
|
||||||
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
|
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
|
||||||
{
|
{
|
||||||
@@ -376,10 +347,27 @@ where
|
|||||||
return Err(Error::Transport);
|
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? {
|
let client_id = match get_current_nonce_and_id(&props.db, &pubkey).await? {
|
||||||
Some((id, _)) => {
|
Some((nonce, id)) => {
|
||||||
verify_integrity(&props.db, &props.actors.key_holder, &pubkey).await?;
|
let mut db_conn = props.db.get().await.map_err(|e| {
|
||||||
id
|
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 => {
|
None => {
|
||||||
approve_new_client(
|
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?;
|
let challenge_nonce = create_nonce(&props.db, &props.actors.key_holder, &pubkey).await?;
|
||||||
challenge_client(transport, pubkey, challenge_nonce).await?;
|
challenge_client(transport, pubkey, challenge_nonce).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -5,23 +5,25 @@ use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
|
||||||
client::ClientConnection,
|
client::ClientConnection,
|
||||||
evm::{ClientSignTransaction, SignTransactionError},
|
evm::{ClientSignTransaction, SignTransactionError},
|
||||||
flow_coordinator::RegisterClient,
|
flow_coordinator::RegisterClient,
|
||||||
keyholder::KeyHolderState,
|
keyholder::KeyHolderState,
|
||||||
},
|
},
|
||||||
db,
|
crypto::integrity::Verified,
|
||||||
evm::VetError,
|
evm::VetError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::{actors::GlobalActors, db};
|
||||||
|
|
||||||
pub struct ClientSession {
|
pub struct ClientSession {
|
||||||
props: ClientConnection,
|
props: ClientConnection,
|
||||||
client_id: i32,
|
client_id: Verified<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientSession {
|
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 }
|
Self { props, client_id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +56,7 @@ impl ClientSession {
|
|||||||
.actors
|
.actors
|
||||||
.evm
|
.evm
|
||||||
.ask(ClientSignTransaction {
|
.ask(ClientSignTransaction {
|
||||||
client_id: self.client_id,
|
client_id: *self.client_id,
|
||||||
wallet_address,
|
wallet_address,
|
||||||
transaction,
|
transaction,
|
||||||
})
|
})
|
||||||
@@ -92,11 +94,12 @@ impl Actor for ClientSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ClientSession {
|
impl ClientSession {
|
||||||
|
#[cfg(test)]
|
||||||
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
||||||
let props = ClientConnection::new(db, actors);
|
let props = ClientConnection::new(db, actors);
|
||||||
Self {
|
Self {
|
||||||
props,
|
props,
|
||||||
client_id: 0,
|
client_id: Verified::new_unchecked(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use rand::{SeedableRng, rng, rngs::StdRng};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
||||||
crypto::integrity,
|
crypto::integrity::{self, Integrable, Verified, hashing::Hashable},
|
||||||
db::{
|
db::{
|
||||||
DatabaseError, DatabasePool,
|
DatabaseError, DatabasePool,
|
||||||
models::{self},
|
models::{self},
|
||||||
@@ -26,11 +26,37 @@ use crate::{
|
|||||||
|
|
||||||
pub use crate::evm::safe_signer;
|
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)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SignTransactionError {
|
pub enum SignTransactionError {
|
||||||
#[error("Wallet not found")]
|
#[error("Wallet not found")]
|
||||||
WalletNotFound,
|
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}")]
|
#[error("Database error: {0}")]
|
||||||
Database(#[from] DatabaseError),
|
Database(#[from] DatabaseError),
|
||||||
|
|
||||||
@@ -45,6 +71,9 @@ pub enum SignTransactionError {
|
|||||||
|
|
||||||
#[error("Policy error: {0}")]
|
#[error("Policy error: {0}")]
|
||||||
Vet(#[from] evm::VetError),
|
Vet(#[from] evm::VetError),
|
||||||
|
|
||||||
|
#[error("Integrity error: {0}")]
|
||||||
|
Integrity(#[from] integrity::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@@ -88,7 +117,7 @@ impl EvmActor {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl EvmActor {
|
impl EvmActor {
|
||||||
#[message]
|
#[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 (mut key_cell, address) = safe_signer::generate(&mut self.rng);
|
||||||
|
|
||||||
let plaintext = key_cell.read_inline(|reader| SafeCell::new(reader.to_vec()));
|
let plaintext = key_cell.read_inline(|reader| SafeCell::new(reader.to_vec()));
|
||||||
@@ -110,7 +139,16 @@ impl EvmActor {
|
|||||||
.await
|
.await
|
||||||
.map_err(DatabaseError::from)?;
|
.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]
|
#[message]
|
||||||
@@ -207,9 +245,23 @@ impl EvmActor {
|
|||||||
.optional()
|
.optional()
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or(SignTransactionError::WalletNotFound)?;
|
.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
|
let wallet_access = schema::evm_wallet_access::table
|
||||||
.select(models::EvmWalletAccess::as_select())
|
.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))
|
.filter(schema::evm_wallet_access::client_id.eq(client_id))
|
||||||
.first(&mut conn)
|
.first(&mut conn)
|
||||||
.await
|
.await
|
||||||
@@ -242,9 +294,23 @@ impl EvmActor {
|
|||||||
.optional()
|
.optional()
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or(SignTransactionError::WalletNotFound)?;
|
.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
|
let wallet_access = schema::evm_wallet_access::table
|
||||||
.select(models::EvmWalletAccess::as_select())
|
.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))
|
.filter(schema::evm_wallet_access::client_id.eq(client_id))
|
||||||
.first(&mut conn)
|
.first(&mut conn)
|
||||||
.await
|
.await
|
||||||
@@ -263,6 +329,12 @@ impl EvmActor {
|
|||||||
|
|
||||||
let signer = safe_signer::SafeSigner::from_cell(raw_key)?;
|
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
|
self.engine
|
||||||
.evaluate_transaction(wallet_access, transaction.clone(), RunKind::Execution)
|
.evaluate_transaction(wallet_access, transaction.clone(), RunKind::Execution)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ async fn create_nonce(
|
|||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
.await
|
.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)
|
Result::<_, Error>::Ok(new_nonce)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ impl UserAgentSession {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
Error::internal(format!("Failed to backfill user-agent integrity: {e}"))
|
Error::internal(format!("Failed to backfill user-agent integrity: {e}"))
|
||||||
})?;
|
})?
|
||||||
|
.drop_verification_provenance();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result::<_, Error>::Ok(())
|
Result::<_, Error>::Ok(())
|
||||||
@@ -357,7 +358,9 @@ impl UserAgentSession {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
#[message]
|
#[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 {
|
match self.props.actors.evm.ask(Generate {}).await {
|
||||||
Ok(address) => Ok(address),
|
Ok(address) => Ok(address),
|
||||||
Err(SendError::HandlerError(err)) => Err(Error::internal(format!(
|
Err(SendError::HandlerError(err)) => Err(Error::internal(format!(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use kameo::{actor::ActorRef, error::SendError};
|
|||||||
use sha2::Digest as _;
|
use sha2::Digest as _;
|
||||||
|
|
||||||
pub mod hashing;
|
pub mod hashing;
|
||||||
|
pub mod verified;
|
||||||
use self::hashing::Hashable;
|
use self::hashing::Hashable;
|
||||||
|
|
||||||
use crate::{
|
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)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
@@ -56,62 +63,14 @@ pub enum AttestationStatus {
|
|||||||
Unavailable,
|
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 {
|
pub trait Integrable: Hashable {
|
||||||
const KIND: &'static str;
|
const KIND: &'static str;
|
||||||
const VERSION: i32 = 1;
|
const VERSION: i32 = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
|
impl<T: Integrable> Integrable for &T {
|
||||||
let mut hasher = Sha256::new();
|
const KIND: &'static str = T::KIND;
|
||||||
payload.hash(&mut hasher);
|
const VERSION: i32 = T::VERSION;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[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,
|
conn: &mut C,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
entity_id: impl Into<EntityId>,
|
entity_id: Id,
|
||||||
load: F,
|
load: F,
|
||||||
) -> Result<Verified<E>, Error>
|
) -> Result<Verified<Entity<E, Id>>, Error>
|
||||||
where
|
where
|
||||||
C: AsyncConnection<Backend = Sqlite>,
|
C: AsyncConnection<Backend = Sqlite>,
|
||||||
E: Integrable,
|
E: Integrable,
|
||||||
|
Id: Into<EntityId> + Clone,
|
||||||
F: FnOnce(&mut C) -> Fut,
|
F: FnOnce(&mut C) -> Fut,
|
||||||
Fut: Future<Output = Result<E, db::DatabaseError>>,
|
Fut: Future<Output = Result<E, db::DatabaseError>>,
|
||||||
{
|
{
|
||||||
let entity = load(conn).await?;
|
let entity = load(conn).await?;
|
||||||
verify_entity(conn, keyholder, &entity, entity_id).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)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn lookup_verified_from_query<E, Id, C, F>(
|
pub async fn lookup_verified_from_query<E, Id, C, F>(
|
||||||
conn: &mut C,
|
conn: &mut C,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
load: F,
|
load: F,
|
||||||
) -> Result<Verified<E>, Error>
|
) -> Result<Verified<Entity<E, Id>>, Error>
|
||||||
where
|
where
|
||||||
C: AsyncConnection<Backend = Sqlite> + Send,
|
C: AsyncConnection<Backend = Sqlite> + Send,
|
||||||
E: Integrable,
|
E: Integrable,
|
||||||
Id: Into<EntityId>,
|
Id: Into<EntityId> + Clone,
|
||||||
F: for<'a> FnOnce(
|
F: for<'a> FnOnce(
|
||||||
&'a mut C,
|
&'a mut C,
|
||||||
) -> Pin<
|
) -> Pin<
|
||||||
@@ -190,8 +129,7 @@ where
|
|||||||
>,
|
>,
|
||||||
{
|
{
|
||||||
let (entity_id, entity) = load(conn).await?;
|
let (entity_id, entity) = load(conn).await?;
|
||||||
verify_entity(conn, keyholder, &entity, entity_id).await?;
|
verify_entity(conn, keyholder, entity, entity_id).await
|
||||||
Ok(Verified(entity))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sign_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
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
|
.await
|
||||||
.map_err(db::DatabaseError::from)?;
|
.map_err(db::DatabaseError::from)?;
|
||||||
|
|
||||||
Ok(Verified(as_entity_id))
|
Ok(Verified::new(as_entity_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_entity_attestation<E: Integrable>(
|
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>,
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
entity: &'a E,
|
entity: E,
|
||||||
entity_id: impl Into<EntityId>,
|
entity_id: Id,
|
||||||
) -> Result<Verified<&'a E>, Error> {
|
) -> Result<Verified<Entity<E, Id>>, Error> {
|
||||||
match check_entity_attestation::<E>(conn, keyholder, entity, entity_id).await? {
|
match check_entity_attestation(conn, keyholder, &entity, entity_id.clone()).await? {
|
||||||
AttestationStatus::Attested => Ok(Verified(entity)),
|
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)),
|
AttestationStatus::Unavailable => Err(Error::Keyholder(keyholder::Error::NotBootstrapped)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,363 +284,30 @@ pub async fn delete_envelope<E: Integrable>(
|
|||||||
Ok(affected)
|
Ok(affected)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
|
||||||
mod tests {
|
let mut hasher = Sha256::new();
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl};
|
payload.hash(&mut hasher);
|
||||||
use diesel_async::RunQueryDsl;
|
hasher.finalize().into()
|
||||||
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 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::{
|
use crate::{
|
||||||
actors::keyholder::KeyHolder,
|
actors::keyholder::KeyHolder,
|
||||||
crypto::integrity::{self, Verified},
|
crypto::integrity::{self, Verified, VerifiedEntity, verified::VerifiedFieldsAccessor},
|
||||||
db::{
|
db::{
|
||||||
self, DatabaseError,
|
self, DatabaseError,
|
||||||
models::{
|
models::{
|
||||||
@@ -182,7 +182,10 @@ impl Engine {
|
|||||||
// IMPORTANT: policy evaluation uses extra non-integrity fields from Grant
|
// IMPORTANT: policy evaluation uses extra non-integrity fields from Grant
|
||||||
// (e.g., per-policy ids), so we currently reload Grant after the query-native
|
// (e.g., per-policy ids), so we currently reload Grant after the query-native
|
||||||
// integrity check over canonicalized settings.
|
// 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(
|
let mut violations = check_shared_constraints(
|
||||||
&context,
|
&context,
|
||||||
@@ -310,18 +313,24 @@ impl Engine {
|
|||||||
|
|
||||||
// Verify integrity of all grants before returning any results.
|
// Verify integrity of all grants before returning any results.
|
||||||
for grant in all_grants {
|
for grant in all_grants {
|
||||||
integrity::verify_entity(
|
let VerifiedEntity {
|
||||||
|
entity: verified_settings,
|
||||||
|
entity_id: _,
|
||||||
|
} = integrity::verify_entity(
|
||||||
conn,
|
conn,
|
||||||
&self.keyholder,
|
&self.keyholder,
|
||||||
&grant.settings,
|
grant.settings,
|
||||||
grant.common_settings_id,
|
grant.common_settings_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?
|
||||||
|
.inherit();
|
||||||
|
|
||||||
verified_grants.push(Grant {
|
verified_grants.push(Grant {
|
||||||
id: grant.id,
|
id: grant.id,
|
||||||
common_settings_id: grant.common_settings_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::{
|
use crate::{
|
||||||
actors::client::{self, ClientConnection, auth},
|
actors::client::{self, ClientConnection, auth},
|
||||||
|
crypto::integrity::Verified,
|
||||||
grpc::request_tracker::RequestTracker,
|
grpc::request_tracker::RequestTracker,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,7 +201,7 @@ pub async fn start(
|
|||||||
conn: &mut ClientConnection,
|
conn: &mut ClientConnection,
|
||||||
bi: &mut GrpcBi<ClientRequest, ClientResponse>,
|
bi: &mut GrpcBi<ClientRequest, ClientResponse>,
|
||||||
request_tracker: &mut RequestTracker,
|
request_tracker: &mut RequestTracker,
|
||||||
) -> Result<i32, auth::Error> {
|
) -> Result<Verified<i32>, auth::Error> {
|
||||||
let mut transport = AuthTransportAdapter::new(bi, request_tracker);
|
let mut transport = AuthTransportAdapter::new(bi, request_tracker);
|
||||||
client::auth::authenticate(conn, &mut transport).await
|
client::auth::authenticate(conn, &mut transport).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ async fn handle_wallet_create(
|
|||||||
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
let result = match actor.ask(HandleEvmWalletCreate {}).await {
|
let result = match actor.ask(HandleEvmWalletCreate {}).await {
|
||||||
Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry {
|
Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry {
|
||||||
id: wallet_id,
|
id: wallet_id.drop_verification_provenance(),
|
||||||
address: address.to_vec(),
|
address: address.to_vec(),
|
||||||
}),
|
}),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -150,7 +150,7 @@ async fn handle_grant_create(
|
|||||||
.try_convert()?;
|
.try_convert()?;
|
||||||
|
|
||||||
let result = match actor.ask(HandleGrantCreate { basic, grant }).await {
|
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)) => {
|
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
||||||
EvmGrantCreateResult::Error(ProtoEvmError::VaultSealed.into())
|
EvmGrantCreateResult::Error(ProtoEvmError::VaultSealed.into())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#![forbid(unsafe_code)]
|
|
||||||
use crate::context::ServerContext;
|
use crate::context::ServerContext;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate macro_rules_attribute;
|
||||||
|
|
||||||
pub mod actors;
|
pub mod actors;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
struct DeferClosure<F: FnOnce()> {
|
struct DeferClosure<F: FnOnce()> {
|
||||||
f: Option<F>,
|
f: Option<F>,
|
||||||
}
|
}
|
||||||
@@ -14,3 +16,19 @@ impl<F: FnOnce()> Drop for DeferClosure<F> {
|
|||||||
pub fn defer<F: FnOnce()>(f: F) -> impl Drop + Sized {
|
pub fn defer<F: FnOnce()>(f: F) -> impl Drop + Sized {
|
||||||
DeferClosure { f: Some(f) }
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
integrity::sign_entity(
|
let _ = integrity::sign_entity(
|
||||||
&mut conn,
|
&mut conn,
|
||||||
&actors.key_holder,
|
&actors.key_holder,
|
||||||
&ClientCredentials { pubkey, nonce: 1 },
|
&ClientCredentials { pubkey, nonce: 1 },
|
||||||
|
|||||||
@@ -139,7 +139,8 @@ pub async fn test_challenge_auth() {
|
|||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap()
|
||||||
|
.drop_verification_provenance();
|
||||||
}
|
}
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
@@ -278,7 +279,8 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
|||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap()
|
||||||
|
.drop_verification_provenance();
|
||||||
}
|
}
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user