use std::{
fmt::{self, Display, Formatter},
sync::atomic::{AtomicU64, Ordering},
};
use iroh_base::{PublicKey, SecretKey, Signature};
use n0_error::{e, stack_error};
use simple_dns::{CLASS, Name, Packet, ResourceRecord, rdata::RData};
const MAX_DNS_PACKET_SIZE: usize = 1000;
const HEADER_SIZE: usize = 104;
pub const MAX_SIGNED_PACKET_SIZE: usize = HEADER_SIZE + MAX_DNS_PACKET_SIZE;
#[derive(Clone, PartialEq, Eq, derive_more::Debug)]
#[debug("SignedPacket {{ public_key: {}, timestamp: {:?} }}", self.public_key(), self.timestamp())]
pub struct SignedPacket {
bytes: Vec<u8>,
}
impl SignedPacket {
pub const MAX_BYTES: usize = MAX_SIGNED_PACKET_SIZE;
pub fn from_txt_strings(
secret_key: &SecretKey,
name: &str,
values: impl IntoIterator<Item = impl AsRef<str>>,
ttl: u32,
) -> Result<SignedPacket, SignedPacketBuildError> {
let public_key = secret_key.public();
let origin = public_key.to_z32();
let normalized = normalize_name(&origin, name.to_string());
let dns_name = Name::new_unchecked(&normalized).into_owned();
let mut packet = Packet::new_reply(0);
for value in values {
let mut txt = simple_dns::rdata::TXT::new();
txt.add_string(value.as_ref())
.map_err(|e| e!(SignedPacketBuildError::DnsError, e))?;
packet.answers.push(ResourceRecord::new(
dns_name.clone(),
CLASS::IN,
ttl,
RData::TXT(txt.into_owned()),
));
}
let encoded_packet = packet
.build_bytes_vec_compressed()
.map_err(|e| e!(SignedPacketBuildError::DnsError, e))?;
if encoded_packet.len() > MAX_DNS_PACKET_SIZE {
return Err(e!(SignedPacketBuildError::PacketTooLarge {
len: encoded_packet.len()
}));
}
let timestamp = Timestamp::now();
let signature = secret_key.sign(&signable(timestamp.as_micros(), &encoded_packet));
let mut bytes = Vec::with_capacity(HEADER_SIZE + encoded_packet.len());
bytes.extend_from_slice(public_key.as_bytes());
bytes.extend_from_slice(&signature.to_bytes());
bytes.extend_from_slice(×tamp.to_be_bytes());
bytes.extend_from_slice(&encoded_packet);
Ok(SignedPacket { bytes })
}
pub fn from_bytes(bytes: &[u8]) -> Result<SignedPacket, SignedPacketVerifyError> {
if bytes.len() < HEADER_SIZE {
return Err(e!(SignedPacketVerifyError::TooShort { len: bytes.len() }));
}
if bytes.len() > MAX_SIGNED_PACKET_SIZE {
return Err(e!(SignedPacketVerifyError::TooLarge { len: bytes.len() }));
}
let public_key = PublicKey::try_from(&bytes[..32])
.map_err(|e| e!(SignedPacketVerifyError::InvalidKey, e))?;
let signature =
Signature::from_bytes(bytes[32..96].try_into().expect("64 bytes for signature"));
let timestamp =
u64::from_be_bytes(bytes[96..104].try_into().expect("8 bytes for timestamp"));
let encoded_packet = &bytes[104..];
public_key
.verify(&signable(timestamp, encoded_packet), &signature)
.map_err(|e| e!(SignedPacketVerifyError::SignatureError, e))?;
Packet::parse(encoded_packet).map_err(|e| e!(SignedPacketVerifyError::DnsError, e))?;
Ok(SignedPacket {
bytes: bytes.to_vec(),
})
}
pub fn from_relay_payload(
public_key: &PublicKey,
payload: &[u8],
) -> Result<SignedPacket, SignedPacketVerifyError> {
let mut bytes = Vec::with_capacity(32 + payload.len());
bytes.extend_from_slice(public_key.as_bytes());
bytes.extend_from_slice(payload);
Self::from_bytes(&bytes)
}
pub fn from_bytes_unchecked(bytes: &[u8]) -> Result<SignedPacket, SignedPacketVerifyError> {
if bytes.len() < HEADER_SIZE {
return Err(e!(SignedPacketVerifyError::TooShort { len: bytes.len() }));
}
if bytes.len() > MAX_SIGNED_PACKET_SIZE {
return Err(e!(SignedPacketVerifyError::TooLarge { len: bytes.len() }));
}
Packet::parse(&bytes[104..]).map_err(|e| e!(SignedPacketVerifyError::DnsError, e))?;
Ok(SignedPacket {
bytes: bytes.to_vec(),
})
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
pub fn to_relay_payload(&self) -> Vec<u8> {
self.bytes[32..].to_vec()
}
pub fn public_key(&self) -> PublicKey {
PublicKey::try_from(&self.bytes[..32]).expect("valid public key in SignedPacket")
}
pub fn signature(&self) -> Signature {
Signature::from_bytes(
self.bytes[32..96]
.try_into()
.expect("64 bytes for signature"),
)
}
pub fn timestamp(&self) -> Timestamp {
Timestamp::from_be_bytes(
self.bytes[96..104]
.try_into()
.expect("8 bytes for timestamp"),
)
}
pub fn encoded_packet(&self) -> &[u8] {
&self.bytes[104..]
}
pub fn txt_records(&self, name: &str) -> Vec<String> {
let origin = self.public_key().to_z32();
let normalized = normalize_name(&origin, name.to_string());
let Ok(packet) = Packet::parse(self.encoded_packet()) else {
return Vec::new();
};
let Ok(zone) = Name::new(&origin) else {
return Vec::new();
};
packet
.answers
.iter()
.filter_map(|rr| match &rr.rdata {
RData::TXT(txt) => {
let rr_name = rr.name.to_string();
if rr_name == normalized {
String::try_from(txt.clone()).ok()
} else if let Some(relative) = rr.name.without(&zone) {
if relative.to_string() == name {
String::try_from(txt.clone()).ok()
} else {
None
}
} else {
None
}
}
_ => None,
})
.collect()
}
pub fn all_txt_records(&self) -> Vec<(String, String)> {
let origin = self.public_key().to_z32();
let Ok(packet) = Packet::parse(self.encoded_packet()) else {
return Vec::new();
};
let Ok(zone) = Name::new(&origin) else {
return Vec::new();
};
packet
.answers
.iter()
.filter_map(|rr| match &rr.rdata {
RData::TXT(txt) => {
let relative_name = rr
.name
.without(&zone)
.map(|n| n.to_string())
.unwrap_or_default();
let value = String::try_from(txt.clone()).ok()?;
Some((relative_name, value))
}
_ => None,
})
.collect()
}
pub fn from_parts_unchecked(
public_key: &[u8],
signature: &[u8],
timestamp: Timestamp,
encoded_packet: &[u8],
) -> Result<Self, SignedPacketVerifyError> {
let mut bytes = Vec::with_capacity(HEADER_SIZE + encoded_packet.len());
bytes.extend_from_slice(public_key);
bytes.extend_from_slice(signature);
bytes.extend_from_slice(×tamp.to_be_bytes());
bytes.extend_from_slice(encoded_packet);
Self::from_bytes_unchecked(&bytes)
}
pub fn more_recent_than(&self, other: &SignedPacket) -> bool {
if self.timestamp() == other.timestamp() {
self.encoded_packet() > other.encoded_packet()
} else {
self.timestamp() > other.timestamp()
}
}
}
impl Display for SignedPacket {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(f, "SignedPacket ({}):", self.public_key().to_z32())?;
writeln!(f, " timestamp: {}µs", self.timestamp().as_micros())?;
for (name, value) in self.all_txt_records() {
writeln!(f, " {name} TXT \"{value}\"")?;
}
Ok(())
}
}
fn signable(timestamp: u64, v: &[u8]) -> Vec<u8> {
let mut signable = format!("3:seqi{}e1:v{}:", timestamp, v.len()).into_bytes();
signable.extend(v);
signable
}
fn normalize_name(origin: &str, name: String) -> String {
let name = if name.ends_with('.') {
name[..name.len() - 1].to_string()
} else {
name
};
let parts: Vec<&str> = name.split('.').collect();
let last = *parts.last().unwrap_or(&"");
if last == origin {
return name;
}
if last == "@" || last.is_empty() {
return origin.to_string();
}
format!("{name}.{origin}")
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, derive_more::Debug)]
#[debug("Timestamp({}µs)", _0)]
pub struct Timestamp(u64);
static LAST_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
impl Timestamp {
pub fn now() -> Self {
use n0_future::time::SystemTime;
let micros = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("system time before UNIX epoch")
.as_micros() as u64;
let mut last = LAST_TIMESTAMP.load(Ordering::Relaxed);
loop {
let next = micros.max(last + 1);
match LAST_TIMESTAMP.compare_exchange_weak(
last,
next,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => return Self(next),
Err(actual) => last = actual,
}
}
}
pub fn from_micros(micros: u64) -> Self {
Self(micros)
}
pub fn as_micros(self) -> u64 {
self.0
}
pub fn to_be_bytes(self) -> [u8; 8] {
self.0.to_be_bytes()
}
pub fn from_be_bytes(bytes: [u8; 8]) -> Self {
Self(u64::from_be_bytes(bytes))
}
}
#[allow(missing_docs)]
#[stack_error(derive, add_meta)]
#[non_exhaustive]
pub enum SignedPacketBuildError {
#[error("DNS packet too large: {len} bytes (max {MAX_DNS_PACKET_SIZE})")]
PacketTooLarge { len: usize },
#[error("DNS encoding error")]
DnsError {
#[error(std_err)]
source: simple_dns::SimpleDnsError,
},
}
#[allow(missing_docs)]
#[stack_error(derive, add_meta)]
#[non_exhaustive]
pub enum SignedPacketVerifyError {
#[error("Signed packet too short: {len} bytes (min {HEADER_SIZE})")]
TooShort { len: usize },
#[error("Signed packet too large: {len} bytes (max {MAX_SIGNED_PACKET_SIZE})")]
TooLarge { len: usize },
#[error("Invalid signature")]
SignatureError {
#[error(std_err)]
source: iroh_base::SignatureError,
},
#[error("DNS decoding error")]
DnsError {
#[error(std_err)]
source: simple_dns::SimpleDnsError,
},
#[error("Invalid public key")]
InvalidKey { source: iroh_base::KeyParsingError },
}