mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-06-28 03:59:27 +00:00
chore: refactor account directory to service
This commit is contained in:
parent
d7ce1d58a6
commit
8835492d6f
@ -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());
|
||||
@ -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()))?;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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> {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user