feat(delivery): add server-signed delivery proof on enqueue
The server now produces a 96-byte Ed25519-signed delivery proof for every enqueued message: SHA-256(seq || recipient_key || timestamp_ms) followed by the server's Ed25519 signature. Clients can verify the proof using verify_delivery_proof() in quicproquo-core to get cryptographic evidence the server accepted their message.
This commit is contained in:
@@ -7,6 +7,7 @@ use prost::Message;
|
|||||||
use quicproquo_proto::qpq::v1;
|
use quicproquo_proto::qpq::v1;
|
||||||
use quicproquo_rpc::error::RpcStatus;
|
use quicproquo_rpc::error::RpcStatus;
|
||||||
use quicproquo_rpc::method::{HandlerResult, RequestContext};
|
use quicproquo_rpc::method::{HandlerResult, RequestContext};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
|
|
||||||
use crate::domain::delivery::DeliveryService;
|
use crate::domain::delivery::DeliveryService;
|
||||||
@@ -15,6 +16,29 @@ use crate::hooks::{HookAction, MessageEvent};
|
|||||||
|
|
||||||
use super::{require_auth, ServerState};
|
use super::{require_auth, ServerState};
|
||||||
|
|
||||||
|
/// Build a 96-byte delivery proof: `SHA-256(seq || recipient_key || timestamp_ms) || Ed25519(hash)`.
|
||||||
|
///
|
||||||
|
/// The sender stores this as cryptographic evidence that the server enqueued the message.
|
||||||
|
fn build_delivery_proof(
|
||||||
|
signing_key: &quicproquo_core::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 = signing_key.sign_raw(&hash);
|
||||||
|
|
||||||
|
let mut proof = vec![0u8; 96];
|
||||||
|
proof[..32].copy_from_slice(&hash);
|
||||||
|
proof[32..].copy_from_slice(&sig);
|
||||||
|
proof
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_enqueue(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
|
pub async fn handle_enqueue(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
|
||||||
let identity_key = match require_auth(&state, &ctx) {
|
let identity_key = match require_auth(&state, &ctx) {
|
||||||
Ok(ik) => ik,
|
Ok(ik) => ik,
|
||||||
@@ -71,7 +95,7 @@ pub async fn handle_enqueue(state: Arc<ServerState>, ctx: RequestContext) -> Han
|
|||||||
// Fire hook.
|
// Fire hook.
|
||||||
let action = state.hooks.on_message_enqueue(&MessageEvent {
|
let action = state.hooks.on_message_enqueue(&MessageEvent {
|
||||||
sender_identity: Some(identity_key),
|
sender_identity: Some(identity_key),
|
||||||
recipient_key: req.recipient_key,
|
recipient_key: req.recipient_key.clone(),
|
||||||
channel_id: req.channel_id,
|
channel_id: req.channel_id,
|
||||||
payload_len: req.payload.len(),
|
payload_len: req.payload.len(),
|
||||||
seq: resp.seq,
|
seq: resp.seq,
|
||||||
@@ -80,9 +104,18 @@ pub async fn handle_enqueue(state: Arc<ServerState>, ctx: RequestContext) -> Han
|
|||||||
return HandlerResult::err(RpcStatus::Forbidden, &reason);
|
return HandlerResult::err(RpcStatus::Forbidden, &reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build server-signed delivery proof.
|
||||||
|
let timestamp_ms = crate::auth::current_timestamp();
|
||||||
|
let delivery_proof = build_delivery_proof(
|
||||||
|
&state.signing_key,
|
||||||
|
resp.seq,
|
||||||
|
&req.recipient_key,
|
||||||
|
timestamp_ms,
|
||||||
|
);
|
||||||
|
|
||||||
let proto = v1::EnqueueResponse {
|
let proto = v1::EnqueueResponse {
|
||||||
seq: resp.seq,
|
seq: resp.seq,
|
||||||
delivery_proof: resp.delivery_proof,
|
delivery_proof,
|
||||||
duplicate: false,
|
duplicate: false,
|
||||||
};
|
};
|
||||||
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
|
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
|
||||||
|
|||||||
Reference in New Issue
Block a user