Compare commits
3 Commits
check-uac-
...
enforcing-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
694c569c08 | ||
|
|
bec82e036e | ||
|
|
763058b014 |
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"
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ pub struct ArbiterEvmWallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ArbiterEvmWallet {
|
impl ArbiterEvmWallet {
|
||||||
|
#[expect(
|
||||||
|
dead_code,
|
||||||
|
reason = "constructor may be used in future extensions, e.g. to support wallet listing"
|
||||||
|
)]
|
||||||
pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
|
pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
|
||||||
Self {
|
Self {
|
||||||
transport,
|
transport,
|
||||||
|
|||||||
@@ -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, AttestationStatus},
|
crypto::integrity::{self, Verified, verified::VerifiedFieldsAccessor},
|
||||||
db::{
|
db::{
|
||||||
self,
|
self,
|
||||||
models::{ProgramClientMetadata, SqliteTimestamp},
|
models::{ProgramClientMetadata, SqliteTimestamp},
|
||||||
@@ -99,46 +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
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let attestation = integrity::verify_entity(
|
|
||||||
&mut db_conn,
|
|
||||||
keyholder,
|
|
||||||
&ClientCredentials {
|
|
||||||
pubkey: pubkey.clone(),
|
|
||||||
nonce,
|
|
||||||
},
|
|
||||||
id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!(?e, "Integrity verification failed");
|
|
||||||
Error::IntegrityCheckFailed
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if attestation != AttestationStatus::Attested {
|
|
||||||
error!("Integrity attestation unavailable for client {id}");
|
|
||||||
return Err(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(
|
||||||
@@ -147,7 +107,6 @@ async fn create_nonce(
|
|||||||
pubkey: &VerifyingKey,
|
pubkey: &VerifyingKey,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<i32, Error> {
|
||||||
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
||||||
let pubkey = pubkey.clone();
|
|
||||||
|
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
error!(error = ?e, "Database pool error");
|
error!(error = ?e, "Database pool error");
|
||||||
@@ -156,7 +115,6 @@ async fn create_nonce(
|
|||||||
|
|
||||||
conn.exclusive_transaction(|conn| {
|
conn.exclusive_transaction(|conn| {
|
||||||
let keyholder = keyholder.clone();
|
let keyholder = keyholder.clone();
|
||||||
let pubkey = pubkey.clone();
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let (id, new_nonce): (i32, i32) = update(program_client::table)
|
let (id, new_nonce): (i32, i32) = update(program_client::table)
|
||||||
.filter(program_client::public_key.eq(&pubkey_bytes))
|
.filter(program_client::public_key.eq(&pubkey_bytes))
|
||||||
@@ -169,7 +127,7 @@ async fn create_nonce(
|
|||||||
conn,
|
conn,
|
||||||
&keyholder,
|
&keyholder,
|
||||||
&ClientCredentials {
|
&ClientCredentials {
|
||||||
pubkey: pubkey.clone(),
|
pubkey: *pubkey,
|
||||||
nonce: new_nonce,
|
nonce: new_nonce,
|
||||||
},
|
},
|
||||||
id,
|
id,
|
||||||
@@ -178,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)
|
||||||
})
|
})
|
||||||
@@ -214,9 +173,8 @@ 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 pubkey = pubkey.clone();
|
|
||||||
let metadata = metadata.clone();
|
let metadata = metadata.clone();
|
||||||
|
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
@@ -226,7 +184,6 @@ async fn insert_client(
|
|||||||
|
|
||||||
conn.exclusive_transaction(|conn| {
|
conn.exclusive_transaction(|conn| {
|
||||||
let keyholder = keyholder.clone();
|
let keyholder = keyholder.clone();
|
||||||
let pubkey = pubkey.clone();
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
const NONCE_START: i32 = 1;
|
const NONCE_START: i32 = 1;
|
||||||
|
|
||||||
@@ -251,11 +208,11 @@ 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 {
|
||||||
pubkey: pubkey.clone(),
|
pubkey: *pubkey,
|
||||||
nonce: NONCE_START,
|
nonce: NONCE_START,
|
||||||
},
|
},
|
||||||
client_id,
|
client_id,
|
||||||
@@ -264,9 +221,10 @@ async fn insert_client(
|
|||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(error = ?e, "Failed to sign integrity tag for new client key");
|
error!(error = ?e, "Failed to sign integrity tag for new client key");
|
||||||
Error::DatabaseOperationFailed
|
Error::DatabaseOperationFailed
|
||||||
})?;
|
})?
|
||||||
|
.unqualify_origin();
|
||||||
|
|
||||||
Ok(client_id)
|
Ok(verified_id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -379,7 +337,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,
|
||||||
{
|
{
|
||||||
@@ -387,10 +348,28 @@ 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
|
||||||
|
.unqualify_origin()
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
approve_new_client(
|
approve_new_client(
|
||||||
@@ -405,7 +384,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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ use kameo::{Actor, actor::ActorRef, messages};
|
|||||||
use rand::{SeedableRng, rng, rngs::StdRng};
|
use rand::{SeedableRng, rng, rngs::StdRng};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::keyholder::{CreateNew, Decrypt, GetState, KeyHolder, KeyHolderState},
|
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
||||||
crypto::integrity,
|
crypto::integrity::{self, Integrable, Verified, hashing::Hashable},
|
||||||
db::{
|
db::{
|
||||||
DatabaseError, DatabasePool,
|
DatabaseError, DatabasePool,
|
||||||
models::{self, SqliteTimestamp},
|
models::{self},
|
||||||
schema,
|
schema,
|
||||||
},
|
},
|
||||||
evm::{
|
evm::{
|
||||||
@@ -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()));
|
||||||
@@ -100,7 +129,7 @@ impl EvmActor {
|
|||||||
.map_err(|_| Error::KeyholderSend)?;
|
.map_err(|_| Error::KeyholderSend)?;
|
||||||
|
|
||||||
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
let wallet_id = insert_into(schema::evm_wallet::table)
|
let wallet_id: i32 = insert_into(schema::evm_wallet::table)
|
||||||
.values(&models::NewEvmWallet {
|
.values(&models::NewEvmWallet {
|
||||||
address: address.as_slice().to_vec(),
|
address: address.as_slice().to_vec(),
|
||||||
aead_encrypted_id: aead_id,
|
aead_encrypted_id: aead_id,
|
||||||
@@ -110,7 +139,17 @@ 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?
|
||||||
|
.unqualify_origin();
|
||||||
|
|
||||||
|
Ok((verified_wallet_id, address))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
@@ -136,7 +175,7 @@ impl EvmActor {
|
|||||||
&mut self,
|
&mut self,
|
||||||
basic: SharedGrantSettings,
|
basic: SharedGrantSettings,
|
||||||
grant: SpecificGrant,
|
grant: SpecificGrant,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<integrity::Verified<i32>, Error> {
|
||||||
match grant {
|
match grant {
|
||||||
SpecificGrant::EtherTransfer(settings) => self
|
SpecificGrant::EtherTransfer(settings) => self
|
||||||
.engine
|
.engine
|
||||||
@@ -158,7 +197,7 @@ impl EvmActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
|
pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> {
|
||||||
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
// let keyholder = self.keyholder.clone();
|
// let keyholder = self.keyholder.clone();
|
||||||
|
|
||||||
@@ -207,9 +246,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 +295,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 +330,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?;
|
||||||
|
|||||||
@@ -30,17 +30,26 @@ pub enum Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
fn internal(details: impl Into<String>) -> Self {
|
#[track_caller]
|
||||||
Self::Internal {
|
pub(super) fn internal(details: impl Into<String>, err: &impl std::fmt::Debug) -> Self {
|
||||||
details: details.into(),
|
let details = details.into();
|
||||||
}
|
let caller = std::panic::Location::caller();
|
||||||
|
error!(
|
||||||
|
caller_file = %caller.file(),
|
||||||
|
caller_line = caller.line(),
|
||||||
|
caller_column = caller.column(),
|
||||||
|
details = %details,
|
||||||
|
error = ?err,
|
||||||
|
"Internal error"
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::Internal { details }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<diesel::result::Error> for Error {
|
impl From<diesel::result::Error> for Error {
|
||||||
fn from(e: diesel::result::Error) -> Self {
|
fn from(e: diesel::result::Error) -> Self {
|
||||||
error!(?e, "Database error");
|
Self::internal("Database error", &e)
|
||||||
Self::internal("Database error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::transport::Bi;
|
||||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
use kameo::{actor::ActorRef, error::SendError};
|
use kameo::actor::ActorRef;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use super::Error;
|
use super::Error;
|
||||||
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
keyholder::KeyHolder,
|
keyholder::KeyHolder,
|
||||||
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth::Outbound},
|
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth::Outbound},
|
||||||
},
|
},
|
||||||
crypto::integrity::{self, AttestationStatus},
|
crypto::integrity,
|
||||||
db::{DatabasePool, schema::useragent_client},
|
db::{DatabasePool, schema::useragent_client},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,10 +48,10 @@ async fn get_current_nonce_and_id(
|
|||||||
db: &DatabasePool,
|
db: &DatabasePool,
|
||||||
key: &AuthPublicKey,
|
key: &AuthPublicKey,
|
||||||
) -> Result<(i32, i32), Error> {
|
) -> Result<(i32, i32), Error> {
|
||||||
let mut db_conn = db.get().await.map_err(|e| {
|
let mut db_conn = db
|
||||||
error!(error = ?e, "Database pool error");
|
.get()
|
||||||
Error::internal("Database unavailable")
|
.await
|
||||||
})?;
|
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||||
db_conn
|
db_conn
|
||||||
.exclusive_transaction(|conn| {
|
.exclusive_transaction(|conn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -65,10 +65,7 @@ async fn get_current_nonce_and_id(
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.optional()
|
.optional()
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Database operation failed", &e))?
|
||||||
error!(error = ?e, "Database error");
|
|
||||||
Error::internal("Database operation failed")
|
|
||||||
})?
|
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
error!(?key, "Public key not found in database");
|
error!(?key, "Public key not found in database");
|
||||||
Error::UnregisteredPublicKey
|
Error::UnregisteredPublicKey
|
||||||
@@ -80,14 +77,14 @@ async fn verify_integrity(
|
|||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
pubkey: &AuthPublicKey,
|
pubkey: &AuthPublicKey,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut db_conn = db.get().await.map_err(|e| {
|
let mut db_conn = db
|
||||||
error!(error = ?e, "Database pool error");
|
.get()
|
||||||
Error::internal("Database unavailable")
|
.await
|
||||||
})?;
|
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||||
|
|
||||||
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?;
|
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?;
|
||||||
|
|
||||||
let result = integrity::verify_entity(
|
let attestation_status = integrity::check_entity_attestation(
|
||||||
&mut db_conn,
|
&mut db_conn,
|
||||||
keyholder,
|
keyholder,
|
||||||
&UserAgentCredentials {
|
&UserAgentCredentials {
|
||||||
@@ -97,12 +94,17 @@ async fn verify_integrity(
|
|||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Integrity verification failed", &e))?;
|
||||||
error!(?e, "Integrity verification failed");
|
|
||||||
Error::internal("Integrity verification failed")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
use integrity::AttestationStatus as AS;
|
||||||
|
// SAFETY (policy): challenge auth must work in both vault states.
|
||||||
|
// While sealed, integrity checks can only report `Unavailable` because key material is not
|
||||||
|
// accessible. While unsealed, the same check can report `Attested`.
|
||||||
|
// This path intentionally accepts both outcomes to keep challenge auth available across state
|
||||||
|
// transitions; stricter verification is enforced in sensitive post-auth flows.
|
||||||
|
match attestation_status {
|
||||||
|
AS::Attested | AS::Unavailable => Ok(()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_nonce(
|
async fn create_nonce(
|
||||||
@@ -110,10 +112,10 @@ async fn create_nonce(
|
|||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
pubkey: &AuthPublicKey,
|
pubkey: &AuthPublicKey,
|
||||||
) -> Result<i32, Error> {
|
) -> Result<i32, Error> {
|
||||||
let mut db_conn = db.get().await.map_err(|e| {
|
let mut db_conn = db
|
||||||
error!(error = ?e, "Database pool error");
|
.get()
|
||||||
Error::internal("Database unavailable")
|
.await
|
||||||
})?;
|
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||||
let new_nonce = db_conn
|
let new_nonce = db_conn
|
||||||
.exclusive_transaction(|conn| {
|
.exclusive_transaction(|conn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -124,10 +126,7 @@ async fn create_nonce(
|
|||||||
.returning((useragent_client::id, useragent_client::nonce))
|
.returning((useragent_client::id, useragent_client::nonce))
|
||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Database operation failed", &e))?;
|
||||||
error!(error = ?e, "Database error");
|
|
||||||
Error::internal("Database operation failed")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
integrity::sign_entity(
|
integrity::sign_entity(
|
||||||
conn,
|
conn,
|
||||||
@@ -139,10 +138,8 @@ async fn create_nonce(
|
|||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Database error", &e))?
|
||||||
error!(?e, "Integrity signature update failed");
|
.drop_verification_provenance();
|
||||||
Error::internal("Database error")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Result::<_, Error>::Ok(new_nonce)
|
Result::<_, Error>::Ok(new_nonce)
|
||||||
})
|
})
|
||||||
@@ -158,10 +155,10 @@ async fn register_key(
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let pubkey_bytes = pubkey.to_stored_bytes();
|
let pubkey_bytes = pubkey.to_stored_bytes();
|
||||||
let key_type = pubkey.key_type();
|
let key_type = pubkey.key_type();
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db
|
||||||
error!(error = ?e, "Database pool error");
|
.get()
|
||||||
Error::internal("Database unavailable")
|
.await
|
||||||
})?;
|
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||||
|
|
||||||
conn.transaction(|conn| {
|
conn.transaction(|conn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -176,22 +173,32 @@ async fn register_key(
|
|||||||
.returning(useragent_client::id)
|
.returning(useragent_client::id)
|
||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Database operation failed", &e))?;
|
||||||
error!(error = ?e, "Database error");
|
|
||||||
Error::internal("Database operation failed")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let entity = UserAgentCredentials {
|
if let Err(e) = integrity::sign_entity(
|
||||||
|
conn,
|
||||||
|
keyholder,
|
||||||
|
&UserAgentCredentials {
|
||||||
pubkey: pubkey.clone(),
|
pubkey: pubkey.clone(),
|
||||||
nonce: NONCE_START,
|
nonce: NONCE_START,
|
||||||
};
|
},
|
||||||
|
id,
|
||||||
integrity::sign_entity(conn, &keyholder, &entity, id)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
{
|
||||||
error!(error = ?e, "Failed to sign integrity tag for new user-agent key");
|
match e {
|
||||||
Error::internal("Failed to register public key")
|
integrity::Error::Keyholder(
|
||||||
})?;
|
crate::actors::keyholder::Error::NotBootstrapped,
|
||||||
|
) => {
|
||||||
|
// IMPORTANT: bootstrap-token auth must work before the vault has a root key.
|
||||||
|
// We intentionally allow creating the DB row first and backfill envelopes
|
||||||
|
// after bootstrap/unseal to keep the bootstrap flow possible.
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(Error::internal("Failed to register public key", &other));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Result::<_, Error>::Ok(())
|
Result::<_, Error>::Ok(())
|
||||||
})
|
})
|
||||||
@@ -254,10 +261,7 @@ where
|
|||||||
token: token.clone(),
|
token: token.clone(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| Error::internal("Failed to consume bootstrap token", &e))?;
|
||||||
error!(?e, "Failed to consume bootstrap token");
|
|
||||||
Error::internal("Failed to consume bootstrap token")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !token_ok {
|
if !token_ok {
|
||||||
error!("Invalid bootstrap token provided");
|
error!("Invalid bootstrap token provided");
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ use crate::crypto::integrity::hashing::Hashable;
|
|||||||
|
|
||||||
impl Hashable for AuthPublicKey {
|
impl Hashable for AuthPublicKey {
|
||||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||||
hasher.update(&self.to_stored_bytes());
|
hasher.update(self.to_stored_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use kameo::prelude::Context;
|
|||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
|
|
||||||
use crate::actors::keyholder::KeyHolderState;
|
use crate::actors::keyholder::KeyHolderState;
|
||||||
use crate::actors::user_agent::session::Error;
|
use crate::actors::user_agent::session::Error;
|
||||||
use crate::db::models::{
|
use crate::db::models::{
|
||||||
@@ -18,6 +17,10 @@ use crate::db::models::{
|
|||||||
};
|
};
|
||||||
use crate::evm::policies::{Grant, SpecificGrant};
|
use crate::evm::policies::{Grant, SpecificGrant};
|
||||||
use crate::safe_cell::SafeCell;
|
use crate::safe_cell::SafeCell;
|
||||||
|
use crate::{
|
||||||
|
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer,
|
||||||
|
crypto::integrity::{self, Verified},
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
evm::{
|
evm::{
|
||||||
@@ -29,11 +32,67 @@ use crate::{
|
|||||||
UserAgentSession,
|
UserAgentSession,
|
||||||
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
||||||
},
|
},
|
||||||
|
user_agent::{AuthPublicKey, UserAgentCredentials},
|
||||||
},
|
},
|
||||||
|
db::schema::useragent_client,
|
||||||
safe_cell::SafeCellHandle as _,
|
safe_cell::SafeCellHandle as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn is_vault_sealed_from_evm<M>(err: &SendError<M, crate::actors::evm::Error>) -> bool {
|
||||||
|
matches!(
|
||||||
|
err,
|
||||||
|
SendError::HandlerError(crate::actors::evm::Error::Keyholder(
|
||||||
|
keyholder::Error::NotBootstrapped
|
||||||
|
)) | SendError::HandlerError(crate::actors::evm::Error::Integrity(
|
||||||
|
crate::crypto::integrity::Error::Keyholder(keyholder::Error::NotBootstrapped)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
|
async fn backfill_useragent_integrity(&self) -> Result<(), Error> {
|
||||||
|
let mut conn = self.props.db.get().await?;
|
||||||
|
let keyholder = self.props.actors.key_holder.clone();
|
||||||
|
|
||||||
|
conn.transaction(|conn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let rows: Vec<(i32, i32, Vec<u8>, crate::db::models::KeyType)> =
|
||||||
|
useragent_client::table
|
||||||
|
.select((
|
||||||
|
useragent_client::id,
|
||||||
|
useragent_client::nonce,
|
||||||
|
useragent_client::public_key,
|
||||||
|
useragent_client::key_type,
|
||||||
|
))
|
||||||
|
.load(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for (id, nonce, public_key, key_type) in rows {
|
||||||
|
let pubkey = AuthPublicKey::try_from((key_type, public_key)).map_err(|e| {
|
||||||
|
Error::internal(format!("Invalid user-agent key in db: {e}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
integrity::sign_entity(
|
||||||
|
conn,
|
||||||
|
&keyholder,
|
||||||
|
&UserAgentCredentials { pubkey, nonce },
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
Error::internal(format!("Failed to backfill user-agent integrity: {e}"))
|
||||||
|
})?
|
||||||
|
.drop_verification_provenance();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result::<_, Error>::Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
|
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
|
||||||
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
||||||
error!("Received encrypted key in invalid state");
|
error!("Received encrypted key in invalid state");
|
||||||
@@ -191,6 +250,7 @@ impl UserAgentSession {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
self.backfill_useragent_integrity().await?;
|
||||||
info!("Successfully unsealed key with client-provided key");
|
info!("Successfully unsealed key with client-provided key");
|
||||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -252,6 +312,7 @@ impl UserAgentSession {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
self.backfill_useragent_integrity().await?;
|
||||||
info!("Successfully bootstrapped vault with client-provided key");
|
info!("Successfully bootstrapped vault with client-provided key");
|
||||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -297,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!(
|
||||||
@@ -325,12 +388,15 @@ impl UserAgentSession {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
#[message]
|
#[message]
|
||||||
pub(crate) async fn handle_grant_list(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
pub(crate) async fn handle_grant_list(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Vec<Grant<SpecificGrant>>, GrantMutationError> {
|
||||||
match self.props.actors.evm.ask(UseragentListGrants {}).await {
|
match self.props.actors.evm.ask(UseragentListGrants {}).await {
|
||||||
Ok(grants) => Ok(grants),
|
Ok(grants) => Ok(grants),
|
||||||
|
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM grant list failed");
|
error!(?err, "EVM grant list failed");
|
||||||
Err(Error::internal("Failed to list EVM grants"))
|
Err(GrantMutationError::Internal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,7 +406,7 @@ impl UserAgentSession {
|
|||||||
&mut self,
|
&mut self,
|
||||||
basic: crate::evm::policies::SharedGrantSettings,
|
basic: crate::evm::policies::SharedGrantSettings,
|
||||||
grant: crate::evm::policies::SpecificGrant,
|
grant: crate::evm::policies::SpecificGrant,
|
||||||
) -> Result<i32, GrantMutationError> {
|
) -> Result<Verified<i32>, GrantMutationError> {
|
||||||
match self
|
match self
|
||||||
.props
|
.props
|
||||||
.actors
|
.actors
|
||||||
@@ -349,6 +415,7 @@ impl UserAgentSession {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(grant_id) => Ok(grant_id),
|
Ok(grant_id) => Ok(grant_id),
|
||||||
|
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM grant create failed");
|
error!(?err, "EVM grant create failed");
|
||||||
Err(GrantMutationError::Internal)
|
Err(GrantMutationError::Internal)
|
||||||
@@ -365,10 +432,13 @@ impl UserAgentSession {
|
|||||||
.props
|
.props
|
||||||
.actors
|
.actors
|
||||||
.evm
|
.evm
|
||||||
.ask(UseragentDeleteGrant { grant_id })
|
.ask(UseragentDeleteGrant {
|
||||||
|
_grant_id: grant_id,
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
|
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM grant delete failed");
|
error!(?err, "EVM grant delete failed");
|
||||||
Err(GrantMutationError::Internal)
|
Err(GrantMutationError::Internal)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::{
|
use crate::actors::keyholder;
|
||||||
actors::keyholder, crypto::integrity::hashing::Hashable, safe_cell::SafeCellHandle as _,
|
use hmac::Hmac;
|
||||||
};
|
|
||||||
use hmac::{Hmac, Mac as _};
|
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, dsl::insert_into, sqlite::Sqlite};
|
use diesel::{ExpressionMethods as _, QueryDsl, dsl::insert_into, sqlite::Sqlite};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
@@ -10,16 +11,24 @@ 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 crate::{
|
use crate::{
|
||||||
actors::keyholder::{KeyHolder, SignIntegrity, VerifyIntegrity},
|
actors::keyholder::{KeyHolder, SignIntegrity, VerifyIntegrity},
|
||||||
db::{
|
db::{
|
||||||
self,
|
self,
|
||||||
models::{IntegrityEnvelope, NewIntegrityEnvelope},
|
models::{IntegrityEnvelope as IntegrityEnvelopeRow, NewIntegrityEnvelope},
|
||||||
schema::integrity_envelope,
|
schema::integrity_envelope,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const CURRENT_PAYLOAD_VERSION: i32 = 1;
|
||||||
|
pub const INTEGRITY_SUBKEY_TAG: &[u8] = b"arbiter/db-integrity-key/v1";
|
||||||
|
|
||||||
|
pub type HmacSha256 = Hmac<Sha256>;
|
||||||
|
pub use self::verified::{Nested, Root, VerificationOrigin, Verified};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
@@ -48,71 +57,90 @@ pub enum Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[must_use]
|
||||||
pub enum AttestationStatus {
|
pub enum AttestationStatus {
|
||||||
Attested,
|
Attested,
|
||||||
Unavailable,
|
Unavailable,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const CURRENT_PAYLOAD_VERSION: i32 = 1;
|
|
||||||
pub const INTEGRITY_SUBKEY_TAG: &[u8] = b"arbiter/db-integrity-key/v1";
|
|
||||||
|
|
||||||
pub type HmacSha256 = Hmac<Sha256>;
|
|
||||||
|
|
||||||
pub trait Integrable: Hashable {
|
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]) {
|
#[derive(Debug, Clone)]
|
||||||
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
|
pub struct EntityId(Vec<u8>);
|
||||||
out.extend_from_slice(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_mac_input(
|
impl Deref for EntityId {
|
||||||
entity_kind: &str,
|
type Target = [u8];
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IntoId {
|
fn deref(&self) -> &Self::Target {
|
||||||
fn into_id(self) -> Vec<u8>;
|
&self.0
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoId for i32 {
|
|
||||||
fn into_id(self) -> Vec<u8> {
|
|
||||||
self.to_be_bytes().to_vec()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoId for &'_ [u8] {
|
impl From<i32> for EntityId {
|
||||||
fn into_id(self) -> Vec<u8> {
|
fn from(value: i32) -> Self {
|
||||||
self.to_vec()
|
Self(value.to_be_bytes().to_vec())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sign_entity<E: Integrable>(
|
impl From<&'_ [u8]> for EntityId {
|
||||||
|
fn from(bytes: &'_ [u8]) -> Self {
|
||||||
|
Self(bytes.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn lookup_verified<E, Id, C, F, Fut>(
|
||||||
|
conn: &mut C,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
entity_id: Id,
|
||||||
|
load: F,
|
||||||
|
) -> Result<Verified<Entity<E, Id>, Nested<E>>, Error>
|
||||||
|
where
|
||||||
|
C: AsyncConnection<Backend = Sqlite>,
|
||||||
|
E: Integrable,
|
||||||
|
Id: Into<EntityId> + Clone,
|
||||||
|
F: FnOnce(&mut C) -> Fut,
|
||||||
|
Fut: Future<Output = Result<E, db::DatabaseError>>,
|
||||||
|
{
|
||||||
|
let entity = load(conn).await?;
|
||||||
|
verify_entity(conn, keyholder, entity, entity_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn lookup_verified_from_query<E, Id, C, F>(
|
||||||
|
conn: &mut C,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
load: F,
|
||||||
|
) -> Result<Verified<Entity<E, Id>, Nested<E>>, Error>
|
||||||
|
where
|
||||||
|
C: AsyncConnection<Backend = Sqlite> + Send,
|
||||||
|
E: Integrable,
|
||||||
|
Id: Into<EntityId> + Clone,
|
||||||
|
F: for<'a> FnOnce(
|
||||||
|
&'a mut C,
|
||||||
|
) -> Pin<
|
||||||
|
Box<dyn Future<Output = Result<(Id, E), db::DatabaseError>> + Send + 'a>,
|
||||||
|
>,
|
||||||
|
{
|
||||||
|
let (entity_id, entity) = load(conn).await?;
|
||||||
|
verify_entity(conn, keyholder, entity, entity_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sign_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
||||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
entity: &E,
|
entity: &E,
|
||||||
entity_id: impl IntoId,
|
as_entity_id: Id,
|
||||||
) -> Result<(), Error> {
|
) -> Result<Verified<Id, Nested<E>>, Error> {
|
||||||
let payload_hash = payload_hash(&entity);
|
let payload_hash = payload_hash(entity);
|
||||||
|
|
||||||
let entity_id = entity_id.into_id();
|
let entity_id = as_entity_id.clone().into();
|
||||||
|
|
||||||
let mac_input = build_mac_input(E::KIND, &entity_id, E::VERSION, &payload_hash);
|
let mac_input = build_mac_input(E::KIND, &entity_id, E::VERSION, &payload_hash);
|
||||||
|
|
||||||
@@ -127,7 +155,7 @@ pub async fn sign_entity<E: Integrable>(
|
|||||||
insert_into(integrity_envelope::table)
|
insert_into(integrity_envelope::table)
|
||||||
.values(NewIntegrityEnvelope {
|
.values(NewIntegrityEnvelope {
|
||||||
entity_kind: E::KIND.to_owned(),
|
entity_kind: E::KIND.to_owned(),
|
||||||
entity_id: entity_id,
|
entity_id: entity_id.to_vec(),
|
||||||
payload_version: E::VERSION,
|
payload_version: E::VERSION,
|
||||||
key_version,
|
key_version,
|
||||||
mac: mac.to_vec(),
|
mac: mac.to_vec(),
|
||||||
@@ -146,19 +174,19 @@ pub async fn sign_entity<E: Integrable>(
|
|||||||
.await
|
.await
|
||||||
.map_err(db::DatabaseError::from)?;
|
.map_err(db::DatabaseError::from)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(Verified::<Id, Nested<E>>::new(as_entity_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn verify_entity<E: Integrable>(
|
pub async fn check_entity_attestation<E: Integrable>(
|
||||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
keyholder: &ActorRef<KeyHolder>,
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
entity: &E,
|
entity: &E,
|
||||||
entity_id: impl IntoId,
|
entity_id: impl Into<EntityId>,
|
||||||
) -> Result<AttestationStatus, Error> {
|
) -> Result<AttestationStatus, Error> {
|
||||||
let entity_id = entity_id.into_id();
|
let entity_id = entity_id.into();
|
||||||
let envelope: IntegrityEnvelope = integrity_envelope::table
|
let envelope: IntegrityEnvelopeRow = integrity_envelope::table
|
||||||
.filter(integrity_envelope::entity_kind.eq(E::KIND))
|
.filter(integrity_envelope::entity_kind.eq(E::KIND))
|
||||||
.filter(integrity_envelope::entity_id.eq(&entity_id))
|
.filter(integrity_envelope::entity_id.eq(&*entity_id))
|
||||||
.first(conn)
|
.first(conn)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| match err {
|
.map_err(|err| match err {
|
||||||
@@ -176,7 +204,7 @@ pub async fn verify_entity<E: Integrable>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload_hash = payload_hash(&entity);
|
let payload_hash = payload_hash(entity);
|
||||||
let mac_input = build_mac_input(E::KIND, &entity_id, envelope.payload_version, &payload_hash);
|
let mac_input = build_mac_input(E::KIND, &entity_id, envelope.payload_version, &payload_hash);
|
||||||
|
|
||||||
let result = keyholder
|
let result = keyholder
|
||||||
@@ -199,139 +227,93 @@ pub async fn verify_entity<E: Integrable>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[derive(Debug, Clone, crate::VerifiedFields!)]
|
||||||
mod tests {
|
#[repr(C)]
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl};
|
pub struct Entity<E, Id> {
|
||||||
use diesel_async::RunQueryDsl;
|
pub entity: E,
|
||||||
use kameo::{actor::ActorRef, prelude::Spawn};
|
pub entity_id: Id,
|
||||||
use rand::seq::SliceRandom;
|
}
|
||||||
use sha2::Digest;
|
|
||||||
|
|
||||||
use proptest::prelude::*;
|
impl<E, Id> Deref for Entity<E, Id> {
|
||||||
|
type Target = E;
|
||||||
|
|
||||||
use crate::{
|
fn deref(&self) -> &Self::Target {
|
||||||
actors::keyholder::{Bootstrap, KeyHolder},
|
&self.entity
|
||||||
db::{self, schema},
|
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{Error, Integrable, sign_entity, verify_entity};
|
|
||||||
use super::{hashing::Hashable, payload_hash};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct DummyEntity {
|
|
||||||
payload_version: i32,
|
|
||||||
payload: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hashable for DummyEntity {
|
|
||||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
|
||||||
self.payload_version.hash(hasher);
|
|
||||||
self.payload.hash(hasher);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Integrable for DummyEntity {
|
|
||||||
const KIND: &'static str = "dummy_entity";
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn bootstrapped_keyholder(db: &db::DatabasePool) -> ActorRef<KeyHolder> {
|
|
||||||
let actor = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
|
||||||
actor
|
|
||||||
.ask(Bootstrap {
|
|
||||||
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
actor
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn sign_writes_envelope_and_verify_passes() {
|
|
||||||
let db = db::create_test_pool().await;
|
|
||||||
let keyholder = bootstrapped_keyholder(&db).await;
|
|
||||||
let mut conn = db.get().await.unwrap();
|
|
||||||
|
|
||||||
const ENTITY_ID: &[u8] = b"entity-id-7";
|
|
||||||
|
|
||||||
let entity = DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let count: i64 = schema::integrity_envelope::table
|
|
||||||
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
|
||||||
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
|
||||||
.count()
|
|
||||||
.get_result(&mut conn)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(count, 1, "envelope row must be created exactly once");
|
|
||||||
verify_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn tampered_mac_fails_verification() {
|
|
||||||
let db = db::create_test_pool().await;
|
|
||||||
let keyholder = bootstrapped_keyholder(&db).await;
|
|
||||||
let mut conn = db.get().await.unwrap();
|
|
||||||
|
|
||||||
const ENTITY_ID: &[u8] = b"entity-id-11";
|
|
||||||
|
|
||||||
let entity = DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
diesel::update(schema::integrity_envelope::table)
|
|
||||||
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
|
||||||
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
|
||||||
.set(schema::integrity_envelope::mac.eq(vec![0u8; 32]))
|
|
||||||
.execute(&mut conn)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let err = verify_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
assert!(matches!(err, Error::MacMismatch { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn changed_payload_fails_verification() {
|
|
||||||
let db = db::create_test_pool().await;
|
|
||||||
let keyholder = bootstrapped_keyholder(&db).await;
|
|
||||||
let mut conn = db.get().await.unwrap();
|
|
||||||
|
|
||||||
const ENTITY_ID: &[u8] = b"entity-id-21";
|
|
||||||
|
|
||||||
let entity = DummyEntity {
|
|
||||||
payload_version: 1,
|
|
||||||
payload: b"payload-v1".to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let tampered = DummyEntity {
|
|
||||||
payload: b"payload-v1-but-tampered".to_vec(),
|
|
||||||
..entity
|
|
||||||
};
|
|
||||||
|
|
||||||
let err = verify_entity(&mut conn, &keyholder, &tampered, ENTITY_ID)
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
assert!(matches!(err, Error::MacMismatch { .. }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn verify_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
||||||
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
|
keyholder: &ActorRef<KeyHolder>,
|
||||||
|
entity: E,
|
||||||
|
entity_id: Id,
|
||||||
|
) -> Result<Verified<Entity<E, Id>, Nested<E>>, Error> {
|
||||||
|
match check_entity_attestation(conn, keyholder, &entity, entity_id.clone()).await? {
|
||||||
|
AttestationStatus::Attested => Ok(Verified::<Entity<E, Id>, Nested<E>>::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>, Nested<E>>, Error> {
|
||||||
|
match check_entity_attestation(conn, keyholder, entity, entity_id.clone()).await? {
|
||||||
|
AttestationStatus::Attested => Ok(Verified::<Entity<&'e E, Id>, Nested<E>>::new(Entity {
|
||||||
|
entity,
|
||||||
|
entity_id,
|
||||||
|
})),
|
||||||
|
AttestationStatus::Unavailable => Err(Error::Keyholder(keyholder::Error::NotBootstrapped)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_envelope<E: Integrable>(
|
||||||
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
|
entity_id: impl Into<EntityId>,
|
||||||
|
) -> Result<usize, Error> {
|
||||||
|
let entity_id = entity_id.into();
|
||||||
|
|
||||||
|
let affected = diesel::delete(
|
||||||
|
integrity_envelope::table
|
||||||
|
.filter(integrity_envelope::entity_kind.eq(E::KIND))
|
||||||
|
.filter(integrity_envelope::entity_id.eq(&*entity_id)),
|
||||||
|
)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.map_err(db::DatabaseError::from)?;
|
||||||
|
|
||||||
|
Ok(affected)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
payload.hash(&mut hasher);
|
||||||
|
hasher.finalize().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_mac_input(
|
||||||
|
entity_kind: &str,
|
||||||
|
entity_id: &[u8],
|
||||||
|
payload_version: i32,
|
||||||
|
payload_hash: &[u8; 32],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(8 + entity_kind.len() + entity_id.len() + 32);
|
||||||
|
push_len_prefixed(&mut out, entity_kind.as_bytes());
|
||||||
|
push_len_prefixed(&mut out, entity_id);
|
||||||
|
out.extend_from_slice(&payload_version.to_be_bytes());
|
||||||
|
out.extend_from_slice(payload_hash);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_len_prefixed(out: &mut Vec<u8>, bytes: &[u8]) {
|
||||||
|
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
|
||||||
|
out.extend_from_slice(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ impl<T: Hashable> Hashable for Option<T> {
|
|||||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
match self {
|
match self {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
hasher.update(&[1]);
|
hasher.update([1]);
|
||||||
value.hash(hasher);
|
value.hash(hasher);
|
||||||
}
|
}
|
||||||
None => hasher.update(&[0]),
|
None => hasher.update([0]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,12 +96,12 @@ impl Hashable for alloy::primitives::U256 {
|
|||||||
|
|
||||||
impl Hashable for chrono::Duration {
|
impl Hashable for chrono::Duration {
|
||||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
hasher.update(&self.num_seconds().to_be_bytes());
|
hasher.update(self.num_seconds().to_be_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hashable for chrono::DateTime<chrono::Utc> {
|
impl Hashable for chrono::DateTime<chrono::Utc> {
|
||||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
hasher.update(&self.timestamp_millis().to_be_bytes());
|
hasher.update(self.timestamp_millis().to_be_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
298
server/crates/arbiter-server/src/crypto/integrity/v1/tests.rs
Normal file
298
server/crates/arbiter-server/src/crypto/integrity/v1/tests.rs
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
use diesel::{ExpressionMethods as _, QueryDsl};
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
|
use kameo::{actor::ActorRef, prelude::Spawn};
|
||||||
|
use sha2::Digest;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
actors::keyholder::{Bootstrap, KeyHolder},
|
||||||
|
db::{self, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::hashing::Hashable;
|
||||||
|
use super::{
|
||||||
|
Error, Integrable, check_entity_attestation, lookup_verified, lookup_verified_from_query,
|
||||||
|
sign_entity, verify_entity,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct DummyEntity {
|
||||||
|
payload_version: i32,
|
||||||
|
payload: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hashable for DummyEntity {
|
||||||
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
|
self.payload_version.hash(hasher);
|
||||||
|
self.payload.hash(hasher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Integrable for DummyEntity {
|
||||||
|
const KIND: &'static str = "dummy_entity";
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn bootstrapped_keyholder(db: &db::DatabasePool) -> ActorRef<KeyHolder> {
|
||||||
|
let actor = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||||
|
actor
|
||||||
|
.ask(Bootstrap {
|
||||||
|
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
actor
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn sign_writes_envelope_and_verify_passes() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: &[u8] = b"entity-id-7";
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.drop_verification_provenance();
|
||||||
|
|
||||||
|
let count: i64 = schema::integrity_envelope::table
|
||||||
|
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
||||||
|
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
||||||
|
.count()
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(count, 1, "envelope row must be created exactly once");
|
||||||
|
let _ = check_entity_attestation(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tampered_mac_fails_verification() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: &[u8] = b"entity-id-11";
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.drop_verification_provenance();
|
||||||
|
|
||||||
|
diesel::update(schema::integrity_envelope::table)
|
||||||
|
.filter(schema::integrity_envelope::entity_kind.eq("dummy_entity"))
|
||||||
|
.filter(schema::integrity_envelope::entity_id.eq(ENTITY_ID))
|
||||||
|
.set(schema::integrity_envelope::mac.eq(vec![0u8; 32]))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = check_entity_attestation(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, Error::MacMismatch { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn changed_payload_fails_verification() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: &[u8] = b"entity-id-21";
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.drop_verification_provenance();
|
||||||
|
|
||||||
|
let tampered = DummyEntity {
|
||||||
|
payload: b"payload-v1-but-tampered".to_vec(),
|
||||||
|
..entity
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = check_entity_attestation(&mut conn, &keyholder, &tampered, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, Error::MacMismatch { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn strict_verify_fails_closed_while_sealed() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: &[u8] = b"entity-id-41";
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.drop_verification_provenance();
|
||||||
|
drop(keyholder);
|
||||||
|
|
||||||
|
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||||
|
|
||||||
|
let err = verify_entity(&mut conn, &sealed_keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
||||||
|
));
|
||||||
|
|
||||||
|
let err = lookup_verified(&mut conn, &sealed_keyholder, ENTITY_ID, |_| async {
|
||||||
|
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn lookup_verified_supports_loaded_aggregate() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: i32 = 77;
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.drop_verification_provenance();
|
||||||
|
|
||||||
|
let verified = lookup_verified(&mut conn, &keyholder, ENTITY_ID, |_| async {
|
||||||
|
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(verified.entity.payload, b"payload-v1".to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn extension_trait_lookup_verified_required_works() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: i32 = 79;
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.drop_verification_provenance();
|
||||||
|
|
||||||
|
let verified = lookup_verified(&mut conn, &keyholder, ENTITY_ID, |_| {
|
||||||
|
Box::pin(async {
|
||||||
|
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(verified.entity.payload, b"payload-v1".to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn lookup_verified_from_query_helpers_work() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let keyholder = bootstrapped_keyholder(&db).await;
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
|
||||||
|
const ENTITY_ID: i32 = 80;
|
||||||
|
|
||||||
|
let entity = DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.drop_verification_provenance();
|
||||||
|
|
||||||
|
let verified = lookup_verified_from_query(&mut conn, &keyholder, |_| {
|
||||||
|
Box::pin(async {
|
||||||
|
Ok::<_, db::DatabaseError>((
|
||||||
|
ENTITY_ID,
|
||||||
|
DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(verified.entity.payload, b"payload-v1".to_vec());
|
||||||
|
|
||||||
|
drop(keyholder);
|
||||||
|
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||||
|
|
||||||
|
let err = lookup_verified_from_query(&mut conn, &sealed_keyholder, |_| {
|
||||||
|
Box::pin(async {
|
||||||
|
Ok::<_, db::DatabaseError>((
|
||||||
|
ENTITY_ID,
|
||||||
|
DummyEntity {
|
||||||
|
payload_version: 1,
|
||||||
|
payload: b"payload-v1".to_vec(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
||||||
|
));
|
||||||
|
}
|
||||||
593
server/crates/arbiter-server/src/crypto/integrity/v1/verified.rs
Normal file
593
server/crates/arbiter-server/src/crypto/integrity/v1/verified.rs
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use super::Integrable;
|
||||||
|
|
||||||
|
mod private {
|
||||||
|
pub trait Sealed {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marker trait for type-level verification provenance.
|
||||||
|
///
|
||||||
|
/// This trait is intentionally sealed so external code cannot invent arbitrary
|
||||||
|
/// provenance tags and bypass the intended type-level guarantees.
|
||||||
|
pub trait VerificationOrigin: private::Sealed {
|
||||||
|
type Origin: VerificationOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Root provenance marker for values directly produced by integrity APIs.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct Root;
|
||||||
|
|
||||||
|
/// Nested provenance marker carrying the source integrable type and previous
|
||||||
|
/// provenance marker in the chain.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Nested<From, P: VerificationOrigin = Root>(core::marker::PhantomData<(From, P)>);
|
||||||
|
|
||||||
|
impl private::Sealed for Root {}
|
||||||
|
impl VerificationOrigin for Root {
|
||||||
|
type Origin = Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, P: VerificationOrigin> private::Sealed for Nested<T, P> {}
|
||||||
|
impl<T, P: VerificationOrigin> VerificationOrigin for Nested<T, P> {
|
||||||
|
type Origin = P;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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, O: VerificationOrigin = Root> {
|
||||||
|
inner: T,
|
||||||
|
origin: core::marker::PhantomData<O>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, O: VerificationOrigin> AsRef<Verified<T, O>> for Verified<&T, O> {
|
||||||
|
fn as_ref(&self) -> &Verified<T, O> {
|
||||||
|
// SAFETY: `Verified<T>` is `#[repr(transparent)]` over `T`, so `&T`
|
||||||
|
// and `&Verified<T>` have identical layout.
|
||||||
|
unsafe { reinterpret_layout_ref::<T, Verified<T, O>>(self.inner) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U: Integrable, O: VerificationOrigin> Deref for Verified<T, Nested<U, O>> {
|
||||||
|
type Target = Verified<T, O::Origin>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
// SAFETY: `Verified<T, Nested<U, O>>` is `#[repr(transparent)]` over `T`, so `&Verified<T, Nested<U, O>>`
|
||||||
|
// and `&Nested<U, O>` have identical layout.
|
||||||
|
unsafe { reinterpret_layout_ref::<Self, Verified<T, O::Origin>>(self) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> Deref for Verified<T, Root> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, O: VerificationOrigin> Verified<T, O> {
|
||||||
|
/// 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.inner
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downgrades the origin provenance to any lower nestedness level,
|
||||||
|
/// e.g. `Verified<T, Nested<Other>>` to `Verified<T, Root>`.
|
||||||
|
pub fn unqualify_origin<Target: VerificationOrigin>(self) -> Verified<T, Target>
|
||||||
|
where
|
||||||
|
O: VerificationOrigin<Origin = Target>,
|
||||||
|
{
|
||||||
|
Verified {
|
||||||
|
inner: self.inner,
|
||||||
|
origin: core::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a `Verified<T>` by wrapping a `T`.
|
||||||
|
pub(super) fn new(value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: value,
|
||||||
|
origin: core::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
inner: value,
|
||||||
|
origin: core::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo! rewrite macro_rules to derive crate
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! VerifiedFields {
|
||||||
|
// --- Entry point (no source generics) ---
|
||||||
|
(
|
||||||
|
$(#$attr:tt)*
|
||||||
|
$vis:vis struct $name:ident
|
||||||
|
{
|
||||||
|
$(
|
||||||
|
$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>]<P: $crate::crypto::integrity::v1::verified::VerificationOrigin>
|
||||||
|
{
|
||||||
|
$(
|
||||||
|
$field_vis $field_name : $crate::crypto::integrity::Verified<$field_ty, P>
|
||||||
|
),*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: $crate::crypto::integrity::v1::verified::VerificationOrigin>
|
||||||
|
$crate::crypto::integrity::v1::verified::VerifiedFieldsAccessor
|
||||||
|
for $crate::crypto::integrity::Verified<$name, P>
|
||||||
|
{
|
||||||
|
type Counterpart = [<Verified $name>]<P>;
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Entry point (source has generics) ---
|
||||||
|
(
|
||||||
|
$(#$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),*, P: $crate::crypto::integrity::v1::verified::VerificationOrigin>
|
||||||
|
{
|
||||||
|
$(
|
||||||
|
$field_vis $field_name : $crate::crypto::integrity::Verified<$field_ty, P>
|
||||||
|
),*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<$($gen),*, P: $crate::crypto::integrity::v1::verified::VerificationOrigin>
|
||||||
|
$crate::crypto::integrity::v1::verified::VerifiedFieldsAccessor
|
||||||
|
for $crate::crypto::integrity::Verified<$name<$($gen),*>, P>
|
||||||
|
{
|
||||||
|
type Counterpart = [<Verified $name>]<$($gen),*, P>;
|
||||||
|
|
||||||
|
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 []) => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
inner: t,
|
||||||
|
origin: core::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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, Root> = v.inherit_ref();
|
||||||
|
let back_ptr =
|
||||||
|
fields as *const VerifiedMyStruct<u32, Root> 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>::new_unchecked(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>::new_unchecked(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::new_unchecked(&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,
|
crypto::integrity::{self, Verified, VerifiedEntity, verified::VerifiedFieldsAccessor},
|
||||||
db::{
|
db::{
|
||||||
self, DatabaseError,
|
self, DatabaseError,
|
||||||
models::{
|
models::{
|
||||||
@@ -153,12 +153,39 @@ impl Engine {
|
|||||||
{
|
{
|
||||||
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
let grant = P::try_find_grant(&context, &mut conn)
|
let verified_settings =
|
||||||
|
match integrity::lookup_verified_from_query(&mut conn, &self.keyholder, |conn| {
|
||||||
|
let context = context.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
let grant = P::try_find_grant(&context, conn)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::from)?
|
||||||
|
.ok_or_else(|| DatabaseError::from(diesel::result::Error::NotFound))?;
|
||||||
|
|
||||||
|
Ok::<_, DatabaseError>((grant.common_settings_id, grant.settings))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(verified) => verified,
|
||||||
|
Err(integrity::Error::Database(DatabaseError::Connection(
|
||||||
|
diesel::result::Error::NotFound,
|
||||||
|
))) => return Err(PolicyError::NoMatchingGrant),
|
||||||
|
Err(err) => return Err(PolicyError::Integrity(err)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut grant = P::try_find_grant(&context, &mut conn)
|
||||||
.await
|
.await
|
||||||
.map_err(DatabaseError::from)?
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or(PolicyError::NoMatchingGrant)?;
|
.ok_or(PolicyError::NoMatchingGrant)?;
|
||||||
|
|
||||||
integrity::verify_entity(&mut conn, &self.keyholder, &grant.settings, grant.id).await?;
|
// IMPORTANT: policy evaluation uses extra non-integrity fields from Grant
|
||||||
|
// (e.g., per-policy ids), so we currently reload Grant after the query-native
|
||||||
|
// integrity check over canonicalized settings.
|
||||||
|
grant.settings = verified_settings
|
||||||
|
.inherit()
|
||||||
|
.entity
|
||||||
|
.drop_verification_provenance();
|
||||||
|
|
||||||
let mut violations = check_shared_constraints(
|
let mut violations = check_shared_constraints(
|
||||||
&context,
|
&context,
|
||||||
@@ -214,7 +241,7 @@ impl Engine {
|
|||||||
pub async fn create_grant<P: Policy>(
|
pub async fn create_grant<P: Policy>(
|
||||||
&self,
|
&self,
|
||||||
full_grant: CombinedSettings<P::Settings>,
|
full_grant: CombinedSettings<P::Settings>,
|
||||||
) -> Result<i32, DatabaseError>
|
) -> Result<Verified<i32>, DatabaseError>
|
||||||
where
|
where
|
||||||
P::Settings: Clone,
|
P::Settings: Clone,
|
||||||
{
|
{
|
||||||
@@ -258,22 +285,23 @@ impl Engine {
|
|||||||
|
|
||||||
P::create_grant(&basic_grant, &full_grant.specific, conn).await?;
|
P::create_grant(&basic_grant, &full_grant.specific, conn).await?;
|
||||||
|
|
||||||
|
let verified_entity_id =
|
||||||
integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id)
|
integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||||
|
|
||||||
QueryResult::Ok(basic_grant.id)
|
QueryResult::Ok(verified_entity_id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(id)
|
Ok(id.unqualify_origin())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_one_kind<Kind: Policy, Y>(
|
async fn list_one_kind<Kind: Policy, Y>(
|
||||||
&self,
|
&self,
|
||||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||||
) -> Result<impl Iterator<Item = Grant<Y>>, ListError>
|
) -> Result<Vec<Grant<Y>>, ListError>
|
||||||
where
|
where
|
||||||
Y: From<Kind::Settings>,
|
Y: From<Kind::Settings>,
|
||||||
{
|
{
|
||||||
@@ -281,16 +309,32 @@ impl Engine {
|
|||||||
.await
|
.await
|
||||||
.map_err(DatabaseError::from)?;
|
.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
// Verify integrity of all grants before returning any results
|
let mut verified_grants = Vec::with_capacity(all_grants.len());
|
||||||
for grant in &all_grants {
|
|
||||||
integrity::verify_entity(conn, &self.keyholder, &grant.settings, grant.id).await?;
|
// Verify integrity of all grants before returning any results.
|
||||||
|
for grant in all_grants {
|
||||||
|
let VerifiedEntity {
|
||||||
|
entity: verified_settings,
|
||||||
|
entity_id: _,
|
||||||
|
} = integrity::verify_entity(
|
||||||
|
conn,
|
||||||
|
&self.keyholder,
|
||||||
|
grant.settings,
|
||||||
|
grant.common_settings_id,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.inherit();
|
||||||
|
|
||||||
|
verified_grants.push(Grant {
|
||||||
|
id: grant.id,
|
||||||
|
common_settings_id: grant.common_settings_id,
|
||||||
|
settings: verified_settings
|
||||||
|
.drop_verification_provenance()
|
||||||
|
.generalize(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(all_grants.into_iter().map(|g| Grant {
|
Ok(verified_grants)
|
||||||
id: g.id,
|
|
||||||
common_settings_id: g.common_settings_id,
|
|
||||||
settings: g.settings.generalize(),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListError> {
|
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListError> {
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ pub enum SpecificGrant {
|
|||||||
TokenTransfer(token_transfers::Settings),
|
TokenTransfer(token_transfers::Settings),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CombinedSettings<PolicyGrant> {
|
pub struct CombinedSettings<PolicyGrant> {
|
||||||
pub shared: SharedGrantSettings,
|
pub shared: SharedGrantSettings,
|
||||||
pub specific: PolicyGrant,
|
pub specific: PolicyGrant,
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ async fn check_rate_limits(
|
|||||||
let mut violations = Vec::new();
|
let mut violations = Vec::new();
|
||||||
let window = grant.settings.specific.limit.window;
|
let window = grant.settings.specific.limit.window;
|
||||||
|
|
||||||
let past_transaction = query_relevant_past_transaction(grant.id, window, db).await?;
|
let past_transaction =
|
||||||
|
query_relevant_past_transaction(grant.common_settings_id, window, db).await?;
|
||||||
|
|
||||||
let window_start = chrono::Utc::now() - grant.settings.specific.limit.window;
|
let window_start = chrono::Utc::now() - grant.settings.specific.limit.window;
|
||||||
let prospective_cumulative_volume: U256 = past_transaction
|
let prospective_cumulative_volume: U256 = past_transaction
|
||||||
@@ -249,21 +250,20 @@ impl Policy for EtherTransfer {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let settings = Settings {
|
|
||||||
target: targets,
|
|
||||||
limit: VolumeRateLimit {
|
|
||||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume)
|
|
||||||
.map_err(|err| diesel::result::Error::DeserializationError(Box::new(err)))?,
|
|
||||||
window: chrono::Duration::seconds(limit.window_secs as i64),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(Grant {
|
Ok(Some(Grant {
|
||||||
id: grant.id,
|
id: grant.id,
|
||||||
common_settings_id: grant.basic_grant_id,
|
common_settings_id: grant.basic_grant_id,
|
||||||
settings: CombinedSettings {
|
settings: CombinedSettings {
|
||||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||||
specific: settings,
|
specific: Settings {
|
||||||
|
target: targets,
|
||||||
|
limit: VolumeRateLimit {
|
||||||
|
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(|err| {
|
||||||
|
diesel::result::Error::DeserializationError(Box::new(err))
|
||||||
|
})?,
|
||||||
|
window: chrono::Duration::seconds(limit.window_secs as i64),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,18 +286,16 @@ impl Policy for TokenTransfer {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let settings = Settings {
|
|
||||||
token_contract: Address::from(token_contract),
|
|
||||||
target,
|
|
||||||
volume_limits,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(Grant {
|
Ok(Some(Grant {
|
||||||
id: token_grant.id,
|
id: token_grant.id,
|
||||||
common_settings_id: token_grant.basic_grant_id,
|
common_settings_id: token_grant.basic_grant_id,
|
||||||
settings: CombinedSettings {
|
settings: CombinedSettings {
|
||||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||||
specific: settings,
|
specific: Settings {
|
||||||
|
token_contract: Address::from(token_contract),
|
||||||
|
target,
|
||||||
|
volume_limits,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use tracing::warn;
|
|||||||
|
|
||||||
use crate::{
|
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) => {
|
||||||
@@ -121,6 +121,9 @@ async fn handle_grant_list(
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}),
|
}),
|
||||||
|
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
||||||
|
EvmGrantListResult::Error(ProtoEvmError::VaultSealed.into())
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = ?err, "Failed to list EVM grants");
|
warn!(error = ?err, "Failed to list EVM grants");
|
||||||
EvmGrantListResult::Error(ProtoEvmError::Internal.into())
|
EvmGrantListResult::Error(ProtoEvmError::Internal.into())
|
||||||
@@ -147,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),
|
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();
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ use arbiter_server::{
|
|||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl as _, insert_into};
|
|
||||||
use diesel_async::RunQueryDsl;
|
|
||||||
use kameo::actor::Spawn as _;
|
use kameo::actor::Spawn as _;
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user