diff --git a/server/crates/arbiter-server/src/actors/vault/mod.rs b/server/crates/arbiter-server/src/actors/vault/mod.rs index 23b4cad..b995891 100644 --- a/server/crates/arbiter-server/src/actors/vault/mod.rs +++ b/server/crates/arbiter-server/src/actors/vault/mod.rs @@ -63,6 +63,9 @@ pub enum Error { #[error("Broken database")] BrokenDatabase, + + #[error("Integrity key version mismatch: envelope uses key {envelope}, current key is {current}")] + KeyVersionMismatch { envelope: i32, current: i32 }, } struct Unsealed { @@ -393,7 +396,10 @@ impl Vault { } = Self::expect_unsealed(&mut self.state)?; if *root_key_history_id != key_version { - return Ok(false); + return Err(Error::KeyVersionMismatch { + envelope: key_version, + current: *root_key_history_id, + }); } let mut hmac = root_key.0.read_inline(|k| { diff --git a/server/crates/arbiter-server/src/crypto/integrity/v1.rs b/server/crates/arbiter-server/src/crypto/integrity/v1.rs index edb2274..a88506f 100644 --- a/server/crates/arbiter-server/src/crypto/integrity/v1.rs +++ b/server/crates/arbiter-server/src/crypto/integrity/v1.rs @@ -192,7 +192,10 @@ pub async fn verify_entity( Ok(false) => Err(Error::MacMismatch { entity_kind: E::KIND, }), - Err(SendError::HandlerError(vault::Error::Sealed)) => Ok(AttestationStatus::Unavailable), + Err(SendError::HandlerError(vault::Error::Sealed)) + | Err(SendError::HandlerError(vault::Error::KeyVersionMismatch { .. })) => { + Ok(AttestationStatus::Unavailable) + } Err(_) => Err(Error::VaultSend), } } @@ -331,4 +334,47 @@ mod tests { .unwrap_err(); assert!(matches!(err, Error::MacMismatch { .. })); } + + #[tokio::test] + async fn key_version_mismatch_returns_unavailable_not_mac_mismatch() { + use crate::db::schema::integrity_envelope; + use super::AttestationStatus; + + const ENTITY_ID: &[u8] = b"entity-id-rotation-test"; + + let db = db::create_test_pool().await; + let vault = bootstrapped_vault(&db).await; + let mut conn = db.get().await.unwrap(); + + let entity = DummyEntity { + payload_version: 1, + payload: b"payload-v1".to_vec(), + }; + + sign_entity(&mut conn, &vault, &entity, ENTITY_ID) + .await + .unwrap(); + + // Simulate key rotation: update the stored key_version to a stale value. + // After real rotation the vault's root_key_history_id would advance, but + // here we achieve the same mismatch by back-dating the envelope's key_version. + diesel::update(integrity_envelope::table) + .filter(integrity_envelope::entity_kind.eq("dummy_entity")) + .filter(integrity_envelope::entity_id.eq(ENTITY_ID)) + .set(integrity_envelope::key_version.eq(0)) + .execute(&mut conn) + .await + .unwrap(); + + // Must NOT error — version mismatch is Unavailable, not tampered. + let status = verify_entity(&mut conn, &vault, &entity, ENTITY_ID) + .await + .expect("key version mismatch must not be treated as an error"); + + assert_eq!( + status, + AttestationStatus::Unavailable, + "stale key_version must yield Unavailable, not MacMismatch" + ); + } }