use alloc::vec::Vec;
use core::fmt;
use rns_crypto::identity::Identity;
use rns_crypto::sha256::Sha256;
use crate::constants;
use crate::hash;
use crate::stamp::{stamp_valid, stamp_value, stamp_workblock, StampCost, StampValue};
#[derive(Debug)]
pub enum AnnounceError {
DataTooShort,
InvalidSignature,
DestinationMismatch,
SigningError,
}
impl fmt::Display for AnnounceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AnnounceError::DataTooShort => write!(f, "Announce data too short"),
AnnounceError::InvalidSignature => write!(f, "Invalid announce signature"),
AnnounceError::DestinationMismatch => write!(f, "Destination hash mismatch"),
AnnounceError::SigningError => write!(f, "Could not sign announce"),
}
}
}
#[derive(Debug, Clone)]
pub struct AnnounceData {
pub public_key: [u8; 64],
pub name_hash: [u8; 10],
pub random_hash: [u8; 10],
pub ratchet: Option<[u8; 32]>,
pub signature: [u8; 64],
pub app_data: Option<Vec<u8>>,
}
#[derive(Debug)]
pub struct ValidatedAnnounce {
pub identity_hash: [u8; 16],
pub public_key: [u8; 64],
pub name_hash: [u8; 10],
pub random_hash: [u8; 10],
pub ratchet: Option<[u8; 32]>,
pub app_data: Option<Vec<u8>>,
pub stamp: Option<[u8; constants::STAMP_LENGTH]>,
pub stamp_value: StampValue,
pub lr_cost: Option<StampCost>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnnounceStampStatus {
NotStamped,
Valid,
Invalid,
}
impl ValidatedAnnounce {
pub fn apply_stamp(
&mut self,
destination_hash: &[u8; 16],
required_value: StampCost,
) -> AnnounceStampStatus {
if !constants::STAMPS_ENABLED {
return AnnounceStampStatus::NotStamped;
}
let Some(app_data) = self.app_data.as_ref() else {
return AnnounceStampStatus::NotStamped;
};
if app_data.len() < constants::EXT_PAYLOAD_LENGTH {
return AnnounceStampStatus::NotStamped;
}
let ext = &app_data[app_data.len() - constants::EXT_PAYLOAD_LENGTH..];
if ext[0] != constants::EXT_PAYLOAD_MARKER {
return AnnounceStampStatus::NotStamped;
}
let Some(lr_cost) = crate::stamp::link_cost_from_ext_bytes(
ext[constants::EXT_PAYLOAD_LENGTH - 2],
ext[constants::EXT_PAYLOAD_LENGTH - 1],
) else {
return AnnounceStampStatus::NotStamped;
};
let mut stamp = [0u8; constants::STAMP_LENGTH];
stamp.copy_from_slice(
&ext[constants::EXT_HEADER_LENGTH
..constants::EXT_HEADER_LENGTH + constants::STAMP_LENGTH],
);
self.stamp = Some(stamp);
let mut material = Vec::with_capacity(16 + 64 + 10);
material.extend_from_slice(destination_hash);
material.extend_from_slice(&self.public_key);
material.extend_from_slice(&self.name_hash);
let workblock = stamp_workblock(&material, 0);
self.stamp_value = stamp_value(&workblock, &stamp);
if stamp_valid(&stamp, required_value, &workblock) {
let stripped_len = app_data.len() - constants::EXT_PAYLOAD_LENGTH;
self.app_data = if stripped_len == 0 {
None
} else {
Some(app_data[..stripped_len].to_vec())
};
self.lr_cost = Some(lr_cost);
AnnounceStampStatus::Valid
} else {
AnnounceStampStatus::Invalid
}
}
}
impl AnnounceData {
pub fn pack(
identity: &Identity,
destination_hash: &[u8; 16],
name_hash: &[u8; 10],
random_hash: &[u8; 10],
ratchet: Option<&[u8; 32]>,
app_data: Option<&[u8]>,
) -> Result<(Vec<u8>, bool), AnnounceError> {
let public_key = identity
.get_public_key()
.ok_or(AnnounceError::SigningError)?;
let mut signed_data = Vec::new();
signed_data.extend_from_slice(destination_hash);
signed_data.extend_from_slice(&public_key);
signed_data.extend_from_slice(name_hash);
signed_data.extend_from_slice(random_hash);
let has_ratchet = ratchet.is_some();
if let Some(r) = ratchet {
signed_data.extend_from_slice(r);
}
if let Some(ad) = app_data {
signed_data.extend_from_slice(ad);
}
let signature = identity
.sign(&signed_data)
.map_err(|_| AnnounceError::SigningError)?;
let mut announce_data = Vec::new();
announce_data.extend_from_slice(&public_key);
announce_data.extend_from_slice(name_hash);
announce_data.extend_from_slice(random_hash);
if let Some(r) = ratchet {
announce_data.extend_from_slice(r);
}
announce_data.extend_from_slice(&signature);
if let Some(ad) = app_data {
announce_data.extend_from_slice(ad);
}
Ok((announce_data, has_ratchet))
}
pub fn unpack(data: &[u8], has_ratchet: bool) -> Result<Self, AnnounceError> {
let keysize = constants::KEYSIZE / 8; let name_hash_len = constants::NAME_HASH_LENGTH / 8; let sig_len = constants::SIGLENGTH / 8; let ratchet_size = constants::RATCHETSIZE / 8;
let min_len = if has_ratchet {
keysize + name_hash_len + 10 + ratchet_size + sig_len
} else {
keysize + name_hash_len + 10 + sig_len
};
if data.len() < min_len {
return Err(AnnounceError::DataTooShort);
}
let mut public_key = [0u8; 64];
public_key.copy_from_slice(&data[..keysize]);
let mut name_hash = [0u8; 10];
name_hash.copy_from_slice(&data[keysize..keysize + name_hash_len]);
let mut random_hash = [0u8; 10];
random_hash.copy_from_slice(&data[keysize + name_hash_len..keysize + name_hash_len + 10]);
let (ratchet, sig_start) = if has_ratchet {
let mut ratchet = [0u8; 32];
ratchet.copy_from_slice(
&data[keysize + name_hash_len + 10..keysize + name_hash_len + 10 + ratchet_size],
);
(Some(ratchet), keysize + name_hash_len + 10 + ratchet_size)
} else {
(None, keysize + name_hash_len + 10)
};
let mut signature = [0u8; 64];
signature.copy_from_slice(&data[sig_start..sig_start + sig_len]);
let app_data_start = sig_start + sig_len;
let app_data = if data.len() > app_data_start {
Some(data[app_data_start..].to_vec())
} else {
None
};
Ok(AnnounceData {
public_key,
name_hash,
random_hash,
ratchet,
signature,
app_data,
})
}
fn for_each_signed_part(&self, destination_hash: &[u8; 16], mut f: impl FnMut(&[u8])) {
f(destination_hash);
f(&self.public_key);
f(&self.name_hash);
f(&self.random_hash);
if let Some(ref ratchet) = self.ratchet {
f(ratchet);
}
if let Some(ref ad) = self.app_data {
f(ad);
}
}
fn signed_data(&self, destination_hash: &[u8; 16]) -> Vec<u8> {
let mut signed_data = Vec::new();
self.for_each_signed_part(destination_hash, |part| signed_data.extend_from_slice(part));
signed_data
}
pub fn signature_cache_key(&self, destination_hash: &[u8; 16]) -> [u8; 32] {
let mut hasher = Sha256::new();
self.for_each_signed_part(destination_hash, |part| hasher.update(part));
hasher.update(&self.signature);
hasher.digest()
}
fn check_destination_binding(
&self,
destination_hash: &[u8; 16],
identity_hash: &[u8; 16],
) -> Result<(), AnnounceError> {
let mut hash_material = [0u8; 26];
hash_material[..10].copy_from_slice(&self.name_hash);
hash_material[10..].copy_from_slice(identity_hash);
let expected_hash = hash::truncated_hash(&hash_material);
if &expected_hash != destination_hash {
return Err(AnnounceError::DestinationMismatch);
}
Ok(())
}
fn to_validated(&self, identity_hash: [u8; 16]) -> ValidatedAnnounce {
ValidatedAnnounce {
identity_hash,
public_key: self.public_key,
name_hash: self.name_hash,
random_hash: self.random_hash,
ratchet: self.ratchet,
app_data: self.app_data.clone(),
stamp: None,
stamp_value: StampValue::ZERO,
lr_cost: None,
}
}
pub fn validate(
&self,
destination_hash: &[u8; 16],
) -> Result<ValidatedAnnounce, AnnounceError> {
let announced_identity = Identity::from_public_key(&self.public_key)
.map_err(|_| AnnounceError::InvalidSignature)?;
let signed_data = self.signed_data(destination_hash);
if !announced_identity.verify(&self.signature, &signed_data) {
return Err(AnnounceError::InvalidSignature);
}
let identity_hash = *announced_identity.hash();
self.check_destination_binding(destination_hash, &identity_hash)?;
Ok(self.to_validated(identity_hash))
}
pub fn to_validated_with_cached_signature(
&self,
destination_hash: &[u8; 16],
) -> Result<ValidatedAnnounce, AnnounceError> {
let identity_hash = hash::truncated_hash(&self.public_key);
self.check_destination_binding(destination_hash, &identity_hash)?;
Ok(self.to_validated(identity_hash))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::destination;
#[test]
fn test_pack_unpack_roundtrip_no_ratchet() {
let identity = Identity::from_private_key(&[0x42; 64]);
let id_hash = *identity.hash();
let nh = destination::name_hash("testapp", &["aspect"]);
let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
let random = [0xAA; 10];
let (data, has_ratchet) =
AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
assert!(!has_ratchet);
let parsed = AnnounceData::unpack(&data, false).unwrap();
assert_eq!(parsed.public_key, identity.get_public_key().unwrap());
assert_eq!(parsed.name_hash, nh);
assert_eq!(parsed.random_hash, random);
assert!(parsed.ratchet.is_none());
assert!(parsed.app_data.is_none());
}
#[test]
fn test_pack_unpack_roundtrip_with_ratchet() {
let identity = Identity::from_private_key(&[0x42; 64]);
let id_hash = *identity.hash();
let nh = destination::name_hash("testapp", &["aspect"]);
let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
let random = [0xBB; 10];
let ratchet = [0xCC; 32];
let (data, has_ratchet) =
AnnounceData::pack(&identity, &dh, &nh, &random, Some(&ratchet), None).unwrap();
assert!(has_ratchet);
let parsed = AnnounceData::unpack(&data, true).unwrap();
assert_eq!(parsed.ratchet.unwrap(), ratchet);
}
#[test]
fn test_pack_unpack_with_app_data() {
let identity = Identity::from_private_key(&[0x42; 64]);
let id_hash = *identity.hash();
let nh = destination::name_hash("testapp", &["aspect"]);
let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
let random = [0xDD; 10];
let app_data = b"hello app data";
let (data, _) =
AnnounceData::pack(&identity, &dh, &nh, &random, None, Some(app_data)).unwrap();
let parsed = AnnounceData::unpack(&data, false).unwrap();
assert_eq!(parsed.app_data.as_deref(), Some(app_data.as_slice()));
}
#[test]
fn test_validate_valid_announce() {
let identity = Identity::from_private_key(&[0x42; 64]);
let id_hash = *identity.hash();
let nh = destination::name_hash("testapp", &["aspect"]);
let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
let random = [0xEE; 10];
let (data, _) =
AnnounceData::pack(&identity, &dh, &nh, &random, None, Some(b"data")).unwrap();
let parsed = AnnounceData::unpack(&data, false).unwrap();
let validated = parsed.validate(&dh).unwrap();
assert_eq!(validated.identity_hash, id_hash);
assert_eq!(validated.name_hash, nh);
}
#[test]
fn test_validate_tampered_signature() {
let identity = Identity::from_private_key(&[0x42; 64]);
let id_hash = *identity.hash();
let nh = destination::name_hash("testapp", &["aspect"]);
let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
let random = [0xFF; 10];
let (mut data, _) = AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
data[84] ^= 0xFF;
let parsed = AnnounceData::unpack(&data, false).unwrap();
assert!(parsed.validate(&dh).is_err());
}
#[test]
fn test_validate_rejects_announce_with_invalid_signing_key() {
let identity = Identity::from_private_key(&[0x42; 64]);
let id_hash = *identity.hash();
let nh = destination::name_hash("testapp", &["aspect"]);
let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
let random = [0x22; 10];
let (mut data, _) = AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
data[32..64].copy_from_slice(&[0; 32]);
data[32] = 2;
let parsed = AnnounceData::unpack(&data, false).unwrap();
assert!(matches!(
parsed.validate(&dh),
Err(AnnounceError::InvalidSignature)
));
}
#[test]
fn test_validate_wrong_destination_hash() {
let identity = Identity::from_private_key(&[0x42; 64]);
let id_hash = *identity.hash();
let nh = destination::name_hash("testapp", &["aspect"]);
let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
let random = [0x11; 10];
let (data, _) = AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
let parsed = AnnounceData::unpack(&data, false).unwrap();
let wrong_hash = [0x00; 16];
assert!(parsed.validate(&wrong_hash).is_err());
}
#[test]
fn test_to_validated_with_cached_signature_matches_validate() {
let identity = Identity::from_private_key(&[0x42; 64]);
let id_hash = *identity.hash();
let nh = destination::name_hash("testapp", &["aspect"]);
let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
let random = [0xAB; 10];
let (data, _) =
AnnounceData::pack(&identity, &dh, &nh, &random, None, Some(b"appdata")).unwrap();
let parsed = AnnounceData::unpack(&data, false).unwrap();
let validated = parsed.validate(&dh).unwrap();
let unchecked = parsed.to_validated_with_cached_signature(&dh).unwrap();
assert_eq!(validated.identity_hash, unchecked.identity_hash);
assert_eq!(validated.public_key, unchecked.public_key);
assert_eq!(validated.name_hash, unchecked.name_hash);
assert_eq!(validated.random_hash, unchecked.random_hash);
assert_eq!(validated.ratchet, unchecked.ratchet);
assert_eq!(validated.app_data, unchecked.app_data);
}
fn signed_announce(
app_data: Option<&[u8]>,
ratchet: Option<&[u8; 32]>,
) -> (AnnounceData, [u8; 16]) {
let identity = Identity::from_private_key(&[0x42; 64]);
let id_hash = *identity.hash();
let nh = destination::name_hash("testapp", &["aspect"]);
let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
let (data, has_ratchet) =
AnnounceData::pack(&identity, &dh, &nh, &[0x31; 10], ratchet, app_data).unwrap();
(AnnounceData::unpack(&data, has_ratchet).unwrap(), dh)
}
#[test]
fn test_cached_signature_path_checks_destination_binding() {
let (parsed, dh) = signed_announce(None, None);
let mut other_dh = dh;
other_dh[0] ^= 0x01;
assert!(matches!(
parsed.to_validated_with_cached_signature(&other_dh),
Err(AnnounceError::DestinationMismatch)
));
let other = Identity::from_private_key(&[0x43; 64]);
let mut swapped = parsed.clone();
swapped.public_key = other.get_public_key().unwrap();
assert!(matches!(
swapped.to_validated_with_cached_signature(&dh),
Err(AnnounceError::DestinationMismatch)
));
}
#[test]
fn test_signature_cache_key_covers_every_signed_field() {
let ratchet = [0x44; 32];
let (base, dh) = signed_announce(Some(b"app"), Some(&ratchet));
let key = base.signature_cache_key(&dh);
assert_eq!(key, base.clone().signature_cache_key(&dh));
let mut other_dh = dh;
other_dh[15] ^= 0x01;
assert_ne!(key, base.signature_cache_key(&other_dh));
let mut variants = Vec::new();
let mut v = base.clone();
v.public_key[0] ^= 0x01;
variants.push(v);
let mut v = base.clone();
v.name_hash[0] ^= 0x01;
variants.push(v);
let mut v = base.clone();
v.random_hash[0] ^= 0x01;
variants.push(v);
let mut v = base.clone();
v.ratchet = None;
variants.push(v);
let mut v = base.clone();
v.ratchet.as_mut().unwrap()[0] ^= 0x01;
variants.push(v);
let mut v = base.clone();
v.app_data = None;
variants.push(v);
let mut v = base.clone();
v.app_data = Some(b"apq".to_vec());
variants.push(v);
let mut v = base.clone();
v.signature[0] ^= 0x01;
variants.push(v);
for variant in &variants {
assert_ne!(key, variant.signature_cache_key(&dh));
}
}
#[test]
fn test_signature_cache_key_is_hash_of_signed_data_and_signature() {
let (parsed, dh) = signed_announce(Some(b"payload"), None);
let mut material = parsed.signed_data(&dh);
material.extend_from_slice(&parsed.signature);
assert_eq!(parsed.signature_cache_key(&dh), hash::full_hash(&material));
}
const LINK_COST: StampCost = StampCost::from_fp(11 * 256 + 64);
fn stamped_announce(
app_data: Option<&[u8]>,
cost: StampCost,
) -> (
AnnounceData,
[u8; 16],
[u8; constants::STAMP_LENGTH],
StampValue,
) {
use rns_crypto::{OsRng, Rng};
let identity = Identity::from_private_key(&[0x42; 64]);
let id_hash = *identity.hash();
let nh = destination::name_hash("testapp", &["aspect"]);
let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
let public_key = identity.get_public_key().unwrap();
let mut material = Vec::new();
material.extend_from_slice(&dh);
material.extend_from_slice(&public_key);
material.extend_from_slice(&nh);
let workblock = stamp_workblock(&material, 0);
let mut rng = OsRng;
let mut stamp = [0u8; constants::STAMP_LENGTH];
loop {
rng.fill_bytes(&mut stamp);
if stamp_valid(&stamp, cost, &workblock) {
break;
}
}
let value = stamp_value(&workblock, &stamp);
let mut wire = app_data.map(|data| data.to_vec()).unwrap_or_default();
wire.push(constants::EXT_PAYLOAD_MARKER);
wire.push(0);
wire.extend_from_slice(&stamp);
wire.extend_from_slice(&crate::stamp::link_cost_ext_bytes(LINK_COST));
let (data, _) =
AnnounceData::pack(&identity, &dh, &nh, &[0x31; 10], None, Some(&wire)).unwrap();
(
AnnounceData::unpack(&data, false).unwrap(),
dh,
stamp,
value,
)
}
#[test]
fn test_announce_stamp_valid_is_stripped_and_valued() {
let (parsed, dh, stamp, value) =
stamped_announce(Some(b"app-data"), StampCost::from_whole(4));
let mut validated = parsed.validate(&dh).unwrap();
assert_eq!(
validated.apply_stamp(&dh, StampCost::from_whole(4)),
AnnounceStampStatus::Valid
);
assert_eq!(validated.stamp, Some(stamp));
assert_eq!(validated.stamp_value, value);
assert_eq!(validated.lr_cost, Some(LINK_COST));
assert_eq!(validated.app_data.as_deref(), Some(b"app-data".as_slice()));
}
#[test]
fn test_announce_stamp_valid_without_app_data_strips_to_none() {
let (parsed, dh, _, _) = stamped_announce(None, StampCost::from_whole(4));
let mut validated = parsed.validate(&dh).unwrap();
assert_eq!(
validated.apply_stamp(&dh, StampCost::from_whole(4)),
AnnounceStampStatus::Valid
);
assert_eq!(validated.app_data, None);
}
#[test]
fn test_announce_stamp_below_required_value_is_kept_invalid_and_valued() {
let (parsed, dh, stamp, value) =
stamped_announce(Some(b"app-data"), StampCost::from_whole(4));
let mut validated = parsed.validate(&dh).unwrap();
assert_eq!(
validated.apply_stamp(&dh, StampCost::from_whole(200)),
AnnounceStampStatus::Invalid
);
assert_eq!(validated.stamp, Some(stamp));
assert!(value > StampValue::ZERO);
assert_eq!(validated.stamp_value, value);
assert_eq!(validated.lr_cost, None);
assert_eq!(
validated.app_data.as_ref().map(|data| data.len()),
Some(b"app-data".len() + constants::EXT_PAYLOAD_LENGTH)
);
}
#[test]
fn test_announce_without_stamp_is_not_stamped() {
let (parsed, dh) = signed_announce(Some(b"app-data"), None);
let mut validated = parsed.validate(&dh).unwrap();
assert_eq!(
validated.apply_stamp(&dh, StampCost::from_whole(4)),
AnnounceStampStatus::NotStamped
);
assert_eq!(validated.stamp, None);
assert_eq!(validated.stamp_value, StampValue::ZERO);
assert_eq!(validated.lr_cost, None);
assert_eq!(validated.app_data.as_deref(), Some(b"app-data".as_slice()));
}
#[test]
fn test_announce_stamp_with_one_byte_link_cost_is_not_stamped() {
let (parsed, dh, _, _) = stamped_announce(Some(b"app-data"), StampCost::from_whole(4));
let mut app_data = parsed.app_data.clone().unwrap();
let fraction = app_data.len() - 2;
app_data.remove(fraction);
let identity = Identity::from_private_key(&[0x42; 64]);
let (data, _) = AnnounceData::pack(
&identity,
&dh,
&parsed.name_hash,
&parsed.random_hash,
None,
Some(&app_data),
)
.unwrap();
let mut validated = AnnounceData::unpack(&data, false)
.unwrap()
.validate(&dh)
.unwrap();
assert_eq!(
validated.apply_stamp(&dh, StampCost::from_whole(4)),
AnnounceStampStatus::NotStamped
);
assert_eq!(validated.lr_cost, None);
assert_eq!(validated.app_data, Some(app_data));
}
#[test]
fn test_announce_stamp_extended_format_is_not_interpreted() {
let (parsed, dh, _, _) = stamped_announce(Some(b"app-data"), StampCost::from_whole(4));
let mut announce = parsed.clone();
let app_data = announce.app_data.as_mut().unwrap();
let last = app_data.len() - 1;
app_data[last] |= 0x80;
let identity = Identity::from_private_key(&[0x42; 64]);
let (data, _) = AnnounceData::pack(
&identity,
&dh,
&announce.name_hash,
&announce.random_hash,
None,
Some(app_data),
)
.unwrap();
let reparsed = AnnounceData::unpack(&data, false).unwrap();
let mut validated = reparsed.validate(&dh).unwrap();
assert_eq!(
validated.apply_stamp(&dh, StampCost::from_whole(4)),
AnnounceStampStatus::NotStamped
);
assert_eq!(validated.stamp, None);
assert_eq!(validated.lr_cost, None);
}
}