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