feat: Phase 9 — developer experience, extensibility, and community growth

New crates:
- quicproquo-bot: Bot SDK with polling API + JSON pipe mode
- quicproquo-kt: Key Transparency Merkle log (RFC 9162 subset)
- quicproquo-plugin-api: no_std C-compatible plugin vtable API
- quicproquo-gen: scaffolding tool (qpq-gen plugin/bot/rpc/hook)

Server features:
- ServerHooks trait wired into all RPC handlers (enqueue, fetch, auth,
  channel, registration) with plugin rejection support
- Dynamic plugin loader (libloading) with --plugin-dir config
- Delivery proof canary tokens (Ed25519 server signatures on enqueue)
- Key Transparency Merkle log with inclusion proofs on resolveUser

Core library:
- Safety numbers (60-digit HMAC-SHA256 key verification codes)
- Verifiable transcript archive (CBOR + ChaCha20-Poly1305 + hash chain)
- Delivery proof verification utility
- Criterion benchmarks (hybrid KEM, MLS, identity, sealed sender, padding)

Client:
- /verify REPL command for out-of-band key verification
- Full-screen TUI via Ratatui (feature-gated --features tui)
- qpq export / qpq export-verify CLI subcommands
- KT inclusion proof verification on user resolution

Also: ROADMAP Phase 9 added, bot SDK docs, server hooks docs,
crate-responsibilities updated, example plugins (rate_limit, logging).
This commit is contained in:
2026-03-03 22:47:38 +01:00
parent b6483dedbc
commit dc4e4e49a0
62 changed files with 6959 additions and 62 deletions

View File

@@ -81,6 +81,18 @@ pub trait Store: Send + Sync {
/// Load the persisted `ServerSetup`, if any.
fn get_server_setup(&self) -> Result<Option<Vec<u8>>, StorageError>;
/// Persist the server's Ed25519 signing key seed (32 bytes) for delivery proofs.
fn store_signing_key_seed(&self, seed: Vec<u8>) -> Result<(), StorageError>;
/// Load the persisted signing key seed, if any.
fn get_signing_key_seed(&self) -> Result<Option<Vec<u8>>, StorageError>;
/// Persist the Key Transparency Merkle log (bincode-serialised `MerkleLog` bytes).
fn save_kt_log(&self, bytes: Vec<u8>) -> Result<(), StorageError>;
/// Load the persisted KT Merkle log, if any.
fn load_kt_log(&self) -> Result<Option<Vec<u8>>, StorageError>;
/// Store an OPAQUE user record (serialized `ServerRegistration`).
fn store_user_record(&self, username: &str, record: Vec<u8>) -> Result<(), StorageError>;
@@ -213,6 +225,8 @@ pub struct FileBackedStore {
ds_path: PathBuf,
hk_path: PathBuf,
setup_path: PathBuf,
signing_key_path: PathBuf,
kt_log_path: PathBuf,
users_path: PathBuf,
identity_keys_path: PathBuf,
channels_path: PathBuf,
@@ -235,6 +249,8 @@ impl FileBackedStore {
let ds_path = dir.join("deliveries.bin");
let hk_path = dir.join("hybridkeys.bin");
let setup_path = dir.join("server_setup.bin");
let signing_key_path = dir.join("server_signing_key.bin");
let kt_log_path = dir.join("kt_log.bin");
let users_path = dir.join("users.bin");
let identity_keys_path = dir.join("identity_keys.bin");
let channels_path = dir.join("channels.bin");
@@ -251,6 +267,8 @@ impl FileBackedStore {
ds_path,
hk_path,
setup_path,
signing_key_path,
kt_log_path,
users_path,
identity_keys_path,
channels_path,
@@ -541,6 +559,52 @@ impl Store for FileBackedStore {
Ok(Some(bytes))
}
fn store_signing_key_seed(&self, seed: Vec<u8>) -> Result<(), StorageError> {
if let Some(parent) = self.signing_key_path.parent() {
fs::create_dir_all(parent).map_err(|e| StorageError::Io(e.to_string()))?;
}
fs::write(&self.signing_key_path, &seed).map_err(|e| StorageError::Io(e.to_string()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(
&self.signing_key_path,
std::fs::Permissions::from_mode(0o600),
);
}
Ok(())
}
fn get_signing_key_seed(&self) -> Result<Option<Vec<u8>>, StorageError> {
if !self.signing_key_path.exists() {
return Ok(None);
}
let bytes =
fs::read(&self.signing_key_path).map_err(|e| StorageError::Io(e.to_string()))?;
if bytes.is_empty() {
return Ok(None);
}
Ok(Some(bytes))
}
fn save_kt_log(&self, bytes: Vec<u8>) -> Result<(), StorageError> {
if let Some(parent) = self.kt_log_path.parent() {
fs::create_dir_all(parent).map_err(|e| StorageError::Io(e.to_string()))?;
}
fs::write(&self.kt_log_path, &bytes).map_err(|e| StorageError::Io(e.to_string()))
}
fn load_kt_log(&self) -> Result<Option<Vec<u8>>, StorageError> {
if !self.kt_log_path.exists() {
return Ok(None);
}
let bytes = fs::read(&self.kt_log_path).map_err(|e| StorageError::Io(e.to_string()))?;
if bytes.is_empty() {
return Ok(None);
}
Ok(Some(bytes))
}
fn store_user_record(&self, username: &str, record: Vec<u8>) -> Result<(), StorageError> {
let mut map = lock(&self.users)?;
match map.entry(username.to_string()) {