mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-07-09 17:39:28 +00:00
fix: signer-scoped DirectV1 routing (#162)
Core is no longer account-aware: the client resolves an account address to signer ids via the account directory, and the signer's verifying-key hex serves as registry key, inbox subscription, and Welcome routing target end to end. The MLS credential stays the full id(). - GroupV2 reads the de-mls member id from the fetched key package and maps it to the signer id the welcome is delivered to. - All account machinery (directory trait, bundle codec, resolution) moves out of core into logos-account; the RegistrationService supertrait and Core::account_directory() are gone, and the client holds its own directory handle. - The account exposes functionality, never a signer: add_delegate_signer does the lamport upsert and signs internally. - Every client acts for an account (ChatClientBuilder::new(account)). DelegateSigner is a pure keypair; the client composes the wire credential from the signer and the account, so the association is client state. addr() is the account address. - resolve_device_ids fails fast (NotAnAccountKey / NoDeviceBundle / Directory) instead of falling back to treating an unresolved address as a signer id. LogosChatClient::open and chat-cli mint and publish a dev account each launch. - EphemeralRegistry keys key packages by hex pubkey like HttpRegistry. Supersedes #155 (routing_id).
This commit is contained in:
parent
d131a69583
commit
c09459c0a0
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -1317,6 +1317,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"crossterm 0.29.0",
|
"crossterm 0.29.0",
|
||||||
|
"logos-account",
|
||||||
"logos-chat",
|
"logos-chat",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"serde",
|
"serde",
|
||||||
@ -1466,6 +1467,7 @@ dependencies = [
|
|||||||
"crypto",
|
"crypto",
|
||||||
"hex",
|
"hex",
|
||||||
"libchat",
|
"libchat",
|
||||||
|
"logos-account",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -3042,8 +3044,8 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"chat-sqlite",
|
"chat-sqlite",
|
||||||
"components",
|
"components",
|
||||||
|
"crypto",
|
||||||
"libchat",
|
"libchat",
|
||||||
"logos-account",
|
|
||||||
"shared-traits",
|
"shared-traits",
|
||||||
"storage",
|
"storage",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
@ -3589,8 +3591,9 @@ name = "logos-account"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto",
|
"crypto",
|
||||||
"libchat",
|
"hex",
|
||||||
"shared-traits",
|
"shared-traits",
|
||||||
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -10,6 +10,7 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
# Workspace dependencies (sorted)
|
# Workspace dependencies (sorted)
|
||||||
crossbeam-channel = { workspace = true }
|
crossbeam-channel = { workspace = true }
|
||||||
|
logos-account = { workspace = true, features = ["dev"] }
|
||||||
logos-chat = { workspace = true, features = ["embedded-p2p-delivery"] }
|
logos-chat = { workspace = true, features = ["embedded-p2p-delivery"] }
|
||||||
|
|
||||||
# External dependencies (sorted)
|
# External dependencies (sorted)
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use arboard::Clipboard;
|
use arboard::Clipboard;
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use logos_chat::{ChatClient, ChatStore, Event, IdentityProvider, RegistrationService, Transport};
|
use logos_chat::{AccountDirectory, ChatClient, ChatStore, Event, RegistrationService, Transport};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::utils::now;
|
use crate::utils::now;
|
||||||
@ -41,14 +41,13 @@ pub struct AppState {
|
|||||||
pub active_chat: Option<String>,
|
pub active_chat: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChatApp<I, T, R, S>
|
pub struct ChatApp<T, R, S>
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
T: Transport,
|
T: Transport,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
pub client: ChatClient<I, T, R, S>,
|
pub client: ChatClient<T, R, S>,
|
||||||
events: Receiver<Event>,
|
events: Receiver<Event>,
|
||||||
pub state: AppState,
|
pub state: AppState,
|
||||||
/// Ephemeral command output — not persisted, cleared on chat switch.
|
/// Ephemeral command output — not persisted, cleared on chat switch.
|
||||||
@ -59,15 +58,14 @@ where
|
|||||||
state_path: PathBuf,
|
state_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I, T, R, S> ChatApp<I, T, R, S>
|
impl<T, R, S> ChatApp<T, R, S>
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send,
|
|
||||||
T: Transport,
|
T: Transport,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send,
|
S: ChatStore + Send,
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub fn new(
|
||||||
client: ChatClient<I, T, R, S>,
|
client: ChatClient<T, R, S>,
|
||||||
events: Receiver<Event>,
|
events: Receiver<Event>,
|
||||||
user_name: &str,
|
user_name: &str,
|
||||||
data_dir: &Path,
|
data_dir: &Path,
|
||||||
|
|||||||
@ -8,9 +8,10 @@ use std::path::{Path, PathBuf};
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
|
use logos_account::TestLogosAccount;
|
||||||
use logos_chat::{
|
use logos_chat::{
|
||||||
ChatClient, ChatClientBuilder, ChatStore, DelegateSigner, Event, HttpRegistry,
|
AccountDirectory, ChatClient, ChatClientBuilder, ChatStore, DelegateSigner, Event,
|
||||||
IdentityProvider, LogosChatClient, NETWORK_PRESET, REGISTRY_ENDPOINT, RegistrationService,
|
HttpRegistry, LogosChatClient, NETWORK_PRESET, REGISTRY_ENDPOINT, RegistrationService,
|
||||||
StorageConfig, Transport,
|
StorageConfig, Transport,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,10 +107,19 @@ fn main() -> Result<()> {
|
|||||||
.context("failed to create file transport")?;
|
.context("failed to create file transport")?;
|
||||||
|
|
||||||
let endpoint = cli.registry_url.as_deref().unwrap_or(REGISTRY_ENDPOINT);
|
let endpoint = cli.registry_url.as_deref().unwrap_or(REGISTRY_ENDPOINT);
|
||||||
let (client, events) = ChatClientBuilder::new()
|
// A fresh dev account endorsing a fresh delegate each launch,
|
||||||
.ident(DelegateSigner::random())
|
// mirroring `LogosChatClient::open`.
|
||||||
|
let account = TestLogosAccount::new();
|
||||||
|
let delegate = DelegateSigner::random();
|
||||||
|
let mut registry = HttpRegistry::new(endpoint);
|
||||||
|
account
|
||||||
|
.add_delegate_signer(&mut registry, delegate.public_key())
|
||||||
|
.map_err(|e| anyhow::anyhow!("{e:?}"))
|
||||||
|
.context("failed to publish the device bundle")?;
|
||||||
|
let (client, events) = ChatClientBuilder::new(account.address())
|
||||||
|
.ident(delegate)
|
||||||
.transport(transport)
|
.transport(transport)
|
||||||
.registration(HttpRegistry::new(endpoint))
|
.registration(registry)
|
||||||
.storage_config(StorageConfig::Encrypted {
|
.storage_config(StorageConfig::Encrypted {
|
||||||
path: db_str,
|
path: db_str,
|
||||||
key: "chat-cli".to_string(),
|
key: "chat-cli".to_string(),
|
||||||
@ -135,15 +145,14 @@ fn db_path(cli: &Cli) -> Result<String> {
|
|||||||
.to_string())
|
.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn launch_tui<I, T, R, S>(
|
fn launch_tui<T, R, S>(
|
||||||
client: ChatClient<I, T, R, S>,
|
client: ChatClient<T, R, S>,
|
||||||
events: Receiver<Event>,
|
events: Receiver<Event>,
|
||||||
cli: &Cli,
|
cli: &Cli,
|
||||||
) -> Result<()>
|
) -> Result<()>
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send,
|
|
||||||
T: Transport,
|
T: Transport,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send,
|
S: ChatStore + Send,
|
||||||
{
|
{
|
||||||
let mut app = ChatApp::new(client, events, &cli.name, &cli.data)?;
|
let mut app = ChatApp::new(client, events, &cli.name, &cli.data)?;
|
||||||
@ -158,11 +167,10 @@ where
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_app<I, T, R, S>(terminal: &mut ui::Tui, app: &mut ChatApp<I, T, R, S>) -> Result<()>
|
fn run_app<T, R, S>(terminal: &mut ui::Tui, app: &mut ChatApp<T, R, S>) -> Result<()>
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send,
|
|
||||||
T: Transport,
|
T: Transport,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send,
|
S: ChatStore + Send,
|
||||||
{
|
{
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
@ -16,7 +16,7 @@ use ratatui::{
|
|||||||
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
|
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
|
||||||
};
|
};
|
||||||
|
|
||||||
use logos_chat::{ChatStore, IdentityProvider, RegistrationService, Transport};
|
use logos_chat::{AccountDirectory, ChatStore, RegistrationService, Transport};
|
||||||
|
|
||||||
use crate::app::ChatApp;
|
use crate::app::ChatApp;
|
||||||
|
|
||||||
@ -38,11 +38,10 @@ pub fn restore() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the UI.
|
/// Draw the UI.
|
||||||
pub fn draw<I, D, R, S>(frame: &mut Frame, app: &ChatApp<I, D, R, S>)
|
pub fn draw<D, R, S>(frame: &mut Frame, app: &ChatApp<D, R, S>)
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
D: Transport + Send + 'static,
|
D: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
@ -61,11 +60,10 @@ where
|
|||||||
draw_status(frame, app, chunks[3]);
|
draw_status(frame, app, chunks[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_header<I, D, R, S>(frame: &mut Frame, app: &ChatApp<I, D, R, S>, area: Rect)
|
fn draw_header<D, R, S>(frame: &mut Frame, app: &ChatApp<D, R, S>, area: Rect)
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
D: Transport + Send + 'static,
|
D: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
let title = match app.current_session() {
|
let title = match app.current_session() {
|
||||||
@ -90,11 +88,10 @@ where
|
|||||||
frame.render_widget(header, area);
|
frame.render_widget(header, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_messages<I, D, R, S>(frame: &mut Frame, app: &ChatApp<I, D, R, S>, area: Rect)
|
fn draw_messages<D, R, S>(frame: &mut Frame, app: &ChatApp<D, R, S>, area: Rect)
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
D: Transport + Send + 'static,
|
D: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
let remote_name = app
|
let remote_name = app
|
||||||
@ -182,11 +179,10 @@ where
|
|||||||
frame.render_stateful_widget(messages_widget, area, &mut list_state);
|
frame.render_stateful_widget(messages_widget, area, &mut list_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_input<I, D, R, S>(frame: &mut Frame, app: &ChatApp<I, D, R, S>, area: Rect)
|
fn draw_input<D, R, S>(frame: &mut Frame, app: &ChatApp<D, R, S>, area: Rect)
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
D: Transport + Send + 'static,
|
D: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
// Inner width: area minus borders (2).
|
// Inner width: area minus borders (2).
|
||||||
@ -215,11 +211,10 @@ where
|
|||||||
frame.set_cursor_position((cursor_x, area.y + 1));
|
frame.set_cursor_position((cursor_x, area.y + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_status<I, D, R, S>(frame: &mut Frame, app: &ChatApp<I, D, R, S>, area: Rect)
|
fn draw_status<D, R, S>(frame: &mut Frame, app: &ChatApp<D, R, S>, area: Rect)
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
D: Transport + Send + 'static,
|
D: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
let status = Paragraph::new(app.status.as_str())
|
let status = Paragraph::new(app.status.as_str())
|
||||||
@ -231,11 +226,10 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle keyboard events.
|
/// Handle keyboard events.
|
||||||
pub fn handle_events<I, D, R, S>(app: &mut ChatApp<I, D, R, S>) -> io::Result<bool>
|
pub fn handle_events<D, R, S>(app: &mut ChatApp<D, R, S>) -> io::Result<bool>
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
D: Transport + Send + 'static,
|
D: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
// Poll for events with a short timeout to allow checking incoming messages
|
// Poll for events with a short timeout to allow checking incoming messages
|
||||||
|
|||||||
@ -9,7 +9,8 @@ dev = []
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
# Workspace dependencies (sorted)
|
# Workspace dependencies (sorted)
|
||||||
crypto = { workspace = true }
|
crypto = { workspace = true }
|
||||||
libchat = { workspace = true }
|
|
||||||
shared-traits = { workspace = true }
|
shared-traits = { workspace = true }
|
||||||
|
|
||||||
# External dependencies (sorted)
|
# External dependencies (sorted)
|
||||||
|
hex = "0.4.3"
|
||||||
|
thiserror = "2"
|
||||||
|
|||||||
@ -1,43 +1,183 @@
|
|||||||
use crypto::{Ed25519SigningKey, Ed25519VerifyingKey};
|
use crypto::{Ed25519SigningKey, Ed25519VerifyingKey};
|
||||||
use shared_traits::{IdentId, IdentIdRef};
|
|
||||||
|
|
||||||
use libchat::IdentityProvider;
|
use crate::directory::{AccountDirectory, SignedDeviceBundle, encode_bundle_payload};
|
||||||
|
|
||||||
/// A Test Focused LogosAccount using a pre-defined identifier.
|
/// Failures updating an account's device bundle in the directory.
|
||||||
/// The test account is not persisted, and uses a single user provided id.
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AddDelegateSignerError {
|
||||||
|
#[error("directory: {0}")]
|
||||||
|
Directory(String),
|
||||||
|
#[error("directory returned a malformed device id")]
|
||||||
|
MalformedDeviceId,
|
||||||
|
#[error("directory returned a malformed device key")]
|
||||||
|
MalformedDeviceKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Test Focused LogosAccount.
|
||||||
|
/// The test account is not persisted.
|
||||||
/// This account type should not be used in a production system.
|
/// This account type should not be used in a production system.
|
||||||
pub struct TestLogosAccount {
|
pub struct TestLogosAccount {
|
||||||
id: IdentId,
|
|
||||||
signing_key: Ed25519SigningKey,
|
signing_key: Ed25519SigningKey,
|
||||||
verifying_key: Ed25519VerifyingKey,
|
verifying_key: Ed25519VerifyingKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for TestLogosAccount {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TestLogosAccount {
|
impl TestLogosAccount {
|
||||||
pub fn new(explicit_id: impl Into<String>) -> Self {
|
pub fn new() -> Self {
|
||||||
let signing_key = Ed25519SigningKey::generate();
|
let signing_key = Ed25519SigningKey::generate();
|
||||||
let verifying_key = signing_key.verifying_key();
|
let verifying_key = signing_key.verifying_key();
|
||||||
Self {
|
Self {
|
||||||
id: IdentId::new(explicit_id.into()),
|
|
||||||
signing_key,
|
signing_key,
|
||||||
verifying_key,
|
verifying_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl IdentityProvider for TestLogosAccount {
|
/// The account verifying key; its hex is the account address peers share.
|
||||||
fn id(&self) -> IdentIdRef<'_> {
|
pub fn public_key(&self) -> &Ed25519VerifyingKey {
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn display_name(&self) -> String {
|
|
||||||
self.id.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn public_key(&self) -> &Ed25519VerifyingKey {
|
|
||||||
&self.verifying_key
|
&self.verifying_key
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign(&self, payload: &[u8]) -> crypto::Ed25519Signature {
|
/// The account address peers share: the hex of the verifying key.
|
||||||
self.signing_key.sign(payload)
|
pub fn address(&self) -> String {
|
||||||
|
hex::encode(self.verifying_key.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add `signer` (the delegate signer's verifying key) to this account's directory bundle.
|
||||||
|
///
|
||||||
|
/// Fetches the current (verified) device set, adds the signer if absent,
|
||||||
|
/// bumps the lamport, re-signs, and publishes. Safe to call repeatedly:
|
||||||
|
/// an unchanged set is simply re-published, which also refreshes the
|
||||||
|
/// server's retention clock. The account signs internally; its key never
|
||||||
|
/// leaves this type.
|
||||||
|
pub fn add_delegate_signer<D: AccountDirectory>(
|
||||||
|
&self,
|
||||||
|
directory: &mut D,
|
||||||
|
signer: &Ed25519VerifyingKey,
|
||||||
|
) -> Result<(), AddDelegateSignerError> {
|
||||||
|
// Start from the devices already registered so the account's other
|
||||||
|
// installations are preserved across the upsert.
|
||||||
|
let existing = directory
|
||||||
|
.fetch(&self.verifying_key)
|
||||||
|
.map_err(|e| AddDelegateSignerError::Directory(e.to_string()))?;
|
||||||
|
let (mut devices, next_lamport) = match existing {
|
||||||
|
Some(set) => {
|
||||||
|
let mut keys = Vec::with_capacity(set.devices.len() + 1);
|
||||||
|
for hex_id in &set.devices {
|
||||||
|
let bytes: [u8; 32] = hex::decode(hex_id)
|
||||||
|
.ok()
|
||||||
|
.and_then(|b| b.try_into().ok())
|
||||||
|
.ok_or(AddDelegateSignerError::MalformedDeviceId)?;
|
||||||
|
let key = Ed25519VerifyingKey::from_bytes(&bytes)
|
||||||
|
.map_err(|_| AddDelegateSignerError::MalformedDeviceKey)?;
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
(keys, set.lamport + 1)
|
||||||
|
}
|
||||||
|
None => (Vec::new(), 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !devices.iter().any(|d| d.as_ref() == signer.as_ref()) {
|
||||||
|
devices.push(signer.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload = encode_bundle_payload(next_lamport, &devices);
|
||||||
|
let signature = self.signing_key.sign(&payload);
|
||||||
|
let bundle = SignedDeviceBundle {
|
||||||
|
account_pub: self.verifying_key.clone(),
|
||||||
|
payload,
|
||||||
|
signature,
|
||||||
|
};
|
||||||
|
|
||||||
|
directory
|
||||||
|
.publish(&bundle)
|
||||||
|
.map_err(|e| AddDelegateSignerError::Directory(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::directory::{DeviceSet, verify_bundle};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Minimal in-test directory: stores the latest bundle, verifies on fetch.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct FakeDir(Option<SignedDeviceBundle>);
|
||||||
|
|
||||||
|
impl AccountDirectory for FakeDir {
|
||||||
|
type Error = crate::directory::BundleError;
|
||||||
|
fn publish(&mut self, bundle: &SignedDeviceBundle) -> Result<(), Self::Error> {
|
||||||
|
self.0 = Some(bundle.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn fetch(&self, account: &Ed25519VerifyingKey) -> Result<Option<DeviceSet>, Self::Error> {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.map(|b| verify_bundle(account, b))
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device_set(dir: &FakeDir, account: &TestLogosAccount) -> (u64, Vec<String>) {
|
||||||
|
let set = dir
|
||||||
|
.fetch(account.public_key())
|
||||||
|
.unwrap()
|
||||||
|
.expect("bundle published");
|
||||||
|
(set.lamport, set.devices)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// First publish for an account starts at lamport 0 with the one device.
|
||||||
|
#[test]
|
||||||
|
fn first_add_delegate_signer_lists_the_signer() {
|
||||||
|
let mut dir = FakeDir::default();
|
||||||
|
let account = TestLogosAccount::new();
|
||||||
|
let device = Ed25519SigningKey::generate().verifying_key();
|
||||||
|
|
||||||
|
account.add_delegate_signer(&mut dir, &device).unwrap();
|
||||||
|
|
||||||
|
let (lamport, devices) = device_set(&dir, &account);
|
||||||
|
assert_eq!(lamport, 0);
|
||||||
|
assert_eq!(devices, vec![hex::encode(device.as_ref())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A second device is merged into the existing set with a bumped lamport,
|
||||||
|
/// preserving the first device.
|
||||||
|
#[test]
|
||||||
|
fn add_delegate_signer_merges_and_bumps_lamport() {
|
||||||
|
let mut dir = FakeDir::default();
|
||||||
|
let account = TestLogosAccount::new();
|
||||||
|
let first = Ed25519SigningKey::generate().verifying_key();
|
||||||
|
let second = Ed25519SigningKey::generate().verifying_key();
|
||||||
|
|
||||||
|
account.add_delegate_signer(&mut dir, &first).unwrap();
|
||||||
|
account.add_delegate_signer(&mut dir, &second).unwrap();
|
||||||
|
|
||||||
|
let (lamport, devices) = device_set(&dir, &account);
|
||||||
|
assert_eq!(lamport, 1);
|
||||||
|
assert_eq!(
|
||||||
|
devices,
|
||||||
|
vec![hex::encode(first.as_ref()), hex::encode(second.as_ref())]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-adding an already-listed device keeps the set and still bumps the
|
||||||
|
/// lamport (a refresh, not a duplicate).
|
||||||
|
#[test]
|
||||||
|
fn re_adding_a_device_is_idempotent_on_the_set() {
|
||||||
|
let mut dir = FakeDir::default();
|
||||||
|
let account = TestLogosAccount::new();
|
||||||
|
let device = Ed25519SigningKey::generate().verifying_key();
|
||||||
|
|
||||||
|
account.add_delegate_signer(&mut dir, &device).unwrap();
|
||||||
|
account.add_delegate_signer(&mut dir, &device).unwrap();
|
||||||
|
|
||||||
|
let (lamport, devices) = device_set(&dir, &account);
|
||||||
|
assert_eq!(lamport, 1);
|
||||||
|
assert_eq!(devices, vec![hex::encode(device.as_ref())]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,10 @@
|
|||||||
//! one such bundle per account so that an inviter can resolve an account public
|
//! 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.
|
||||||
//!
|
//!
|
||||||
//! Two roles are kept distinct from the per-device [`IdentityProvider`]:
|
//! [`AccountDirectory`] is the client that publishes and fetches+verifies the
|
||||||
//!
|
//! bundle against the directory service. Signing a bundle is account
|
||||||
//! - [`AccountAuthority`] — the injected account key. Custody (wallet, enclave,
|
//! functionality (e.g. [`TestLogosAccount::add_delegate_signer`](crate::TestLogosAccount));
|
||||||
//! another device) stays outside libchat; we only ever ask it to sign. Present
|
//! the account key never leaves the account type.
|
||||||
//! only where the user authorizes a device change.
|
|
||||||
//! - [`AccountDirectory`] — the client that publishes and fetches+verifies the
|
|
||||||
//! bundle against the directory service.
|
|
||||||
//!
|
//!
|
||||||
//! The bundle `payload` is opaque to the server. Both the signing side
|
//! The bundle `payload` is opaque to the server. Both the signing side
|
||||||
//! ([`encode_bundle_payload`]) and the verifying side ([`verify_bundle`]) live
|
//! ([`encode_bundle_payload`]) and the verifying side ([`verify_bundle`]) live
|
||||||
@ -24,8 +21,8 @@ use shared_traits::IdentIdRef;
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// A device (LocalIdentity) verifying key, hex-encoded — the same shape as the
|
/// A device (LocalIdentity) verifying key, hex-encoded — the same shape as the
|
||||||
/// keypackage registry's `device_id`, so values flow straight into
|
/// keypackage registry's `device_id`, so values flow straight into a keypackage
|
||||||
/// [`KeyPackageProvider::retrieve`](crate::service_traits::KeyPackageProvider).
|
/// retrieval.
|
||||||
pub type DeviceId = String;
|
pub type DeviceId = String;
|
||||||
|
|
||||||
/// The account's monotonic version counter, bumped on every membership change.
|
/// The account's monotonic version counter, bumped on every membership change.
|
||||||
@ -48,8 +45,8 @@ pub const BUNDLE_VERSION: u8 = 1;
|
|||||||
pub const BUNDLE_DOMAIN: &[u8] = b"libchat:account-device-bundle\0";
|
pub const BUNDLE_DOMAIN: &[u8] = b"libchat:account-device-bundle\0";
|
||||||
|
|
||||||
/// The signed device-list bundle. The `payload` bytes are exactly
|
/// The signed device-list bundle. The `payload` bytes are exactly
|
||||||
/// what [`AccountAuthority::sign`] signed, so verifiers check the
|
/// what the account signed, so verifiers check the signature over
|
||||||
/// signature over the same bytes they received.
|
/// the same bytes they received.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct SignedDeviceBundle {
|
pub struct SignedDeviceBundle {
|
||||||
/// The account verifying key this bundle belongs to. Used for addressing on
|
/// The account verifying key this bundle belongs to. Used for addressing on
|
||||||
@ -71,30 +68,10 @@ pub struct DeviceSet {
|
|||||||
pub devices: Vec<DeviceId>,
|
pub devices: Vec<DeviceId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The account capability, injected by the platform.
|
|
||||||
///
|
|
||||||
/// Custody of the account key stays outside libchat — the library only ever asks
|
|
||||||
/// it to sign a device-list bundle. The same trait covers a local on-device key
|
|
||||||
/// (testnet) and an external signer (wallet/enclave), which is why [`sign`] is
|
|
||||||
/// fallible: an external signer can be offline or decline the prompt.
|
|
||||||
///
|
|
||||||
/// Verification needs no authority — anyone holding the account verifying key
|
|
||||||
/// verifies with [`verify_bundle`].
|
|
||||||
///
|
|
||||||
/// [`sign`]: AccountAuthority::sign
|
|
||||||
pub trait AccountAuthority {
|
|
||||||
type Error: Display + Debug;
|
|
||||||
|
|
||||||
/// The account verifying key identifying this participant.
|
|
||||||
fn account_pub(&self) -> &Ed25519VerifyingKey;
|
|
||||||
/// Sign the canonical bundle bytes with the account key.
|
|
||||||
fn sign(&self, payload: &[u8]) -> Result<Ed25519Signature, Self::Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Client for the account → device directory service.
|
/// Client for the account → device directory service.
|
||||||
///
|
///
|
||||||
/// Mirrors [`RegistrationService`](crate::service_traits::RegistrationService):
|
/// Mirrors the core's `RegistrationService`: an injected trait with an HTTP
|
||||||
/// an injected trait in core with an HTTP implementation in the extension layer.
|
/// implementation in the extension layer.
|
||||||
/// The service is untrusted, so [`fetch`](AccountDirectory::fetch) verifies the
|
/// The service is untrusted, so [`fetch`](AccountDirectory::fetch) verifies the
|
||||||
/// account signature before returning a [`DeviceSet`].
|
/// account signature before returning a [`DeviceSet`].
|
||||||
pub trait AccountDirectory: Debug {
|
pub trait AccountDirectory: Debug {
|
||||||
@ -209,22 +186,33 @@ pub fn verify_bundle(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Failures resolving an account address to its device ids.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ResolveError {
|
||||||
|
#[error("address is not an account key")]
|
||||||
|
NotAnAccountKey,
|
||||||
|
#[error("account has published no device bundle")]
|
||||||
|
NoDeviceBundle,
|
||||||
|
#[error("directory: {0}")]
|
||||||
|
Directory(String),
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve an account to the device ids whose KeyPackages must be fetched.
|
/// Resolve an account to the device ids whose KeyPackages must be fetched.
|
||||||
///
|
///
|
||||||
/// The directory is keyed by the account verifying key. When `account` is the hex
|
/// The directory is keyed by the account verifying key: `account` must be the
|
||||||
/// of such a key and a bundle exists, returns its verified device set. Otherwise
|
/// hex of such a key, and a reachable account has published a bundle endorsing
|
||||||
/// falls back to treating the identifier itself as a single device id — the
|
/// at least one device. Anything else is an error — the distinct variants tell
|
||||||
/// pre-directory behaviour — so opaque or never-published ids keep working.
|
/// a malformed address, an unpublished account, and a directory outage apart.
|
||||||
pub fn resolve_device_ids<D: AccountDirectory + ?Sized>(
|
pub fn resolve_device_ids<D: AccountDirectory + ?Sized>(
|
||||||
directory: &D,
|
directory: &D,
|
||||||
account: IdentIdRef,
|
account: IdentIdRef,
|
||||||
) -> Result<Vec<DeviceId>, D::Error> {
|
) -> Result<Vec<DeviceId>, ResolveError> {
|
||||||
if let Some(account_key) = account_key_from_id(account)
|
let account_key = account_key_from_id(account).ok_or(ResolveError::NotAnAccountKey)?;
|
||||||
&& let Some(set) = directory.fetch(&account_key)?
|
let set = directory
|
||||||
{
|
.fetch(&account_key)
|
||||||
return Ok(set.devices);
|
.map_err(|e| ResolveError::Directory(e.to_string()))?
|
||||||
}
|
.ok_or(ResolveError::NoDeviceBundle)?;
|
||||||
Ok(vec![account.to_string()])
|
Ok(set.devices)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Interpret an identity id as the hex of an account verifying key, if it is one.
|
/// Interpret an identity id as the hex of an account verifying key, if it is one.
|
||||||
@ -363,12 +351,25 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// No published bundle → fall back to the identifier as a single device id.
|
/// An address that is not the hex of an account key cannot be resolved.
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_falls_back_to_account_id() {
|
fn resolve_rejects_non_key_address() {
|
||||||
let account = IdentId::new("pax");
|
let account = IdentId::new("pax");
|
||||||
let resolved = resolve_device_ids(&FakeDir(None), &account).unwrap();
|
assert!(matches!(
|
||||||
assert_eq!(resolved, vec![account.to_string()]);
|
resolve_device_ids(&FakeDir(None), &account),
|
||||||
|
Err(ResolveError::NotAnAccountKey)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An account that never published a bundle is unreachable.
|
||||||
|
#[test]
|
||||||
|
fn resolve_rejects_unpublished_account() {
|
||||||
|
let account_pub = Ed25519SigningKey::generate().verifying_key();
|
||||||
|
let account_id = IdentId::new(hex::encode(account_pub.as_ref()));
|
||||||
|
assert!(matches!(
|
||||||
|
resolve_device_ids(&FakeDir(None), &account_id),
|
||||||
|
Err(ResolveError::NoDeviceBundle)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A published bundle → resolve to its verified device ids (hex pubkeys).
|
/// A published bundle → resolve to its verified device ids (hex pubkeys).
|
||||||
@ -1,5 +1,13 @@
|
|||||||
|
mod directory;
|
||||||
|
|
||||||
|
pub use directory::{
|
||||||
|
AccountDirectory, BUNDLE_VERSION, BundleError, DecodedBundle, DeviceId, DeviceSet, Lamport,
|
||||||
|
ResolveError, SignedDeviceBundle, decode_bundle_payload, encode_bundle_payload,
|
||||||
|
resolve_device_ids, verify_bundle,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(feature = "dev")]
|
#[cfg(feature = "dev")]
|
||||||
mod account;
|
mod account;
|
||||||
|
|
||||||
#[cfg(feature = "dev")]
|
#[cfg(feature = "dev")]
|
||||||
pub use account::TestLogosAccount;
|
pub use account::{AddDelegateSignerError, TestLogosAccount};
|
||||||
|
|||||||
@ -12,7 +12,6 @@ use shared_traits::IdentIdRef;
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::account_directory::{AccountDirectory, resolve_device_ids};
|
|
||||||
use crate::conversation::ConversationIdRef;
|
use crate::conversation::ConversationIdRef;
|
||||||
use crate::inbox_v2::MlsProvider;
|
use crate::inbox_v2::MlsProvider;
|
||||||
use crate::service_context::{ExternalServices, ServiceContext};
|
use crate::service_context::{ExternalServices, ServiceContext};
|
||||||
@ -137,38 +136,27 @@ impl GroupV1Convo {
|
|||||||
Self::delivery_address_from_id(&self.convo_id)
|
Self::delivery_address_from_id(&self.convo_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve an account to a KeyPackage for *every* device it authorizes.
|
/// Fetch a signer's KeyPackage from the registry. Members are signer
|
||||||
///
|
/// (installation) ids; resolving an account to its signers is the caller's
|
||||||
/// First resolves the account to its device ids through the account
|
/// concern, above the core.
|
||||||
/// directory ([`resolve_device_ids`]), then fetches each device's
|
fn key_package_for_signer(
|
||||||
/// KeyPackage. When the account never published a bundle, resolution falls
|
|
||||||
/// back to a single device id equal to the account id — the pre-directory
|
|
||||||
/// behaviour — so single-device accounts are unaffected.
|
|
||||||
fn key_packages_for_account(
|
|
||||||
&self,
|
&self,
|
||||||
ident: IdentIdRef,
|
signer: IdentIdRef,
|
||||||
provider: &impl MlsProvider,
|
provider: &impl MlsProvider,
|
||||||
registry: &(impl KeyPackageProvider + AccountDirectory),
|
registry: &impl KeyPackageProvider,
|
||||||
) -> Result<Vec<KeyPackage>, ChatError> {
|
) -> Result<KeyPackage, ChatError> {
|
||||||
let device_ids =
|
let retrieved = registry
|
||||||
resolve_device_ids(registry, ident).map_err(|e| ChatError::Generic(e.to_string()))?;
|
.retrieve(signer.as_str())
|
||||||
|
.map_err(|e| ChatError::Generic(e.to_string()))?;
|
||||||
|
let Some(keypkg_bytes) = retrieved else {
|
||||||
|
return Err(ChatError::Protocol(format!(
|
||||||
|
"no keypackage for signer {signer}"
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
let mut keypackages = Vec::with_capacity(device_ids.len());
|
let key_package_in = KeyPackageIn::tls_deserialize(&mut keypkg_bytes.as_slice())?;
|
||||||
for device_id in &device_ids {
|
let keypkg = key_package_in.validate(provider.crypto(), ProtocolVersion::Mls10)?; //TODO: P3 - Hardcoded Protocol Version
|
||||||
let retrieved = registry
|
Ok(keypkg)
|
||||||
.retrieve(device_id)
|
|
||||||
.map_err(|e| ChatError::Generic(e.to_string()))?;
|
|
||||||
let Some(keypkg_bytes) = retrieved else {
|
|
||||||
return Err(ChatError::Protocol(format!(
|
|
||||||
"no keypackage for device {device_id} of account {ident}"
|
|
||||||
)));
|
|
||||||
};
|
|
||||||
|
|
||||||
let key_package_in = KeyPackageIn::tls_deserialize(&mut keypkg_bytes.as_slice())?;
|
|
||||||
let keypkg = key_package_in.validate(provider.crypto(), ProtocolVersion::Mls10)?; //TODO: P3 - Hardcoded Protocol Version
|
|
||||||
keypackages.push(keypkg);
|
|
||||||
}
|
|
||||||
Ok(keypackages)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_message<S: ExternalServices>(
|
fn send_message<S: ExternalServices>(
|
||||||
@ -324,12 +312,12 @@ impl<S: ExternalServices> GroupConvo<S> for GroupV1Convo {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve each account to a KeyPackage per authorized device and flatten
|
// Members are signer (installation) ids: one KeyPackage each, one MLS
|
||||||
// them into one list — every device of every invitee becomes an MLS
|
// leaf each. A caller inviting an account passes every signer id the
|
||||||
// leaf, so all of a user's installations join the group.
|
// account's directory bundle lists.
|
||||||
let mut keypkgs = Vec::with_capacity(members.len());
|
let mut keypkgs = Vec::with_capacity(members.len());
|
||||||
for ident in members {
|
for ident in members {
|
||||||
keypkgs.extend(self.key_packages_for_account(ident, &cx.mls_provider, &cx.registry)?);
|
keypkgs.push(self.key_package_for_signer(ident, &cx.mls_provider, &cx.registry)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (commit, welcome, _group_info) = self
|
let (commit, welcome, _group_info) = self
|
||||||
@ -346,9 +334,9 @@ impl<S: ExternalServices> GroupConvo<S> for GroupV1Convo {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// TODO: (P3) Evaluate privacy/performance implications of an aggregated Welcome for multiple users
|
// TODO: (P3) Evaluate privacy/performance implications of an aggregated Welcome for multiple users
|
||||||
for account_id in members {
|
for signer_id in members {
|
||||||
cx.mls_provider
|
cx.mls_provider
|
||||||
.invite_user(&mut cx.ds, account_id, &welcome)?;
|
.invite_user(&mut cx.ds, signer_id, &welcome)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.send_payload(cx, commit.to_bytes()?)
|
self.send_payload(cx, commit.to_bytes()?)
|
||||||
|
|||||||
@ -17,6 +17,8 @@ use de_mls::{
|
|||||||
};
|
};
|
||||||
use hashgraph_like_consensus::signing::EthereumConsensusSigner;
|
use hashgraph_like_consensus::signing::EthereumConsensusSigner;
|
||||||
use openmls::group::MlsGroupCreateConfig;
|
use openmls::group::MlsGroupCreateConfig;
|
||||||
|
use openmls::prelude::tls_codec::Deserialize as _;
|
||||||
|
use openmls::prelude::{KeyPackageIn, OpenMlsProvider as _, ProtocolVersion};
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use shared_traits::{IdentId, IdentIdRef};
|
use shared_traits::{IdentId, IdentIdRef};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -74,8 +76,10 @@ fn demls_config() -> ConversationConfig {
|
|||||||
pub struct GroupV2Convo {
|
pub struct GroupV2Convo {
|
||||||
convo_id: String,
|
convo_id: String,
|
||||||
conversation: Conversation<DefaultConsensusPlugin, InMemoryPeerScoreStorage>,
|
conversation: Conversation<DefaultConsensusPlugin, InMemoryPeerScoreStorage>,
|
||||||
/// Member-ids we proposed via add_member. We forward a welcome only to joiners WE invited.
|
/// Joiners WE invited, as `(member_id, signer_id)`: the de-mls member id
|
||||||
pending_invites: Vec<Vec<u8>>,
|
/// (the joiner's leaf credential content, read from its key package) paired
|
||||||
|
/// with the signer id its welcome is delivered to.
|
||||||
|
pending_invites: Vec<(Vec<u8>, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for GroupV2Convo {
|
impl std::fmt::Debug for GroupV2Convo {
|
||||||
@ -274,20 +278,32 @@ where
|
|||||||
members: &[IdentIdRef],
|
members: &[IdentIdRef],
|
||||||
) -> Result<(), ChatError> {
|
) -> Result<(), ChatError> {
|
||||||
// Record who WE invited before touching the conversation: after_op
|
// Record who WE invited before touching the conversation: after_op
|
||||||
// forwards a welcome only to joiners in pending_invites (the de-mls
|
// forwards a welcome only to joiners in pending_invites. Members are
|
||||||
// member-id is the invitee's id bytes).
|
// signer ids; the de-mls member id must match the id of the
|
||||||
|
// IdentityProvider that generated the key package (its MLS leaf
|
||||||
|
// credential content — de-mls matches members by credential), so it is
|
||||||
|
// read from the fetched key package rather than assumed equal to the
|
||||||
|
// signer id.
|
||||||
for member in members {
|
for member in members {
|
||||||
let kp_bytes = service_ctx
|
let kp_bytes = service_ctx
|
||||||
.registry
|
.registry
|
||||||
.retrieve(member.as_str())
|
.retrieve(member.as_str())
|
||||||
.map_err(ChatError::generic)?
|
.map_err(ChatError::generic)?
|
||||||
.ok_or_else(|| ChatError::generic("No key package"))?;
|
.ok_or_else(|| ChatError::generic("No key package"))?;
|
||||||
|
let key_package_in = KeyPackageIn::tls_deserialize(&mut kp_bytes.as_slice())?;
|
||||||
|
let keypkg = key_package_in
|
||||||
|
.validate(service_ctx.mls_provider.crypto(), ProtocolVersion::Mls10)?;
|
||||||
|
let member_id = keypkg
|
||||||
|
.leaf_node()
|
||||||
|
.credential()
|
||||||
|
.serialized_content()
|
||||||
|
.to_vec();
|
||||||
self.pending_invites
|
self.pending_invites
|
||||||
.push(member.as_str().as_bytes().to_vec());
|
.push((member_id.clone(), member.to_string()));
|
||||||
self.conversation.add_member(
|
self.conversation.add_member(
|
||||||
&service_ctx.mls_provider,
|
&service_ctx.mls_provider,
|
||||||
&service_ctx.mls_identity,
|
&service_ctx.mls_identity,
|
||||||
member.as_str().as_bytes(),
|
&member_id,
|
||||||
&kp_bytes,
|
&kp_bytes,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
@ -314,16 +330,17 @@ impl GroupV2Convo {
|
|||||||
let outbound = self.conversation.drain_outbound(); // Vec<de_mls::session::Outbound>
|
let outbound = self.conversation.drain_outbound(); // Vec<de_mls::session::Outbound>
|
||||||
let wakeup = self.conversation.next_wakeup_in();
|
let wakeup = self.conversation.next_wakeup_in();
|
||||||
|
|
||||||
// 1. Route welcomes for joiners WE invited (event fires on every member now).
|
// 1. Route welcomes for joiners WE invited (event fires on every member
|
||||||
|
// now). The welcome travels to the joiner's signer id (where its
|
||||||
|
// InboxV2 listens), not its de-mls member id.
|
||||||
for evt in &events {
|
for evt in &events {
|
||||||
if let ConversationEvent::WelcomeReady { welcome, .. } = evt {
|
if let ConversationEvent::WelcomeReady { welcome, .. } = evt {
|
||||||
for joiner in &welcome.joiner_identities {
|
for joiner in &welcome.joiner_identities {
|
||||||
if let Some(i) = self.pending_invites.iter().position(|p| p == joiner) {
|
if let Some(i) = self.pending_invites.iter().position(|(p, _)| p == joiner) {
|
||||||
self.pending_invites.remove(i);
|
let (_, signer_id) = self.pending_invites.remove(i);
|
||||||
let name = String::from_utf8(joiner.clone()).map_err(ChatError::generic)?;
|
|
||||||
crate::inbox_v2::invite_user_v2(
|
crate::inbox_v2::invite_user_v2(
|
||||||
&mut service_ctx.ds,
|
&mut service_ctx.ds,
|
||||||
&IdentId::new(name),
|
&IdentId::new(signer_id),
|
||||||
welcome,
|
welcome,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use crypto::{Identity, PublicKey};
|
use crypto::{Identity, PublicKey};
|
||||||
use openmls::group::GroupId;
|
use openmls::group::GroupId;
|
||||||
use shared_traits::IdentIdRef;
|
use shared_traits::{IdentId, IdentIdRef};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use storage::{ChatStore, ConversationKind, ConversationStore};
|
use storage::{ChatStore, ConversationKind, ConversationStore};
|
||||||
@ -97,7 +97,6 @@ where
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
core.register_keypackage()?;
|
core.register_keypackage()?;
|
||||||
core.register_account_bundle()?;
|
|
||||||
Ok(core)
|
Ok(core)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +111,12 @@ where
|
|||||||
store: CS,
|
store: CS,
|
||||||
) -> Result<Self, ChatError> {
|
) -> Result<Self, ChatError> {
|
||||||
let inbox = Inbox::new(&identity);
|
let inbox = Inbox::new(&identity);
|
||||||
let ident_id = ident.id().clone();
|
// InboxV2 rendezvous is signer-scoped: it subscribes under the hex of
|
||||||
|
// the signer's verifying key — the same string the account → device
|
||||||
|
// directory lists and the registries key key-packages under, so it is
|
||||||
|
// exactly what an inviter can derive for this installation. The MLS
|
||||||
|
// credential below still carries the full `id()`.
|
||||||
|
let ident_id = IdentId::new(hex::encode(ident.public_key().as_ref()));
|
||||||
let mls_identity = MlsIdentityProvider::new(ident);
|
let mls_identity = MlsIdentityProvider::new(ident);
|
||||||
let mls_provider = MlsEphemeralPqProvider::new().map_err(ChatError::generic)?;
|
let mls_provider = MlsEphemeralPqProvider::new().map_err(ChatError::generic)?;
|
||||||
let causal = CausalHistoryStore::new();
|
let causal = CausalHistoryStore::new();
|
||||||
@ -153,19 +157,12 @@ impl<'a, S: ExternalServices + 'static> Core<S> {
|
|||||||
&self.services.store
|
&self.services.store
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The account → device directory (our account store). Used to verify that a
|
|
||||||
/// received message's claimed account actually endorses the sending device
|
|
||||||
/// before the message is surfaced. Exposed as `RegistrationService`, whose
|
|
||||||
/// `AccountDirectory` supertrait provides `fetch`.
|
|
||||||
pub fn account_directory(&self) -> &S::RS {
|
|
||||||
&self.services.registry
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn identity(&self) -> &Identity {
|
pub fn identity(&self) -> &Identity {
|
||||||
&self.services.identity
|
&self.services.identity
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the unique identifier associated with the account
|
/// The signer id this core receives InboxV2 invites under — the hex of the
|
||||||
|
/// signer's verifying key.
|
||||||
pub fn ident_id(&'a self) -> IdentIdRef<'a> {
|
pub fn ident_id(&'a self) -> IdentIdRef<'a> {
|
||||||
self.pq_inbox.ident_id()
|
self.pq_inbox.ident_id()
|
||||||
}
|
}
|
||||||
@ -177,14 +174,6 @@ impl<'a, S: ExternalServices + 'static> Core<S> {
|
|||||||
self.pq_inbox.register(&mut self.services)
|
self.pq_inbox.register(&mut self.services)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish this installation's device key into the account → device
|
|
||||||
/// directory, so inviters can resolve this account to its device(s). Pairs
|
|
||||||
/// with [`register_keypackage`](Self::register_keypackage); call both after
|
|
||||||
/// provisioning so the account is fully discoverable.
|
|
||||||
pub fn register_account_bundle(&mut self) -> Result<(), ChatError> {
|
|
||||||
self.pq_inbox.publish_device_bundle(&mut self.services)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn installation_name(&self) -> &str {
|
pub fn installation_name(&self) -> &str {
|
||||||
self.services.identity.get_name()
|
self.services.identity.get_name()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ mod identity;
|
|||||||
mod mls_provider;
|
mod mls_provider;
|
||||||
|
|
||||||
use chat_proto::logoschat::envelope::EnvelopeV1;
|
use chat_proto::logoschat::envelope::EnvelopeV1;
|
||||||
use crypto::Ed25519VerifyingKey;
|
|
||||||
use de_mls::protos::de_mls::messages::v1::MemberWelcome;
|
use de_mls::protos::de_mls::messages::v1::MemberWelcome;
|
||||||
use openmls::prelude::tls_codec::Serialize;
|
use openmls::prelude::tls_codec::Serialize;
|
||||||
use openmls::prelude::*;
|
use openmls::prelude::*;
|
||||||
@ -23,11 +22,7 @@ use crate::conversation::GroupV2Convo;
|
|||||||
use crate::conversation::Identified as _;
|
use crate::conversation::Identified as _;
|
||||||
use crate::service_context::{ExternalServices, ServiceContext};
|
use crate::service_context::{ExternalServices, ServiceContext};
|
||||||
use crate::utils::{blake2b_hex, hash_size};
|
use crate::utils::{blake2b_hex, hash_size};
|
||||||
use crate::{
|
use crate::{AddressedEnvelope, IdentId, IdentIdRef, IdentityProvider};
|
||||||
AccountAuthority, AccountDirectory, AddressedEnvelope, SignedDeviceBundle,
|
|
||||||
encode_bundle_payload,
|
|
||||||
};
|
|
||||||
use crate::{IdentId, IdentIdRef, IdentityProvider};
|
|
||||||
|
|
||||||
// Downgraded from MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519 until demls accepts an external provider
|
// Downgraded from MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519 until demls accepts an external provider
|
||||||
pub(crate) const CIPHER_SUITE: Ciphersuite =
|
pub(crate) const CIPHER_SUITE: Ciphersuite =
|
||||||
@ -53,33 +48,34 @@ pub trait MlsProvider: OpenMlsProvider {
|
|||||||
) -> Result<(), ChatError>;
|
) -> Result<(), ChatError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deliver a de-mls welcome to `account_id` over its InboxV2 1-1 channel.
|
/// Deliver a de-mls welcome to `signer_id` over its InboxV2 1-1 channel.
|
||||||
/// Function mirroring the GroupV1 `invite_user` path, but carrying a de-mls `MemberWelcome`.
|
/// Function mirroring the GroupV1 `invite_user` path, but carrying a de-mls `MemberWelcome`.
|
||||||
pub fn invite_user_v2<DS: DeliveryService>(
|
pub fn invite_user_v2<DS: DeliveryService>(
|
||||||
ds: &mut DS,
|
ds: &mut DS,
|
||||||
account_id: IdentIdRef,
|
signer_id: IdentIdRef,
|
||||||
welcome: &MemberWelcome,
|
welcome: &MemberWelcome,
|
||||||
) -> Result<(), ChatError> {
|
) -> Result<(), ChatError> {
|
||||||
let frame = InboxV2Frame {
|
let frame = InboxV2Frame {
|
||||||
payload: Some(InviteType::GroupV2(welcome.encode_to_vec())),
|
payload: Some(InviteType::GroupV2(welcome.encode_to_vec())),
|
||||||
};
|
};
|
||||||
let envelope = EnvelopeV1 {
|
let envelope = EnvelopeV1 {
|
||||||
conversation_hint: conversation_id_for(account_id),
|
conversation_hint: conversation_id_for(signer_id),
|
||||||
salt: 0,
|
salt: 0,
|
||||||
payload: frame.encode_to_vec().into(),
|
payload: frame.encode_to_vec().into(),
|
||||||
};
|
};
|
||||||
ds.publish(AddressedEnvelope {
|
ds.publish(AddressedEnvelope {
|
||||||
delivery_address: delivery_address_for(account_id),
|
delivery_address: delivery_address_for(signer_id),
|
||||||
data: envelope.encode_to_vec(),
|
data: envelope.encode_to_vec(),
|
||||||
})
|
})
|
||||||
.map_err(ChatError::generic)
|
.map_err(ChatError::generic)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An PQ focused Conversation initializer.
|
/// A PQ focused Conversation initializer.
|
||||||
/// InboxV2 Incorporates an Account based identity system to support PQ based conversation protocols
|
/// InboxV2 is signer-scoped: it receives invites under this installation's
|
||||||
/// such as MLS.
|
/// signer id (the hex of the signer's verifying key), supporting PQ based
|
||||||
|
/// conversation protocols such as MLS.
|
||||||
pub struct InboxV2 {
|
pub struct InboxV2 {
|
||||||
// Account_id field is an owned value, so it can be returned via reference.
|
// Owned so it can be returned via reference.
|
||||||
ident_id: IdentId,
|
ident_id: IdentId,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,78 +201,6 @@ impl InboxV2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publishing the account → device bundle needs the account key, so this method
|
|
||||||
// is available only when the registry also implements `AccountDirectory`. 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.
|
|
||||||
impl InboxV2 {
|
|
||||||
/// Add this installation's device key to the account's directory bundle.
|
|
||||||
///
|
|
||||||
/// Fetches the current (verified) device set, adds this device if absent,
|
|
||||||
/// bumps the lamport, re-signs with the account key, and publishes. Safe to
|
|
||||||
/// call repeatedly — an unchanged set is simply re-published, which also
|
|
||||||
/// refreshes the server's retention clock.
|
|
||||||
pub fn publish_device_bundle<S: ExternalServices>(
|
|
||||||
&self,
|
|
||||||
cx: &mut ServiceContext<S>,
|
|
||||||
) -> Result<(), ChatError> {
|
|
||||||
// On testnet `mls_identity` doubles as the `AccountAuthority` — the
|
|
||||||
// account key is the installation's own key.
|
|
||||||
let authority = &cx.mls_identity;
|
|
||||||
|
|
||||||
let account_pub = AccountAuthority::account_pub(authority).clone();
|
|
||||||
let device_key = cx.mls_identity.public_key().clone();
|
|
||||||
let device_hex = hex::encode(device_key.as_ref());
|
|
||||||
|
|
||||||
// Start from the devices already registered so other installations of
|
|
||||||
// this account are preserved across the upsert.
|
|
||||||
let existing = cx
|
|
||||||
.registry
|
|
||||||
.fetch(&account_pub)
|
|
||||||
.map_err(|e| ChatError::Generic(e.to_string()))?;
|
|
||||||
let (mut devices, next_lamport) = match existing {
|
|
||||||
Some(set) => {
|
|
||||||
let mut keys = Vec::with_capacity(set.devices.len() + 1);
|
|
||||||
for hex_id in &set.devices {
|
|
||||||
let bytes: [u8; 32] = hex::decode(hex_id)
|
|
||||||
.ok()
|
|
||||||
.and_then(|b| b.try_into().ok())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
ChatError::Generic("directory returned a malformed device id".into())
|
|
||||||
})?;
|
|
||||||
let key = Ed25519VerifyingKey::from_bytes(&bytes).map_err(|_| {
|
|
||||||
ChatError::Generic("directory returned a malformed device key".into())
|
|
||||||
})?;
|
|
||||||
keys.push(key);
|
|
||||||
}
|
|
||||||
(keys, set.lamport + 1)
|
|
||||||
}
|
|
||||||
None => (Vec::new(), 0),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !devices
|
|
||||||
.iter()
|
|
||||||
.any(|d| hex::encode(d.as_ref()) == device_hex)
|
|
||||||
{
|
|
||||||
devices.push(device_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
let payload = encode_bundle_payload(next_lamport, &devices);
|
|
||||||
let signature = AccountAuthority::sign(authority, &payload)
|
|
||||||
.map_err(|e| ChatError::Generic(e.to_string()))?;
|
|
||||||
let bundle = SignedDeviceBundle {
|
|
||||||
account_pub,
|
|
||||||
payload,
|
|
||||||
signature,
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.registry
|
|
||||||
.publish(&bundle)
|
|
||||||
.map_err(|e| ChatError::Generic(e.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Message)]
|
#[derive(Clone, PartialEq, Message)]
|
||||||
pub struct InboxV2Frame {
|
pub struct InboxV2Frame {
|
||||||
#[prost(oneof = "InviteType", tags = "1, 2")]
|
#[prost(oneof = "InviteType", tags = "1, 2")]
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use crypto::{Ed25519Signature, Ed25519VerifyingKey};
|
|
||||||
use openmls::credentials::{BasicCredential, CredentialWithKey};
|
use openmls::credentials::{BasicCredential, CredentialWithKey};
|
||||||
use openmls_traits::{
|
use openmls_traits::{
|
||||||
signatures::{Signer, SignerError},
|
signatures::{Signer, SignerError},
|
||||||
@ -8,7 +7,6 @@ use openmls_traits::{
|
|||||||
};
|
};
|
||||||
use shared_traits::IdentIdRef;
|
use shared_traits::IdentIdRef;
|
||||||
|
|
||||||
use crate::AccountAuthority;
|
|
||||||
use crate::IdentityProvider;
|
use crate::IdentityProvider;
|
||||||
|
|
||||||
/// A Wrapper for an IdentityProvider which provides MLS specific functionality
|
/// A Wrapper for an IdentityProvider which provides MLS specific functionality
|
||||||
@ -57,22 +55,6 @@ impl<T: IdentityProvider> IdentityProvider for MlsIdentityProvider<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// On testnet the installation identity is also the account authority: the
|
|
||||||
// account key is the installation's own key, so the device bundle is signed and
|
|
||||||
// addressed under `public_key()`. A real deployment injects a separate
|
|
||||||
// `AccountAuthority` (wallet/enclave) whose key custody lives outside libchat.
|
|
||||||
impl<T: IdentityProvider> AccountAuthority for MlsIdentityProvider<T> {
|
|
||||||
type Error = std::convert::Infallible;
|
|
||||||
|
|
||||||
fn account_pub(&self) -> &Ed25519VerifyingKey {
|
|
||||||
self.public_key()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign(&self, payload: &[u8]) -> Result<Ed25519Signature, Self::Error> {
|
|
||||||
Ok(IdentityProvider::sign(self, payload))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement Signer directly for MlsIdentityProvider, so that openmls Signer contstraint
|
// Implement Signer directly for MlsIdentityProvider, so that openmls Signer contstraint
|
||||||
// does not leave the module.
|
// does not leave the module.
|
||||||
impl<T: IdentityProvider> Signer for MlsIdentityProvider<T> {
|
impl<T: IdentityProvider> Signer for MlsIdentityProvider<T> {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
mod account_directory;
|
|
||||||
mod causal_history;
|
mod causal_history;
|
||||||
mod conversation;
|
mod conversation;
|
||||||
mod core;
|
mod core;
|
||||||
@ -13,11 +12,6 @@ mod service_traits;
|
|||||||
mod types;
|
mod types;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use account_directory::{
|
|
||||||
AccountAuthority, AccountDirectory, 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};
|
pub use causal_history::{Frontier, MissingMessage};
|
||||||
pub use chat_sqlite::ChatStorage;
|
pub use chat_sqlite::ChatStorage;
|
||||||
pub use chat_sqlite::StorageConfig;
|
pub use chat_sqlite::StorageConfig;
|
||||||
|
|||||||
@ -49,10 +49,8 @@ pub(crate) struct ServiceContext<S: ExternalServices> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_support {
|
mod test_support {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::account_directory::{AccountDirectory, DeviceSet, SignedDeviceBundle};
|
|
||||||
use crate::types::AddressedEnvelope;
|
use crate::types::AddressedEnvelope;
|
||||||
use crate::{ChatError, IdentityProvider};
|
use crate::{ChatError, IdentityProvider};
|
||||||
use crypto::Ed25519VerifyingKey;
|
|
||||||
|
|
||||||
/// Delivery double that drops every payload.
|
/// Delivery double that drops every payload.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -81,32 +79,11 @@ mod test_support {
|
|||||||
&mut self,
|
&mut self,
|
||||||
_identity: &dyn IdentityProvider,
|
_identity: &dyn IdentityProvider,
|
||||||
_key_bundle: Vec<u8>,
|
_key_bundle: Vec<u8>,
|
||||||
) -> Result<(), <Self as RegistrationService>::Error> {
|
) -> Result<(), Self::Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn retrieve(
|
fn retrieve(&self, _device_id: &str) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||||
&self,
|
|
||||||
_device_id: &str,
|
|
||||||
) -> Result<Option<Vec<u8>>, <Self as RegistrationService>::Error> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AccountDirectory for NoopRegistration {
|
|
||||||
type Error = std::convert::Infallible;
|
|
||||||
|
|
||||||
fn publish(
|
|
||||||
&mut self,
|
|
||||||
_bundle: &SignedDeviceBundle,
|
|
||||||
) -> Result<(), <Self as AccountDirectory>::Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch(
|
|
||||||
&self,
|
|
||||||
_account: &Ed25519VerifyingKey,
|
|
||||||
) -> Result<Option<DeviceSet>, <Self as AccountDirectory>::Error> {
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use std::{
|
|||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{AccountDirectory, ConversationId, types::AddressedEnvelope};
|
use crate::{ConversationId, types::AddressedEnvelope};
|
||||||
|
|
||||||
/// A Delivery service is responsible for payload transport.
|
/// A Delivery service is responsible for payload transport.
|
||||||
/// This interface allows Conversations to send payloads on the wire as well as
|
/// This interface allows Conversations to send payloads on the wire as well as
|
||||||
@ -29,25 +29,14 @@ pub trait DeliveryService: Debug {
|
|||||||
/// implementations that need to authenticate the submission — e.g. a network
|
/// implementations that need to authenticate the submission — e.g. a network
|
||||||
/// service that verifies the bundle is signed by the correct account — can
|
/// service that verifies the bundle is signed by the correct account — can
|
||||||
/// sign or attest with the caller's key material.
|
/// sign or attest with the caller's key material.
|
||||||
///
|
pub trait RegistrationService: Debug {
|
||||||
/// 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
|
|
||||||
// `Self::Error` is ambiguous between the two traits' associated types.
|
|
||||||
type Error: Display + Debug;
|
type Error: Display + Debug;
|
||||||
fn register(
|
fn register(
|
||||||
&mut self,
|
&mut self,
|
||||||
identity: &dyn IdentityProvider,
|
identity: &dyn IdentityProvider,
|
||||||
key_bundle: Vec<u8>,
|
key_bundle: Vec<u8>,
|
||||||
) -> Result<(), <Self as RegistrationService>::Error>;
|
) -> Result<(), Self::Error>;
|
||||||
fn retrieve(
|
fn retrieve(&self, device_id: &str) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||||
&self,
|
|
||||||
device_id: &str,
|
|
||||||
) -> Result<Option<Vec<u8>>, <Self as RegistrationService>::Error>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read-only view of a contact registry. Not part of the public API.
|
/// Read-only view of a contact registry. Not part of the public API.
|
||||||
@ -58,9 +47,7 @@ pub trait KeyPackageProvider: Debug {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T: RegistrationService> KeyPackageProvider for T {
|
impl<T: RegistrationService> KeyPackageProvider for T {
|
||||||
// Disambiguate: `RegistrationService` now has `AccountDirectory` as a
|
type Error = T::Error;
|
||||||
// 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> {
|
fn retrieve(&self, device_id: &str) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||||
RegistrationService::retrieve(self, device_id)
|
RegistrationService::retrieve(self, device_id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,8 +10,8 @@ edition = "2024"
|
|||||||
# Workspace dependencies (sorted)
|
# Workspace dependencies (sorted)
|
||||||
chat-sqlite = { workspace = true }
|
chat-sqlite = { workspace = true }
|
||||||
components = { workspace = true }
|
components = { workspace = true }
|
||||||
|
crypto = { workspace = true }
|
||||||
libchat = { workspace = true }
|
libchat = { workspace = true }
|
||||||
logos-account = { workspace = true, features = ["dev"]}
|
|
||||||
shared-traits = { workspace = true }
|
shared-traits = { workspace = true }
|
||||||
|
|
||||||
# External dependencies (sorted)
|
# External dependencies (sorted)
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
mod test_client;
|
mod test_client;
|
||||||
|
mod test_ident;
|
||||||
mod wakeup;
|
mod wakeup;
|
||||||
|
|
||||||
pub use test_client::TestHarness;
|
pub use test_client::TestHarness;
|
||||||
|
pub use test_ident::TestIdent;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
use crate::test_ident::TestIdent;
|
||||||
use libchat::{ConversationId, Core, IdentityProvider, PayloadOutcome};
|
use libchat::{ConversationId, Core, IdentityProvider, PayloadOutcome};
|
||||||
use logos_account::TestLogosAccount;
|
|
||||||
use shared_traits::IdentId;
|
use shared_traits::IdentId;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
@ -21,14 +21,8 @@ const RAYA: usize = 1;
|
|||||||
const PAX: usize = 2;
|
const PAX: usize = 2;
|
||||||
const MIRA: usize = 3;
|
const MIRA: usize = 3;
|
||||||
|
|
||||||
// type ClientType = CoreClient<TestLogosAccount, LocalBroadcaster, EphemeralRegistry, WP, MemStore>;
|
// type ClientType = CoreClient<TestIdent, LocalBroadcaster, EphemeralRegistry, WP, MemStore>;
|
||||||
type ClientType = Core<(
|
type ClientType = Core<(TestIdent, LocalBroadcaster, EphemeralRegistry, WP, MemStore)>;
|
||||||
TestLogosAccount,
|
|
||||||
LocalBroadcaster,
|
|
||||||
EphemeralRegistry,
|
|
||||||
WP,
|
|
||||||
MemStore,
|
|
||||||
)>;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ReceivedMessage<T> {
|
pub struct ReceivedMessage<T> {
|
||||||
@ -151,7 +145,7 @@ impl<const N: usize> TestHarness<N> {
|
|||||||
|
|
||||||
for i in 0..N {
|
for i in 0..N {
|
||||||
let wp = ws.new_provider(i);
|
let wp = ws.new_provider(i);
|
||||||
let ident = TestLogosAccount::new(Self::names(i));
|
let ident = TestIdent::new(Self::names(i));
|
||||||
|
|
||||||
addresses.insert(i, ident.id().clone());
|
addresses.insert(i, ident.id().clone());
|
||||||
let core_client =
|
let core_client =
|
||||||
|
|||||||
41
core/integration_tests_core/src/test_ident.rs
Normal file
41
core/integration_tests_core/src/test_ident.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
use crypto::{Ed25519SigningKey, Ed25519VerifyingKey};
|
||||||
|
use libchat::IdentityProvider;
|
||||||
|
use shared_traits::{IdentId, IdentIdRef};
|
||||||
|
|
||||||
|
/// Test identity with a fixed, human-readable id ("saro"). Stands in for a
|
||||||
|
/// device signer so core tests can address peers by name.
|
||||||
|
pub struct TestIdent {
|
||||||
|
id: IdentId,
|
||||||
|
signing_key: Ed25519SigningKey,
|
||||||
|
verifying_key: Ed25519VerifyingKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestIdent {
|
||||||
|
pub fn new(explicit_id: impl Into<String>) -> Self {
|
||||||
|
let signing_key = Ed25519SigningKey::generate();
|
||||||
|
let verifying_key = signing_key.verifying_key();
|
||||||
|
Self {
|
||||||
|
id: IdentId::new(explicit_id.into()),
|
||||||
|
signing_key,
|
||||||
|
verifying_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdentityProvider for TestIdent {
|
||||||
|
fn id(&self) -> IdentIdRef<'_> {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_name(&self) -> String {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn public_key(&self) -> &Ed25519VerifyingKey {
|
||||||
|
&self.verifying_key
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign(&self, payload: &[u8]) -> crypto::Ed25519Signature {
|
||||||
|
self.signing_key.sign(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,8 +7,8 @@
|
|||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use components::{EphemeralRegistry, LocalBroadcaster, MemStore};
|
use components::{EphemeralRegistry, LocalBroadcaster, MemStore};
|
||||||
|
use integration_tests_core::TestIdent;
|
||||||
use libchat::{Core, MissingMessage, WakeupService};
|
use libchat::{Core, MissingMessage, WakeupService};
|
||||||
use logos_account::TestLogosAccount;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct NoopWakeupService {}
|
struct NoopWakeupService {}
|
||||||
@ -18,7 +18,7 @@ impl WakeupService for NoopWakeupService {
|
|||||||
|
|
||||||
struct Client {
|
struct Client {
|
||||||
inner: Core<(
|
inner: Core<(
|
||||||
TestLogosAccount,
|
TestIdent,
|
||||||
LocalBroadcaster,
|
LocalBroadcaster,
|
||||||
EphemeralRegistry,
|
EphemeralRegistry,
|
||||||
NoopWakeupService,
|
NoopWakeupService,
|
||||||
@ -29,7 +29,7 @@ struct Client {
|
|||||||
impl Client {
|
impl Client {
|
||||||
fn init(
|
fn init(
|
||||||
core: Core<(
|
core: Core<(
|
||||||
TestLogosAccount,
|
TestIdent,
|
||||||
LocalBroadcaster,
|
LocalBroadcaster,
|
||||||
EphemeralRegistry,
|
EphemeralRegistry,
|
||||||
NoopWakeupService,
|
NoopWakeupService,
|
||||||
@ -60,7 +60,7 @@ impl Client {
|
|||||||
|
|
||||||
impl Deref for Client {
|
impl Deref for Client {
|
||||||
type Target = Core<(
|
type Target = Core<(
|
||||||
TestLogosAccount,
|
TestIdent,
|
||||||
LocalBroadcaster,
|
LocalBroadcaster,
|
||||||
EphemeralRegistry,
|
EphemeralRegistry,
|
||||||
NoopWakeupService,
|
NoopWakeupService,
|
||||||
@ -82,9 +82,9 @@ fn missing_group_message_is_detected() {
|
|||||||
let ds = LocalBroadcaster::new();
|
let ds = LocalBroadcaster::new();
|
||||||
let rs = EphemeralRegistry::new();
|
let rs = EphemeralRegistry::new();
|
||||||
|
|
||||||
let saro_account = TestLogosAccount::new("saro");
|
let saro_ident = TestIdent::new("saro");
|
||||||
let saro_ctx = Core::new_with_name(
|
let saro_ctx = Core::new_with_name(
|
||||||
saro_account,
|
saro_ident,
|
||||||
ds.new_consumer(),
|
ds.new_consumer(),
|
||||||
rs.clone(),
|
rs.clone(),
|
||||||
NoopWakeupService {},
|
NoopWakeupService {},
|
||||||
@ -92,9 +92,9 @@ fn missing_group_message_is_detected() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let raya_account = TestLogosAccount::new("raya");
|
let raya_ident = TestIdent::new("raya");
|
||||||
let raya_ctx = Core::new_with_name(
|
let raya_ctx = Core::new_with_name(
|
||||||
raya_account,
|
raya_ident,
|
||||||
ds.clone(),
|
ds.clone(),
|
||||||
rs.clone(),
|
rs.clone(),
|
||||||
NoopWakeupService {},
|
NoopWakeupService {},
|
||||||
@ -135,9 +135,11 @@ fn missing_group_message_is_detected() {
|
|||||||
!missing[0].frontier.message_id().is_empty(),
|
!missing[0].frontier.message_id().is_empty(),
|
||||||
"the missing message must be identified"
|
"the missing message must be identified"
|
||||||
);
|
);
|
||||||
|
// The causal sender hint carries the sender's identity id ("saro"), not
|
||||||
|
// the signer id the inbox and registry key on.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
missing[0].frontier.sender_id(),
|
missing[0].frontier.sender_id(),
|
||||||
saro.ident_id().as_str(),
|
"saro",
|
||||||
"missing-message sender hint should attribute to Saro"
|
"missing-message sender hint should attribute to Saro"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use chat_sqlite::{ChatStorage, StorageConfig};
|
use chat_sqlite::{ChatStorage, StorageConfig};
|
||||||
|
use integration_tests_core::TestIdent;
|
||||||
use libchat::{ConversationClass, Core, Introduction, PayloadOutcome, WakeupService};
|
use libchat::{ConversationClass, Core, Introduction, PayloadOutcome, WakeupService};
|
||||||
use logos_account::TestLogosAccount;
|
|
||||||
use storage::{ConversationStore, IdentityStore};
|
use storage::{ConversationStore, IdentityStore};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ impl WakeupService for NoopWakeupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PrivateCore = Core<(
|
type PrivateCore = Core<(
|
||||||
TestLogosAccount,
|
TestIdent,
|
||||||
LocalBroadcaster,
|
LocalBroadcaster,
|
||||||
EphemeralRegistry,
|
EphemeralRegistry,
|
||||||
NoopWakeupService,
|
NoopWakeupService,
|
||||||
@ -62,18 +62,18 @@ fn ctx_integration() {
|
|||||||
let ds = LocalBroadcaster::new();
|
let ds = LocalBroadcaster::new();
|
||||||
let rs = EphemeralRegistry::new();
|
let rs = EphemeralRegistry::new();
|
||||||
|
|
||||||
let saro_account = TestLogosAccount::new("saro");
|
let saro_ident = TestIdent::new("saro");
|
||||||
let mut saro = Core::new_with_name(
|
let mut saro = Core::new_with_name(
|
||||||
saro_account,
|
saro_ident,
|
||||||
ds.clone(),
|
ds.clone(),
|
||||||
rs.clone(),
|
rs.clone(),
|
||||||
NoopWakeupService {},
|
NoopWakeupService {},
|
||||||
ChatStorage::in_memory(),
|
ChatStorage::in_memory(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let raya_account = TestLogosAccount::new("raya");
|
let raya_ident = TestIdent::new("raya");
|
||||||
let mut raya = Core::new_with_name(
|
let mut raya = Core::new_with_name(
|
||||||
raya_account,
|
raya_ident,
|
||||||
ds,
|
ds,
|
||||||
rs,
|
rs,
|
||||||
NoopWakeupService {},
|
NoopWakeupService {},
|
||||||
@ -121,8 +121,8 @@ fn identity_persistence() {
|
|||||||
let ds = LocalBroadcaster::new();
|
let ds = LocalBroadcaster::new();
|
||||||
let rs = EphemeralRegistry::new();
|
let rs = EphemeralRegistry::new();
|
||||||
let store1 = ChatStorage::new(StorageConfig::InMemory).unwrap();
|
let store1 = ChatStorage::new(StorageConfig::InMemory).unwrap();
|
||||||
let alice_account = TestLogosAccount::new("alice");
|
let alice_ident = TestIdent::new("alice");
|
||||||
let ctx1 = Core::new_with_name(alice_account, ds, rs, NoopWakeupService {}, store1).unwrap();
|
let ctx1 = Core::new_with_name(alice_ident, ds, rs, NoopWakeupService {}, store1).unwrap();
|
||||||
let pubkey1 = ctx1.identity().public_key();
|
let pubkey1 = ctx1.identity().public_key();
|
||||||
let name1 = ctx1.installation_name().to_string();
|
let name1 = ctx1.installation_name().to_string();
|
||||||
|
|
||||||
@ -141,8 +141,8 @@ fn open_persists_new_identity() {
|
|||||||
let ds = LocalBroadcaster::new();
|
let ds = LocalBroadcaster::new();
|
||||||
let rs = EphemeralRegistry::new();
|
let rs = EphemeralRegistry::new();
|
||||||
let store = ChatStorage::new(StorageConfig::File(db_path.clone())).unwrap();
|
let store = ChatStorage::new(StorageConfig::File(db_path.clone())).unwrap();
|
||||||
let alice_account = TestLogosAccount::new("alice");
|
let alice_ident = TestIdent::new("alice");
|
||||||
let core = Core::new_from_store(alice_account, ds, rs, NoopWakeupService {}, store).unwrap();
|
let core = Core::new_from_store(alice_ident, ds, rs, NoopWakeupService {}, store).unwrap();
|
||||||
let pubkey = core.identity().public_key();
|
let pubkey = core.identity().public_key();
|
||||||
drop(core);
|
drop(core);
|
||||||
|
|
||||||
@ -157,18 +157,18 @@ fn open_persists_new_identity() {
|
|||||||
fn conversation_metadata_persistence() {
|
fn conversation_metadata_persistence() {
|
||||||
let ds = LocalBroadcaster::new();
|
let ds = LocalBroadcaster::new();
|
||||||
let rs = EphemeralRegistry::new();
|
let rs = EphemeralRegistry::new();
|
||||||
let alice_account = TestLogosAccount::new("alice");
|
let alice_ident = TestIdent::new("alice");
|
||||||
let mut alice = Core::new_with_name(
|
let mut alice = Core::new_with_name(
|
||||||
alice_account,
|
alice_ident,
|
||||||
ds.clone(),
|
ds.clone(),
|
||||||
rs.clone(),
|
rs.clone(),
|
||||||
NoopWakeupService {},
|
NoopWakeupService {},
|
||||||
ChatStorage::in_memory(),
|
ChatStorage::in_memory(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let bob_account = TestLogosAccount::new("bob");
|
let bob_ident = TestIdent::new("bob");
|
||||||
let mut bob = Core::new_with_name(
|
let mut bob = Core::new_with_name(
|
||||||
bob_account,
|
bob_ident,
|
||||||
ds,
|
ds,
|
||||||
rs,
|
rs,
|
||||||
NoopWakeupService {},
|
NoopWakeupService {},
|
||||||
@ -198,18 +198,18 @@ fn conversation_metadata_persistence() {
|
|||||||
fn conversation_full_flow() {
|
fn conversation_full_flow() {
|
||||||
let ds = LocalBroadcaster::new();
|
let ds = LocalBroadcaster::new();
|
||||||
let rs = EphemeralRegistry::new();
|
let rs = EphemeralRegistry::new();
|
||||||
let alice_account = TestLogosAccount::new("alice");
|
let alice_ident = TestIdent::new("alice");
|
||||||
let mut alice = Core::new_with_name(
|
let mut alice = Core::new_with_name(
|
||||||
alice_account,
|
alice_ident,
|
||||||
ds.clone(),
|
ds.clone(),
|
||||||
rs.clone(),
|
rs.clone(),
|
||||||
NoopWakeupService {},
|
NoopWakeupService {},
|
||||||
ChatStorage::in_memory(),
|
ChatStorage::in_memory(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let bob_account = TestLogosAccount::new("bob");
|
let bob_ident = TestIdent::new("bob");
|
||||||
let mut bob = Core::new_with_name(
|
let mut bob = Core::new_with_name(
|
||||||
bob_account,
|
bob_ident,
|
||||||
ds,
|
ds,
|
||||||
rs,
|
rs,
|
||||||
NoopWakeupService {},
|
NoopWakeupService {},
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use components::EphemeralRegistry;
|
use components::EphemeralRegistry;
|
||||||
|
use logos_account::TestLogosAccount;
|
||||||
use logos_chat::{ChatClientBuilder, Event, InProcessDelivery, MessageBus};
|
use logos_chat::{ChatClientBuilder, Event, InProcessDelivery, MessageBus};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -6,13 +7,13 @@ fn main() {
|
|||||||
let bus = MessageBus::default();
|
let bus = MessageBus::default();
|
||||||
let reg = EphemeralRegistry::new();
|
let reg = EphemeralRegistry::new();
|
||||||
|
|
||||||
let (mut saro, saro_events) = ChatClientBuilder::new()
|
let (mut saro, saro_events) = ChatClientBuilder::new(TestLogosAccount::new().address())
|
||||||
.transport(InProcessDelivery::new(bus.clone()))
|
.transport(InProcessDelivery::new(bus.clone()))
|
||||||
.registration(reg.clone())
|
.registration(reg.clone())
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (mut raya, raya_events) = ChatClientBuilder::new()
|
let (mut raya, raya_events) = ChatClientBuilder::new(TestLogosAccount::new().address())
|
||||||
.transport(InProcessDelivery::new(bus))
|
.transport(InProcessDelivery::new(bus))
|
||||||
.registration(reg)
|
.registration(reg)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use components::EphemeralRegistry;
|
use components::EphemeralRegistry;
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use libchat::{ChatError, ChatStorage, IdentityProvider, RegistrationService, StorageConfig};
|
use libchat::{ChatError, ChatStorage, RegistrationService, StorageConfig};
|
||||||
|
use logos_account::AccountDirectory;
|
||||||
use storage::ChatStore;
|
use storage::ChatStore;
|
||||||
|
|
||||||
use crate::Transport;
|
use crate::Transport;
|
||||||
@ -15,15 +16,22 @@ pub struct Unset;
|
|||||||
|
|
||||||
pub struct ChatClientBuilder<I = Unset, T = Unset, R = Unset, S = Unset> {
|
pub struct ChatClientBuilder<I = Unset, T = Unset, R = Unset, S = Unset> {
|
||||||
ident: I,
|
ident: I,
|
||||||
|
account: String,
|
||||||
transport: T,
|
transport: T,
|
||||||
registration: R,
|
registration: R,
|
||||||
storage: S,
|
storage: S,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ChatClientBuilder {
|
impl ChatClientBuilder {
|
||||||
fn default() -> Self {
|
/// Every client acts for an account, so the builder starts from its
|
||||||
|
/// address. It becomes the client's shareable address
|
||||||
|
/// ([`ChatClient::addr`]) and the account claim in the wire credential;
|
||||||
|
/// the account must endorse the signer in the directory for peers to
|
||||||
|
/// verify that claim.
|
||||||
|
pub fn new(account: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ident: Unset,
|
ident: Unset,
|
||||||
|
account: account.into(),
|
||||||
transport: Unset,
|
transport: Unset,
|
||||||
registration: Unset,
|
registration: Unset,
|
||||||
storage: Unset,
|
storage: Unset,
|
||||||
@ -31,16 +39,11 @@ impl Default for ChatClientBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatClientBuilder {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I, T, R, S> ChatClientBuilder<I, T, R, S> {
|
impl<I, T, R, S> ChatClientBuilder<I, T, R, S> {
|
||||||
pub fn ident<NI>(self, ident: NI) -> ChatClientBuilder<NI, T, R, S> {
|
pub fn ident(self, ident: DelegateSigner) -> ChatClientBuilder<DelegateSigner, T, R, S> {
|
||||||
ChatClientBuilder {
|
ChatClientBuilder {
|
||||||
ident,
|
ident,
|
||||||
|
account: self.account,
|
||||||
transport: self.transport,
|
transport: self.transport,
|
||||||
registration: self.registration,
|
registration: self.registration,
|
||||||
storage: self.storage,
|
storage: self.storage,
|
||||||
@ -50,6 +53,7 @@ impl<I, T, R, S> ChatClientBuilder<I, T, R, S> {
|
|||||||
pub fn transport<NT>(self, transport: NT) -> ChatClientBuilder<I, NT, R, S> {
|
pub fn transport<NT>(self, transport: NT) -> ChatClientBuilder<I, NT, R, S> {
|
||||||
ChatClientBuilder {
|
ChatClientBuilder {
|
||||||
ident: self.ident,
|
ident: self.ident,
|
||||||
|
account: self.account,
|
||||||
transport,
|
transport,
|
||||||
registration: self.registration,
|
registration: self.registration,
|
||||||
storage: self.storage,
|
storage: self.storage,
|
||||||
@ -59,6 +63,7 @@ impl<I, T, R, S> ChatClientBuilder<I, T, R, S> {
|
|||||||
pub fn registration<NR>(self, registration: NR) -> ChatClientBuilder<I, T, NR, S> {
|
pub fn registration<NR>(self, registration: NR) -> ChatClientBuilder<I, T, NR, S> {
|
||||||
ChatClientBuilder {
|
ChatClientBuilder {
|
||||||
ident: self.ident,
|
ident: self.ident,
|
||||||
|
account: self.account,
|
||||||
transport: self.transport,
|
transport: self.transport,
|
||||||
registration,
|
registration,
|
||||||
storage: self.storage,
|
storage: self.storage,
|
||||||
@ -68,6 +73,7 @@ impl<I, T, R, S> ChatClientBuilder<I, T, R, S> {
|
|||||||
pub fn storage<NS>(self, storage: NS) -> ChatClientBuilder<I, T, R, NS> {
|
pub fn storage<NS>(self, storage: NS) -> ChatClientBuilder<I, T, R, NS> {
|
||||||
ChatClientBuilder {
|
ChatClientBuilder {
|
||||||
ident: self.ident,
|
ident: self.ident,
|
||||||
|
account: self.account,
|
||||||
transport: self.transport,
|
transport: self.transport,
|
||||||
registration: self.registration,
|
registration: self.registration,
|
||||||
storage,
|
storage,
|
||||||
@ -81,6 +87,7 @@ impl<I, T, R, S> ChatClientBuilder<I, T, R, S> {
|
|||||||
|
|
||||||
ChatClientBuilder {
|
ChatClientBuilder {
|
||||||
ident: self.ident,
|
ident: self.ident,
|
||||||
|
account: self.account,
|
||||||
transport: self.transport,
|
transport: self.transport,
|
||||||
registration: self.registration,
|
registration: self.registration,
|
||||||
storage,
|
storage,
|
||||||
@ -88,26 +95,32 @@ impl<I, T, R, S> ChatClientBuilder<I, T, R, S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Built<I, T, R, S> = Result<(ChatClient<I, T, R, S>, Receiver<Event>), ClientError>;
|
type Built<T, R, S> = Result<(ChatClient<T, R, S>, Receiver<Event>), ClientError>;
|
||||||
|
|
||||||
// All four explicitly provided.
|
// All four explicitly provided.
|
||||||
impl<I, T, R, S> ChatClientBuilder<I, T, R, S>
|
impl<T, R, S> ChatClientBuilder<DelegateSigner, T, R, S>
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
T: Transport + Send + 'static,
|
T: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
pub fn build(self) -> Built<I, T, R, S> {
|
pub fn build(self) -> Built<T, R, S> {
|
||||||
ChatClient::new(self.ident, self.transport, self.registration, self.storage)
|
ChatClient::new(
|
||||||
|
self.ident,
|
||||||
|
self.account,
|
||||||
|
self.transport,
|
||||||
|
self.registration,
|
||||||
|
self.storage,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transport only; I, R, S all default.
|
// Transport only; I, R, S all default.
|
||||||
impl<T: Transport + Send + 'static> ChatClientBuilder<Unset, T, Unset, Unset> {
|
impl<T: Transport + Send + 'static> ChatClientBuilder<Unset, T, Unset, Unset> {
|
||||||
pub fn build(self) -> Built<DelegateSigner, T, EphemeralRegistry, ChatStorage> {
|
pub fn build(self) -> Built<T, EphemeralRegistry, ChatStorage> {
|
||||||
ChatClient::new(
|
ChatClient::new(
|
||||||
DelegateSigner::random(),
|
DelegateSigner::random(),
|
||||||
|
self.account,
|
||||||
self.transport,
|
self.transport,
|
||||||
EphemeralRegistry::new(),
|
EphemeralRegistry::new(),
|
||||||
ChatStorage::in_memory(),
|
ChatStorage::in_memory(),
|
||||||
@ -116,14 +129,14 @@ impl<T: Transport + Send + 'static> ChatClientBuilder<Unset, T, Unset, Unset> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// I and T; R and S default.
|
// I and T; R and S default.
|
||||||
impl<I, T> ChatClientBuilder<I, T, Unset, Unset>
|
impl<T> ChatClientBuilder<DelegateSigner, T, Unset, Unset>
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
T: Transport + Send + 'static,
|
T: Transport + Send + 'static,
|
||||||
{
|
{
|
||||||
pub fn build(self) -> Built<I, T, EphemeralRegistry, ChatStorage> {
|
pub fn build(self) -> Built<T, EphemeralRegistry, ChatStorage> {
|
||||||
ChatClient::new(
|
ChatClient::new(
|
||||||
self.ident,
|
self.ident,
|
||||||
|
self.account,
|
||||||
self.transport,
|
self.transport,
|
||||||
EphemeralRegistry::new(),
|
EphemeralRegistry::new(),
|
||||||
ChatStorage::in_memory(),
|
ChatStorage::in_memory(),
|
||||||
@ -135,11 +148,12 @@ where
|
|||||||
impl<T, R> ChatClientBuilder<Unset, T, R, Unset>
|
impl<T, R> ChatClientBuilder<Unset, T, R, Unset>
|
||||||
where
|
where
|
||||||
T: Transport + Send + 'static,
|
T: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
{
|
{
|
||||||
pub fn build(self) -> Built<DelegateSigner, T, R, ChatStorage> {
|
pub fn build(self) -> Built<T, R, ChatStorage> {
|
||||||
ChatClient::new(
|
ChatClient::new(
|
||||||
DelegateSigner::random(),
|
DelegateSigner::random(),
|
||||||
|
self.account,
|
||||||
self.transport,
|
self.transport,
|
||||||
self.registration,
|
self.registration,
|
||||||
ChatStorage::in_memory(),
|
ChatStorage::in_memory(),
|
||||||
@ -153,9 +167,10 @@ where
|
|||||||
T: Transport + Send + 'static,
|
T: Transport + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
pub fn build(self) -> Built<DelegateSigner, T, EphemeralRegistry, S> {
|
pub fn build(self) -> Built<T, EphemeralRegistry, S> {
|
||||||
ChatClient::new(
|
ChatClient::new(
|
||||||
DelegateSigner::random(),
|
DelegateSigner::random(),
|
||||||
|
self.account,
|
||||||
self.transport,
|
self.transport,
|
||||||
EphemeralRegistry::new(),
|
EphemeralRegistry::new(),
|
||||||
self.storage,
|
self.storage,
|
||||||
@ -164,15 +179,15 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// I, T, and R; S defaults.
|
// I, T, and R; S defaults.
|
||||||
impl<I, T, R> ChatClientBuilder<I, T, R, Unset>
|
impl<T, R> ChatClientBuilder<DelegateSigner, T, R, Unset>
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
T: Transport + Send + 'static,
|
T: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
{
|
{
|
||||||
pub fn build(self) -> Built<I, T, R, ChatStorage> {
|
pub fn build(self) -> Built<T, R, ChatStorage> {
|
||||||
ChatClient::new(
|
ChatClient::new(
|
||||||
self.ident,
|
self.ident,
|
||||||
|
self.account,
|
||||||
self.transport,
|
self.transport,
|
||||||
self.registration,
|
self.registration,
|
||||||
ChatStorage::in_memory(),
|
ChatStorage::in_memory(),
|
||||||
@ -184,12 +199,13 @@ where
|
|||||||
impl<T, R, S> ChatClientBuilder<Unset, T, R, S>
|
impl<T, R, S> ChatClientBuilder<Unset, T, R, S>
|
||||||
where
|
where
|
||||||
T: Transport + Send + 'static,
|
T: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
pub fn build(self) -> Built<DelegateSigner, T, R, S> {
|
pub fn build(self) -> Built<T, R, S> {
|
||||||
ChatClient::new(
|
ChatClient::new(
|
||||||
DelegateSigner::random(),
|
DelegateSigner::random(),
|
||||||
|
self.account,
|
||||||
self.transport,
|
self.transport,
|
||||||
self.registration,
|
self.registration,
|
||||||
self.storage,
|
self.storage,
|
||||||
@ -198,15 +214,15 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// I, T, and S; R defaults.
|
// I, T, and S; R defaults.
|
||||||
impl<I, T, S> ChatClientBuilder<I, T, Unset, S>
|
impl<T, S> ChatClientBuilder<DelegateSigner, T, Unset, S>
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
T: Transport + Send + 'static,
|
T: Transport + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
pub fn build(self) -> Built<I, T, EphemeralRegistry, S> {
|
pub fn build(self) -> Built<T, EphemeralRegistry, S> {
|
||||||
ChatClient::new(
|
ChatClient::new(
|
||||||
self.ident,
|
self.ident,
|
||||||
|
self.account,
|
||||||
self.transport,
|
self.transport,
|
||||||
EphemeralRegistry::new(),
|
EphemeralRegistry::new(),
|
||||||
self.storage,
|
self.storage,
|
||||||
|
|||||||
@ -5,17 +5,18 @@ use components::{ThreadedWakeupService, WakeupEvent};
|
|||||||
use crossbeam_channel::{Receiver, Sender, select};
|
use crossbeam_channel::{Receiver, Sender, select};
|
||||||
use crypto::Ed25519VerifyingKey;
|
use crypto::Ed25519VerifyingKey;
|
||||||
use libchat::{
|
use libchat::{
|
||||||
AccountDirectory, ConversationId, ConvoOutcome, Core, DeliveryService, IdentId, IdentIdRef,
|
ConversationId, ConvoOutcome, Core, DeliveryService, IdentId, IdentIdRef, InboxOutcome,
|
||||||
IdentityProvider, InboxOutcome, Introduction, PayloadOutcome, RegistrationService,
|
Introduction, PayloadOutcome, RegistrationService,
|
||||||
};
|
};
|
||||||
|
use logos_account::{AccountDirectory, resolve_device_ids};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use storage::ChatStore;
|
use storage::ChatStore;
|
||||||
|
|
||||||
use crate::delegate::DelegateCredential;
|
use crate::delegate::{DelegateCredential, DelegateIdentity, DelegateSigner};
|
||||||
use crate::errors::ClientError;
|
use crate::errors::ClientError;
|
||||||
use crate::event::{Event, MessageSender};
|
use crate::event::{Event, MessageSender};
|
||||||
|
|
||||||
type ClientCore<I, T, R, S> = Core<(I, T, R, ThreadedWakeupService, S)>;
|
type ClientCore<T, R, S> = Core<(DelegateIdentity, T, R, ThreadedWakeupService, S)>;
|
||||||
type AccountAddressRef<'a> = &'a str;
|
type AccountAddressRef<'a> = &'a str;
|
||||||
type LocalSignerId = IdentId;
|
type LocalSignerId = IdentId;
|
||||||
|
|
||||||
@ -40,16 +41,19 @@ pub trait Transport: DeliveryService + Send + 'static {
|
|||||||
/// caller's thread: they briefly lock the core, invoke it, and return — no
|
/// caller's thread: they briefly lock the core, invoke it, and return — no
|
||||||
/// message-passing round-trip. The `Arc`/`Mutex`/threads live entirely here;
|
/// message-passing round-trip. The `Arc`/`Mutex`/threads live entirely here;
|
||||||
/// the core never mentions threads.
|
/// the core never mentions threads.
|
||||||
pub struct ChatClient<I, T, R, S>
|
pub struct ChatClient<T, R, S>
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
T: Transport + Send + 'static,
|
T: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
/// `parking_lot::Mutex` for its eventual fairness: an inbound burst can't
|
/// `parking_lot::Mutex` for its eventual fairness: an inbound burst can't
|
||||||
/// starve caller operations of the lock.
|
/// starve caller operations of the lock.
|
||||||
core: Arc<Mutex<ClientCore<I, T, R, S>>>,
|
core: Arc<Mutex<ClientCore<T, R, S>>>,
|
||||||
|
/// The account → device directory. On testnet the registration service
|
||||||
|
/// doubles as the directory (one deployed registry serves both roles), so
|
||||||
|
/// the client keeps its own clone of `R`; the core sees key packages only.
|
||||||
|
directory: R,
|
||||||
/// Dropped on `Drop` to wake the worker's `select!` and shut it down.
|
/// Dropped on `Drop` to wake the worker's `select!` and shut it down.
|
||||||
shutdown: Option<Sender<()>>,
|
shutdown: Option<Sender<()>>,
|
||||||
worker: Option<JoinHandle<()>>,
|
worker: Option<JoinHandle<()>>,
|
||||||
@ -57,15 +61,15 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -- GenericChatClient
|
// -- GenericChatClient
|
||||||
impl<I, T, R, S> ChatClient<I, T, R, S>
|
impl<T, R, S> ChatClient<T, R, S>
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
T: Transport + Send + 'static,
|
T: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub fn new(
|
||||||
ident: I,
|
ident: DelegateSigner,
|
||||||
|
account: String,
|
||||||
mut transport: T,
|
mut transport: T,
|
||||||
reg: R,
|
reg: R,
|
||||||
storage: S,
|
storage: S,
|
||||||
@ -74,28 +78,42 @@ where
|
|||||||
|
|
||||||
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
|
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
|
||||||
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
|
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
|
||||||
|
let directory = reg.clone();
|
||||||
|
let ident = DelegateIdentity::new(ident, &account);
|
||||||
let core = Core::new_with_name(ident, transport, reg, wakeup_service, storage)?;
|
let core = Core::new_with_name(ident, transport, reg, wakeup_service, storage)?;
|
||||||
Ok(Self::spawn(core, inbound, wakeup_rx))
|
Ok(Self::spawn(core, directory, account, inbound, wakeup_rx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn(
|
fn spawn(
|
||||||
core: ClientCore<I, T, R, S>,
|
core: ClientCore<T, R, S>,
|
||||||
|
directory: R,
|
||||||
|
address: String,
|
||||||
inbound: Receiver<Vec<u8>>,
|
inbound: Receiver<Vec<u8>>,
|
||||||
wakeup_events: Receiver<WakeupEvent>,
|
wakeup_events: Receiver<WakeupEvent>,
|
||||||
) -> (Self, Receiver<Event>) {
|
) -> (Self, Receiver<Event>) {
|
||||||
let address = core.ident_id().to_string();
|
|
||||||
let core = Arc::new(Mutex::new(core));
|
let core = Arc::new(Mutex::new(core));
|
||||||
let (event_tx, event_rx) = crossbeam_channel::unbounded();
|
let (event_tx, event_rx) = crossbeam_channel::unbounded();
|
||||||
let (shutdown_tx, shutdown_rx) = crossbeam_channel::bounded::<()>(0);
|
let (shutdown_tx, shutdown_rx) = crossbeam_channel::bounded::<()>(0);
|
||||||
|
|
||||||
let worker = thread::spawn({
|
let worker = thread::spawn({
|
||||||
let core = Arc::clone(&core);
|
let core = Arc::clone(&core);
|
||||||
move || worker_loop(core, inbound, wakeup_events, shutdown_rx, event_tx)
|
let directory = directory.clone();
|
||||||
|
move || {
|
||||||
|
worker_loop(
|
||||||
|
core,
|
||||||
|
directory,
|
||||||
|
inbound,
|
||||||
|
wakeup_events,
|
||||||
|
shutdown_rx,
|
||||||
|
event_tx,
|
||||||
|
)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
(
|
(
|
||||||
Self {
|
Self {
|
||||||
core,
|
core,
|
||||||
|
directory,
|
||||||
shutdown: Some(shutdown_tx),
|
shutdown: Some(shutdown_tx),
|
||||||
worker: Some(worker),
|
worker: Some(worker),
|
||||||
address,
|
address,
|
||||||
@ -104,7 +122,8 @@ where
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn addr(&self) -> AccountAddressRef<'_> {
|
/// The account address peers use to reach this client.
|
||||||
|
pub fn addr(&self) -> &str {
|
||||||
&self.address
|
&self.address
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,21 +181,24 @@ where
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get signers for a given AccountAddress.
|
/// Resolve an account address to the signer (device) ids its published
|
||||||
|
/// directory bundle endorses. A reachable account has published at least
|
||||||
|
/// one signer; anything else is an error.
|
||||||
fn signers_from_account(
|
fn signers_from_account(
|
||||||
&self,
|
&self,
|
||||||
account: AccountAddressRef,
|
account: AccountAddressRef,
|
||||||
) -> Result<Vec<LocalSignerId>, ClientError> {
|
) -> Result<Vec<LocalSignerId>, ClientError> {
|
||||||
// Assume Account = LocalSigner until Account is ready
|
let account = IdentId::new(account.to_string());
|
||||||
Ok(vec![IdentId::new(account.to_string())])
|
let device_ids = resolve_device_ids(&self.directory, &account)
|
||||||
|
.map_err(|e| ClientError::AccountResolution(e.to_string()))?;
|
||||||
|
Ok(device_ids.into_iter().map(IdentId::new).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I, T, R, S> Drop for ChatClient<I, T, R, S>
|
impl<T, R, S> Drop for ChatClient<T, R, S>
|
||||||
where
|
where
|
||||||
I: IdentityProvider + Send + 'static,
|
|
||||||
T: Transport + Send + 'static,
|
T: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Clone + Send + 'static,
|
||||||
S: ChatStore + Send + 'static,
|
S: ChatStore + Send + 'static,
|
||||||
{
|
{
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
@ -192,15 +214,16 @@ where
|
|||||||
/// Background loop: block until an inbound payload or shutdown arrives, drive
|
/// Background loop: block until an inbound payload or shutdown arrives, drive
|
||||||
/// the core on each payload, and forward events. No polling — `select!` parks
|
/// the core on each payload, and forward events. No polling — `select!` parks
|
||||||
/// the thread until one of the channels is ready.
|
/// the thread until one of the channels is ready.
|
||||||
fn worker_loop<I: IdentityProvider + 'static, T, R, S: ChatStore + 'static>(
|
fn worker_loop<T, R, S: ChatStore + 'static>(
|
||||||
core: Arc<Mutex<ClientCore<I, T, R, S>>>,
|
core: Arc<Mutex<ClientCore<T, R, S>>>,
|
||||||
|
directory: R,
|
||||||
inbound: Receiver<Vec<u8>>,
|
inbound: Receiver<Vec<u8>>,
|
||||||
wakeup_events: Receiver<WakeupEvent>,
|
wakeup_events: Receiver<WakeupEvent>,
|
||||||
shutdown: Receiver<()>,
|
shutdown: Receiver<()>,
|
||||||
event_tx: Sender<Event>,
|
event_tx: Sender<Event>,
|
||||||
) where
|
) where
|
||||||
T: DeliveryService + Send + 'static,
|
T: DeliveryService + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + AccountDirectory + Send + 'static,
|
||||||
{
|
{
|
||||||
loop {
|
loop {
|
||||||
select! {
|
select! {
|
||||||
@ -211,7 +234,7 @@ fn worker_loop<I: IdentityProvider + 'static, T, R, S: ChatStore + 'static>(
|
|||||||
let events = {
|
let events = {
|
||||||
let mut core = core.lock();
|
let mut core = core.lock();
|
||||||
match core.handle_payload(&bytes) {
|
match core.handle_payload(&bytes) {
|
||||||
Ok(outcome) => events_from_inbound(outcome, core.account_directory()),
|
Ok(outcome) => events_from_inbound(outcome, &directory),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("inbound handle_payload failed: {e:?}");
|
tracing::warn!("inbound handle_payload failed: {e:?}");
|
||||||
vec![Event::InboundError {
|
vec![Event::InboundError {
|
||||||
@ -371,7 +394,8 @@ mod sender_check_tests {
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crypto::{Ed25519SigningKey, Ed25519VerifyingKey};
|
use crypto::{Ed25519SigningKey, Ed25519VerifyingKey};
|
||||||
use libchat::{DeviceSet, IdentId, SignedDeviceBundle};
|
use libchat::IdentId;
|
||||||
|
use logos_account::{DeviceSet, SignedDeviceBundle};
|
||||||
|
|
||||||
use super::{MessageSender, SenderError, decode_sender};
|
use super::{MessageSender, SenderError, decode_sender};
|
||||||
use crate::delegate::DelegateCredential;
|
use crate::delegate::DelegateCredential;
|
||||||
@ -399,7 +423,7 @@ mod sender_check_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl libchat::AccountDirectory for FakeDir {
|
impl logos_account::AccountDirectory for FakeDir {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
||||||
fn publish(&mut self, _: &SignedDeviceBundle) -> Result<(), Self::Error> {
|
fn publish(&mut self, _: &SignedDeviceBundle) -> Result<(), Self::Error> {
|
||||||
|
|||||||
@ -5,15 +5,12 @@ use crate::ClientError;
|
|||||||
|
|
||||||
type AccountAddr = String;
|
type AccountAddr = String;
|
||||||
|
|
||||||
/// A local signing identity that holds an Ed25519 keypair.
|
/// A local signing identity that holds an Ed25519 keypair — the per-device
|
||||||
///
|
/// (installation) signer. It knows nothing about accounts: the client composes
|
||||||
/// Can be standalone (unassociated) or authorized to act on behalf of an account
|
/// the account association into the wire credential ([`DelegateIdentity`]).
|
||||||
/// via [`DelegateSigner::associate`].
|
|
||||||
pub struct DelegateSigner {
|
pub struct DelegateSigner {
|
||||||
signing_key: Ed25519SigningKey,
|
signing_key: Ed25519SigningKey,
|
||||||
verifying_key: Ed25519VerifyingKey,
|
verifying_key: Ed25519VerifyingKey,
|
||||||
identifier: IdentId,
|
|
||||||
account_addr: Option<AccountAddr>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DelegateSigner {
|
impl DelegateSigner {
|
||||||
@ -21,28 +18,40 @@ impl DelegateSigner {
|
|||||||
pub fn random() -> Self {
|
pub fn random() -> Self {
|
||||||
let signing_key = Ed25519SigningKey::generate();
|
let signing_key = Ed25519SigningKey::generate();
|
||||||
let verifying_key = signing_key.verifying_key();
|
let verifying_key = signing_key.verifying_key();
|
||||||
let identifier = DelegateCredential::unassociated(&verifying_key).into();
|
|
||||||
Self {
|
Self {
|
||||||
signing_key,
|
signing_key,
|
||||||
verifying_key,
|
verifying_key,
|
||||||
identifier,
|
|
||||||
account_addr: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Associate a DelegateSigner with an Account.
|
pub fn public_key(&self) -> &Ed25519VerifyingKey {
|
||||||
pub fn associate(&mut self, account_addr: AccountAddr) {
|
&self.verifying_key
|
||||||
self.identifier =
|
|
||||||
DelegateCredential::associated(&self.verifying_key, account_addr.as_str()).into();
|
|
||||||
self.account_addr = Some(account_addr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn account_addr(&self) -> Option<&str> {
|
pub fn sign(&self, payload: &[u8]) -> crypto::Ed25519Signature {
|
||||||
self.account_addr.as_deref()
|
self.signing_key.sign(payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IdentityProvider for DelegateSigner {
|
/// The identity the core sees: a [`DelegateSigner`] plus the wire credential
|
||||||
|
/// the client composed from it and its account address. The account
|
||||||
|
/// association is client state; neither the signer nor the core knows it.
|
||||||
|
pub(crate) struct DelegateIdentity {
|
||||||
|
signer: DelegateSigner,
|
||||||
|
identifier: IdentId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DelegateIdentity {
|
||||||
|
pub(crate) fn new(signer: DelegateSigner, account: &str) -> Self {
|
||||||
|
let credential = DelegateCredential::associated(signer.public_key(), account);
|
||||||
|
Self {
|
||||||
|
identifier: credential.into(),
|
||||||
|
signer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdentityProvider for DelegateIdentity {
|
||||||
fn id(&self) -> libchat::IdentIdRef<'_> {
|
fn id(&self) -> libchat::IdentIdRef<'_> {
|
||||||
&self.identifier
|
&self.identifier
|
||||||
}
|
}
|
||||||
@ -52,11 +61,11 @@ impl IdentityProvider for DelegateSigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sign(&self, payload: &[u8]) -> crypto::Ed25519Signature {
|
fn sign(&self, payload: &[u8]) -> crypto::Ed25519Signature {
|
||||||
self.signing_key.sign(payload)
|
self.signer.sign(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn public_key(&self) -> &Ed25519VerifyingKey {
|
fn public_key(&self) -> &Ed25519VerifyingKey {
|
||||||
&self.verifying_key
|
self.signer.public_key()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,4 +8,8 @@ pub enum ClientError {
|
|||||||
BadlyFormedCredential,
|
BadlyFormedCredential,
|
||||||
#[error("failed to start the transport: {0}")]
|
#[error("failed to start the transport: {0}")]
|
||||||
Transport(String),
|
Transport(String),
|
||||||
|
#[error("account resolution failed: {0}")]
|
||||||
|
AccountResolution(String),
|
||||||
|
#[error("device bundle publish failed: {0}")]
|
||||||
|
BundlePublish(String),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,9 @@ pub use libchat::{
|
|||||||
AddressedEnvelope, ChatStore, ConversationClass, ConversationId, DeliveryService,
|
AddressedEnvelope, ChatStore, ConversationClass, ConversationId, DeliveryService,
|
||||||
IdentityProvider, RegistrationService, StorageConfig,
|
IdentityProvider, RegistrationService, StorageConfig,
|
||||||
};
|
};
|
||||||
|
// The directory trait bounds ChatClient's registry parameter, so callers
|
||||||
|
// writing code generic over ChatClient need it too.
|
||||||
|
pub use logos_account::AccountDirectory;
|
||||||
|
|
||||||
// Re-export bundled registry implementations so callers can pick one without
|
// Re-export bundled registry implementations so callers can pick one without
|
||||||
// pulling in `components` directly.
|
// pulling in `components` directly.
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
use components::{EmbeddedP2pDeliveryService, HttpRegistry, P2pConfig};
|
use components::{EmbeddedP2pDeliveryService, HttpRegistry, P2pConfig};
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use libchat::{ChatStorage, StorageConfig};
|
use libchat::{ChatStorage, StorageConfig};
|
||||||
|
use logos_account::TestLogosAccount;
|
||||||
|
|
||||||
use crate::ChatClientBuilder;
|
use crate::ChatClientBuilder;
|
||||||
use crate::client::{ChatClient, Transport};
|
use crate::client::{ChatClient, Transport};
|
||||||
@ -34,11 +35,11 @@ impl Transport for EmbeddedP2pDeliveryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A [`ChatClient`] wired to the Logos service stack: a [`DelegateSigner`]
|
/// A [`ChatClient`] wired to the Logos service stack: a [`DelegateSigner`]
|
||||||
/// identity, the HTTP keypackage + account registry ([`HttpRegistry`], which is
|
/// identity acting for a fresh dev account, the HTTP keypackage + account
|
||||||
/// both the keypackage store and the account → device directory), encrypted
|
/// registry ([`HttpRegistry`], which is both the keypackage store and the
|
||||||
/// [`ChatStorage`], and the logos-delivery transport.
|
/// account → device directory), encrypted [`ChatStorage`], and the
|
||||||
pub type LogosChatClient =
|
/// logos-delivery transport.
|
||||||
ChatClient<DelegateSigner, EmbeddedP2pDeliveryService, HttpRegistry, ChatStorage>;
|
pub type LogosChatClient = ChatClient<EmbeddedP2pDeliveryService, HttpRegistry, ChatStorage>;
|
||||||
|
|
||||||
impl LogosChatClient {
|
impl LogosChatClient {
|
||||||
/// Open a client on the Logos stack, starting a logos-delivery node on
|
/// Open a client on the Logos stack, starting a logos-delivery node on
|
||||||
@ -65,10 +66,20 @@ impl LogosChatClient {
|
|||||||
.map_err(|e| ClientError::Transport(e.to_string()))?;
|
.map_err(|e| ClientError::Transport(e.to_string()))?;
|
||||||
|
|
||||||
let endpoint = registry_url.unwrap_or(REGISTRY_ENDPOINT);
|
let endpoint = registry_url.unwrap_or(REGISTRY_ENDPOINT);
|
||||||
ChatClientBuilder::new()
|
// A fresh account endorsing a fresh delegate each open: the account
|
||||||
.ident(DelegateSigner::random())
|
// key is dropped after publishing the bundle, so devices cannot be
|
||||||
|
// added later. A caller-supplied, custody-holding account replaces
|
||||||
|
// this once the platform provides one.
|
||||||
|
let account = TestLogosAccount::new();
|
||||||
|
let delegate = DelegateSigner::random();
|
||||||
|
let mut registry = HttpRegistry::new(endpoint);
|
||||||
|
account
|
||||||
|
.add_delegate_signer(&mut registry, delegate.public_key())
|
||||||
|
.map_err(|e| ClientError::BundlePublish(e.to_string()))?;
|
||||||
|
ChatClientBuilder::new(account.address())
|
||||||
|
.ident(delegate)
|
||||||
.transport(transport)
|
.transport(transport)
|
||||||
.registration(HttpRegistry::new(endpoint))
|
.registration(registry)
|
||||||
.storage_config(StorageConfig::Encrypted {
|
.storage_config(StorageConfig::Encrypted {
|
||||||
path: db_path.into(),
|
path: db_path.into(),
|
||||||
key: db_key.into(),
|
key: db_key.into(),
|
||||||
|
|||||||
@ -3,7 +3,6 @@ use std::time::Duration;
|
|||||||
use components::EphemeralRegistry;
|
use components::EphemeralRegistry;
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
use crypto::Ed25519VerifyingKey;
|
use crypto::Ed25519VerifyingKey;
|
||||||
use libchat::{AccountDirectory, IdentityProvider, SignedDeviceBundle, encode_bundle_payload};
|
|
||||||
use logos_account::TestLogosAccount;
|
use logos_account::TestLogosAccount;
|
||||||
use logos_chat::{
|
use logos_chat::{
|
||||||
AddressedEnvelope, ChatClient, ChatClientBuilder, DelegateSigner, DeliveryService, Event,
|
AddressedEnvelope, ChatClient, ChatClientBuilder, DelegateSigner, DeliveryService, Event,
|
||||||
@ -17,29 +16,28 @@ fn publish_device_bundle(
|
|||||||
account: &TestLogosAccount,
|
account: &TestLogosAccount,
|
||||||
device: &Ed25519VerifyingKey,
|
device: &Ed25519VerifyingKey,
|
||||||
) {
|
) {
|
||||||
let payload = encode_bundle_payload(0, std::slice::from_ref(device));
|
account.add_delegate_signer(reg, device).unwrap();
|
||||||
let signature = account.sign(&payload);
|
|
||||||
let bundle = SignedDeviceBundle {
|
|
||||||
account_pub: account.public_key().clone(),
|
|
||||||
payload,
|
|
||||||
signature,
|
|
||||||
};
|
|
||||||
reg.publish(&bundle).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A client for a fresh account: mints the account and a delegate, publishes
|
||||||
|
/// the endorsing bundle, and builds the client on the shared bus/registry.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn create_test_client(
|
fn create_test_client(
|
||||||
message_bus: MessageBus,
|
message_bus: MessageBus,
|
||||||
reg: EphemeralRegistry,
|
mut reg: EphemeralRegistry,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(
|
(
|
||||||
ChatClient<DelegateSigner, InProcessDelivery, EphemeralRegistry, libchat::ChatStorage>,
|
ChatClient<InProcessDelivery, EphemeralRegistry, libchat::ChatStorage>,
|
||||||
Receiver<Event>,
|
Receiver<Event>,
|
||||||
),
|
),
|
||||||
logos_chat::ClientError,
|
logos_chat::ClientError,
|
||||||
> {
|
> {
|
||||||
|
let account = TestLogosAccount::new();
|
||||||
|
let delegate = DelegateSigner::random();
|
||||||
|
publish_device_bundle(&mut reg, &account, delegate.public_key());
|
||||||
let d = InProcessDelivery::new(message_bus);
|
let d = InProcessDelivery::new(message_bus);
|
||||||
ChatClientBuilder::new()
|
ChatClientBuilder::new(account.address())
|
||||||
|
.ident(delegate)
|
||||||
.transport(d)
|
.transport(d)
|
||||||
.registration(reg)
|
.registration(reg)
|
||||||
.build()
|
.build()
|
||||||
@ -91,24 +89,18 @@ fn direct_v1_standalone_integration() {
|
|||||||
|
|
||||||
let mut reg_service = EphemeralRegistry::new();
|
let mut reg_service = EphemeralRegistry::new();
|
||||||
|
|
||||||
// Create accounts and delegates, associate each delegate with its account
|
// Create accounts and delegates, and publish device bundles so the
|
||||||
// address, and publish a device bundle so the receiver can verify the
|
// receiver can verify the account → device mapping carried in the
|
||||||
// account → device mapping carried in the sender's credential.
|
// sender's credential.
|
||||||
let saro_account = TestLogosAccount::new("Saro");
|
let saro_account = TestLogosAccount::new();
|
||||||
let saro_account_id = hex::encode(saro_account.public_key().as_ref());
|
let saro_account_id = saro_account.address();
|
||||||
let mut saro_delegate = DelegateSigner::random();
|
let saro_delegate = DelegateSigner::random();
|
||||||
saro_delegate.associate(saro_account_id.clone());
|
|
||||||
let saro_device_id = hex::encode(saro_delegate.public_key().as_ref());
|
let saro_device_id = hex::encode(saro_delegate.public_key().as_ref());
|
||||||
publish_device_bundle(&mut reg_service, &saro_account, saro_delegate.public_key());
|
publish_device_bundle(&mut reg_service, &saro_account, saro_delegate.public_key());
|
||||||
|
|
||||||
let raya_account = TestLogosAccount::new("Raya");
|
// Build saro's client with its account so its outbound messages carry a
|
||||||
let mut raya_delegate = DelegateSigner::random();
|
// credential the receiver can verify against the published bundle.
|
||||||
raya_delegate.associate(hex::encode(raya_account.public_key().as_ref()));
|
let (mut saro, _saro_events) = ChatClientBuilder::new(saro_account_id.clone())
|
||||||
publish_device_bundle(&mut reg_service, &raya_account, raya_delegate.public_key());
|
|
||||||
|
|
||||||
// Build saro's client with its associated delegate so its outbound messages
|
|
||||||
// carry a credential the receiver can verify against the published bundle.
|
|
||||||
let (mut saro, _saro_events) = ChatClientBuilder::new()
|
|
||||||
.ident(saro_delegate)
|
.ident(saro_delegate)
|
||||||
.transport(InProcessDelivery::new(bus.clone()))
|
.transport(InProcessDelivery::new(bus.clone()))
|
||||||
.registration(reg_service.clone())
|
.registration(reg_service.clone())
|
||||||
@ -146,6 +138,66 @@ fn direct_v1_standalone_integration() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A peer is reachable by its *account address* alone: the initiator resolves
|
||||||
|
/// the account to its signer ids through the directory (client layer), fetches
|
||||||
|
/// each signer's key package, and the Welcome arrives on the signer-scoped
|
||||||
|
/// inbox. The registry keys key packages by device id (hex verifying key),
|
||||||
|
/// exactly like the deployed HTTP registry.
|
||||||
|
#[test]
|
||||||
|
fn direct_v1_by_account_address() {
|
||||||
|
let bus = MessageBus::default();
|
||||||
|
let mut reg_service = EphemeralRegistry::new();
|
||||||
|
|
||||||
|
let raya_account = TestLogosAccount::new();
|
||||||
|
let raya_account_addr = raya_account.address();
|
||||||
|
let raya_delegate = DelegateSigner::random();
|
||||||
|
publish_device_bundle(&mut reg_service, &raya_account, raya_delegate.public_key());
|
||||||
|
|
||||||
|
let (mut raya, raya_events) = ChatClientBuilder::new(raya_account_addr.clone())
|
||||||
|
.ident(raya_delegate)
|
||||||
|
.transport(InProcessDelivery::new(bus.clone()))
|
||||||
|
.registration(reg_service.clone())
|
||||||
|
.build()
|
||||||
|
.expect("client create");
|
||||||
|
let (mut saro, saro_events) =
|
||||||
|
create_test_client(bus.clone(), reg_service.clone()).expect("client create");
|
||||||
|
|
||||||
|
// Raya's shared address is her account address, not her signer id.
|
||||||
|
assert_eq!(raya.addr(), raya_account_addr.as_str());
|
||||||
|
let convo_id = saro.create_direct_conversation(&raya_account_addr).unwrap();
|
||||||
|
|
||||||
|
let raya_convo_id = expect_event(&raya_events, "ConversationStarted", |e| match e {
|
||||||
|
Event::ConversationStarted { convo_id, .. } => Ok(convo_id),
|
||||||
|
other => Err(other),
|
||||||
|
});
|
||||||
|
|
||||||
|
saro.send_message(&convo_id, b"hello raya").unwrap();
|
||||||
|
expect_event(&raya_events, "MessageReceived", |e| match e {
|
||||||
|
Event::MessageReceived { content, .. } => {
|
||||||
|
assert_eq!(content.as_slice(), b"hello raya");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
other => Err(other),
|
||||||
|
});
|
||||||
|
|
||||||
|
raya.send_message(&raya_convo_id, b"hi saro").unwrap();
|
||||||
|
expect_event(&saro_events, "MessageReceived", |e| match e {
|
||||||
|
Event::MessageReceived {
|
||||||
|
content, sender, ..
|
||||||
|
} => {
|
||||||
|
assert_eq!(content.as_slice(), b"hi saro");
|
||||||
|
// raya's bundle endorses her delegate, so her sender surfaces with
|
||||||
|
// the verified account.
|
||||||
|
assert_eq!(
|
||||||
|
sender.account.as_ref().map(|a| a.as_str()),
|
||||||
|
Some(raya_account_addr.as_str())
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
other => Err(other),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn saro_raya_message_exchange() {
|
fn saro_raya_message_exchange() {
|
||||||
let bus = MessageBus::default();
|
let bus = MessageBus::default();
|
||||||
@ -177,9 +229,9 @@ fn saro_raya_message_exchange() {
|
|||||||
} => {
|
} => {
|
||||||
assert_eq!(convo_id, raya_convo_id);
|
assert_eq!(convo_id, raya_convo_id);
|
||||||
assert_eq!(content.as_slice(), b"hello raya");
|
assert_eq!(content.as_slice(), b"hello raya");
|
||||||
// saro's delegate is unassociated, so the sender surfaces its device
|
// saro's account published a bundle endorsing its delegate, so the
|
||||||
// but claims no account.
|
// sender surfaces a verified account.
|
||||||
assert!(sender.account.is_none());
|
assert!(sender.account.is_some());
|
||||||
assert!(!sender.local_identity.as_str().is_empty());
|
assert!(!sender.local_identity.as_str().is_empty());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -293,7 +345,7 @@ fn malformed_inbound_surfaces_as_error_event() {
|
|||||||
let delivery = FailingDelivery::new();
|
let delivery = FailingDelivery::new();
|
||||||
let inbound_tx = delivery.inbound_sender();
|
let inbound_tx = delivery.inbound_sender();
|
||||||
|
|
||||||
let (_client, events) = ChatClientBuilder::new()
|
let (_client, events) = ChatClientBuilder::new(TestLogosAccount::new().address())
|
||||||
.transport(delivery)
|
.transport(delivery)
|
||||||
.build()
|
.build()
|
||||||
.expect("client create");
|
.expect("client create");
|
||||||
@ -308,3 +360,25 @@ fn malformed_inbound_surfaces_as_error_event() {
|
|||||||
other => Err(other),
|
other => Err(other),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opening a conversation by an address whose account never published a
|
||||||
|
/// device bundle fails at resolution, not with a late key-package miss.
|
||||||
|
#[test]
|
||||||
|
fn unpublished_account_address_is_an_error() {
|
||||||
|
let bus = MessageBus::default();
|
||||||
|
let reg_service = EphemeralRegistry::new();
|
||||||
|
|
||||||
|
let (mut saro, _saro_events) =
|
||||||
|
create_test_client(bus.clone(), reg_service.clone()).expect("client create");
|
||||||
|
|
||||||
|
let unpublished = TestLogosAccount::new();
|
||||||
|
let err = saro
|
||||||
|
.create_direct_conversation(&unpublished.address())
|
||||||
|
.expect_err("no bundle published for the account");
|
||||||
|
assert!(matches!(err, logos_chat::ClientError::AccountResolution(_)));
|
||||||
|
|
||||||
|
let err = saro
|
||||||
|
.create_direct_conversation("not-an-account-address")
|
||||||
|
.expect_err("not an account key");
|
||||||
|
assert!(matches!(err, logos_chat::ClientError::AccountResolution(_)));
|
||||||
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ embedded_p2p_delivery = []
|
|||||||
# Workspace dependencies (sorted)
|
# Workspace dependencies (sorted)
|
||||||
crypto = { workspace = true }
|
crypto = { workspace = true }
|
||||||
libchat = { workspace = true }
|
libchat = { workspace = true }
|
||||||
|
logos-account = { workspace = true }
|
||||||
storage = { workspace = true }
|
storage = { workspace = true }
|
||||||
|
|
||||||
# External dependencies (sorted)
|
# External dependencies (sorted)
|
||||||
|
|||||||
@ -5,10 +5,8 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crypto::Ed25519VerifyingKey;
|
use crypto::Ed25519VerifyingKey;
|
||||||
use libchat::{
|
use libchat::{IdentityProvider, RegistrationService};
|
||||||
AccountDirectory, DeviceSet, IdentityProvider, RegistrationService, SignedDeviceBundle,
|
use logos_account::{AccountDirectory, DeviceSet, SignedDeviceBundle, verify_bundle};
|
||||||
verify_bundle,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A Contact Registry used for Tests.
|
/// A Contact Registry used for Tests.
|
||||||
/// This implementation stores bundle bytes and then returns them when
|
/// This implementation stores bundle bytes and then returns them when
|
||||||
@ -61,10 +59,12 @@ impl RegistrationService for EphemeralRegistry {
|
|||||||
identity: &dyn IdentityProvider,
|
identity: &dyn IdentityProvider,
|
||||||
key_bundle: Vec<u8>,
|
key_bundle: Vec<u8>,
|
||||||
) -> Result<(), <Self as RegistrationService>::Error> {
|
) -> Result<(), <Self as RegistrationService>::Error> {
|
||||||
|
// Keyed by device id — the hex of the signer's verifying key — exactly
|
||||||
|
// like the HTTP registry, so tests exercise the deployed keying.
|
||||||
self.key_packages
|
self.key_packages
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(identity.id().to_string(), key_bundle);
|
.insert(hex::encode(identity.public_key().as_ref()), key_bundle);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,8 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||||
use crypto::{Ed25519Signature, Ed25519VerifyingKey};
|
use crypto::{Ed25519Signature, Ed25519VerifyingKey};
|
||||||
use libchat::{
|
use libchat::{IdentityProvider, RegistrationService};
|
||||||
AccountDirectory, BundleError, DeviceSet, IdentityProvider, RegistrationService,
|
use logos_account::{AccountDirectory, BundleError, DeviceSet, SignedDeviceBundle, verify_bundle};
|
||||||
SignedDeviceBundle, verify_bundle,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// HTTP client for the testnet KeyPackage Registry service.
|
/// HTTP client for the testnet KeyPackage Registry service.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user