feat(server): unify integrity API and propagate verified IDs through auth/EVM flows
This commit is contained in:
@@ -18,7 +18,7 @@ use crate::{
|
||||
flow_coordinator::{self, RequestClientApproval},
|
||||
keyholder::KeyHolder,
|
||||
},
|
||||
crypto::integrity::{self, AttestationStatus},
|
||||
crypto::integrity::{self},
|
||||
db::{
|
||||
self,
|
||||
models::{ProgramClientMetadata, SqliteTimestamp},
|
||||
@@ -109,18 +109,16 @@ async fn verify_integrity(
|
||||
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 (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(
|
||||
integrity::verify_entity(
|
||||
&mut db_conn,
|
||||
keyholder,
|
||||
&ClientCredentials {
|
||||
pubkey: pubkey.clone(),
|
||||
pubkey: *pubkey,
|
||||
nonce,
|
||||
},
|
||||
id,
|
||||
@@ -131,11 +129,6 @@ async fn verify_integrity(
|
||||
Error::IntegrityCheckFailed
|
||||
})?;
|
||||
|
||||
if attestation != AttestationStatus::Attested {
|
||||
error!("Integrity attestation unavailable for client {id}");
|
||||
return Err(Error::IntegrityCheckFailed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -147,7 +140,6 @@ async fn create_nonce(
|
||||
pubkey: &VerifyingKey,
|
||||
) -> Result<i32, Error> {
|
||||
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
||||
let pubkey = pubkey.clone();
|
||||
|
||||
let mut conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
@@ -156,7 +148,6 @@ async fn create_nonce(
|
||||
|
||||
conn.exclusive_transaction(|conn| {
|
||||
let keyholder = keyholder.clone();
|
||||
let pubkey = pubkey.clone();
|
||||
Box::pin(async move {
|
||||
let (id, new_nonce): (i32, i32) = update(program_client::table)
|
||||
.filter(program_client::public_key.eq(&pubkey_bytes))
|
||||
@@ -169,7 +160,7 @@ async fn create_nonce(
|
||||
conn,
|
||||
&keyholder,
|
||||
&ClientCredentials {
|
||||
pubkey: pubkey.clone(),
|
||||
pubkey: *pubkey,
|
||||
nonce: new_nonce,
|
||||
},
|
||||
id,
|
||||
@@ -216,7 +207,6 @@ async fn insert_client(
|
||||
metadata: &ClientMetadata,
|
||||
) -> Result<i32, Error> {
|
||||
use crate::db::schema::{client_metadata, program_client};
|
||||
let pubkey = pubkey.clone();
|
||||
let metadata = metadata.clone();
|
||||
|
||||
let mut conn = db.get().await.map_err(|e| {
|
||||
@@ -226,7 +216,6 @@ async fn insert_client(
|
||||
|
||||
conn.exclusive_transaction(|conn| {
|
||||
let keyholder = keyholder.clone();
|
||||
let pubkey = pubkey.clone();
|
||||
Box::pin(async move {
|
||||
const NONCE_START: i32 = 1;
|
||||
|
||||
@@ -255,7 +244,7 @@ async fn insert_client(
|
||||
conn,
|
||||
&keyholder,
|
||||
&ClientCredentials {
|
||||
pubkey: pubkey.clone(),
|
||||
pubkey: *pubkey,
|
||||
nonce: NONCE_START,
|
||||
},
|
||||
client_id,
|
||||
|
||||
@@ -7,11 +7,11 @@ use kameo::{Actor, actor::ActorRef, messages};
|
||||
use rand::{SeedableRng, rng, rngs::StdRng};
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::{CreateNew, Decrypt, GetState, KeyHolder, KeyHolderState},
|
||||
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
||||
crypto::integrity,
|
||||
db::{
|
||||
DatabaseError, DatabasePool,
|
||||
models::{self, SqliteTimestamp},
|
||||
models::{self},
|
||||
schema,
|
||||
},
|
||||
evm::{
|
||||
@@ -136,7 +136,7 @@ impl EvmActor {
|
||||
&mut self,
|
||||
basic: SharedGrantSettings,
|
||||
grant: SpecificGrant,
|
||||
) -> Result<i32, Error> {
|
||||
) -> Result<integrity::Verified<i32>, Error> {
|
||||
match grant {
|
||||
SpecificGrant::EtherTransfer(settings) => self
|
||||
.engine
|
||||
@@ -158,7 +158,7 @@ impl EvmActor {
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
|
||||
pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> {
|
||||
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||
// let keyholder = self.keyholder.clone();
|
||||
|
||||
|
||||
@@ -30,17 +30,26 @@ pub enum Error {
|
||||
}
|
||||
|
||||
impl Error {
|
||||
fn internal(details: impl Into<String>) -> Self {
|
||||
Self::Internal {
|
||||
details: details.into(),
|
||||
}
|
||||
#[track_caller]
|
||||
pub(super) fn internal(details: impl Into<String>, err: &impl std::fmt::Debug) -> Self {
|
||||
let details = details.into();
|
||||
let caller = std::panic::Location::caller();
|
||||
error!(
|
||||
caller_file = %caller.file(),
|
||||
caller_line = caller.line(),
|
||||
caller_column = caller.column(),
|
||||
details = %details,
|
||||
error = ?err,
|
||||
"Internal error"
|
||||
);
|
||||
|
||||
Self::Internal { details }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diesel::result::Error> for Error {
|
||||
fn from(e: diesel::result::Error) -> Self {
|
||||
error!(?e, "Database error");
|
||||
Self::internal("Database error")
|
||||
Self::internal("Database error", &e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use arbiter_proto::transport::Bi;
|
||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
use kameo::{actor::ActorRef, error::SendError};
|
||||
use kameo::actor::ActorRef;
|
||||
use tracing::error;
|
||||
|
||||
use super::Error;
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
keyholder::KeyHolder,
|
||||
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth::Outbound},
|
||||
},
|
||||
crypto::integrity::{self, AttestationStatus},
|
||||
crypto::integrity,
|
||||
db::{DatabasePool, schema::useragent_client},
|
||||
};
|
||||
|
||||
@@ -48,10 +48,10 @@ async fn get_current_nonce_and_id(
|
||||
db: &DatabasePool,
|
||||
key: &AuthPublicKey,
|
||||
) -> Result<(i32, i32), Error> {
|
||||
let mut db_conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
let mut db_conn = db
|
||||
.get()
|
||||
.await
|
||||
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||
db_conn
|
||||
.exclusive_transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
@@ -65,10 +65,7 @@ async fn get_current_nonce_and_id(
|
||||
})
|
||||
.await
|
||||
.optional()
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::internal("Database operation failed")
|
||||
})?
|
||||
.map_err(|e| Error::internal("Database operation failed", &e))?
|
||||
.ok_or_else(|| {
|
||||
error!(?key, "Public key not found in database");
|
||||
Error::UnregisteredPublicKey
|
||||
@@ -80,14 +77,14 @@ async fn verify_integrity(
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
pubkey: &AuthPublicKey,
|
||||
) -> Result<(), Error> {
|
||||
let mut db_conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
let mut db_conn = db
|
||||
.get()
|
||||
.await
|
||||
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||
|
||||
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?;
|
||||
|
||||
let result = integrity::verify_entity(
|
||||
let attestation_status = integrity::check_entity_attestation(
|
||||
&mut db_conn,
|
||||
keyholder,
|
||||
&UserAgentCredentials {
|
||||
@@ -97,12 +94,17 @@ async fn verify_integrity(
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Integrity verification failed");
|
||||
Error::internal("Integrity verification failed")
|
||||
})?;
|
||||
.map_err(|e| Error::internal("Integrity verification failed", &e))?;
|
||||
|
||||
Ok(())
|
||||
use integrity::AttestationStatus as AS;
|
||||
// SAFETY (policy): challenge auth must work in both vault states.
|
||||
// While sealed, integrity checks can only report `Unavailable` because key material is not
|
||||
// accessible. While unsealed, the same check can report `Attested`.
|
||||
// This path intentionally accepts both outcomes to keep challenge auth available across state
|
||||
// transitions; stricter verification is enforced in sensitive post-auth flows.
|
||||
match attestation_status {
|
||||
AS::Attested | AS::Unavailable => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_nonce(
|
||||
@@ -110,10 +112,10 @@ async fn create_nonce(
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
pubkey: &AuthPublicKey,
|
||||
) -> Result<i32, Error> {
|
||||
let mut db_conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
let mut db_conn = db
|
||||
.get()
|
||||
.await
|
||||
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||
let new_nonce = db_conn
|
||||
.exclusive_transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
@@ -124,10 +126,7 @@ async fn create_nonce(
|
||||
.returning((useragent_client::id, useragent_client::nonce))
|
||||
.get_result(conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::internal("Database operation failed")
|
||||
})?;
|
||||
.map_err(|e| Error::internal("Database operation failed", &e))?;
|
||||
|
||||
integrity::sign_entity(
|
||||
conn,
|
||||
@@ -139,10 +138,7 @@ async fn create_nonce(
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Integrity signature update failed");
|
||||
Error::internal("Database error")
|
||||
})?;
|
||||
.map_err(|e| Error::internal("Database error", &e))?;
|
||||
|
||||
Result::<_, Error>::Ok(new_nonce)
|
||||
})
|
||||
@@ -158,10 +154,10 @@ async fn register_key(
|
||||
) -> Result<(), Error> {
|
||||
let pubkey_bytes = pubkey.to_stored_bytes();
|
||||
let key_type = pubkey.key_type();
|
||||
let mut conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
let mut conn = db
|
||||
.get()
|
||||
.await
|
||||
.map_err(|e| Error::internal("Database unavailable", &e))?;
|
||||
|
||||
conn.transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
@@ -176,22 +172,32 @@ async fn register_key(
|
||||
.returning(useragent_client::id)
|
||||
.get_result(conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::internal("Database operation failed")
|
||||
})?;
|
||||
.map_err(|e| Error::internal("Database operation failed", &e))?;
|
||||
|
||||
let entity = UserAgentCredentials {
|
||||
pubkey: pubkey.clone(),
|
||||
nonce: NONCE_START,
|
||||
};
|
||||
|
||||
integrity::sign_entity(conn, &keyholder, &entity, id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Failed to sign integrity tag for new user-agent key");
|
||||
Error::internal("Failed to register public key")
|
||||
})?;
|
||||
if let Err(e) = integrity::sign_entity(
|
||||
conn,
|
||||
keyholder,
|
||||
&UserAgentCredentials {
|
||||
pubkey: pubkey.clone(),
|
||||
nonce: NONCE_START,
|
||||
},
|
||||
id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
match e {
|
||||
integrity::Error::Keyholder(
|
||||
crate::actors::keyholder::Error::NotBootstrapped,
|
||||
) => {
|
||||
// IMPORTANT: bootstrap-token auth must work before the vault has a root key.
|
||||
// We intentionally allow creating the DB row first and backfill envelopes
|
||||
// after bootstrap/unseal to keep the bootstrap flow possible.
|
||||
}
|
||||
other => {
|
||||
return Err(Error::internal("Failed to register public key", &other));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result::<_, Error>::Ok(())
|
||||
})
|
||||
@@ -254,10 +260,7 @@ where
|
||||
token: token.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Failed to consume bootstrap token");
|
||||
Error::internal("Failed to consume bootstrap token")
|
||||
})?;
|
||||
.map_err(|e| Error::internal("Failed to consume bootstrap token", &e))?;
|
||||
|
||||
if !token_ok {
|
||||
error!("Invalid bootstrap token provided");
|
||||
|
||||
@@ -108,7 +108,7 @@ use crate::crypto::integrity::hashing::Hashable;
|
||||
|
||||
impl Hashable for AuthPublicKey {
|
||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||
hasher.update(&self.to_stored_bytes());
|
||||
hasher.update(self.to_stored_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ use kameo::prelude::Context;
|
||||
use tracing::{error, info};
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
|
||||
use crate::actors::keyholder::KeyHolderState;
|
||||
use crate::actors::user_agent::session::Error;
|
||||
use crate::db::models::{
|
||||
@@ -18,6 +17,10 @@ use crate::db::models::{
|
||||
};
|
||||
use crate::evm::policies::{Grant, SpecificGrant};
|
||||
use crate::safe_cell::SafeCell;
|
||||
use crate::{
|
||||
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer,
|
||||
crypto::integrity::{self, Verified},
|
||||
};
|
||||
use crate::{
|
||||
actors::{
|
||||
evm::{
|
||||
@@ -29,11 +32,66 @@ use crate::{
|
||||
UserAgentSession,
|
||||
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
||||
},
|
||||
user_agent::{AuthPublicKey, UserAgentCredentials},
|
||||
},
|
||||
db::schema::useragent_client,
|
||||
safe_cell::SafeCellHandle as _,
|
||||
};
|
||||
|
||||
fn is_vault_sealed_from_evm<M>(err: &SendError<M, crate::actors::evm::Error>) -> bool {
|
||||
matches!(
|
||||
err,
|
||||
SendError::HandlerError(crate::actors::evm::Error::Keyholder(
|
||||
keyholder::Error::NotBootstrapped
|
||||
)) | SendError::HandlerError(crate::actors::evm::Error::Integrity(
|
||||
crate::crypto::integrity::Error::Keyholder(keyholder::Error::NotBootstrapped)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
impl UserAgentSession {
|
||||
async fn backfill_useragent_integrity(&self) -> Result<(), Error> {
|
||||
let mut conn = self.props.db.get().await?;
|
||||
let keyholder = self.props.actors.key_holder.clone();
|
||||
|
||||
conn.transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
let rows: Vec<(i32, i32, Vec<u8>, crate::db::models::KeyType)> =
|
||||
useragent_client::table
|
||||
.select((
|
||||
useragent_client::id,
|
||||
useragent_client::nonce,
|
||||
useragent_client::public_key,
|
||||
useragent_client::key_type,
|
||||
))
|
||||
.load(conn)
|
||||
.await?;
|
||||
|
||||
for (id, nonce, public_key, key_type) in rows {
|
||||
let pubkey = AuthPublicKey::try_from((key_type, public_key)).map_err(|e| {
|
||||
Error::internal(format!("Invalid user-agent key in db: {e}"))
|
||||
})?;
|
||||
|
||||
integrity::sign_entity(
|
||||
conn,
|
||||
&keyholder,
|
||||
&UserAgentCredentials { pubkey, nonce },
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::internal(format!("Failed to backfill user-agent integrity: {e}"))
|
||||
})?;
|
||||
}
|
||||
|
||||
Result::<_, Error>::Ok(())
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
|
||||
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
||||
error!("Received encrypted key in invalid state");
|
||||
@@ -191,6 +249,7 @@ impl UserAgentSession {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
self.backfill_useragent_integrity().await?;
|
||||
info!("Successfully unsealed key with client-provided key");
|
||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||
Ok(())
|
||||
@@ -252,6 +311,7 @@ impl UserAgentSession {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
self.backfill_useragent_integrity().await?;
|
||||
info!("Successfully bootstrapped vault with client-provided key");
|
||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||
Ok(())
|
||||
@@ -325,12 +385,15 @@ impl UserAgentSession {
|
||||
#[messages]
|
||||
impl UserAgentSession {
|
||||
#[message]
|
||||
pub(crate) async fn handle_grant_list(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
||||
pub(crate) async fn handle_grant_list(
|
||||
&mut self,
|
||||
) -> Result<Vec<Grant<SpecificGrant>>, GrantMutationError> {
|
||||
match self.props.actors.evm.ask(UseragentListGrants {}).await {
|
||||
Ok(grants) => Ok(grants),
|
||||
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||
Err(err) => {
|
||||
error!(?err, "EVM grant list failed");
|
||||
Err(Error::internal("Failed to list EVM grants"))
|
||||
Err(GrantMutationError::Internal)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,7 +403,7 @@ impl UserAgentSession {
|
||||
&mut self,
|
||||
basic: crate::evm::policies::SharedGrantSettings,
|
||||
grant: crate::evm::policies::SpecificGrant,
|
||||
) -> Result<i32, GrantMutationError> {
|
||||
) -> Result<Verified<i32>, GrantMutationError> {
|
||||
match self
|
||||
.props
|
||||
.actors
|
||||
@@ -349,6 +412,7 @@ impl UserAgentSession {
|
||||
.await
|
||||
{
|
||||
Ok(grant_id) => Ok(grant_id),
|
||||
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||
Err(err) => {
|
||||
error!(?err, "EVM grant create failed");
|
||||
Err(GrantMutationError::Internal)
|
||||
@@ -365,10 +429,13 @@ impl UserAgentSession {
|
||||
.props
|
||||
.actors
|
||||
.evm
|
||||
.ask(UseragentDeleteGrant { grant_id })
|
||||
.ask(UseragentDeleteGrant {
|
||||
_grant_id: grant_id,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if is_vault_sealed_from_evm(&err) => Err(GrantMutationError::VaultSealed),
|
||||
Err(err) => {
|
||||
error!(?err, "EVM grant delete failed");
|
||||
Err(GrantMutationError::Internal)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::{
|
||||
actors::keyholder, crypto::integrity::hashing::Hashable, safe_cell::SafeCellHandle as _,
|
||||
};
|
||||
use hmac::{Hmac, Mac as _};
|
||||
use crate::actors::keyholder;
|
||||
use hmac::Hmac;
|
||||
use sha2::Sha256;
|
||||
use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
|
||||
use diesel::{ExpressionMethods as _, QueryDsl, dsl::insert_into, sqlite::Sqlite};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
@@ -10,12 +11,13 @@ use kameo::{actor::ActorRef, error::SendError};
|
||||
use sha2::Digest as _;
|
||||
|
||||
pub mod hashing;
|
||||
use self::hashing::Hashable;
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::{KeyHolder, SignIntegrity, VerifyIntegrity},
|
||||
db::{
|
||||
self,
|
||||
models::{IntegrityEnvelope, NewIntegrityEnvelope},
|
||||
models::{IntegrityEnvelope as IntegrityEnvelopeRow, NewIntegrityEnvelope},
|
||||
schema::integrity_envelope,
|
||||
},
|
||||
};
|
||||
@@ -48,11 +50,35 @@ pub enum Error {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[must_use]
|
||||
pub enum AttestationStatus {
|
||||
Attested,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Verified<T>(T);
|
||||
|
||||
impl<T> AsRef<T> for Verified<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Verified<T> {
|
||||
pub fn into_inner(self) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Verified<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub const CURRENT_PAYLOAD_VERSION: i32 = 1;
|
||||
pub const INTEGRITY_SUBKEY_TAG: &[u8] = b"arbiter/db-integrity-key/v1";
|
||||
|
||||
@@ -88,31 +114,95 @@ fn build_mac_input(
|
||||
out
|
||||
}
|
||||
|
||||
pub trait IntoId {
|
||||
fn into_id(self) -> Vec<u8>;
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EntityId(Vec<u8>);
|
||||
|
||||
impl IntoId for i32 {
|
||||
fn into_id(self) -> Vec<u8> {
|
||||
self.to_be_bytes().to_vec()
|
||||
impl Deref for EntityId {
|
||||
type Target = [u8];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoId for &'_ [u8] {
|
||||
fn into_id(self) -> Vec<u8> {
|
||||
self.to_vec()
|
||||
impl From<i32> for EntityId {
|
||||
fn from(value: i32) -> Self {
|
||||
Self(value.to_be_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sign_entity<E: Integrable>(
|
||||
impl From<&'_ [u8]> for EntityId {
|
||||
fn from(bytes: &'_ [u8]) -> Self {
|
||||
Self(bytes.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lookup_verified<E, C, F, Fut>(
|
||||
conn: &mut C,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
entity_id: impl Into<EntityId>,
|
||||
load: F,
|
||||
) -> Result<Verified<E>, Error>
|
||||
where
|
||||
C: AsyncConnection<Backend = Sqlite>,
|
||||
E: Integrable,
|
||||
F: FnOnce(&mut C) -> Fut,
|
||||
Fut: Future<Output = Result<E, db::DatabaseError>>,
|
||||
{
|
||||
let entity = load(conn).await?;
|
||||
verify_entity(conn, keyholder, &entity, entity_id).await?;
|
||||
Ok(Verified(entity))
|
||||
}
|
||||
|
||||
pub async fn lookup_verified_allow_unavailable<E, C, F, Fut>(
|
||||
conn: &mut C,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
entity_id: impl Into<EntityId>,
|
||||
load: F,
|
||||
) -> Result<Verified<E>, Error>
|
||||
where
|
||||
C: AsyncConnection<Backend = Sqlite>,
|
||||
E: Integrable+ 'static,
|
||||
F: FnOnce(&mut C) -> Fut,
|
||||
Fut: Future<Output = Result<E, db::DatabaseError>>,
|
||||
{
|
||||
let entity = load(conn).await?;
|
||||
match check_entity_attestation(conn, keyholder, &entity, entity_id.into()).await? {
|
||||
// IMPORTANT: allow_unavailable mode must succeed with an unattested result when vault key
|
||||
// material is unavailable, otherwise integrity checks can be silently bypassed while sealed.
|
||||
AttestationStatus::Attested | AttestationStatus::Unavailable => Ok(Verified(entity)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lookup_verified_from_query<E, Id, C, F>(
|
||||
conn: &mut C,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
load: F,
|
||||
) -> Result<Verified<E>, Error>
|
||||
where
|
||||
C: AsyncConnection<Backend = Sqlite> + Send,
|
||||
E: Integrable,
|
||||
Id: Into<EntityId>,
|
||||
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?;
|
||||
Ok(Verified(entity))
|
||||
}
|
||||
|
||||
pub async fn sign_entity<E: Integrable, Id: Into<EntityId> + Clone>(
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
entity: &E,
|
||||
entity_id: impl IntoId,
|
||||
) -> Result<(), Error> {
|
||||
let payload_hash = payload_hash(&entity);
|
||||
as_entity_id: Id,
|
||||
) -> Result<Verified<Id>, Error> {
|
||||
let payload_hash = payload_hash(entity);
|
||||
|
||||
let entity_id = entity_id.into_id();
|
||||
let entity_id = as_entity_id.clone().into();
|
||||
|
||||
let mac_input = build_mac_input(E::KIND, &entity_id, E::VERSION, &payload_hash);
|
||||
|
||||
@@ -127,7 +217,7 @@ pub async fn sign_entity<E: Integrable>(
|
||||
insert_into(integrity_envelope::table)
|
||||
.values(NewIntegrityEnvelope {
|
||||
entity_kind: E::KIND.to_owned(),
|
||||
entity_id: entity_id,
|
||||
entity_id: entity_id.to_vec(),
|
||||
payload_version: E::VERSION,
|
||||
key_version,
|
||||
mac: mac.to_vec(),
|
||||
@@ -146,19 +236,19 @@ pub async fn sign_entity<E: Integrable>(
|
||||
.await
|
||||
.map_err(db::DatabaseError::from)?;
|
||||
|
||||
Ok(())
|
||||
Ok(Verified(as_entity_id))
|
||||
}
|
||||
|
||||
pub async fn verify_entity<E: Integrable>(
|
||||
pub async fn check_entity_attestation<E: Integrable>(
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
entity: &E,
|
||||
entity_id: impl IntoId,
|
||||
entity_id: impl Into<EntityId>,
|
||||
) -> Result<AttestationStatus, Error> {
|
||||
let entity_id = entity_id.into_id();
|
||||
let envelope: IntegrityEnvelope = integrity_envelope::table
|
||||
let entity_id = entity_id.into();
|
||||
let envelope: IntegrityEnvelopeRow = integrity_envelope::table
|
||||
.filter(integrity_envelope::entity_kind.eq(E::KIND))
|
||||
.filter(integrity_envelope::entity_id.eq(&entity_id))
|
||||
.filter(integrity_envelope::entity_id.eq(&*entity_id))
|
||||
.first(conn)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
@@ -176,7 +266,7 @@ pub async fn verify_entity<E: Integrable>(
|
||||
});
|
||||
}
|
||||
|
||||
let payload_hash = payload_hash(&entity);
|
||||
let payload_hash = payload_hash(entity);
|
||||
let mac_input = build_mac_input(E::KIND, &entity_id, envelope.payload_version, &payload_hash);
|
||||
|
||||
let result = keyholder
|
||||
@@ -199,26 +289,56 @@ pub async fn verify_entity<E: Integrable>(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify_entity<'a, E: Integrable>(
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
entity: &'a E,
|
||||
entity_id: impl Into<EntityId>,
|
||||
) -> Result<Verified<&'a E>, Error> {
|
||||
match check_entity_attestation::<E>(conn, keyholder, entity, entity_id).await? {
|
||||
AttestationStatus::Attested => Ok(Verified(entity)),
|
||||
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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use diesel::{ExpressionMethods as _, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::{actor::ActorRef, prelude::Spawn};
|
||||
use rand::seq::SliceRandom;
|
||||
use sha2::Digest;
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::{Bootstrap, KeyHolder},
|
||||
db::{self, schema},
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
|
||||
use super::{Error, Integrable, sign_entity, verify_entity};
|
||||
use super::{hashing::Hashable, payload_hash};
|
||||
use super::hashing::Hashable;
|
||||
use super::{
|
||||
check_entity_attestation, AttestationStatus, Error, Integrable, lookup_verified,
|
||||
lookup_verified_allow_unavailable, lookup_verified_from_query, sign_entity, verify_entity,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
struct DummyEntity {
|
||||
payload_version: i32,
|
||||
payload: Vec<u8>,
|
||||
@@ -271,7 +391,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(count, 1, "envelope row must be created exactly once");
|
||||
verify_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
let _ = check_entity_attestation(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -301,7 +421,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let err = verify_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
let err = check_entity_attestation(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, Error::MacMismatch { .. }));
|
||||
@@ -329,9 +449,233 @@ mod tests {
|
||||
..entity
|
||||
};
|
||||
|
||||
let err = verify_entity(&mut conn, &keyholder, &tampered, ENTITY_ID)
|
||||
let err = check_entity_attestation(&mut conn, &keyholder, &tampered, ENTITY_ID)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, Error::MacMismatch { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn allow_unavailable_lookup_passes_while_sealed() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: &[u8] = b"entity-id-31";
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
drop(keyholder);
|
||||
|
||||
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||
let status = check_entity_attestation(&mut conn, &sealed_keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(status, AttestationStatus::Unavailable);
|
||||
|
||||
#[expect(clippy::disallowed_methods, reason = "test only")]
|
||||
lookup_verified_allow_unavailable(&mut conn, &sealed_keyholder, ENTITY_ID, |_| async {
|
||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn strict_verify_fails_closed_while_sealed() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: &[u8] = b"entity-id-41";
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
drop(keyholder);
|
||||
|
||||
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||
|
||||
let err = verify_entity(&mut conn, &sealed_keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
||||
));
|
||||
|
||||
let err = lookup_verified(&mut conn, &sealed_keyholder, ENTITY_ID, |_| async {
|
||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lookup_verified_supports_loaded_aggregate() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: i32 = 77;
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let verified = lookup_verified(&mut conn, &keyholder, ENTITY_ID, |_| async {
|
||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(verified.payload, b"payload-v1".to_vec());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lookup_verified_allow_unavailable_works_while_sealed() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: i32 = 78;
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
drop(keyholder);
|
||||
|
||||
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||
|
||||
#[expect(clippy::disallowed_methods, reason = "test only")]
|
||||
lookup_verified_allow_unavailable(&mut conn, &sealed_keyholder, ENTITY_ID, |_| async {
|
||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extension_trait_lookup_verified_required_works() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: i32 = 79;
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let verified = lookup_verified(&mut conn, &keyholder, ENTITY_ID, |_| {
|
||||
Box::pin(async {
|
||||
Ok::<_, db::DatabaseError>(DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
})
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(verified.payload, b"payload-v1".to_vec());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lookup_verified_from_query_helpers_work() {
|
||||
let db = db::create_test_pool().await;
|
||||
let keyholder = bootstrapped_keyholder(&db).await;
|
||||
let mut conn = db.get().await.unwrap();
|
||||
|
||||
const ENTITY_ID: i32 = 80;
|
||||
|
||||
let entity = DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
};
|
||||
|
||||
sign_entity(&mut conn, &keyholder, &entity, ENTITY_ID)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let verified = lookup_verified_from_query(&mut conn, &keyholder, |_| {
|
||||
Box::pin(async {
|
||||
Ok::<_, db::DatabaseError>((
|
||||
ENTITY_ID,
|
||||
DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
},
|
||||
))
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(verified.payload, b"payload-v1".to_vec());
|
||||
|
||||
drop(keyholder);
|
||||
let sealed_keyholder = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
|
||||
|
||||
let err = lookup_verified_from_query(&mut conn, &sealed_keyholder, |_| {
|
||||
Box::pin(async {
|
||||
Ok::<_, db::DatabaseError>((
|
||||
ENTITY_ID,
|
||||
DummyEntity {
|
||||
payload_version: 1,
|
||||
payload: b"payload-v1".to_vec(),
|
||||
},
|
||||
))
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::Keyholder(crate::actors::keyholder::Error::NotBootstrapped)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,10 +62,10 @@ impl<T: Hashable> Hashable for Option<T> {
|
||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||
match self {
|
||||
Some(value) => {
|
||||
hasher.update(&[1]);
|
||||
hasher.update([1]);
|
||||
value.hash(hasher);
|
||||
}
|
||||
None => hasher.update(&[0]),
|
||||
None => hasher.update([0]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,12 +96,12 @@ impl Hashable for alloy::primitives::U256 {
|
||||
|
||||
impl Hashable for chrono::Duration {
|
||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||
hasher.update(&self.num_seconds().to_be_bytes());
|
||||
hasher.update(self.num_seconds().to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl Hashable for chrono::DateTime<chrono::Utc> {
|
||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||
hasher.update(&self.timestamp_millis().to_be_bytes());
|
||||
hasher.update(self.timestamp_millis().to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use kameo::actor::ActorRef;
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::KeyHolder,
|
||||
crypto::integrity,
|
||||
crypto::integrity::{self, Verified},
|
||||
db::{
|
||||
self, DatabaseError,
|
||||
models::{
|
||||
@@ -153,12 +153,36 @@ impl Engine {
|
||||
{
|
||||
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||
|
||||
let grant = P::try_find_grant(&context, &mut conn)
|
||||
let verified_settings =
|
||||
match integrity::lookup_verified_from_query(&mut conn, &self.keyholder, |conn| {
|
||||
let context = context.clone();
|
||||
Box::pin(async move {
|
||||
let grant = P::try_find_grant(&context, conn)
|
||||
.await
|
||||
.map_err(DatabaseError::from)?
|
||||
.ok_or_else(|| DatabaseError::from(diesel::result::Error::NotFound))?;
|
||||
|
||||
Ok::<_, DatabaseError>((grant.common_settings_id, grant.settings))
|
||||
})
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(verified) => verified,
|
||||
Err(integrity::Error::Database(DatabaseError::Connection(
|
||||
diesel::result::Error::NotFound,
|
||||
))) => return Err(PolicyError::NoMatchingGrant),
|
||||
Err(err) => return Err(PolicyError::Integrity(err)),
|
||||
};
|
||||
|
||||
let mut grant = P::try_find_grant(&context, &mut conn)
|
||||
.await
|
||||
.map_err(DatabaseError::from)?
|
||||
.ok_or(PolicyError::NoMatchingGrant)?;
|
||||
|
||||
integrity::verify_entity(&mut conn, &self.keyholder, &grant.settings, grant.id).await?;
|
||||
// IMPORTANT: policy evaluation uses extra non-integrity fields from Grant
|
||||
// (e.g., per-policy ids), so we currently reload Grant after the query-native
|
||||
// integrity check over canonicalized settings.
|
||||
grant.settings = verified_settings.into_inner();
|
||||
|
||||
let mut violations = check_shared_constraints(
|
||||
&context,
|
||||
@@ -214,7 +238,7 @@ impl Engine {
|
||||
pub async fn create_grant<P: Policy>(
|
||||
&self,
|
||||
full_grant: CombinedSettings<P::Settings>,
|
||||
) -> Result<i32, DatabaseError>
|
||||
) -> Result<Verified<i32>, DatabaseError>
|
||||
where
|
||||
P::Settings: Clone,
|
||||
{
|
||||
@@ -258,11 +282,12 @@ impl Engine {
|
||||
|
||||
P::create_grant(&basic_grant, &full_grant.specific, conn).await?;
|
||||
|
||||
integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id)
|
||||
.await
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
let verified_entity_id =
|
||||
integrity::sign_entity(conn, &keyholder, &full_grant, basic_grant.id)
|
||||
.await
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
|
||||
QueryResult::Ok(basic_grant.id)
|
||||
QueryResult::Ok(verified_entity_id)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
@@ -273,7 +298,7 @@ impl Engine {
|
||||
async fn list_one_kind<Kind: Policy, Y>(
|
||||
&self,
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
) -> Result<impl Iterator<Item = Grant<Y>>, ListError>
|
||||
) -> Result<Vec<Grant<Y>>, ListError>
|
||||
where
|
||||
Y: From<Kind::Settings>,
|
||||
{
|
||||
@@ -281,16 +306,26 @@ impl Engine {
|
||||
.await
|
||||
.map_err(DatabaseError::from)?;
|
||||
|
||||
// Verify integrity of all grants before returning any results
|
||||
for grant in &all_grants {
|
||||
integrity::verify_entity(conn, &self.keyholder, &grant.settings, grant.id).await?;
|
||||
let mut verified_grants = Vec::with_capacity(all_grants.len());
|
||||
|
||||
// Verify integrity of all grants before returning any results.
|
||||
for grant in all_grants {
|
||||
integrity::verify_entity(
|
||||
conn,
|
||||
&self.keyholder,
|
||||
&grant.settings,
|
||||
grant.common_settings_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
verified_grants.push(Grant {
|
||||
id: grant.id,
|
||||
common_settings_id: grant.common_settings_id,
|
||||
settings: grant.settings.generalize(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(all_grants.into_iter().map(|g| Grant {
|
||||
id: g.id,
|
||||
common_settings_id: g.common_settings_id,
|
||||
settings: g.settings.generalize(),
|
||||
}))
|
||||
Ok(verified_grants)
|
||||
}
|
||||
|
||||
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListError> {
|
||||
|
||||
@@ -200,7 +200,7 @@ pub enum SpecificGrant {
|
||||
TokenTransfer(token_transfers::Settings),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CombinedSettings<PolicyGrant> {
|
||||
pub shared: SharedGrantSettings,
|
||||
pub specific: PolicyGrant,
|
||||
|
||||
@@ -110,7 +110,8 @@ async fn check_rate_limits(
|
||||
let mut violations = Vec::new();
|
||||
let window = grant.settings.specific.limit.window;
|
||||
|
||||
let past_transaction = query_relevant_past_transaction(grant.id, window, db).await?;
|
||||
let past_transaction =
|
||||
query_relevant_past_transaction(grant.common_settings_id, window, db).await?;
|
||||
|
||||
let window_start = chrono::Utc::now() - grant.settings.specific.limit.window;
|
||||
let prospective_cumulative_volume: U256 = past_transaction
|
||||
@@ -249,21 +250,20 @@ impl Policy for EtherTransfer {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let settings = Settings {
|
||||
target: targets,
|
||||
limit: VolumeRateLimit {
|
||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume)
|
||||
.map_err(|err| diesel::result::Error::DeserializationError(Box::new(err)))?,
|
||||
window: chrono::Duration::seconds(limit.window_secs as i64),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Some(Grant {
|
||||
id: grant.id,
|
||||
common_settings_id: grant.basic_grant_id,
|
||||
settings: CombinedSettings {
|
||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||
specific: settings,
|
||||
specific: Settings {
|
||||
target: targets,
|
||||
limit: VolumeRateLimit {
|
||||
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(|err| {
|
||||
diesel::result::Error::DeserializationError(Box::new(err))
|
||||
})?,
|
||||
window: chrono::Duration::seconds(limit.window_secs as i64),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -286,18 +286,16 @@ impl Policy for TokenTransfer {
|
||||
}
|
||||
};
|
||||
|
||||
let settings = Settings {
|
||||
token_contract: Address::from(token_contract),
|
||||
target,
|
||||
volume_limits,
|
||||
};
|
||||
|
||||
Ok(Some(Grant {
|
||||
id: token_grant.id,
|
||||
common_settings_id: token_grant.basic_grant_id,
|
||||
settings: CombinedSettings {
|
||||
shared: SharedGrantSettings::try_from_model(basic_grant)?,
|
||||
specific: settings,
|
||||
specific: Settings {
|
||||
token_contract: Address::from(token_contract),
|
||||
target,
|
||||
volume_limits,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -121,6 +121,9 @@ async fn handle_grant_list(
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
||||
EvmGrantListResult::Error(ProtoEvmError::VaultSealed.into())
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(error = ?err, "Failed to list EVM grants");
|
||||
EvmGrantListResult::Error(ProtoEvmError::Internal.into())
|
||||
@@ -147,7 +150,7 @@ async fn handle_grant_create(
|
||||
.try_convert()?;
|
||||
|
||||
let result = match actor.ask(HandleGrantCreate { basic, grant }).await {
|
||||
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id),
|
||||
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id.into_inner()),
|
||||
Err(kameo::error::SendError::HandlerError(GrantMutationError::VaultSealed)) => {
|
||||
EvmGrantCreateResult::Error(ProtoEvmError::VaultSealed.into())
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ use arbiter_server::{
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||
use diesel::{ExpressionMethods as _, QueryDsl as _, insert_into};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::actor::Spawn as _;
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user