Scaffold Rust workspace: warzone-protocol, server, client, mule

4 crates, all compile. 16/17 tests pass.

warzone-protocol (core crypto):
- Seed-based identity (Ed25519 + X25519 from 32-byte seed via HKDF)
- BIP39 mnemonic encode/decode (24 words)
- Fingerprint type (SHA-256 truncated, displayed as xxxx:xxxx:xxxx:xxxx)
- ChaCha20-Poly1305 AEAD encrypt/decrypt with random nonce
- HKDF-SHA256 key derivation
- Pre-key bundle generation with Ed25519 signatures
- X3DH key exchange (simplified, needs X25519 identity key in bundle)
- Double Ratchet: full implementation with DH ratchet, chain ratchet,
  out-of-order message handling via skipped keys cache
- Message format (WarzoneMessage envelope + RatchetHeader)
- Session type with ratchet state
- Storage trait definitions (PreKeyStore, SessionStore, MessageQueue)

warzone-server (axum):
- sled database (keys, messages, one-time pre-keys)
- Routes: /v1/health, /v1/keys/register, /v1/keys/{fp},
  /v1/messages/send, /v1/messages/poll/{fp}, /v1/messages/{id}/ack

warzone-client (CLI):
- `warzone init` — generate seed, show mnemonic, save to ~/.warzone/
- `warzone recover <words>` — restore from mnemonic
- `warzone info` — show fingerprint and keys
- Seed storage at ~/.warzone/identity.seed (600 perms)
- Stubs for send, recv, chat commands

warzone-mule: Phase 4 placeholder

Known issue: X3DH test fails (initiate/respond use different DH ops
due to missing X25519 identity key in bundle). Fix in next step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-26 21:27:48 +04:00
parent 1e2a83402d
commit 651396fa13
5075 changed files with 36186 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
use ed25519_dalek::{Signature, Signer, Verifier};
use serde::{Deserialize, Serialize};
use x25519_dalek::{PublicKey, StaticSecret};
use crate::errors::ProtocolError;
use crate::identity::IdentityKeyPair;
/// A signed pre-key (medium-term, rotated periodically).
#[derive(Clone, Serialize, Deserialize)]
pub struct SignedPreKey {
pub id: u32,
pub public_key: [u8; 32],
pub signature: Vec<u8>,
pub timestamp: i64,
}
impl SignedPreKey {
/// Verify the signature against the identity signing key.
pub fn verify(&self, identity_key: &ed25519_dalek::VerifyingKey) -> Result<(), ProtocolError> {
let sig =
Signature::from_slice(&self.signature).map_err(|_| ProtocolError::InvalidSignature)?;
identity_key
.verify(&self.public_key, &sig)
.map_err(|_| ProtocolError::PreKeySignatureInvalid)
}
}
/// A one-time pre-key (used once, then discarded).
pub struct OneTimePreKey {
pub id: u32,
pub secret: StaticSecret,
pub public: PublicKey,
}
/// The public portion of a one-time pre-key (sent to server).
#[derive(Clone, Serialize, Deserialize)]
pub struct OneTimePreKeyPublic {
pub id: u32,
pub public_key: [u8; 32],
}
/// A full pre-key bundle that the server stores for a user.
/// Fetched by others to initiate X3DH key exchange.
#[derive(Clone, Serialize, Deserialize)]
pub struct PreKeyBundle {
pub identity_key: [u8; 32], // Ed25519 verifying key bytes
pub signed_pre_key: SignedPreKey,
pub one_time_pre_key: Option<OneTimePreKeyPublic>,
}
/// Generate a signed pre-key.
pub fn generate_signed_pre_key(identity: &IdentityKeyPair, id: u32) -> (StaticSecret, SignedPreKey) {
let secret = StaticSecret::random_from_rng(rand::rngs::OsRng);
let public = PublicKey::from(&secret);
let signature = identity.signing.sign(public.as_bytes());
let spk = SignedPreKey {
id,
public_key: *public.as_bytes(),
signature: signature.to_bytes().to_vec(),
timestamp: chrono::Utc::now().timestamp(),
};
(secret, spk)
}
/// Generate a batch of one-time pre-keys.
pub fn generate_one_time_pre_keys(start_id: u32, count: u32) -> Vec<OneTimePreKey> {
(start_id..start_id + count)
.map(|id| {
let secret = StaticSecret::random_from_rng(rand::rngs::OsRng);
let public = PublicKey::from(&secret);
OneTimePreKey {
id,
secret,
public,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::Seed;
#[test]
fn signed_pre_key_verify() {
let seed = Seed::generate();
let identity = seed.derive_identity();
let (_secret, spk) = generate_signed_pre_key(&identity, 1);
let pub_id = identity.public_identity();
assert!(spk.verify(&pub_id.signing).is_ok());
}
#[test]
fn signed_pre_key_reject_tampered() {
let seed = Seed::generate();
let identity = seed.derive_identity();
let (_secret, mut spk) = generate_signed_pre_key(&identity, 1);
spk.public_key[0] ^= 0xff; // tamper
let pub_id = identity.public_identity();
assert!(spk.verify(&pub_id.signing).is_err());
}
#[test]
fn generate_otpks() {
let keys = generate_one_time_pre_keys(0, 10);
assert_eq!(keys.len(), 10);
// All public keys should be unique
let pubs: Vec<_> = keys.iter().map(|k| *k.public.as_bytes()).collect();
let unique: std::collections::HashSet<_> = pubs.iter().collect();
assert_eq!(unique.len(), 10);
}
}