use alloc::vec::Vec;
use base64ct::{Base64, Encoding};
use crate::crypto::{sha512, verify_ed25519};
use crate::error::Error;
use crate::floor;
use crate::tags::{
TAG_CERT, TAG_DELE, TAG_INDX, TAG_MAXT, TAG_MIDP, TAG_MINT, TAG_PATH, TAG_PUBK, TAG_RADI,
TAG_ROOT, TAG_SIG, TAG_SREP,
};
use crate::wire::RtMessage;
const DELEGATION_SIGNATURE_CONTEXT: &[u8] = b"RoughTime v1 delegation signature--\x00";
const RESPONSE_SIGNATURE_CONTEXT: &[u8] = b"RoughTime v1 response signature\x00";
#[derive(Debug, Clone, Copy)]
pub struct VerifiedTime {
pub midpoint_micros: u64,
pub radius_micros: u32,
}
impl VerifiedTime {
#[must_use]
pub const fn as_unix_seconds(&self) -> u64 {
self.midpoint_micros / 1_000_000
}
}
pub fn verify_response(
data: &[u8],
nonce: &crate::request::Nonce,
server_pubkey_base64: &str,
) -> Result<VerifiedTime, Error> {
let resp = RtMessage::parse(data)?;
let cert_bytes = resp.get(TAG_CERT).ok_or(Error::MissingTag(TAG_CERT))?;
let srep_bytes = resp.get(TAG_SREP).ok_or(Error::MissingTag(TAG_SREP))?;
let sig_bytes = resp.get(TAG_SIG).ok_or(Error::MissingTag(TAG_SIG))?;
let cert = RtMessage::parse(cert_bytes)?;
let dele_bytes = cert.get(TAG_DELE).ok_or(Error::MissingTag(TAG_DELE))?;
let cert_sig_bytes = cert.get(TAG_SIG).ok_or(Error::MissingTag(TAG_SIG))?;
let root_pubkey_bytes =
Base64::decode_vec(server_pubkey_base64).map_err(|_err| Error::InvalidPublicKeyEncoding)?;
let mut dele_signed_data =
Vec::with_capacity(DELEGATION_SIGNATURE_CONTEXT.len() + dele_bytes.len());
dele_signed_data.extend_from_slice(DELEGATION_SIGNATURE_CONTEXT);
dele_signed_data.extend_from_slice(dele_bytes);
verify_ed25519(&root_pubkey_bytes, &dele_signed_data, cert_sig_bytes)
.map_err(|_err| Error::SignatureInvalid)?;
let dele = RtMessage::parse(dele_bytes)?;
let online_pubkey = dele.get(TAG_PUBK).ok_or(Error::MissingTag(TAG_PUBK))?;
let mint_bytes = dele.get(TAG_MINT).ok_or(Error::MissingTag(TAG_MINT))?;
let maxt_bytes = dele.get(TAG_MAXT).ok_or(Error::MissingTag(TAG_MAXT))?;
let mint = u64::from_le_bytes(
mint_bytes
.try_into()
.map_err(|_err| Error::InvalidFieldSize(TAG_MINT))?,
);
let maxt = u64::from_le_bytes(
maxt_bytes
.try_into()
.map_err(|_err| Error::InvalidFieldSize(TAG_MAXT))?,
);
let mut srep_signed_data =
Vec::with_capacity(RESPONSE_SIGNATURE_CONTEXT.len() + srep_bytes.len());
srep_signed_data.extend_from_slice(RESPONSE_SIGNATURE_CONTEXT);
srep_signed_data.extend_from_slice(srep_bytes);
verify_ed25519(online_pubkey, &srep_signed_data, sig_bytes)
.map_err(|_err| Error::SignatureInvalid)?;
let srep = RtMessage::parse(srep_bytes)?;
let root_hash = srep.get(TAG_ROOT).ok_or(Error::MissingTag(TAG_ROOT))?;
let midpoint_bytes = srep.get(TAG_MIDP).ok_or(Error::MissingTag(TAG_MIDP))?;
let radius_bytes = srep.get(TAG_RADI).ok_or(Error::MissingTag(TAG_RADI))?;
let midpoint = u64::from_le_bytes(
midpoint_bytes
.try_into()
.map_err(|_err| Error::InvalidFieldSize(TAG_MIDP))?,
);
let radius = u32::from_le_bytes(
radius_bytes
.try_into()
.map_err(|_err| Error::InvalidFieldSize(TAG_RADI))?,
);
if midpoint < mint || midpoint > maxt {
return Err(Error::DelegationNotValidAtMidpoint);
}
let index_bytes = resp.get(TAG_INDX).ok_or(Error::MissingTag(TAG_INDX))?;
let path_bytes = resp.get(TAG_PATH).ok_or(Error::MissingTag(TAG_PATH))?;
let index = u32::from_le_bytes(
index_bytes
.try_into()
.map_err(|_err| Error::InvalidFieldSize(TAG_INDX))?,
);
if path_bytes.len() % 32 != 0 {
return Err(Error::InvalidMerklePathLength);
}
let mut current_hash = truncated_sha512(&[&[0x00], nonce.as_bytes().as_slice()]);
let mut idx = index;
for sibling in path_bytes.chunks_exact(32) {
current_hash = if idx & 1 == 0 {
truncated_sha512(&[&[0x01], ¤t_hash, sibling])
} else {
truncated_sha512(&[&[0x01], sibling, ¤t_hash])
};
idx >>= 1;
}
if root_hash.len() < 32 {
return Err(Error::InvalidFieldSize(TAG_ROOT));
}
if current_hash != root_hash[0..32] {
return Err(Error::MerkleRootMismatch);
}
if idx != 0 {
return Err(Error::MerkleIndexOutOfBounds);
}
floor::check_floor(midpoint / 1_000_000)?;
Ok(VerifiedTime {
midpoint_micros: midpoint,
radius_micros: radius,
})
}
fn truncated_sha512(parts: &[&[u8]]) -> [u8; 32] {
let mut buf = Vec::new();
for part in parts {
buf.extend_from_slice(part);
}
let full = sha512(&buf);
let mut out = [0u8; 32];
out.copy_from_slice(&full[0..32]);
out
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::verify_response;
use crate::Error;
use crate::floor::EFFECTIVE_FLOOR_SECS;
use crate::request::Nonce;
use crate::test_fixtures::{FixtureParams, build_signed_response};
const VALID_MIDPOINT_MICROS: u64 = (EFFECTIVE_FLOOR_SECS + 3600) * 1_000_000;
#[test]
fn accepts_a_valid_response() {
let params = FixtureParams::valid(0x11, VALID_MIDPOINT_MICROS);
let signed = build_signed_response(¶ms);
let verified =
verify_response(&signed.bytes, ¶ms.nonce, &signed.root_pubkey_base64).unwrap();
assert_eq!(verified.midpoint_micros, VALID_MIDPOINT_MICROS);
assert_eq!(verified.as_unix_seconds(), VALID_MIDPOINT_MICROS / 1_000_000);
}
#[test]
fn accepts_a_response_with_a_multi_level_merkle_path() {
let mut params = FixtureParams::valid(0x22, VALID_MIDPOINT_MICROS);
params.index = 2;
params.path = alloc::vec![[0xAA; 32], [0xBB; 32]];
let signed = build_signed_response(¶ms);
let verified =
verify_response(&signed.bytes, ¶ms.nonce, &signed.root_pubkey_base64).unwrap();
assert_eq!(verified.midpoint_micros, VALID_MIDPOINT_MICROS);
}
#[test]
fn rejects_wrong_nonce_via_merkle_root_mismatch() {
let params = FixtureParams::valid(0x33, VALID_MIDPOINT_MICROS);
let signed = build_signed_response(¶ms);
let wrong_nonce = Nonce::new([0x99; 64]);
let err = verify_response(&signed.bytes, &wrong_nonce, &signed.root_pubkey_base64)
.unwrap_err();
assert_eq!(err, Error::MerkleRootMismatch);
}
#[test]
fn rejects_index_not_fully_consumed_by_path() {
let mut params = FixtureParams::valid(0x44, VALID_MIDPOINT_MICROS);
params.index = 1; let signed = build_signed_response(¶ms);
let err =
verify_response(&signed.bytes, ¶ms.nonce, &signed.root_pubkey_base64).unwrap_err();
assert_eq!(err, Error::MerkleIndexOutOfBounds);
}
#[test]
fn rejects_corrupt_delegation_signature() {
let mut params = FixtureParams::valid(0x55, VALID_MIDPOINT_MICROS);
params.corrupt_cert_sig = true;
let signed = build_signed_response(¶ms);
let err =
verify_response(&signed.bytes, ¶ms.nonce, &signed.root_pubkey_base64).unwrap_err();
assert_eq!(err, Error::SignatureInvalid);
}
#[test]
fn rejects_corrupt_response_signature() {
let mut params = FixtureParams::valid(0x66, VALID_MIDPOINT_MICROS);
params.corrupt_response_sig = true;
let signed = build_signed_response(¶ms);
let err =
verify_response(&signed.bytes, ¶ms.nonce, &signed.root_pubkey_base64).unwrap_err();
assert_eq!(err, Error::SignatureInvalid);
}
#[test]
fn rejects_midpoint_outside_delegation_validity() {
let mut params = FixtureParams::valid(0x77, VALID_MIDPOINT_MICROS);
params.maxt_micros = VALID_MIDPOINT_MICROS - 1;
let signed = build_signed_response(¶ms);
let err =
verify_response(&signed.bytes, ¶ms.nonce, &signed.root_pubkey_base64).unwrap_err();
assert_eq!(err, Error::DelegationNotValidAtMidpoint);
}
#[test]
fn rejects_midpoint_before_the_anti_rollback_floor() {
let params = FixtureParams::valid(0x88, (EFFECTIVE_FLOOR_SECS - 10) * 1_000_000);
let signed = build_signed_response(¶ms);
let err =
verify_response(&signed.bytes, ¶ms.nonce, &signed.root_pubkey_base64).unwrap_err();
assert!(matches!(err, Error::BeforeFloor { .. }));
}
#[test]
fn rejects_invalid_public_key_encoding() {
let params = FixtureParams::valid(0x99, VALID_MIDPOINT_MICROS);
let signed = build_signed_response(¶ms);
let err = verify_response(&signed.bytes, ¶ms.nonce, "not valid base64!!").unwrap_err();
assert_eq!(err, Error::InvalidPublicKeyEncoding);
}
#[test]
fn rejects_response_missing_cert_tag() {
let mut params = FixtureParams::valid(0xAA, VALID_MIDPOINT_MICROS);
params.omit_tag = Some(crate::tags::TAG_CERT);
let signed = build_signed_response(¶ms);
let err =
verify_response(&signed.bytes, ¶ms.nonce, &signed.root_pubkey_base64).unwrap_err();
assert_eq!(err, Error::MissingTag(crate::tags::TAG_CERT));
}
#[test]
fn rejects_response_missing_srep_tag() {
let mut params = FixtureParams::valid(0xBB, VALID_MIDPOINT_MICROS);
params.omit_tag = Some(crate::tags::TAG_SREP);
let signed = build_signed_response(¶ms);
let err =
verify_response(&signed.bytes, ¶ms.nonce, &signed.root_pubkey_base64).unwrap_err();
assert_eq!(err, Error::MissingTag(crate::tags::TAG_SREP));
}
#[test]
fn rejects_truncated_garbage() {
let err = verify_response(&[0x01, 0x02], &Nonce::new([0; 64]), "AAAA").unwrap_err();
assert_eq!(err, Error::MessageTooShort);
}
}