Skip to main content

iroh_dns/
pkarr.rs

1//! Pkarr signed packet encoding.
2//!
3//! Implements the [pkarr] signed DNS packet format: `<32 pubkey><64 sig><8 timestamp><DNS packet>`.
4//!
5//! [pkarr]: https://pkarr.org
6
7use std::{
8    fmt::{self, Display, Formatter},
9    sync::atomic::Ordering,
10};
11
12use iroh_base::{PublicKey, SecretKey, Signature};
13use n0_error::{AnyError, anyerr, e, stack_error};
14use portable_atomic::AtomicU64;
15use simple_dns::{CLASS, Name, Packet, ResourceRecord, rdata::RData};
16
17/// Maximum size of the encoded DNS packet within a signed packet.
18const MAX_DNS_PACKET_SIZE: usize = 1000;
19
20/// Total header size: 32 (pubkey) + 64 (signature) + 8 (timestamp).
21const HEADER_SIZE: usize = 104;
22
23/// Maximum total size of a serialized signed packet.
24pub const MAX_SIGNED_PACKET_SIZE: usize = HEADER_SIZE + MAX_DNS_PACKET_SIZE;
25
26/// A signed DNS packet in the pkarr format.
27///
28/// Wire format: `<32 bytes pubkey><64 bytes signature><8 bytes BE timestamp><DNS wire format>`
29///
30/// The DNS packet must be at most 1000 bytes. Total max size is 1104 bytes.
31#[derive(Clone, PartialEq, Eq, derive_more::Debug)]
32#[debug("SignedPacket {{ public_key: {}, timestamp: {:?} }}", self.public_key(), self.timestamp())]
33pub struct SignedPacket {
34    bytes: Vec<u8>,
35}
36
37impl SignedPacket {
38    /// Maximum total byte size of a signed packet.
39    pub const MAX_BYTES: usize = MAX_SIGNED_PACKET_SIZE;
40
41    /// Create a signed packet containing TXT records under a single name.
42    ///
43    /// This is the common case: multiple TXT values under the same DNS name (e.g. `"_iroh"`).
44    pub fn from_txt_strings(
45        secret_key: &SecretKey,
46        name: &str,
47        values: impl IntoIterator<Item = impl AsRef<str>>,
48        ttl: u32,
49    ) -> Result<SignedPacket, SignedPacketBuildError> {
50        let public_key = secret_key.public();
51        let origin = public_key.to_z32();
52        let normalized = normalize_name(&origin, name.to_string());
53        let dns_name = Name::new_unchecked(&normalized).into_owned();
54        let mut packet = Packet::new_reply(0);
55
56        for value in values {
57            let mut txt = simple_dns::rdata::TXT::new();
58            txt.add_string(value.as_ref())
59                .map_err(|e| e!(SignedPacketBuildError::DnsError, anyerr!(e)))?;
60            packet.answers.push(ResourceRecord::new(
61                dns_name.clone(),
62                CLASS::IN,
63                ttl,
64                RData::TXT(txt.into_owned()),
65            ));
66        }
67
68        let encoded_packet = packet
69            .build_bytes_vec_compressed()
70            .map_err(|e| e!(SignedPacketBuildError::DnsError, anyerr!(e)))?;
71
72        if encoded_packet.len() > MAX_DNS_PACKET_SIZE {
73            return Err(e!(SignedPacketBuildError::PacketTooLarge {
74                len: encoded_packet.len()
75            }));
76        }
77
78        let timestamp = Timestamp::now();
79        let signature = secret_key.sign(&signable(timestamp.as_micros(), &encoded_packet));
80
81        let mut bytes = Vec::with_capacity(HEADER_SIZE + encoded_packet.len());
82        bytes.extend_from_slice(public_key.as_bytes());
83        bytes.extend_from_slice(&signature.to_bytes());
84        bytes.extend_from_slice(&timestamp.to_be_bytes());
85        bytes.extend_from_slice(&encoded_packet);
86
87        Ok(SignedPacket { bytes })
88    }
89
90    /// Parse and verify a signed packet from its wire representation.
91    pub fn from_bytes(bytes: &[u8]) -> Result<SignedPacket, SignedPacketVerifyError> {
92        if bytes.len() < HEADER_SIZE {
93            return Err(e!(SignedPacketVerifyError::TooShort { len: bytes.len() }));
94        }
95        if bytes.len() > MAX_SIGNED_PACKET_SIZE {
96            return Err(e!(SignedPacketVerifyError::TooLarge { len: bytes.len() }));
97        }
98
99        let public_key = PublicKey::try_from(&bytes[..32])
100            .map_err(|e| e!(SignedPacketVerifyError::InvalidKey, e))?;
101        let signature =
102            Signature::from_bytes(bytes[32..96].try_into().expect("64 bytes for signature"));
103        let timestamp =
104            u64::from_be_bytes(bytes[96..104].try_into().expect("8 bytes for timestamp"));
105        let encoded_packet = &bytes[104..];
106
107        public_key
108            .verify(&signable(timestamp, encoded_packet), &signature)
109            .map_err(|e| e!(SignedPacketVerifyError::SignatureError, e))?;
110
111        Packet::parse(encoded_packet)
112            .map_err(|e| e!(SignedPacketVerifyError::DnsError, anyerr!(e)))?;
113
114        Ok(SignedPacket {
115            bytes: bytes.to_vec(),
116        })
117    }
118
119    /// Create from a public key and relay payload (signature + timestamp + dns).
120    pub fn from_relay_payload(
121        public_key: &PublicKey,
122        payload: &[u8],
123    ) -> Result<SignedPacket, SignedPacketVerifyError> {
124        let mut bytes = Vec::with_capacity(32 + payload.len());
125        bytes.extend_from_slice(public_key.as_bytes());
126        bytes.extend_from_slice(payload);
127        Self::from_bytes(&bytes)
128    }
129
130    /// Parse a signed packet without verifying the signature.
131    ///
132    /// Still validates minimum length and DNS parsing.
133    pub fn from_bytes_unchecked(bytes: &[u8]) -> Result<SignedPacket, SignedPacketVerifyError> {
134        if bytes.len() < HEADER_SIZE {
135            return Err(e!(SignedPacketVerifyError::TooShort { len: bytes.len() }));
136        }
137        if bytes.len() > MAX_SIGNED_PACKET_SIZE {
138            return Err(e!(SignedPacketVerifyError::TooLarge { len: bytes.len() }));
139        }
140        Packet::parse(&bytes[104..])
141            .map_err(|e| e!(SignedPacketVerifyError::DnsError, anyerr!(e)))?;
142        Ok(SignedPacket {
143            bytes: bytes.to_vec(),
144        })
145    }
146
147    /// Return the full serialized bytes.
148    pub fn as_bytes(&self) -> &[u8] {
149        &self.bytes
150    }
151
152    /// Return the relay payload (everything after the public key).
153    pub fn to_relay_payload(&self) -> Vec<u8> {
154        self.bytes[32..].to_vec()
155    }
156
157    /// Return the public key.
158    pub fn public_key(&self) -> PublicKey {
159        PublicKey::try_from(&self.bytes[..32]).expect("valid public key in SignedPacket")
160    }
161
162    /// Return the signature.
163    pub fn signature(&self) -> Signature {
164        Signature::from_bytes(
165            self.bytes[32..96]
166                .try_into()
167                .expect("64 bytes for signature"),
168        )
169    }
170
171    /// Return the timestamp.
172    pub fn timestamp(&self) -> Timestamp {
173        Timestamp::from_be_bytes(
174            self.bytes[96..104]
175                .try_into()
176                .expect("8 bytes for timestamp"),
177        )
178    }
179
180    /// Return the encoded DNS packet bytes.
181    pub fn encoded_packet(&self) -> &[u8] {
182        &self.bytes[104..]
183    }
184
185    /// Iterate over TXT records under a specific DNS name.
186    ///
187    /// The `name` is normalized relative to the signer's z-base-32 public key.
188    /// Returns the TXT string values.
189    pub fn txt_records(&self, name: &str) -> Vec<String> {
190        let origin = self.public_key().to_z32();
191        let normalized = normalize_name(&origin, name.to_string());
192        let Ok(packet) = Packet::parse(self.encoded_packet()) else {
193            return Vec::new();
194        };
195        let Ok(zone) = Name::new(&origin) else {
196            return Vec::new();
197        };
198        packet
199            .answers
200            .iter()
201            .filter_map(|rr| match &rr.rdata {
202                RData::TXT(txt) => {
203                    // Check if the name matches, either directly or via zone-relative comparison
204                    let rr_name = rr.name.to_string();
205                    if rr_name == normalized {
206                        String::try_from(txt.clone()).ok()
207                    } else if let Some(relative) = rr.name.without(&zone) {
208                        if relative.to_string() == name {
209                            String::try_from(txt.clone()).ok()
210                        } else {
211                            None
212                        }
213                    } else {
214                        None
215                    }
216                }
217                _ => None,
218            })
219            .collect()
220    }
221
222    /// Iterate over all TXT records, yielding `(name_relative_to_origin, value)` pairs.
223    ///
224    /// The name is the part before the z-base-32 public key zone.
225    pub fn all_txt_records(&self) -> Vec<(String, String)> {
226        let origin = self.public_key().to_z32();
227        let Ok(packet) = Packet::parse(self.encoded_packet()) else {
228            return Vec::new();
229        };
230        let Ok(zone) = Name::new(&origin) else {
231            return Vec::new();
232        };
233        packet
234            .answers
235            .iter()
236            .filter_map(|rr| match &rr.rdata {
237                RData::TXT(txt) => {
238                    let relative_name = rr
239                        .name
240                        .without(&zone)
241                        .map(|n| n.to_string())
242                        .unwrap_or_default();
243                    let value = String::try_from(txt.clone()).ok()?;
244                    Some((relative_name, value))
245                }
246                _ => None,
247            })
248            .collect()
249    }
250
251    /// Reconstruct a signed packet from its raw parts without verifying the signature.
252    ///
253    /// This is useful for reconstructing a packet from storage or DHT mutable items
254    /// where the components are stored separately.
255    pub fn from_parts_unchecked(
256        public_key: &[u8],
257        signature: &[u8],
258        timestamp: Timestamp,
259        encoded_packet: &[u8],
260    ) -> Result<Self, SignedPacketVerifyError> {
261        let mut bytes = Vec::with_capacity(HEADER_SIZE + encoded_packet.len());
262        bytes.extend_from_slice(public_key);
263        bytes.extend_from_slice(signature);
264        bytes.extend_from_slice(&timestamp.to_be_bytes());
265        bytes.extend_from_slice(encoded_packet);
266        Self::from_bytes_unchecked(&bytes)
267    }
268
269    /// Return whether this packet is more recent than another.
270    pub fn more_recent_than(&self, other: &SignedPacket) -> bool {
271        if self.timestamp() == other.timestamp() {
272            self.encoded_packet() > other.encoded_packet()
273        } else {
274            self.timestamp() > other.timestamp()
275        }
276    }
277}
278
279impl Display for SignedPacket {
280    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
281        writeln!(f, "SignedPacket ({}):", self.public_key().to_z32())?;
282        writeln!(f, "  timestamp: {}µs", self.timestamp().as_micros())?;
283        for (name, value) in self.all_txt_records() {
284            writeln!(f, "  {name} TXT \"{value}\"")?;
285        }
286        Ok(())
287    }
288}
289
290/// Construct the signable bytes per BEP_0044.
291fn signable(timestamp: u64, v: &[u8]) -> Vec<u8> {
292    let mut signable = format!("3:seqi{}e1:v{}:", timestamp, v.len()).into_bytes();
293    signable.extend(v);
294    signable
295}
296
297/// Normalize a DNS name relative to the pkarr origin (z-base-32 public key).
298fn normalize_name(origin: &str, name: String) -> String {
299    let name = if name.ends_with('.') {
300        name[..name.len() - 1].to_string()
301    } else {
302        name
303    };
304
305    let parts: Vec<&str> = name.split('.').collect();
306    let last = *parts.last().unwrap_or(&"");
307
308    if last == origin {
309        return name;
310    }
311
312    if last == "@" || last.is_empty() {
313        return origin.to_string();
314    }
315
316    format!("{name}.{origin}")
317}
318
319/// A pkarr timestamp in microseconds since the UNIX epoch.
320///
321/// Used as the `seq` field in BEP_0044 DHT mutable items. Per the spec, a new
322/// publish must have a strictly higher timestamp than the previous one, or DHT
323/// nodes will reject the update.
324///
325/// [`Timestamp::now`] is guaranteed to be strictly monotonic: it will never
326/// return the same value twice and will never go backward, even if the system
327/// clock is corrected by NTP. This is achieved by tracking the last returned
328/// value and ensuring each call returns at least `last + 1`.
329#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, derive_more::Debug)]
330#[debug("Timestamp({}µs)", _0)]
331pub struct Timestamp(u64);
332
333/// Tracks the last timestamp returned by [`Timestamp::now`] to ensure monotonicity.
334static LAST_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
335
336impl Timestamp {
337    /// Returns a strictly monotonic timestamp.
338    ///
339    /// Guaranteed to return a value greater than any previous call, even if the
340    /// system clock jumps backward (e.g. due to NTP correction).
341    pub fn now() -> Self {
342        use n0_future::time::SystemTime;
343        let micros = SystemTime::now()
344            .duration_since(SystemTime::UNIX_EPOCH)
345            .expect("system time before UNIX epoch")
346            .as_micros() as u64;
347        // Ensure strictly monotonic: if the clock went backward or two calls
348        // land in the same microsecond, we increment from the last value.
349        let mut last = LAST_TIMESTAMP.load(Ordering::Relaxed);
350        loop {
351            let next = micros.max(last + 1);
352            match LAST_TIMESTAMP.compare_exchange_weak(
353                last,
354                next,
355                Ordering::Relaxed,
356                Ordering::Relaxed,
357            ) {
358                Ok(_) => return Self(next),
359                Err(actual) => last = actual,
360            }
361        }
362    }
363
364    /// Creates a timestamp from a raw microseconds value.
365    pub fn from_micros(micros: u64) -> Self {
366        Self(micros)
367    }
368
369    /// Returns the raw microseconds value.
370    pub fn as_micros(self) -> u64 {
371        self.0
372    }
373
374    /// Returns the big-endian byte representation.
375    pub fn to_be_bytes(self) -> [u8; 8] {
376        self.0.to_be_bytes()
377    }
378
379    /// Parses from big-endian bytes.
380    pub fn from_be_bytes(bytes: [u8; 8]) -> Self {
381        Self(u64::from_be_bytes(bytes))
382    }
383}
384
385/// Error building a signed packet.
386#[allow(missing_docs)]
387#[stack_error(derive, add_meta)]
388#[non_exhaustive]
389pub enum SignedPacketBuildError {
390    #[error("DNS packet too large: {len} bytes (max {MAX_DNS_PACKET_SIZE})")]
391    PacketTooLarge { len: usize },
392    #[error("DNS encoding error")]
393    DnsError { source: AnyError },
394}
395
396/// Error verifying a signed packet.
397#[allow(missing_docs)]
398#[stack_error(derive, add_meta)]
399#[non_exhaustive]
400pub enum SignedPacketVerifyError {
401    #[error("Signed packet too short: {len} bytes (min {HEADER_SIZE})")]
402    TooShort { len: usize },
403    #[error("Signed packet too large: {len} bytes (max {MAX_SIGNED_PACKET_SIZE})")]
404    TooLarge { len: usize },
405    #[error("Invalid signature")]
406    SignatureError {
407        #[error(std_err)]
408        source: iroh_base::SignatureError,
409    },
410    #[error("DNS decoding error")]
411    DnsError { source: AnyError },
412    #[error("Invalid public key")]
413    InvalidKey { source: iroh_base::KeyParsingError },
414}