fix(vault): keep the root key row identity across a seal-key re-key

This commit is contained in:
CleverWild
2026-09-07 13:33:09 +02:00
parent d916997ef3
commit 3907241644

View File

@@ -277,8 +277,10 @@ impl Vault {
Ok(()) Ok(())
} }
/// Re-encrypts the root key with `new_seal_key` and records a new root_key_history row. /// Re-encrypts the root key with `new_seal_key`, updating its `root_key_history` row in
/// Called after a Shamir re-key so the old seal key is no longer sufficient to unseal. /// place. Called after a Shamir re-key, so the old seal key is no longer sufficient to
/// unseal. The root key itself does not change, so its row identity (and the nonce counter
/// and integrity envelopes bound to it) must not change either.
#[message] #[message]
pub async fn rekey_root_key(&mut self, mut new_seal_key: KeyCell) -> Result<(), Error> { pub async fn rekey_root_key(&mut self, mut new_seal_key: KeyCell) -> Result<(), Error> {
let Unsealed { let Unsealed {
@@ -298,34 +300,31 @@ impl Vault {
}) })
})?; })?;
let data_encryption_nonce = Nonce::default();
let mut conn = self.db.get().await?; let mut conn = self.db.get().await?;
let new_root_key_history_id: i32 = conn
.transaction(async |conn| {
let new_id = insert_into(schema::root_key_history::table)
.values(&models::NewRootKeyHistory {
ciphertext: new_ciphertext,
tag: v1::ROOT_KEY_TAG.to_vec(),
root_key_encryption_nonce: new_nonce.to_vec(),
data_encryption_nonce: data_encryption_nonce.to_vec(),
schema_version: 1,
salt: new_salt.to_vec(),
})
.returning(schema::root_key_history::id)
.get_result::<i32>(&mut *conn)
.await?;
update(schema::arbiter_settings::table) // The root key is unchanged, so its row keeps its identity: `data_encryption_nonce`
.set(schema::arbiter_settings::root_key_id.eq(new_id)) // keeps counting up, and every integrity envelope stays bound to the same key version.
.execute(&mut *conn) // Only the seal-key material is replaced, retiring the previous one. `tag` and
.await?; // `schema_version` are deliberately left untouched: the seal-key encryption scheme
// itself is unchanged by a re-key, so there is nothing new for them to describe.
Result::<_, diesel::result::Error>::Ok(new_id) let rows_updated = update(schema::root_key_history::table)
}) .filter(schema::root_key_history::id.eq(*root_key_history_id))
.set((
schema::root_key_history::ciphertext.eq(new_ciphertext),
schema::root_key_history::root_key_encryption_nonce.eq(new_nonce.to_vec()),
schema::root_key_history::salt.eq(new_salt.to_vec()),
))
.execute(&mut conn)
.await?; .await?;
*root_key_history_id = RootKeyHistoryId::from_raw(new_root_key_history_id); if rows_updated == 0 {
error!(
"Broken database: rekey matched no root_key_history row id={:#?}",
root_key_history_id
);
return Err(Error::BrokenDatabase);
}
info!("Vault root key rekeyed successfully"); info!("Vault root key rekeyed successfully");
Ok(()) Ok(())
} }
@@ -522,7 +521,6 @@ impl Vault {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::actors::GlobalActors; use crate::actors::GlobalActors;
use arbiter_crypto::safecell::SafeCellHandle as _;
use super::*; use super::*;
@@ -578,4 +576,77 @@ mod tests {
"next write must advance nonce" "next write must advance nonce"
); );
} }
#[tokio::test]
#[test_log::test]
async fn rekey_does_not_restart_the_data_nonce_counter() {
let db = db::create_test_pool().await;
let mut actor = bootstrapped_actor(&db).await;
let before = actor
.create_new(SafeCell::new(b"before-rekey".to_vec()))
.await
.unwrap();
actor.rekey_root_key(KeyCell::from([7u8; 32])).await.unwrap();
let after = actor
.create_new(SafeCell::new(b"after-rekey".to_vec()))
.await
.unwrap();
let mut conn = db.get().await.unwrap();
// One root key, one row: the root key never changed, so its history did not fork.
let rows: i64 = schema::root_key_history::table
.count()
.get_result(&mut conn)
.await
.unwrap();
assert_eq!(rows, 1, "a seal-key re-key must not append a root key row");
// Fetch each nonce by its own id, rather than `eq_any` (whose row order is
// unspecified), and assert the counter strictly advanced. A weaker `assert_ne!` would
// still pass if the counter reset, as long as the two nonces happened to differ.
let before_nonce: Vec<u8> = schema::aead_encrypted::table
.find(before)
.select(schema::aead_encrypted::current_nonce)
.first(&mut conn)
.await
.unwrap();
let after_nonce: Vec<u8> = schema::aead_encrypted::table
.find(after)
.select(schema::aead_encrypted::current_nonce)
.first(&mut conn)
.await
.unwrap();
assert!(
after_nonce > before_nonce,
"nonce counter must keep advancing across a rekey, not reset"
);
}
#[tokio::test]
#[test_log::test]
async fn rekey_invalidates_the_old_seal_key() {
let db = db::create_test_pool().await;
let mut actor = bootstrapped_actor(&db).await;
actor.rekey_root_key(KeyCell::from([7u8; 32])).await.unwrap();
actor.seal().await.unwrap();
// A no-op rekey would leave the old seal key working; it must not.
let err = actor
.try_unseal(KeyCell::from([0u8; 32]))
.await
.unwrap_err();
assert!(
matches!(err, Error::InvalidKey),
"old seal key must no longer unseal after a rekey, got {err:?}"
);
// A failed unseal must leave the sealed state intact: the new seal key must still be
// able to unseal on the next attempt.
actor.try_unseal(KeyCell::from([7u8; 32])).await.unwrap();
}
} }