chore: rename project quicnprotochat -> quicproquo (binaries: qpq)
Rename the entire workspace:
- Crate packages: quicnprotochat-{core,proto,server,client,gui,p2p,mobile} -> quicproquo-*
- Binary names: quicnprotochat -> qpq, quicnprotochat-server -> qpq-server,
quicnprotochat-gui -> qpq-gui
- Default files: *-state.bin -> qpq-state.bin, *-server.toml -> qpq-server.toml,
*.db -> qpq.db
- Environment variable prefix: QUICNPROTOCHAT_* -> QPQ_*
- App identifier: chat.quicnproto.gui -> chat.quicproquo.gui
- Proto package: quicnprotochat.bench -> quicproquo.bench
- All documentation, Docker, CI, and script references updated
HKDF domain-separation strings and P2P ALPN remain unchanged for
backward compatibility with existing encrypted state and wire protocol.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
154
crates/quicproquo-core/src/sealed_sender.rs
Normal file
154
crates/quicproquo-core/src/sealed_sender.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
//! Sealed sender: embed sender identity + Ed25519 signature inside the MLS
|
||||
//! application payload so recipients can verify the sender from decrypted
|
||||
//! content, independent of MLS framing.
|
||||
//!
|
||||
//! # Wire format
|
||||
//!
|
||||
//! ```text
|
||||
//! [magic: 1 byte (0x53 = 'S')]
|
||||
//! [sender_identity_key: 32 bytes (Ed25519 public key)]
|
||||
//! [signature: 64 bytes (Ed25519)]
|
||||
//! [inner_payload: variable (the original app_message bytes)]
|
||||
//! ```
|
||||
//!
|
||||
//! The signature covers: `magic || sender_identity_key || inner_payload`.
|
||||
//! Total overhead: 1 + 32 + 64 = 97 bytes per message.
|
||||
|
||||
use crate::error::CoreError;
|
||||
use crate::identity::IdentityKeypair;
|
||||
|
||||
/// Magic byte identifying a sealed sender envelope.
|
||||
pub const SEALED_MAGIC: u8 = 0x53; // 'S'
|
||||
|
||||
/// Fixed overhead: magic(1) + sender_key(32) + signature(64).
|
||||
const SEALED_OVERHEAD: usize = 1 + 32 + 64;
|
||||
|
||||
/// Wrap an app_message payload in a sealed sender envelope.
|
||||
///
|
||||
/// Signs `magic || sender_key || payload` with the sender's Ed25519 key.
|
||||
pub fn seal(identity: &IdentityKeypair, app_message_bytes: &[u8]) -> Vec<u8> {
|
||||
let sender_key = identity.public_key_bytes();
|
||||
|
||||
// Build signing input
|
||||
let mut sign_input = Vec::with_capacity(1 + 32 + app_message_bytes.len());
|
||||
sign_input.push(SEALED_MAGIC);
|
||||
sign_input.extend_from_slice(&sender_key);
|
||||
sign_input.extend_from_slice(app_message_bytes);
|
||||
|
||||
let signature = identity.sign_raw(&sign_input);
|
||||
|
||||
let mut out = Vec::with_capacity(SEALED_OVERHEAD + app_message_bytes.len());
|
||||
out.push(SEALED_MAGIC);
|
||||
out.extend_from_slice(&sender_key);
|
||||
out.extend_from_slice(&signature);
|
||||
out.extend_from_slice(app_message_bytes);
|
||||
out
|
||||
}
|
||||
|
||||
/// Unseal: verify the Ed25519 signature, return `(sender_identity_key, inner_app_message_bytes)`.
|
||||
pub fn unseal(bytes: &[u8]) -> Result<([u8; 32], Vec<u8>), CoreError> {
|
||||
if bytes.len() < SEALED_OVERHEAD {
|
||||
return Err(CoreError::AppMessage(
|
||||
"sealed sender envelope too short".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if bytes[0] != SEALED_MAGIC {
|
||||
return Err(CoreError::AppMessage(format!(
|
||||
"sealed sender: expected magic 0x{:02X}, got 0x{:02X}",
|
||||
SEALED_MAGIC, bytes[0]
|
||||
)));
|
||||
}
|
||||
|
||||
let mut sender_key = [0u8; 32];
|
||||
sender_key.copy_from_slice(&bytes[1..33]);
|
||||
|
||||
let mut signature = [0u8; 64];
|
||||
signature.copy_from_slice(&bytes[33..97]);
|
||||
|
||||
let inner_payload = &bytes[97..];
|
||||
|
||||
// Reconstruct signing input: magic || sender_key || inner_payload
|
||||
let mut sign_input = Vec::with_capacity(1 + 32 + inner_payload.len());
|
||||
sign_input.push(SEALED_MAGIC);
|
||||
sign_input.extend_from_slice(&sender_key);
|
||||
sign_input.extend_from_slice(inner_payload);
|
||||
|
||||
IdentityKeypair::verify_raw(&sender_key, &sign_input, &signature)?;
|
||||
|
||||
Ok((sender_key, inner_payload.to_vec()))
|
||||
}
|
||||
|
||||
/// Check if bytes start with the sealed sender magic byte.
|
||||
pub fn is_sealed(bytes: &[u8]) -> bool {
|
||||
bytes.first() == Some(&SEALED_MAGIC)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn seal_unseal_round_trip() {
|
||||
let identity = IdentityKeypair::generate();
|
||||
let payload = b"hello sealed sender";
|
||||
let sealed = seal(&identity, payload);
|
||||
assert!(is_sealed(&sealed));
|
||||
|
||||
let (sender_key, inner) = unseal(&sealed).unwrap();
|
||||
assert_eq!(sender_key, identity.public_key_bytes());
|
||||
assert_eq!(inner, payload);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unseal_tampered_payload_fails() {
|
||||
let identity = IdentityKeypair::generate();
|
||||
let payload = b"hello";
|
||||
let mut sealed = seal(&identity, payload);
|
||||
// Tamper with the inner payload
|
||||
if let Some(last) = sealed.last_mut() {
|
||||
*last ^= 0xFF;
|
||||
}
|
||||
assert!(unseal(&sealed).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unseal_wrong_sender_fails() {
|
||||
let alice = IdentityKeypair::generate();
|
||||
let bob = IdentityKeypair::generate();
|
||||
let payload = b"from alice";
|
||||
let mut sealed = seal(&alice, payload);
|
||||
// Replace sender key with Bob's
|
||||
let bob_key = bob.public_key_bytes();
|
||||
sealed[1..33].copy_from_slice(&bob_key);
|
||||
assert!(unseal(&sealed).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unseal_too_short_fails() {
|
||||
assert!(unseal(&[SEALED_MAGIC; 10]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unseal_wrong_magic_fails() {
|
||||
let identity = IdentityKeypair::generate();
|
||||
let mut sealed = seal(&identity, b"test");
|
||||
sealed[0] = 0x00;
|
||||
assert!(unseal(&sealed).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_sealed_detected() {
|
||||
assert!(!is_sealed(b"\x01\x01hello"));
|
||||
assert!(is_sealed(&[SEALED_MAGIC, 0, 0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_payload_round_trip() {
|
||||
let identity = IdentityKeypair::generate();
|
||||
let sealed = seal(&identity, b"");
|
||||
let (sender_key, inner) = unseal(&sealed).unwrap();
|
||||
assert_eq!(sender_key, identity.public_key_bytes());
|
||||
assert!(inner.is_empty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user