diff --git a/server/Cargo.lock b/server/Cargo.lock index 32a1587..3e54e1c 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -737,6 +737,7 @@ dependencies = [ "ed25519-dalek", "fatality", "futures", + "hmac", "insta", "k256", "kameo", diff --git a/server/crates/arbiter-server/Cargo.toml b/server/crates/arbiter-server/Cargo.toml index 8996fce..e74b866 100644 --- a/server/crates/arbiter-server/Cargo.toml +++ b/server/crates/arbiter-server/Cargo.toml @@ -49,6 +49,7 @@ pem = "3.0.6" k256.workspace = true rsa.workspace = true sha2.workspace = true +hmac = "0.12" spki.workspace = true alloy.workspace = true prost-types.workspace = true diff --git a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql index 79b1f7a..a34aa5a 100644 --- a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql +++ b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql @@ -47,6 +47,7 @@ create table if not exists useragent_client ( id integer not null primary key, nonce integer not null default(1), -- used for auth challenge public_key blob not null, + pubkey_integrity_tag blob, key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1) created_at integer not null default(unixepoch ('now')), updated_at integer not null default(unixepoch ('now')) diff --git a/server/crates/arbiter-server/src/actors/keyholder/encryption/v1.rs b/server/crates/arbiter-server/src/actors/keyholder/encryption/v1.rs index 8befeb1..2002502 100644 --- a/server/crates/arbiter-server/src/actors/keyholder/encryption/v1.rs +++ b/server/crates/arbiter-server/src/actors/keyholder/encryption/v1.rs @@ -5,6 +5,7 @@ use chacha20poly1305::{ AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce, aead::{AeadMut, Error, Payload}, }; +use hmac::Mac as _; use rand::{ Rng as _, SeedableRng, rngs::{StdRng, SysRng}, @@ -14,6 +15,8 @@ use crate::safe_cell::{SafeCell, SafeCellHandle as _}; pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes(); pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes(); +pub const USERAGENT_INTEGRITY_DERIVE_TAG: &[u8] = "arbiter/useragent/integrity-key/v1".as_bytes(); +pub const USERAGENT_INTEGRITY_TAG: &[u8] = "arbiter/useragent/pubkey-entry/v1".as_bytes(); pub const NONCE_LENGTH: usize = 24; @@ -169,6 +172,46 @@ pub fn derive_seal_key(mut password: SafeCell>, salt: &Salt) -> KeyCell key.into() } +/// Derives a dedicated key used only for user-agent pubkey integrity tags. +pub fn derive_useragent_integrity_key(seal_key: &mut KeyCell) -> KeyCell { + type HmacSha256 = hmac::Hmac; + + let mut derived = SafeCell::new(Key::default()); + seal_key.0.read_inline(|seal_key_bytes| { + let mut mac = ::new_from_slice(seal_key_bytes.as_ref()) + .expect("HMAC key initialization must not fail for 32-byte key"); + mac.update(USERAGENT_INTEGRITY_DERIVE_TAG); + let output = mac.finalize().into_bytes(); + + let mut writer = derived.write(); + let writer: &mut [u8] = writer.as_mut(); + writer.copy_from_slice(&output); + }); + + derived.into() +} + +/// Computes an integrity tag for a user-agent pubkey DB entry. +pub fn compute_useragent_pubkey_integrity_tag( + integrity_key: &mut KeyCell, + key_type_discriminant: i32, + public_key: &[u8], +) -> [u8; 32] { + type HmacSha256 = hmac::Hmac; + + let mut tag = [0u8; 32]; + integrity_key.0.read_inline(|integrity_key_bytes| { + let mut mac = ::new_from_slice(integrity_key_bytes.as_ref()) + .expect("HMAC key initialization must not fail for 32-byte key"); + mac.update(USERAGENT_INTEGRITY_TAG); + mac.update(&key_type_discriminant.to_be_bytes()); + mac.update(public_key); + tag.copy_from_slice(&mac.finalize().into_bytes()); + }); + + tag +} + #[cfg(test)] mod tests { use super::*; @@ -240,4 +283,24 @@ mod tests { ] ); } + + #[test] + pub fn useragent_integrity_tag_deterministic() { + let salt = generate_salt(); + let mut seal_key = derive_seal_key(SafeCell::new(b"password".to_vec()), &salt); + let mut integrity_key = derive_useragent_integrity_key(&mut seal_key); + let t1 = compute_useragent_pubkey_integrity_tag(&mut integrity_key, 1, b"pubkey"); + let t2 = compute_useragent_pubkey_integrity_tag(&mut integrity_key, 1, b"pubkey"); + assert_eq!(t1, t2); + } + + #[test] + pub fn useragent_integrity_tag_changes_with_key_type() { + let salt = generate_salt(); + let mut seal_key = derive_seal_key(SafeCell::new(b"password".to_vec()), &salt); + let mut integrity_key = derive_useragent_integrity_key(&mut seal_key); + let t1 = compute_useragent_pubkey_integrity_tag(&mut integrity_key, 1, b"pubkey"); + let t2 = compute_useragent_pubkey_integrity_tag(&mut integrity_key, 2, b"pubkey"); + assert_ne!(t1, t2); + } } diff --git a/server/crates/arbiter-server/src/actors/keyholder/mod.rs b/server/crates/arbiter-server/src/actors/keyholder/mod.rs index 3a245af..d71241d 100644 --- a/server/crates/arbiter-server/src/actors/keyholder/mod.rs +++ b/server/crates/arbiter-server/src/actors/keyholder/mod.rs @@ -32,6 +32,7 @@ enum State { Unsealed { root_key_history_id: i32, root_key: KeyCell, + useragent_integrity_key: KeyCell, }, } @@ -145,6 +146,7 @@ impl KeyHolder { } let salt = v1::generate_salt(); let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt); + let useragent_integrity_key = v1::derive_useragent_integrity_key(&mut seal_key); let mut root_key = KeyCell::new_secure_random(); // Zero nonces are fine because they are one-time @@ -193,6 +195,7 @@ impl KeyHolder { self.state = State::Unsealed { root_key, root_key_history_id, + useragent_integrity_key, }; info!("Keyholder bootstrapped successfully"); @@ -226,6 +229,7 @@ impl KeyHolder { Error::BrokenDatabase })?; let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt); + let useragent_integrity_key = v1::derive_useragent_integrity_key(&mut seal_key); let mut root_key = SafeCell::new(current_key.ciphertext.clone()); @@ -249,6 +253,7 @@ impl KeyHolder { error!(?err, "Broken database: invalid encryption key size"); Error::BrokenDatabase })?, + useragent_integrity_key, }; info!("Keyholder unsealed successfully"); @@ -257,6 +262,28 @@ impl KeyHolder { } // Decrypts the `aead_encrypted` entry with the given ID and returns the plaintext + #[message] + pub fn sign_useragent_pubkey_integrity_tag( + &mut self, + public_key: Vec, + key_type: models::KeyType, + ) -> Result, Error> { + let State::Unsealed { + useragent_integrity_key, + .. + } = &mut self.state + else { + return Err(Error::NotBootstrapped); + }; + + let tag = v1::compute_useragent_pubkey_integrity_tag( + useragent_integrity_key, + key_type as i32, + &public_key, + ); + Ok(tag.to_vec()) + } + #[message] pub async fn decrypt(&mut self, aead_id: i32) -> Result>, Error> { let State::Unsealed { root_key, .. } = &mut self.state else { @@ -292,6 +319,7 @@ impl KeyHolder { let State::Unsealed { root_key, root_key_history_id, + .. } = &mut self.state else { return Err(Error::NotBootstrapped); diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs index c422589..8931f2c 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs @@ -1,12 +1,14 @@ use arbiter_proto::transport::Bi; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update}; use diesel_async::RunQueryDsl; +use kameo::error::SendError; use tracing::error; use super::Error; use crate::{ actors::{ bootstrap::ConsumeToken, + keyholder::{self, SignUseragentPubkeyIntegrityTag}, user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound}, }, db::schema, @@ -40,7 +42,11 @@ smlang::statemachine!( } ); -async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result { +async fn create_nonce( + db: &crate::db::DatabasePool, + pubkey_bytes: &[u8], + key_type: crate::db::models::KeyType, +) -> Result { let mut db_conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); Error::internal("Database unavailable") @@ -50,12 +56,14 @@ async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Resu Box::pin(async move { let current_nonce = schema::useragent_client::table .filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec())) + .filter(schema::useragent_client::key_type.eq(key_type)) .select(schema::useragent_client::nonce) .first::(conn) .await?; update(schema::useragent_client::table) .filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec())) + .filter(schema::useragent_client::key_type.eq(key_type)) .set(schema::useragent_client::nonce.eq(current_nonce + 1)) .execute(conn) .await?; @@ -75,7 +83,11 @@ async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Resu }) } -async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> Result<(), Error> { +async fn register_key( + db: &crate::db::DatabasePool, + pubkey: &AuthPublicKey, + integrity_tag: Option>, +) -> Result<(), Error> { let pubkey_bytes = pubkey.to_stored_bytes(); let key_type = pubkey.key_type(); let mut conn = db.get().await.map_err(|e| { @@ -88,6 +100,7 @@ async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> R schema::useragent_client::public_key.eq(pubkey_bytes), schema::useragent_client::nonce.eq(1), schema::useragent_client::key_type.eq(key_type), + schema::useragent_client::pubkey_integrity_tag.eq(integrity_tag), )) .execute(&mut conn) .await @@ -120,8 +133,11 @@ where &mut self, ChallengeRequest { pubkey }: ChallengeRequest, ) -> Result { + self.verify_pubkey_integrity_before_challenge(&pubkey) + .await?; + let stored_bytes = pubkey.to_stored_bytes(); - let nonce = create_nonce(&self.conn.db, &stored_bytes).await?; + let nonce = create_nonce(&self.conn.db, &stored_bytes, pubkey.key_type()).await?; self.transport .send(Ok(Outbound::AuthChallenge { nonce })) @@ -161,7 +177,15 @@ where return Err(Error::InvalidBootstrapToken); } - register_key(&self.conn.db, &pubkey).await?; + let integrity_tag = self + .try_sign_pubkey_integrity_tag(&pubkey) + .await + .map_err(|err| { + error!(?err, "Failed to sign user-agent pubkey integrity tag"); + Error::internal("Failed to sign user-agent pubkey integrity tag") + })?; + + register_key(&self.conn.db, &pubkey, integrity_tag).await?; self.transport .send(Ok(Outbound::AuthSuccess)) @@ -220,3 +244,91 @@ where Ok(key.clone()) } } + +impl AuthContext<'_, T> +where + T: Bi> + Send, +{ + async fn try_sign_pubkey_integrity_tag( + &self, + pubkey: &AuthPublicKey, + ) -> Result>, Error> { + let signed = self + .conn + .actors + .key_holder + .ask(SignUseragentPubkeyIntegrityTag { + public_key: pubkey.to_stored_bytes(), + key_type: pubkey.key_type(), + }) + .await; + + match signed { + Ok(tag) => Ok(Some(tag)), + Err(SendError::HandlerError(keyholder::Error::NotBootstrapped)) => Ok(None), + Err(SendError::HandlerError(err)) => { + error!( + ?err, + "Keyholder failed to sign user-agent pubkey integrity tag" + ); + Err(Error::internal( + "Keyholder failed to sign user-agent pubkey integrity tag", + )) + } + Err(err) => { + error!( + ?err, + "Failed to contact keyholder for user-agent pubkey integrity tag" + ); + Err(Error::internal( + "Failed to contact keyholder for user-agent pubkey integrity tag", + )) + } + } + } + + async fn verify_pubkey_integrity_before_challenge( + &self, + pubkey: &AuthPublicKey, + ) -> Result<(), Error> { + let stored_tag: Option>> = { + let mut conn = self.conn.db.get().await.map_err(|e| { + error!(error = ?e, "Database pool error"); + Error::internal("Database unavailable") + })?; + + schema::useragent_client::table + .filter(schema::useragent_client::public_key.eq(pubkey.to_stored_bytes())) + .filter(schema::useragent_client::key_type.eq(pubkey.key_type())) + .select(schema::useragent_client::pubkey_integrity_tag) + .first::>>(&mut conn) + .await + .optional() + .map_err(|e| { + error!(error = ?e, "Database error"); + Error::internal("Database operation failed") + })? + }; + + let Some(stored_tag) = stored_tag else { + return Err(Error::UnregisteredPublicKey); + }; + + let Some(expected_tag) = self.try_sign_pubkey_integrity_tag(pubkey).await? else { + // Vault sealed/unbootstrapped: cannot verify integrity yet. + return Ok(()); + }; + + let Some(stored_tag) = stored_tag else { + error!("Missing pubkey integrity tag for registered key while vault is unsealed"); + return Err(Error::InvalidChallengeSolution); + }; + + if stored_tag != expected_tag { + error!("User-agent pubkey integrity tag mismatch"); + return Err(Error::InvalidChallengeSolution); + } + + Ok(()) + } +} diff --git a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs index 30b10ae..df2a752 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs @@ -2,12 +2,11 @@ use std::sync::Mutex; use alloy::primitives::Address; use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; -use diesel::sql_types::ops::Add; -use diesel::{BoolExpressionMethods as _, ExpressionMethods as _, QueryDsl as _, SelectableHelper}; +use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper, dsl::update}; use diesel_async::{AsyncConnection, RunQueryDsl}; use kameo::error::SendError; +use kameo::messages; use kameo::prelude::Context; -use kameo::{message, messages}; use tracing::{error, info}; use x25519_dalek::{EphemeralSecret, PublicKey}; @@ -15,9 +14,8 @@ use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnsw use crate::actors::keyholder::KeyHolderState; use crate::actors::user_agent::session::Error; use crate::db::models::{ - CoreEvmWalletAccess, EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata, + EvmWalletAccess, KeyType, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata, }; -use crate::db::schema::evm_wallet_access; use crate::evm::policies::{Grant, SpecificGrant}; use crate::safe_cell::SafeCell; use crate::{ @@ -25,7 +23,7 @@ use crate::{ evm::{ Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants, }, - keyholder::{self, Bootstrap, TryUnseal}, + keyholder::{self, Bootstrap, SignUseragentPubkeyIntegrityTag, TryUnseal}, user_agent::session::{ UserAgentSession, state::{UnsealContext, UserAgentEvents, UserAgentStates}, @@ -87,6 +85,56 @@ impl UserAgentSession { } } } + + async fn backfill_missing_useragent_pubkey_integrity_tags(&mut self) -> Result<(), Error> { + use crate::db::schema::useragent_client; + + let mut conn = self.props.db.get().await?; + let missing_rows: Vec<(i32, Vec, KeyType)> = useragent_client::table + .filter(useragent_client::pubkey_integrity_tag.is_null()) + .select(( + useragent_client::id, + useragent_client::public_key, + useragent_client::key_type, + )) + .load(&mut conn) + .await?; + drop(conn); + + if missing_rows.is_empty() { + return Ok(()); + } + + let mut updates = Vec::with_capacity(missing_rows.len()); + for (id, public_key, key_type) in missing_rows { + let tag = self + .props + .actors + .key_holder + .ask(SignUseragentPubkeyIntegrityTag { + public_key, + key_type, + }) + .await + .map_err(|err| { + error!(?err, "Failed to sign user-agent pubkey integrity tag"); + Error::internal("Failed to sign user-agent pubkey integrity tag") + })?; + updates.push((id, tag)); + } + + let mut conn = self.props.db.get().await?; + for (id, tag) in updates { + update(useragent_client::table) + .filter(useragent_client::id.eq(id)) + .set(useragent_client::pubkey_integrity_tag.eq(Some(tag))) + .execute(&mut conn) + .await?; + } + + info!("Backfilled missing user-agent pubkey integrity tags"); + Ok(()) + } } pub struct UnsealStartResponse { @@ -174,6 +222,8 @@ impl UserAgentSession { .await { Ok(_) => { + self.backfill_missing_useragent_pubkey_integrity_tags() + .await?; info!("Successfully unsealed key with client-provided key"); self.transition(UserAgentEvents::ReceivedValidKey)?; Ok(()) @@ -235,6 +285,8 @@ impl UserAgentSession { .await { Ok(_) => { + self.backfill_missing_useragent_pubkey_integrity_tags() + .await?; info!("Successfully bootstrapped vault with client-provided key"); self.transition(UserAgentEvents::ReceivedValidKey)?; Ok(()) diff --git a/server/crates/arbiter-server/src/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index 48d2c22..6fb171c 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -242,6 +242,7 @@ pub struct UseragentClient { pub id: i32, pub nonce: i32, pub public_key: Vec, + pub pubkey_integrity_tag: Option>, pub created_at: SqliteTimestamp, pub updated_at: SqliteTimestamp, pub key_type: KeyType, diff --git a/server/crates/arbiter-server/src/db/schema.rs b/server/crates/arbiter-server/src/db/schema.rs index 8668089..88bdef3 100644 --- a/server/crates/arbiter-server/src/db/schema.rs +++ b/server/crates/arbiter-server/src/db/schema.rs @@ -178,6 +178,7 @@ diesel::table! { id -> Integer, nonce -> Integer, public_key -> Binary, + pubkey_integrity_tag -> Nullable, key_type -> Integer, created_at -> Integer, updated_at -> Integer, diff --git a/server/crates/arbiter-server/tests/user_agent/auth.rs b/server/crates/arbiter-server/tests/user_agent/auth.rs index 285ddcf..f130999 100644 --- a/server/crates/arbiter-server/tests/user_agent/auth.rs +++ b/server/crates/arbiter-server/tests/user_agent/auth.rs @@ -3,9 +3,11 @@ use arbiter_server::{ actors::{ GlobalActors, bootstrap::GetToken, + keyholder::Bootstrap, user_agent::{AuthPublicKey, UserAgentConnection, auth}, }, db::{self, schema}, + safe_cell::{SafeCell, SafeCellHandle as _}, }; use diesel::{ExpressionMethods as _, QueryDsl, insert_into}; use diesel_async::RunQueryDsl; @@ -165,3 +167,120 @@ pub async fn test_challenge_auth() { task.await.unwrap().unwrap(); } + +#[tokio::test] +#[test_log::test] +pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() { + let db = db::create_test_pool().await; + let actors = GlobalActors::spawn(db.clone()).await.unwrap(); + + actors + .key_holder + .ask(Bootstrap { + seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()), + }) + .await + .unwrap(); + + let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + + { + let mut conn = db.get().await.unwrap(); + insert_into(schema::useragent_client::table) + .values(( + schema::useragent_client::public_key.eq(pubkey_bytes.clone()), + schema::useragent_client::key_type.eq(1i32), + schema::useragent_client::pubkey_integrity_tag.eq(Some(vec![0u8; 32])), + )) + .execute(&mut conn) + .await + .unwrap(); + } + + let (server_transport, mut test_transport) = ChannelTransport::new(); + let db_for_task = db.clone(); + let task = tokio::spawn(async move { + let mut props = UserAgentConnection::new(db_for_task, actors); + auth::authenticate(&mut props, server_transport).await + }); + + test_transport + .send(auth::Inbound::AuthChallengeRequest { + pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), + bootstrap_token: None, + }) + .await + .unwrap(); + + assert!(matches!( + task.await.unwrap(), + Err(auth::Error::InvalidChallengeSolution) + )); +} + +#[tokio::test] +#[test_log::test] +pub async fn test_challenge_auth_rejects_invalid_signature() { + let db = db::create_test_pool().await; + let actors = GlobalActors::spawn(db.clone()).await.unwrap(); + + let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + + // Pre-register key with key_type + { + let mut conn = db.get().await.unwrap(); + insert_into(schema::useragent_client::table) + .values(( + schema::useragent_client::public_key.eq(pubkey_bytes.clone()), + schema::useragent_client::key_type.eq(1i32), + )) + .execute(&mut conn) + .await + .unwrap(); + } + + let (server_transport, mut test_transport) = ChannelTransport::new(); + let db_for_task = db.clone(); + let task = tokio::spawn(async move { + let mut props = UserAgentConnection::new(db_for_task, actors); + auth::authenticate(&mut props, server_transport).await + }); + + test_transport + .send(auth::Inbound::AuthChallengeRequest { + pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()), + bootstrap_token: None, + }) + .await + .unwrap(); + + let response = test_transport + .recv() + .await + .expect("should receive challenge"); + let challenge = match response { + Ok(resp) => match resp { + auth::Outbound::AuthChallenge { nonce } => nonce, + other => panic!("Expected AuthChallenge, got {other:?}"), + }, + Err(err) => panic!("Expected Ok response, got Err({err:?})"), + }; + + // Sign a different challenge value so signature format is valid but verification must fail. + let wrong_challenge = arbiter_proto::format_challenge(challenge + 1, &pubkey_bytes); + let signature = new_key.sign(&wrong_challenge); + + test_transport + .send(auth::Inbound::AuthChallengeSolution { + signature: signature.to_bytes().to_vec(), + }) + .await + .unwrap(); + + assert!(matches!( + task.await.unwrap(), + Err(auth::Error::InvalidChallengeSolution) + )); +} diff --git a/server/crates/arbiter-server/tests/user_agent/unseal.rs b/server/crates/arbiter-server/tests/user_agent/unseal.rs index 76a68aa..16bb257 100644 --- a/server/crates/arbiter-server/tests/user_agent/unseal.rs +++ b/server/crates/arbiter-server/tests/user_agent/unseal.rs @@ -2,14 +2,17 @@ use arbiter_server::{ actors::{ GlobalActors, keyholder::{Bootstrap, Seal}, - user_agent::{UserAgentSession, session::connection::{ - HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, - }}, + user_agent::{ + UserAgentSession, + session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError}, + }, }, db, 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}; @@ -149,3 +152,42 @@ pub async fn test_unseal_retry_after_invalid_key() { assert!(matches!(response, Ok(()))); } } + +#[tokio::test] +#[test_log::test] +pub async fn test_unseal_backfills_missing_pubkey_integrity_tags() { + let seal_key = b"test-seal-key"; + let (db, user_agent) = setup_sealed_user_agent(seal_key).await; + + { + let mut conn = db.get().await.unwrap(); + insert_into(arbiter_server::db::schema::useragent_client::table) + .values(( + arbiter_server::db::schema::useragent_client::public_key + .eq(vec![1u8, 2u8, 3u8, 4u8]), + arbiter_server::db::schema::useragent_client::key_type.eq(1i32), + arbiter_server::db::schema::useragent_client::pubkey_integrity_tag + .eq(Option::>::None), + )) + .execute(&mut conn) + .await + .unwrap(); + } + + let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await; + let response = user_agent.ask(encrypted_key).await; + assert!(matches!(response, Ok(()))); + + { + let mut conn = db.get().await.unwrap(); + let tags: Vec>> = arbiter_server::db::schema::useragent_client::table + .select(arbiter_server::db::schema::useragent_client::pubkey_integrity_tag) + .load(&mut conn) + .await + .unwrap(); + assert!( + tags.iter() + .all(|tag| matches!(tag, Some(v) if v.len() == 32)) + ); + } +}