#![deny(missing_docs)]
use serde::Deserialize;
use serde::Serialize;
use crate::consts::DEFAULT_TTL_MS;
use crate::consts::MAX_TTL_MS;
use crate::consts::TS_OFFSET_TOLERANCE_MS;
use crate::dht::Did;
use crate::error::Result;
use crate::session::Session;
use crate::session::SessionSk;
use crate::utils::get_epoch_ms;
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct MessageVerification {
pub session: Session,
pub ttl_ms: u64,
pub ts_ms: u128,
pub sig: Vec<u8>,
}
fn pack_msg(data: &[u8], ts_ms: u128, ttl_ms: u64) -> Vec<u8> {
let mut msg = vec![];
msg.extend_from_slice(&ts_ms.to_be_bytes());
msg.extend_from_slice(&ttl_ms.to_be_bytes());
msg.extend_from_slice(data);
msg
}
impl MessageVerification {
pub fn new(data: &[u8], session_sk: &SessionSk) -> Result<Self> {
let ts_ms = get_epoch_ms();
let ttl_ms = DEFAULT_TTL_MS;
let msg = pack_msg(data, ts_ms, ttl_ms);
let verification = MessageVerification {
session: session_sk.session(),
sig: session_sk.sign(&msg)?,
ttl_ms,
ts_ms,
};
Ok(verification)
}
pub fn verify(&self, data: &[u8]) -> bool {
let msg = pack_msg(data, self.ts_ms, self.ttl_ms);
self.session
.verify(&msg, &self.sig)
.map_err(|e| {
tracing::warn!("MessageVerification verify failed: {:?}", e);
})
.is_ok()
}
pub fn is_expired(&self) -> bool {
!self.is_live_at(get_epoch_ms())
}
pub fn is_live_at(&self, now_ms: u128) -> bool {
self.ttl_ms <= MAX_TTL_MS
&& self.ts_ms.saturating_sub(TS_OFFSET_TOLERANCE_MS) <= now_ms
&& now_ms <= self.ts_ms.saturating_add(self.ttl_ms as u128)
}
pub fn verify_unexpired(&self, data: &[u8]) -> bool {
if self.is_expired() {
tracing::warn!("message expired");
return false;
}
self.verify(data)
}
}
pub trait MessageVerificationExt {
fn verification_data(&self) -> Result<Vec<u8>>;
fn verification(&self) -> &MessageVerification;
fn is_expired(&self) -> bool {
self.verification().is_expired()
}
fn verify(&self) -> bool {
if self.is_expired() {
tracing::warn!("message expired");
return false;
}
let Ok(data) = self.verification_data() else {
tracing::warn!("MessageVerificationExt verify get verification_data failed");
return false;
};
self.verification().verify_unexpired(&data)
}
fn signer(&self) -> Did {
self.verification().session.account_did()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ecc::SecretKey;
struct VerifiedFixture {
verification: MessageVerification,
}
impl MessageVerificationExt for VerifiedFixture {
fn verification_data(&self) -> Result<Vec<u8>> {
Ok(Vec::new())
}
fn verification(&self) -> &MessageVerification {
&self.verification
}
}
#[test]
fn test_expiration_handles_timestamp_below_tolerance_without_underflow() -> Result<()> {
let key = SecretKey::random();
let session_sk = SessionSk::new_with_seckey(&key)?;
let mut verification = MessageVerification::new(&[], &session_sk)?;
verification.ts_ms = 0;
let fixture = VerifiedFixture { verification };
assert!(fixture.is_expired());
Ok(())
}
fn signed_verification(
data: &[u8],
session_sk: &SessionSk,
ts_ms: u128,
ttl_ms: u64,
) -> Result<MessageVerification> {
let msg = pack_msg(data, ts_ms, ttl_ms);
Ok(MessageVerification {
session: session_sk.session(),
ttl_ms,
ts_ms,
sig: session_sk.sign(&msg)?,
})
}
#[test]
fn test_verify_unexpired_rejects_ttl_above_max() -> Result<()> {
let key = SecretKey::random();
let session_sk = SessionSk::new_with_seckey(&key)?;
let proof = signed_verification(&[], &session_sk, get_epoch_ms(), MAX_TTL_MS + 1)?;
assert!(proof.is_expired());
assert!(!proof.verify_unexpired(&[]));
Ok(())
}
#[test]
fn test_verify_unexpired_rejects_timestamp_beyond_future_tolerance() -> Result<()> {
let key = SecretKey::random();
let session_sk = SessionSk::new_with_seckey(&key)?;
let proof = signed_verification(
&[],
&session_sk,
get_epoch_ms() + TS_OFFSET_TOLERANCE_MS + 60_000,
1_000,
)?;
assert!(proof.is_expired());
assert!(!proof.verify_unexpired(&[]));
Ok(())
}
}