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:
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user