offline_protocol_core/address.rs
1//! Self-certifying protocol addresses.
2//!
3//! An address is a bech32m encoding of `0x01 ‖ SHA-256(ed25519_pub)[..20]`,
4//! rendered as `off1…` (44 characters). Because the payload is a hash of the
5//! identity key, an address authenticates itself: a peer claiming an address
6//! can be checked by re-deriving it from the public key it presents, with no
7//! trust-on-first-use pin store in between.
8//!
9//! This module never touches key material — hashing happens upstream (see
10//! `MlsManager::derive_address`); [`Address`] is constructed from the already
11//! truncated hash.
12//!
13//! # Exactly one string form
14//!
15//! Every payload has exactly one accepted spelling. [`Address::from_str`]
16//! rejects uppercase input, the non-`m` bech32 checksum, wrong HRP, wrong
17//! version byte, wrong payload length, and — critically — encodings that
18//! decode to the right payload but are not the canonical spelling of it.
19//! The last class is not hypothetical: the `bech32` crate's decoder discards
20//! the trailing sub-byte bits without checking they are zero, and its
21//! fes-to-bytes length rule (`n * 5 / 8`) maps both 34- and 35-character data
22//! parts onto a 21-byte payload. Both shapes are checksum-valid and both are
23//! caught here by re-encoding and requiring byte-for-byte equality with the
24//! input. Briar's "spare bit" address bug is the reference failure for what
25//! happens when this is skipped.
26
27use std::fmt;
28use std::str::FromStr;
29
30use bech32::primitives::decode::CheckedHrpstring;
31use bech32::{Bech32m, Hrp};
32use serde::{Deserialize, Serialize};
33
34/// Why an address string was rejected by [`Address::from_str`].
35// Adding a variant to a public error enum is a breaking change without
36// this attribute; downstream crates must carry a wildcard arm.
37#[non_exhaustive]
38#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
39pub enum AddressError {
40 /// The string contains ASCII uppercase.
41 ///
42 /// BIP-173 permits an all-uppercase spelling and the `bech32` crate
43 /// accepts it; the protocol admits the lowercase form only.
44 #[error("address is not lowercase (the canonical form is lowercase-only)")]
45 NotLowercase,
46 /// The string is not structurally valid bech32m: bad charset, missing or
47 /// misplaced separator, mixed case, or a checksum that is absent, corrupt,
48 /// or computed with the original bech32 constant instead of bech32m.
49 #[error("address is not valid bech32m: {reason}")]
50 InvalidBech32m {
51 /// Rendering of the underlying `bech32` decode error.
52 reason: String,
53 },
54 /// Valid bech32m, but the human-readable part is not [`Address::HRP_STR`].
55 #[error("address has human-readable part '{found}', expected '{expected}'")]
56 HrpMismatch {
57 /// Human-readable part found in the input.
58 found: String,
59 /// The required human-readable part.
60 expected: &'static str,
61 },
62 /// The decoded payload is not [`Address::PAYLOAD_LEN`] bytes.
63 #[error("address payload is {len} bytes, expected {expected}")]
64 PayloadLength {
65 /// Decoded payload length.
66 len: usize,
67 /// Required payload length.
68 expected: usize,
69 },
70 /// The payload does not start with a supported version byte.
71 #[error("address version byte is {found:#04x}, expected {expected:#04x}")]
72 VersionMismatch {
73 /// Version byte found in the payload.
74 found: u8,
75 /// The only version byte this build understands.
76 expected: u8,
77 },
78 /// The string decodes to a well-formed payload but is not that payload's
79 /// canonical spelling — non-zero padding bits, or a data part padded out
80 /// with extra characters that truncate to the same bytes.
81 ///
82 /// Accepting these would give one identity several valid addresses, so
83 /// they are rejected rather than normalized.
84 #[error("address is a non-canonical encoding of its payload")]
85 NonCanonical,
86}
87
88/// A self-certifying protocol address: `off1…`.
89///
90/// # Ordering
91///
92/// [`Ord`] compares the underlying hash bytes, **not** the rendered string.
93/// The two orders differ: the bech32 charset (`qpzry9x8gf2tvdw0s3jn54khce6mua7l`)
94/// is not monotonic in ASCII — value 4 renders as `y` (0x79) and value 5 as
95/// `9` (0x39) — and a string comparison would also weigh the checksum
96/// characters.
97///
98/// **Which order a tiebreaker uses is fixed per site, and the sites disagree.**
99/// Both-create session ownership compares `Address` values (hash bytes); group
100/// leave election, admin auto-promotion and fork leader election sort rendered
101/// strings. Each converges on its own, because every peer running that
102/// tiebreaker sorts the same way.
103///
104/// Do not "harmonize" one site onto the other order. Peers that changed and
105/// peers that did not would then elect different winners from identical input,
106/// with no way to detect the disagreement locally. Prefer hash-byte order for
107/// anything new. See `docs/adr/0003-self-certifying-addresses.md`.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
109pub struct Address {
110 hash: [u8; Address::HASH_LEN],
111}
112
113impl Address {
114 /// Human-readable part carried by every address.
115 pub const HRP_STR: &'static str = "off";
116
117 /// The only address version this build understands.
118 pub const VERSION: u8 = 0x01;
119
120 /// Length of the truncated identity-key hash, in bytes.
121 ///
122 /// 160 bits, which is a **second-preimage** margin of ~2^160 and a
123 /// **collision** margin of ~2^80 — the birthday bound on the same
124 /// truncation. Both are stated here because they buy different things and
125 /// only the first is what the impersonation claim rests on:
126 ///
127 /// - ~2^160 is the cost of aiming at an address that already exists:
128 /// producing a key that derives to *someone else's* address. That is the
129 /// attack the sender-derivation gate replaced the trust-pin store to
130 /// stop, and it is the figure the guides and changelog quote.
131 /// - ~2^80 is the cost of producing *two* identity keys that share one
132 /// address, neither fixed in advance. It impersonates nobody, but it does
133 /// let a single entity hold two signing keys that are indistinguishable
134 /// at the address layer — enough to equivocate, and enough to defeat the
135 /// "one identity cannot hold two leaves" property that the MLS leaf
136 /// binding otherwise inherits from MLS signature-key uniqueness.
137 ///
138 /// ~2^80 is below the ~2^128 a greenfield design would target, and is a
139 /// deliberate trade rather than an oversight: every mesh frame carries a
140 /// sender and a recipient address, and BLE budget is the binding
141 /// constraint. Widening the hash changes the wire, so it is a [`VERSION`]
142 /// bump and a migration, not a patch.
143 ///
144 /// [`VERSION`]: Address::VERSION
145 pub const HASH_LEN: usize = 20;
146
147 /// Length of the encoded payload: version byte followed by the hash.
148 pub const PAYLOAD_LEN: usize = 1 + Self::HASH_LEN;
149
150 /// Length of the canonical string form.
151 ///
152 /// `off` (3) + separator (1) + data (34) + checksum (6). The data part is
153 /// 34 characters because 21 bytes is 168 bits, which needs 34 five-bit
154 /// groups and leaves 2 padding bits.
155 pub const ENCODED_LEN: usize = 44;
156
157 /// `Hrp` form of [`Self::HRP_STR`].
158 ///
159 /// `parse_unchecked` is the only `const` constructor and performs no
160 /// validation; `hrp_const_matches_parsed` pins it against the checked
161 /// parser.
162 const HRP: Hrp = Hrp::parse_unchecked(Self::HRP_STR);
163
164 /// Builds an address from a truncated identity-key hash.
165 ///
166 /// Infallible: every 20-byte value is a valid address payload.
167 pub fn from_hash_bytes(hash: [u8; Self::HASH_LEN]) -> Self {
168 Self { hash }
169 }
170
171 /// Builds an address from a `version ‖ hash` payload.
172 pub fn from_payload(payload: &[u8]) -> Result<Self, AddressError> {
173 if payload.len() != Self::PAYLOAD_LEN {
174 return Err(AddressError::PayloadLength {
175 len: payload.len(),
176 expected: Self::PAYLOAD_LEN,
177 });
178 }
179 if payload[0] != Self::VERSION {
180 return Err(AddressError::VersionMismatch {
181 found: payload[0],
182 expected: Self::VERSION,
183 });
184 }
185 let mut hash = [0u8; Self::HASH_LEN];
186 hash.copy_from_slice(&payload[1..]);
187 Ok(Self { hash })
188 }
189
190 /// Returns the truncated identity-key hash.
191 pub fn hash_bytes(&self) -> &[u8; Self::HASH_LEN] {
192 &self.hash
193 }
194
195 /// Returns the `version ‖ hash` payload.
196 pub fn payload(&self) -> [u8; Self::PAYLOAD_LEN] {
197 let mut payload = [0u8; Self::PAYLOAD_LEN];
198 payload[0] = Self::VERSION;
199 payload[1..].copy_from_slice(&self.hash);
200 payload
201 }
202
203 /// Returns the address version byte.
204 pub fn version(&self) -> u8 {
205 Self::VERSION
206 }
207}
208
209impl fmt::Display for Address {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 // `encode_lower_to_fmt` writes straight to the formatter and always
212 // emits lowercase. Its error covers only a formatter failure and an
213 // over-long code, the latter unreachable for a fixed 21-byte payload.
214 bech32::encode_lower_to_fmt::<Bech32m, _>(f, Self::HRP, &self.payload())
215 .map_err(|_| fmt::Error)
216 }
217}
218
219impl FromStr for Address {
220 type Err = AddressError;
221
222 fn from_str(s: &str) -> Result<Self, Self::Err> {
223 // The crate rejects mixed case but accepts an all-uppercase string;
224 // the protocol accepts one spelling, so screen case out first.
225 if s.bytes().any(|b| b.is_ascii_uppercase()) {
226 return Err(AddressError::NotLowercase);
227 }
228
229 // `CheckedHrpstring::new::<Bech32m>` pins the checksum algorithm.
230 // `bech32::decode` must never be used here: it validates bech32m and
231 // then silently falls back to the original bech32 constant.
232 let checked =
233 CheckedHrpstring::new::<Bech32m>(s).map_err(|e| AddressError::InvalidBech32m {
234 reason: e.to_string(),
235 })?;
236
237 if checked.hrp() != Self::HRP {
238 return Err(AddressError::HrpMismatch {
239 found: checked.hrp().to_string(),
240 expected: Self::HRP_STR,
241 });
242 }
243
244 let payload: Vec<u8> = checked.byte_iter().collect();
245 let address = Self::from_payload(&payload)?;
246
247 // Every remaining degree of freedom — the two padding bits, and a
248 // 35-character data part that truncates to the same 21 bytes — leaves
249 // a checksum-valid string that decodes to this exact payload, so no
250 // earlier check can see it. Re-encoding and requiring byte equality
251 // collapses them all: one payload, one string.
252 if address.to_string() != s {
253 return Err(AddressError::NonCanonical);
254 }
255
256 Ok(address)
257 }
258}
259
260impl Serialize for Address {
261 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
262 serializer.collect_str(self)
263 }
264}
265
266impl<'de> Deserialize<'de> for Address {
267 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
268 let s = String::deserialize(deserializer)?;
269 s.parse().map_err(serde::de::Error::custom)
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 // Golden vectors. Generated with the BIP-350 reference implementation
278 // (github.com/sipa/bech32, ref/python/segwit_addr.py) — an implementation
279 // independent of the `bech32` crate, so agreement between these literals
280 // and the code below is a genuine two-implementation cross-check.
281 //
282 // These strings are the protocol's addressing format. Changing one means
283 // every peer's identity changed: re-derive them from the reference
284 // implementation, never edit them to match new code output.
285
286 /// SHA-256 of the RFC 8032 §7.1 TEST 1 Ed25519 public key, truncated.
287 const RFC8032_TV1_HASH: [u8; 20] = [
288 0x21, 0xfe, 0x31, 0xdf, 0xa1, 0x54, 0xa2, 0x61, 0x62, 0x6b, 0xf8, 0x54, 0x04, 0x6f, 0xd2,
289 0x27, 0x1b, 0x7b, 0xed, 0x4b,
290 ];
291 const RFC8032_TV1_ADDR: &str = "off1qysluvwl5922yctzd0u9gpr06gn3k7ldfvgtwgvn";
292
293 const ZEROS_HASH: [u8; 20] = [0u8; 20];
294 const ZEROS_ADDR: &str = "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqn8antf";
295
296 const FF_HASH: [u8; 20] = [0xffu8; 20];
297 const FF_ADDR: &str = "off1q8llllllllllllllllllllllllllllllluvweh4p";
298
299 const RAMP_HASH: [u8; 20] = [
300 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
301 ];
302 const RAMP_ADDR: &str = "off1qyqqzqsrqszsvpcgpy9qkrqdpc83qygjzvpgkpr5";
303
304 fn vectors() -> [([u8; 20], &'static str); 4] {
305 [
306 (RFC8032_TV1_HASH, RFC8032_TV1_ADDR),
307 (ZEROS_HASH, ZEROS_ADDR),
308 (FF_HASH, FF_ADDR),
309 (RAMP_HASH, RAMP_ADDR),
310 ]
311 }
312
313 #[test]
314 fn encodes_reference_vectors() {
315 for (hash, expected) in vectors() {
316 let rendered = Address::from_hash_bytes(hash).to_string();
317 assert_eq!(rendered, expected);
318 assert_eq!(rendered.len(), Address::ENCODED_LEN);
319 }
320 }
321
322 #[test]
323 fn decodes_reference_vectors() {
324 for (hash, encoded) in vectors() {
325 let address: Address = encoded.parse().expect("reference vector must parse");
326 assert_eq!(address.hash_bytes(), &hash);
327 assert_eq!(address.version(), Address::VERSION);
328 }
329 }
330
331 #[test]
332 fn reencoding_a_parsed_vector_is_byte_identical() {
333 for (_, encoded) in vectors() {
334 let address: Address = encoded.parse().expect("reference vector must parse");
335 assert_eq!(address.to_string(), encoded);
336 }
337 }
338
339 #[test]
340 fn roundtrips_arbitrary_hashes() {
341 // Deterministic spread without pulling in a rng dev-dependency.
342 let mut state: u64 = 0x2545_F491_4F6C_DD1D;
343 for _ in 0..256 {
344 let mut hash = [0u8; Address::HASH_LEN];
345 for byte in hash.iter_mut() {
346 state = state
347 .wrapping_mul(6364136223846793005)
348 .wrapping_add(1442695040888963407);
349 *byte = (state >> 33) as u8;
350 }
351 let address = Address::from_hash_bytes(hash);
352 let encoded = address.to_string();
353 assert_eq!(encoded.len(), Address::ENCODED_LEN);
354 assert_eq!(encoded.parse::<Address>().expect("roundtrip"), address);
355 }
356 }
357
358 #[test]
359 fn payload_roundtrips() {
360 let address = Address::from_hash_bytes(RAMP_HASH);
361 let payload = address.payload();
362 assert_eq!(payload[0], Address::VERSION);
363 assert_eq!(&payload[1..], &RAMP_HASH);
364 assert_eq!(Address::from_payload(&payload), Ok(address));
365 }
366
367 #[test]
368 fn hrp_const_matches_parsed() {
369 // `Hrp::parse_unchecked` performs no validation, so pin the const
370 // against the checked parser.
371 let parsed = Hrp::parse(Address::HRP_STR).expect("hrp must be valid");
372 assert_eq!(parsed, Address::HRP);
373 }
374
375 #[test]
376 fn serde_uses_the_canonical_string() {
377 let address = Address::from_hash_bytes(RFC8032_TV1_HASH);
378 let json = serde_json::to_string(&address).expect("serialize");
379 assert_eq!(json, format!("\"{RFC8032_TV1_ADDR}\""));
380 let back: Address = serde_json::from_str(&json).expect("deserialize");
381 assert_eq!(back, address);
382 }
383
384 #[test]
385 fn serde_rejects_non_canonical_forms() {
386 for bad in [
387 ZEROS_ADDR.to_uppercase(),
388 // Non-zero padding bits, valid checksum.
389 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqra5g99k".to_string(),
390 ] {
391 let json = format!("\"{bad}\"");
392 assert!(serde_json::from_str::<Address>(&json).is_err(), "{bad}");
393 }
394 }
395
396 #[test]
397 fn ord_follows_hash_bytes_not_the_rendered_string() {
398 let low = Address::from_hash_bytes(ZEROS_HASH);
399 let high = Address::from_hash_bytes(FF_HASH);
400 assert!(low < high);
401 assert_eq!(low.hash_bytes() < high.hash_bytes(), low < high);
402
403 // The two orders genuinely disagree, which is why tiebreakers must not
404 // compare rendered strings: value 4 encodes as 'y' (0x79) and value 5
405 // as '9' (0x39), inverting the comparison.
406 let mut a = ZEROS_HASH;
407 a[0] = 0x20; // leading fe = 4 -> 'y'
408 let mut b = ZEROS_HASH;
409 b[0] = 0x28; // leading fe = 5 -> '9'
410 let (a, b) = (Address::from_hash_bytes(a), Address::from_hash_bytes(b));
411 assert!(a < b, "byte order");
412 assert!(a.to_string() > b.to_string(), "string order inverts");
413 }
414
415 #[test]
416 fn rejects_uppercase_and_mixed_case() {
417 // All-uppercase is a valid bech32m string per BIP-173 and the crate
418 // accepts it; the protocol must not.
419 assert_eq!(
420 ZEROS_ADDR.to_uppercase().parse::<Address>(),
421 Err(AddressError::NotLowercase)
422 );
423 let mixed = format!("{}N8antf", &ZEROS_ADDR[..ZEROS_ADDR.len() - 6]);
424 assert_eq!(mixed.parse::<Address>(), Err(AddressError::NotLowercase));
425 }
426
427 #[test]
428 fn rejects_wrong_hrp() {
429 // Same payload, HRP "ofx", freshly checksummed.
430 let other = "ofx1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2e32t6";
431 assert_eq!(
432 other.parse::<Address>(),
433 Err(AddressError::HrpMismatch {
434 found: "ofx".to_string(),
435 expected: Address::HRP_STR,
436 })
437 );
438 }
439
440 #[test]
441 fn rejects_the_original_bech32_checksum() {
442 // Same payload and HRP, checksummed with bech32 instead of bech32m.
443 // `bech32::decode` would accept this; `CheckedHrpstring::<Bech32m>`
444 // must not.
445 let bech32_variant = "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqxmdlwt";
446 assert!(matches!(
447 bech32_variant.parse::<Address>(),
448 Err(AddressError::InvalidBech32m { .. })
449 ));
450 }
451
452 #[test]
453 fn rejects_corrupted_characters() {
454 let mut corrupted = ZEROS_ADDR.to_string();
455 corrupted.pop();
456 corrupted.push('p'); // valid charset member, wrong checksum
457 assert!(matches!(
458 corrupted.parse::<Address>(),
459 Err(AddressError::InvalidBech32m { .. })
460 ));
461
462 // 'b', 'i', 'o' and '1' are excluded from the bech32 charset.
463 for bad_char in ['b', 'i', 'o'] {
464 let mut off_charset = ZEROS_ADDR.to_string();
465 off_charset.pop();
466 off_charset.push(bad_char);
467 assert!(
468 matches!(
469 off_charset.parse::<Address>(),
470 Err(AddressError::InvalidBech32m { .. })
471 ),
472 "{bad_char}"
473 );
474 }
475 }
476
477 #[test]
478 fn rejects_unsupported_version_bytes() {
479 for (encoded, expected_version) in [
480 ("off1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkynkkv", 0x00u8),
481 ("off1qgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqquz0u9x", 0x02),
482 ("off1luqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqn347dq", 0xff),
483 ] {
484 assert_eq!(
485 encoded.parse::<Address>(),
486 Err(AddressError::VersionMismatch {
487 found: expected_version,
488 expected: Address::VERSION,
489 })
490 );
491 }
492 }
493
494 #[test]
495 fn rejects_wrong_payload_lengths() {
496 // 20-byte payload (19-byte hash) and 22-byte payload (21-byte hash),
497 // each correctly checksummed.
498 for (encoded, len) in [
499 ("off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqxfx3pw", 20usize),
500 ("off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkev2nx", 22),
501 ] {
502 assert_eq!(
503 encoded.parse::<Address>(),
504 Err(AddressError::PayloadLength {
505 len,
506 expected: Address::PAYLOAD_LEN,
507 })
508 );
509 }
510 }
511
512 #[test]
513 fn rejects_non_zero_padding_bits() {
514 // 21 bytes is 168 bits carried in 34 five-bit groups, leaving 2
515 // padding bits. Each mutant sets those bits and carries a freshly
516 // computed, valid bech32m checksum, so it decodes to exactly the
517 // all-zeros payload — the `bech32` crate discards the dirty bits
518 // without complaint. This is the Briar "spare bit" bug; these three
519 // assertions are the regression guard against reintroducing it.
520 for mutant in [
521 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpw3fxkm",
522 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqzuscy",
523 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqra5g99k",
524 ] {
525 assert_eq!(
526 mutant.parse::<Address>(),
527 Err(AddressError::NonCanonical),
528 "{mutant}"
529 );
530 // The canonical form of that payload parses, proving the mutant is
531 // a second spelling rather than a different address.
532 assert!(ZEROS_ADDR.parse::<Address>().is_ok());
533 }
534 }
535
536 #[test]
537 fn rejects_over_long_data_parts() {
538 // The crate's fes-to-bytes rule is `n * 5 / 8`, so a 35-character data
539 // part also yields 21 bytes: a checksum-valid second spelling that the
540 // length and version checks cannot see.
541 for mutant in [
542 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqj7v8n5",
543 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqp0gcjwx",
544 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqlcjhrel",
545 ] {
546 assert_eq!(
547 mutant.parse::<Address>(),
548 Err(AddressError::NonCanonical),
549 "{mutant}"
550 );
551 }
552 }
553
554 #[test]
555 fn rejects_structural_garbage() {
556 for bad in [
557 "",
558 "off",
559 "off1",
560 "off1qqq",
561 "1qyqqqq",
562 ZEROS_ADDR.trim_end_matches('f'),
563 ] {
564 assert!(
565 matches!(
566 bad.parse::<Address>(),
567 Err(AddressError::InvalidBech32m { .. })
568 ),
569 "{bad:?}"
570 );
571 }
572 }
573
574 #[test]
575 fn from_payload_rejects_bad_input() {
576 assert_eq!(
577 Address::from_payload(&[]),
578 Err(AddressError::PayloadLength {
579 len: 0,
580 expected: Address::PAYLOAD_LEN,
581 })
582 );
583 let mut payload = [0u8; Address::PAYLOAD_LEN];
584 payload[0] = 0x02;
585 assert_eq!(
586 Address::from_payload(&payload),
587 Err(AddressError::VersionMismatch {
588 found: 0x02,
589 expected: Address::VERSION,
590 })
591 );
592 }
593}