diff --git a/core/conversations/src/account_directory.rs b/core/conversations/src/account_service.rs similarity index 92% rename from core/conversations/src/account_directory.rs rename to core/conversations/src/account_service.rs index f242ccb..4a81a4d 100644 --- a/core/conversations/src/account_directory.rs +++ b/core/conversations/src/account_service.rs @@ -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; } -/// 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, 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; -} - -impl AccountService for D { - type Error = ::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 { - // 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( +pub fn resolve_device_ids( directory: &D, account: IdentIdRef, ) -> Result, D::Error> { @@ -383,7 +370,7 @@ mod tests { #[derive(Debug, Default)] struct FakeDir(Option); - 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()); diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index 78c733a..8181e43 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -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, ChatError> { let device_ids = resolve_device_ids(registry, ident).map_err(|e| ChatError::Generic(e.to_string()))?; diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index 57d68c0..4309363 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -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. diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index 2583f0c..46bedb5 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -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}; diff --git a/core/conversations/src/service_context.rs b/core/conversations/src/service_context.rs index eb03210..4309372 100644 --- a/core/conversations/src/service_context.rs +++ b/core/conversations/src/service_context.rs @@ -49,7 +49,7 @@ pub(crate) struct ServiceContext { #[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<(), ::Error> { + ) -> Result<(), ::Error> { Ok(()) } fn fetch( &self, _account: &Ed25519VerifyingKey, - ) -> Result, ::Error> { + ) -> Result, ::Error> { Ok(None) } } diff --git a/core/conversations/src/service_traits.rs b/core/conversations/src/service_traits.rs index 3e4b886..3e142b1 100644 --- a/core/conversations/src/service_traits.rs +++ b/core/conversations/src/service_traits.rs @@ -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 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 = ::Error; fn retrieve(&self, device_id: &str) -> Result>, Self::Error> { diff --git a/extensions/components/src/contact_registry/ephemeral.rs b/extensions/components/src/contact_registry/ephemeral.rs index 8b28c70..7379bb4 100644 --- a/extensions/components/src/contact_registry/ephemeral.rs +++ b/extensions/components/src/contact_registry/ephemeral.rs @@ -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>>>, @@ -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<(), ::Error> { + ) -> Result<(), ::Error> { self.installations .lock() .unwrap() @@ -95,7 +95,7 @@ impl AccountDirectory for EphemeralRegistry { fn fetch( &self, account: &Ed25519VerifyingKey, - ) -> Result, ::Error> { + ) -> Result, ::Error> { let Some(bundle) = self .installations .lock() diff --git a/extensions/components/src/contact_registry/http.rs b/extensions/components/src/contact_registry/http.rs index 7238cab..672cec9 100644 --- a/extensions/components/src/contact_registry/http.rs +++ b/extensions/components/src/contact_registry/http.rs @@ -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> {