chore: refactor account directory to service

This commit is contained in:
kaichaosun 2026-06-18 11:42:08 +08:00
parent d7ce1d58a6
commit 8835492d6f
No known key found for this signature in database
GPG Key ID: 223E0F992F4F03BF
8 changed files with 48 additions and 62 deletions

View File

@ -1,17 +1,19 @@
//! Account → device directory: traits and the signed device-list bundle codec.
//! The account service: the injected client to chat-store's account endpoints,
//! plus the signed device-list bundle codec it deals in.
//!
//! An Account (AccountAddress, an Ed25519 key) endorses a set of device
//! (LocalIdentity) public keys by signing a bundle. The directory service stores
//! (LocalIdentity) public keys by signing a bundle. The account service stores
//! one such bundle per account so that an inviter can resolve an account public
//! key to every device it must invite.
//! key to every device it must invite, and a receiver can confirm a device
//! belongs to a claimed account.
//!
//! Two roles are kept distinct from the per-device [`IdentityProvider`]:
//!
//! - [`AccountAuthority`] — the injected account key. Custody (wallet, enclave,
//! another device) stays outside libchat; we only ever ask it to sign. Present
//! only where the user authorizes a device change.
//! - [`AccountDirectory`] — the client that publishes and fetches+verifies the
//! bundle against the directory service.
//! - [`AccountService`] — the client that publishes, fetches+verifies, and
//! answers membership questions against the account service (chat-store).
//!
//! The bundle `payload` is opaque to the server. Both the signing side
//! ([`encode_bundle_payload`]) and the verifying side ([`verify_bundle`]) live
@ -91,13 +93,17 @@ pub trait AccountAuthority {
fn sign(&self, payload: &[u8]) -> Result<Ed25519Signature, Self::Error>;
}
/// Client for the account → device directory service.
/// The injected client to chat-store's account endpoints.
///
/// Mirrors [`RegistrationService`](crate::service_traits::RegistrationService):
/// an injected trait in core with an HTTP implementation in the extension layer.
/// The service is untrusted, so [`fetch`](AccountDirectory::fetch) verifies the
/// The service is untrusted, so [`fetch`](AccountService::fetch) verifies the
/// account signature before returning a [`DeviceSet`].
pub trait AccountDirectory: Debug {
///
/// Covers the full account surface: publishing this account's device list,
/// fetching another account's, and the derived membership check used to validate
/// a [`SenderCredential`](logos_account::SenderCredential)'s account claim.
pub trait AccountService: Debug {
type Error: Display + Debug;
/// Upsert the signed device list for an account, replacing any previous one.
@ -106,35 +112,16 @@ pub trait AccountDirectory: Debug {
/// Fetch and verify the device set for `account`. `Ok(None)` means the
/// account has never published — callers fall back to legacy 1:1 resolution.
fn fetch(&self, account: &Ed25519VerifyingKey) -> Result<Option<DeviceSet>, Self::Error>;
}
/// Confirms whether a device key really belongs to an account — the trust step
/// a [`SenderCredential`](logos_account::SenderCredential)'s account claim is
/// checked against before it is reported to the application.
///
/// Backed by the [`AccountDirectory`]: any directory client is an
/// `AccountService` via the blanket impl below, which fetches the account's
/// verified device set and checks membership.
pub trait AccountService {
type Error: Display + Debug;
/// True if `device` is a registered LocalIdentity of `account`.
fn is_local_identity_of(
&self,
account: &Ed25519VerifyingKey,
device: &Ed25519VerifyingKey,
) -> Result<bool, Self::Error>;
}
impl<D: AccountDirectory> AccountService for D {
type Error = <D as AccountDirectory>::Error;
/// True if `device` is a registered LocalIdentity of `account` — the trust
/// step a sender's account *claim* is checked against. Derived from
/// [`fetch`](AccountService::fetch): no published bundle means the binding
/// can't be confirmed, so the answer is `false`.
fn is_local_identity_of(
&self,
account: &Ed25519VerifyingKey,
device: &Ed25519VerifyingKey,
) -> Result<bool, Self::Error> {
// No published bundle → can't confirm the device belongs to the account.
let Some(set) = self.fetch(account)? else {
return Ok(false);
};
@ -249,7 +236,7 @@ pub fn verify_bundle(
/// of such a key and a bundle exists, returns its verified device set. Otherwise
/// falls back to treating the identifier itself as a single device id — the
/// pre-directory behaviour — so opaque or never-published ids keep working.
pub fn resolve_device_ids<D: AccountDirectory + ?Sized>(
pub fn resolve_device_ids<D: AccountService + ?Sized>(
directory: &D,
account: IdentIdRef,
) -> Result<Vec<DeviceId>, D::Error> {
@ -383,7 +370,7 @@ mod tests {
#[derive(Debug, Default)]
struct FakeDir(Option<SignedDeviceBundle>);
impl AccountDirectory for FakeDir {
impl AccountService for FakeDir {
type Error = BundleError;
fn publish(&mut self, bundle: &SignedDeviceBundle) -> Result<(), Self::Error> {
self.0 = Some(bundle.clone());

View File

@ -12,7 +12,7 @@ use openmls::prelude::*;
use prost::Message as _;
use shared_traits::IdentIdRef;
use crate::account_directory::{AccountDirectory, resolve_device_ids};
use crate::account_service::{AccountService, resolve_device_ids};
use crate::inbox_v2::MlsProvider;
use crate::service_context::{ExternalServices, ServiceContext};
@ -149,7 +149,7 @@ impl GroupV1Convo {
&self,
ident: IdentIdRef,
provider: &impl MlsProvider,
registry: &(impl KeyPackageProvider + AccountDirectory),
registry: &(impl KeyPackageProvider + AccountService),
) -> Result<Vec<KeyPackage>, ChatError> {
let device_ids =
resolve_device_ids(registry, ident).map_err(|e| ChatError::Generic(e.to_string()))?;

View File

@ -24,8 +24,7 @@ use crate::conversation::GroupV2Convo;
use crate::service_context::{ExternalServices, ServiceContext};
use crate::utils::{blake2b_hex, hash_size};
use crate::{
AccountAuthority, AccountDirectory, AddressedEnvelope, SignedDeviceBundle,
encode_bundle_payload,
AccountAuthority, AccountService, AddressedEnvelope, SignedDeviceBundle, encode_bundle_payload,
};
use crate::{IdentId, IdentIdRef, IdentityProvider};
@ -216,7 +215,7 @@ impl InboxV2 {
}
// Publishing the account → device bundle needs the account key, so this method
// is available only when the registry also implements `AccountDirectory`. The
// is available only when the registry also implements `AccountService`. The
// signing authority is the `LogosAccount` wrapped by `mls_identity`; on testnet
// that is a local key (account key == device key), while an external signer
// would supply its own authority.

View File

@ -1,4 +1,4 @@
mod account_directory;
mod account_service;
mod causal_history;
mod conversation;
mod core;
@ -13,9 +13,9 @@ mod service_traits;
mod types;
mod utils;
pub use account_directory::{
AccountAuthority, AccountDirectory, AccountService, BUNDLE_VERSION, BundleError, DecodedBundle,
DeviceId, DeviceSet, Lamport, SignedDeviceBundle, decode_bundle_payload, encode_bundle_payload,
pub use account_service::{
AccountAuthority, AccountService, BUNDLE_VERSION, BundleError, DecodedBundle, DeviceId,
DeviceSet, Lamport, SignedDeviceBundle, decode_bundle_payload, encode_bundle_payload,
resolve_device_ids, verify_bundle,
};
pub use causal_history::{Frontier, MissingMessage};

View File

@ -49,7 +49,7 @@ pub(crate) struct ServiceContext<S: ExternalServices> {
#[cfg(test)]
mod test_support {
use super::*;
use crate::account_directory::{AccountDirectory, DeviceSet, SignedDeviceBundle};
use crate::account_service::{AccountService, DeviceSet, SignedDeviceBundle};
use crate::types::AddressedEnvelope;
use crate::{ChatError, IdentityProvider};
use crypto::Ed25519VerifyingKey;
@ -93,20 +93,20 @@ mod test_support {
}
}
impl AccountDirectory for NoopRegistration {
impl AccountService for NoopRegistration {
type Error = std::convert::Infallible;
fn publish(
&mut self,
_bundle: &SignedDeviceBundle,
) -> Result<(), <Self as AccountDirectory>::Error> {
) -> Result<(), <Self as AccountService>::Error> {
Ok(())
}
fn fetch(
&self,
_account: &Ed25519VerifyingKey,
) -> Result<Option<DeviceSet>, <Self as AccountDirectory>::Error> {
) -> Result<Option<DeviceSet>, <Self as AccountService>::Error> {
Ok(None)
}
}

View File

@ -7,7 +7,7 @@ use std::{
time::Duration,
};
use crate::{AccountDirectory, ConversationId, types::AddressedEnvelope};
use crate::{AccountService, ConversationId, types::AddressedEnvelope};
/// A Delivery service is responsible for payload transport.
/// This interface allows Conversations to send payloads on the wire as well as
@ -30,13 +30,13 @@ pub trait DeliveryService: Debug {
/// service that verifies the bundle is signed by the correct account — can
/// sign or attest with the caller's key material.
///
/// On testnet a single service (the keypackage-registry) provides both the
/// keypackage store and the account → device directory, so [`AccountDirectory`]
/// is a supertrait: any `RegistrationService` also resolves accounts to devices.
/// This co-location is intentional and temporary; the two can be split into
/// separate injected services once λLEZ lands.
pub trait RegistrationService: Debug + AccountDirectory {
// Disambiguated below: with `AccountDirectory` as a supertrait, a bare
/// On testnet a single service (chat-store) provides both the keypackage store
/// and the account service, so [`AccountService`] is a supertrait: any
/// `RegistrationService` also resolves accounts to devices. This co-location is
/// intentional and temporary; the two can be split into separate injected
/// services once λLEZ lands.
pub trait RegistrationService: Debug + AccountService {
// Disambiguated below: with `AccountService` as a supertrait, a bare
// `Self::Error` is ambiguous between the two traits' associated types.
type Error: Display + Debug;
fn register(
@ -58,7 +58,7 @@ pub trait KeyPackageProvider: Debug {
}
impl<T: RegistrationService> KeyPackageProvider for T {
// Disambiguate: `RegistrationService` now has `AccountDirectory` as a
// Disambiguate: `RegistrationService` now has `AccountService` as a
// supertrait, so both expose an associated `Error`.
type Error = <T as RegistrationService>::Error;
fn retrieve(&self, device_id: &str) -> Result<Option<Vec<u8>>, Self::Error> {

View File

@ -6,7 +6,7 @@ use std::{
use crypto::Ed25519VerifyingKey;
use libchat::{
AccountDirectory, DeviceSet, IdentityProvider, RegistrationService, SignedDeviceBundle,
AccountService, DeviceSet, IdentityProvider, RegistrationService, SignedDeviceBundle,
verify_bundle,
};
@ -16,7 +16,7 @@ use libchat::{
///
/// Like the real `keypackage-registry`, one object serves both roles: a
/// keypackage store ([`RegistrationService`]) keyed by `device_id`, and an
/// account → device directory ([`AccountDirectory`]) keyed by the hex account key.
/// account service ([`AccountService`]) keyed by the hex account key.
#[derive(Clone, Default)]
pub struct EphemeralRegistry {
key_packages: Arc<Mutex<HashMap<String, Vec<u8>>>>,
@ -78,13 +78,13 @@ impl RegistrationService for EphemeralRegistry {
/// Account → device directory, verifying each bundle on `fetch` exactly as the
/// HTTP client does so callers exercise the same trust path without a server.
impl AccountDirectory for EphemeralRegistry {
impl AccountService for EphemeralRegistry {
type Error = String;
fn publish(
&mut self,
bundle: &SignedDeviceBundle,
) -> Result<(), <Self as AccountDirectory>::Error> {
) -> Result<(), <Self as AccountService>::Error> {
self.installations
.lock()
.unwrap()
@ -95,7 +95,7 @@ impl AccountDirectory for EphemeralRegistry {
fn fetch(
&self,
account: &Ed25519VerifyingKey,
) -> Result<Option<DeviceSet>, <Self as AccountDirectory>::Error> {
) -> Result<Option<DeviceSet>, <Self as AccountService>::Error> {
let Some(bundle) = self
.installations
.lock()

View File

@ -5,7 +5,7 @@ use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use crypto::{Ed25519Signature, Ed25519VerifyingKey};
use libchat::{
AccountDirectory, BundleError, DeviceSet, IdentityProvider, RegistrationService,
AccountService, BundleError, DeviceSet, IdentityProvider, RegistrationService,
SignedDeviceBundle, verify_bundle,
};
use serde::{Deserialize, Serialize};
@ -179,7 +179,7 @@ impl RegistrationService for HttpRegistry {
}
}
impl AccountDirectory for HttpRegistry {
impl AccountService for HttpRegistry {
type Error = HttpRegistryError;
fn publish(&mut self, bundle: &SignedDeviceBundle) -> Result<(), Self::Error> {