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

@@ -125,6 +125,87 @@ impl IdentityKeypair {
}
}
/// Verify a 96-byte delivery proof produced by the server's `build_delivery_proof`.
///
/// # Layout
/// ```text
/// bytes 0..32 — SHA-256(seq_le || recipient_key || timestamp_ms_le)
/// bytes 32..96 — Ed25519 signature over those 32 bytes
/// ```
///
/// Returns `Ok(true)` when the proof is structurally valid and the signature verifies,
/// `Ok(false)` when the proof length is wrong (graceful degradation for old servers),
/// or `Err` when the signature is structurally invalid / verification fails.
pub fn verify_delivery_proof(
server_pubkey: &[u8; 32],
proof: &[u8],
) -> Result<bool, crate::error::CoreError> {
if proof.len() != 96 {
return Ok(false);
}
let hash: [u8; 32] = proof[..32].try_into().expect("slice is 32 bytes");
let sig: [u8; 64] = proof[32..96].try_into().expect("slice is 64 bytes");
IdentityKeypair::verify_raw(server_pubkey, &hash, &sig)?;
Ok(true)
}
#[cfg(test)]
mod proof_tests {
use super::*;
use sha2::{Digest, Sha256};
fn make_proof(kp: &IdentityKeypair, seq: u64, recipient_key: &[u8], timestamp_ms: u64) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(seq.to_le_bytes());
hasher.update(recipient_key);
hasher.update(timestamp_ms.to_le_bytes());
let hash: [u8; 32] = hasher.finalize().into();
let sig = kp.sign_raw(&hash);
let mut proof = vec![0u8; 96];
proof[..32].copy_from_slice(&hash);
proof[32..].copy_from_slice(&sig);
proof
}
#[test]
fn verify_valid_proof() {
let kp = IdentityKeypair::generate();
let pk = kp.public_key_bytes();
let rk = [0xabu8; 32];
let proof = make_proof(&kp, 42, &rk, 1_700_000_000_000);
assert!(verify_delivery_proof(&pk, &proof).unwrap());
}
#[test]
fn reject_wrong_length() {
let kp = IdentityKeypair::generate();
let pk = kp.public_key_bytes();
assert!(!verify_delivery_proof(&pk, &[0u8; 64]).unwrap());
assert!(!verify_delivery_proof(&pk, &[]).unwrap());
assert!(!verify_delivery_proof(&pk, &[0u8; 97]).unwrap());
}
#[test]
fn reject_tampered_hash() {
let kp = IdentityKeypair::generate();
let pk = kp.public_key_bytes();
let rk = [0x01u8; 32];
let mut proof = make_proof(&kp, 1, &rk, 999);
proof[0] ^= 0xff; // corrupt the hash bytes
assert!(verify_delivery_proof(&pk, &proof).is_err());
}
#[test]
fn reject_wrong_pubkey() {
let kp = IdentityKeypair::generate();
let other = IdentityKeypair::generate();
let pk = other.public_key_bytes();
let rk = [0x02u8; 32];
let proof = make_proof(&kp, 5, &rk, 0);
assert!(verify_delivery_proof(&pk, &proof).is_err());
}
}
impl Serialize for IdentityKeypair {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where