use alloc::vec::Vec;
use super::fors_c;
use super::hypertree::LayerSignature;
use crate::abi::{encode_dynamic_array, encode_tuple, AbiReader, Field};
use crate::profiles::NUM_HYPERTREE_LAYERS;
use crate::HASH_LEN;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signature {
pub fors: fors_c::Signature,
pub hypertree: Vec<LayerSignature>,
}
impl Signature {
pub(crate) fn encode_body(&self) -> Vec<u8> {
encode_tuple(alloc::vec![
Field::Dynamic(self.fors.to_bytes()),
Field::Dynamic(encode_dynamic_array(
self.hypertree
.iter()
.map(LayerSignature::to_bytes)
.collect(),
)),
])
}
pub fn to_bytes(&self) -> Vec<u8> {
encode_tuple(alloc::vec![Field::Dynamic(self.encode_body())])
}
pub(crate) fn decode(reader: &AbiReader, base: usize) -> Option<Self> {
let fors_start = reader.decode_offset(base, base)?;
Some(Self {
fors: fors_c::Signature::decode(reader, fors_start)?,
hypertree: reader.decode_dynamic_array(
base,
base.checked_add(32)?,
NUM_HYPERTREE_LAYERS as usize,
LayerSignature::decode,
)?,
})
}
pub fn from_bytes(data: &[u8]) -> Option<Self> {
let reader = AbiReader::new(data);
let signature_start = reader.decode_offset(0, 0)?;
let decoded = Self::decode(&reader, signature_start)?;
reader.finish()?;
Some(decoded)
}
}
impl TryFrom<&[u8]> for Signature {
type Error = ();
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Self::from_bytes(value).ok_or(())
}
}
pub fn encode_public_key(pk_seed: [u8; HASH_LEN], hypertree_root: [u8; HASH_LEN]) -> [u8; 64] {
let mut out = [0u8; 64];
out[..32].copy_from_slice(&pk_seed);
out[32..].copy_from_slice(&hypertree_root);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sphincs_plus_c::fors_c::Entry;
use crate::wots_c::Signature as WotsCSignature;
use alloc::vec;
fn sample_signature() -> Signature {
Signature {
fors: fors_c::Signature {
randomizer: [0x77; HASH_LEN],
counter: 7,
entries: vec![
Entry {
secret_leaf: [0x88; HASH_LEN],
auth_path: vec![[0x99; HASH_LEN], [0xA0; HASH_LEN]],
},
Entry {
secret_leaf: [0xA1; HASH_LEN],
auth_path: vec![[0xA2; HASH_LEN]],
},
],
},
hypertree: (0..NUM_HYPERTREE_LAYERS)
.map(|layer| LayerSignature {
wots_c_pk_hash: [0xB1 ^ layer; HASH_LEN],
wots_c_signature: WotsCSignature {
randomizer: [0xB2 ^ layer; HASH_LEN],
counter: 9 + layer as u32,
chains: vec![[0xB3 ^ layer; HASH_LEN], [0xB4 ^ layer; HASH_LEN]],
},
auth_path: vec![[0xB5 ^ layer; HASH_LEN]],
})
.collect(),
}
}
#[test]
fn to_bytes_from_bytes_round_trips() {
let signature = sample_signature();
let encoded = signature.to_bytes();
let decoded = Signature::from_bytes(&encoded).expect("valid encoding must decode");
assert_eq!(decoded, signature);
assert_eq!(decoded.to_bytes(), encoded);
}
#[test]
fn empty_hypertree_round_trips_as_empty() {
let signature = Signature {
fors: fors_c::Signature {
randomizer: [0x01; HASH_LEN],
counter: 0,
entries: vec![],
},
hypertree: vec![],
};
let encoded = signature.to_bytes();
assert_eq!(Signature::from_bytes(&encoded), Some(signature));
}
#[test]
fn from_bytes_rejects_trailing_bytes() {
let mut encoded = sample_signature().to_bytes();
encoded.extend_from_slice(&[0xAA, 0xBB]);
assert!(
Signature::from_bytes(&encoded).is_none(),
"trailing junk on the signature envelope must be rejected"
);
}
#[test]
fn try_from_delegates_to_from_bytes() {
let signature = sample_signature();
let encoded = signature.to_bytes();
let decoded = Signature::try_from(encoded.as_slice()).expect("valid encoding must decode");
assert_eq!(decoded, signature);
let truncated = &encoded[..encoded.len() - 1];
assert!(Signature::try_from(truncated).is_err());
}
fn read_abi_usize(buf: &[u8], pos: usize) -> usize {
usize::try_from(u64::from_be_bytes(
buf[pos + 24..pos + 32].try_into().unwrap(),
))
.unwrap()
}
fn write_abi_usize(buf: &mut [u8], pos: usize, value: usize) {
buf[pos..pos + 24].fill(0);
buf[pos + 24..pos + 32].copy_from_slice(&(value as u64).to_be_bytes());
}
#[test]
fn oversized_fors_entries_length_is_rejected() {
use crate::profiles::NUM_FORS_TREES;
let mut encoded = sample_signature().to_bytes();
let sig_start = read_abi_usize(&encoded, 0);
let fors_start = sig_start + read_abi_usize(&encoded, sig_start);
let entries_start = fors_start + read_abi_usize(&encoded, fors_start + 64);
write_abi_usize(&mut encoded, entries_start, NUM_FORS_TREES as usize + 1);
assert!(
Signature::from_bytes(&encoded).is_none(),
"FORS entries length NUM_FORS_TREES+1 must be rejected"
);
}
#[test]
fn aliased_dynamic_array_offsets_are_rejected() {
let mut encoded = sample_signature().to_bytes();
let sig_start = read_abi_usize(&encoded, 0);
let fors_start = sig_start + read_abi_usize(&encoded, sig_start);
let entries_start = fors_start + read_abi_usize(&encoded, fors_start + 64);
let entry0_start = entries_start + 32 + read_abi_usize(&encoded, entries_start + 32);
let auth_start = entry0_start + read_abi_usize(&encoded, entry0_start + 32);
let first_elem_rel = read_abi_usize(&encoded, auth_start + 32);
write_abi_usize(&mut encoded, auth_start + 64, first_elem_rel);
assert!(
Signature::from_bytes(&encoded).is_none(),
"aliased bytes[] element offsets must be rejected"
);
}
#[test]
fn stateless_key_encodes_to_64_byte_layout() {
let pk_seed = [0x12; HASH_LEN];
let hypertree_root = [0x34; HASH_LEN];
let encoded = encode_public_key(pk_seed, hypertree_root);
assert_eq!(encoded.len(), 64);
assert_eq!(&encoded[..HASH_LEN], &pk_seed); assert_eq!(&encoded[HASH_LEN..], &hypertree_root); }
}