use crate::error::VerifierError;
pub const RECEIPT_SIZE: usize = 42;
pub const RECEIPT_VERSION: u8 = 0x01;
pub const VERIFICATION_HASH_OFFSET: usize = 1;
pub const VERIFICATION_HASH_SIZE: usize = 32;
pub const VERIFIED_AT_OFFSET: usize = 33;
pub const VERIFIED_AT_SIZE: usize = 8;
pub const ALGORITHM_FLAGS_OFFSET: usize = 41;
pub const ALG_DILITHIUM: u8 = 0b0000_0001;
pub const ALG_FALCON: u8 = 0b0000_0010;
pub const ALG_SPHINCS: u8 = 0b0000_0100;
pub const ALG_KNOWN_MASK: u8 = ALG_DILITHIUM | ALG_FALCON | ALG_SPHINCS;
pub const ALG_ALL_THREE: u8 = ALG_DILITHIUM | ALG_FALCON | ALG_SPHINCS;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AlgorithmFlags(u8);
impl AlgorithmFlags {
#[must_use]
pub const fn all_three() -> Self {
Self(ALG_ALL_THREE)
}
#[must_use]
pub const fn from_byte(byte: u8) -> Self {
Self(byte)
}
pub const fn validated_from_byte(byte: u8) -> Result<Self, VerifierError> {
if byte & !ALG_KNOWN_MASK != 0 {
return Err(VerifierError::UnknownAlgorithmBits { flags: byte });
}
Ok(Self(byte))
}
#[must_use]
pub const fn as_byte(self) -> u8 {
self.0
}
#[must_use]
pub const fn has_dilithium(self) -> bool {
self.0 & ALG_DILITHIUM != 0
}
#[must_use]
pub const fn has_falcon(self) -> bool {
self.0 & ALG_FALCON != 0
}
#[must_use]
pub const fn has_sphincs(self) -> bool {
self.0 & ALG_SPHINCS != 0
}
#[must_use]
pub const fn count(self) -> u32 {
self.0.count_ones()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompactReceipt {
verification_hash: [u8; VERIFICATION_HASH_SIZE],
verified_at_ms: u64,
flags: AlgorithmFlags,
}
impl CompactReceipt {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, VerifierError> {
if bytes.len() != RECEIPT_SIZE {
return Err(VerifierError::InvalidReceiptSize {
actual: bytes.len(),
expected: RECEIPT_SIZE,
});
}
let version = bytes.first().copied().unwrap_or(0);
if version != RECEIPT_VERSION {
return Err(VerifierError::UnsupportedReceiptVersion {
actual: version,
expected: RECEIPT_VERSION,
});
}
let mut verification_hash = [0u8; VERIFICATION_HASH_SIZE];
let hash_end = VERIFICATION_HASH_OFFSET + VERIFICATION_HASH_SIZE;
let hash_slice = bytes
.get(VERIFICATION_HASH_OFFSET..hash_end)
.ok_or(VerifierError::InvalidReceiptSize {
actual: bytes.len(),
expected: RECEIPT_SIZE,
})?;
verification_hash.copy_from_slice(hash_slice);
let mut ts_bytes = [0u8; VERIFIED_AT_SIZE];
let ts_end = VERIFIED_AT_OFFSET + VERIFIED_AT_SIZE;
let ts_slice = bytes
.get(VERIFIED_AT_OFFSET..ts_end)
.ok_or(VerifierError::InvalidReceiptSize {
actual: bytes.len(),
expected: RECEIPT_SIZE,
})?;
ts_bytes.copy_from_slice(ts_slice);
let verified_at_ms = u64::from_be_bytes(ts_bytes);
let flags_byte = bytes
.get(ALGORITHM_FLAGS_OFFSET)
.copied()
.ok_or(VerifierError::InvalidReceiptSize {
actual: bytes.len(),
expected: RECEIPT_SIZE,
})?;
let flags = AlgorithmFlags::validated_from_byte(flags_byte)?;
Ok(Self {
verification_hash,
verified_at_ms,
flags,
})
}
pub fn from_hex(hex_str: &str) -> Result<Self, VerifierError> {
if hex_str.len() != RECEIPT_SIZE * 2 {
return Err(VerifierError::InvalidReceiptHeaderLength {
actual: hex_str.len(),
});
}
let bytes = hex::decode(hex_str).map_err(|e| {
VerifierError::InvalidReceiptHeaderHex(alloc::format!("{e}"))
})?;
Self::from_bytes(&bytes)
}
#[must_use]
pub const fn verification_hash(&self) -> &[u8; VERIFICATION_HASH_SIZE] {
&self.verification_hash
}
#[must_use]
pub const fn verified_at_ms(&self) -> u64 {
self.verified_at_ms
}
#[must_use]
pub const fn flags(&self) -> AlgorithmFlags {
self.flags
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture_bytes() -> [u8; RECEIPT_SIZE] {
let mut bytes = [0u8; RECEIPT_SIZE];
bytes[0] = RECEIPT_VERSION;
for b in &mut bytes[VERIFICATION_HASH_OFFSET..VERIFIED_AT_OFFSET] {
*b = 0xAB;
}
bytes[VERIFIED_AT_OFFSET..VERIFIED_AT_OFFSET + VERIFIED_AT_SIZE]
.copy_from_slice(&0x1234_5678_u64.to_be_bytes());
bytes[ALGORITHM_FLAGS_OFFSET] = ALG_ALL_THREE;
bytes
}
#[test]
fn parses_known_good_receipt() {
let receipt = CompactReceipt::from_bytes(&fixture_bytes()).unwrap();
assert_eq!(receipt.verified_at_ms(), 0x1234_5678);
assert!(receipt.flags().has_dilithium());
assert!(receipt.flags().has_falcon());
assert!(receipt.flags().has_sphincs());
assert_eq!(receipt.flags().count(), 3);
assert_eq!(receipt.verification_hash()[0], 0xAB);
assert_eq!(receipt.verification_hash()[31], 0xAB);
}
#[test]
fn rejects_wrong_size() {
let too_small = [0u8; RECEIPT_SIZE - 1];
assert!(matches!(
CompactReceipt::from_bytes(&too_small),
Err(VerifierError::InvalidReceiptSize { actual: 41, .. })
));
let too_big = [0u8; RECEIPT_SIZE + 1];
assert!(matches!(
CompactReceipt::from_bytes(&too_big),
Err(VerifierError::InvalidReceiptSize { actual: 43, .. })
));
}
#[test]
fn rejects_wrong_version() {
let mut bytes = fixture_bytes();
bytes[0] = 0x02;
assert!(matches!(
CompactReceipt::from_bytes(&bytes),
Err(VerifierError::UnsupportedReceiptVersion {
actual: 0x02,
expected: 0x01
})
));
}
#[test]
fn rejects_unknown_algorithm_bits() {
let mut bytes = fixture_bytes();
bytes[ALGORITHM_FLAGS_OFFSET] = 0b0000_1111; assert!(matches!(
CompactReceipt::from_bytes(&bytes),
Err(VerifierError::UnknownAlgorithmBits { flags: 0b0000_1111 })
));
}
#[test]
fn accepts_partial_algorithm_sets() {
let mut bytes = fixture_bytes();
bytes[ALGORITHM_FLAGS_OFFSET] = ALG_DILITHIUM;
let receipt = CompactReceipt::from_bytes(&bytes).unwrap();
assert!(receipt.flags().has_dilithium());
assert!(!receipt.flags().has_falcon());
assert!(!receipt.flags().has_sphincs());
assert_eq!(receipt.flags().count(), 1);
bytes[ALGORITHM_FLAGS_OFFSET] = ALG_DILITHIUM | ALG_FALCON;
let receipt = CompactReceipt::from_bytes(&bytes).unwrap();
assert_eq!(receipt.flags().count(), 2);
}
#[test]
fn parses_hex_from_header() {
let bytes = fixture_bytes();
let hex_str = hex::encode(bytes);
let receipt = CompactReceipt::from_hex(&hex_str).unwrap();
assert_eq!(receipt.verified_at_ms(), 0x1234_5678);
}
#[test]
fn rejects_wrong_hex_length() {
let short = "ab".repeat(41) + "a";
assert!(matches!(
CompactReceipt::from_hex(&short),
Err(VerifierError::InvalidReceiptHeaderLength { actual: 83 })
));
}
#[test]
fn rejects_invalid_hex_characters() {
let bad = "z".repeat(RECEIPT_SIZE * 2);
assert!(matches!(
CompactReceipt::from_hex(&bad),
Err(VerifierError::InvalidReceiptHeaderHex(_))
));
}
}