From: Matt Corallo Date: Sat, 13 Jul 2024 16:23:47 +0000 (+0000) Subject: Add a type to track `HumanReadableName`s X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=75d20e5a5c23eac3ce0850cc9969a6b7feeae807;p=rust-lightning Add a type to track `HumanReadableName`s BIP 353 `HumanReadableName`s are represented as `₿user@domain` and can be resolved using DNS into a `bitcoin:` URI. In the next commit, we will add such a resolver using onion messages to fetch records from the DNS, which will rely on this new type to get name information from outside LDK. --- diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index 3d531264a..489b2ed06 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -144,3 +144,94 @@ impl OnionMessageContents for DNSResolverMessage { } } } + +/// A struct containing the two parts of a BIP 353 Human Readable Name - the user and domain parts. +/// +/// The `user` and `domain` parts, together, cannot exceed 232 bytes in length, and both must be +/// non-empty. +/// +/// To protect against [Homograph Attacks], both parts of a Human Readable Name must be plain +/// ASCII. +/// +/// [Homograph Attacks]: https://en.wikipedia.org/wiki/IDN_homograph_attack +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct HumanReadableName { + // TODO Remove the heap allocations given the whole data can't be more than 256 bytes. + user: String, + domain: String, +} + +impl HumanReadableName { + /// Constructs a new [`HumanReadableName`] from the `user` and `domain` parts. See the + /// struct-level documentation for more on the requirements on each. + pub fn new(user: String, domain: String) -> Result { + const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1; + if user.len() + domain.len() + REQUIRED_EXTRA_LEN > 255 { + return Err(()); + } + if user.is_empty() || domain.is_empty() { + return Err(()); + } + if !Hostname::str_is_valid_hostname(&user) || !Hostname::str_is_valid_hostname(&domain) { + return Err(()); + } + Ok(HumanReadableName { user, domain }) + } + + /// Constructs a new [`HumanReadableName`] from the standard encoding - `user`@`domain`. + /// + /// If `user` includes the standard BIP 353 ₿ prefix it is automatically removed as required by + /// BIP 353. + pub fn from_encoded(encoded: &str) -> Result { + if let Some((user, domain)) = encoded.strip_prefix('₿').unwrap_or(encoded).split_once("@") + { + Self::new(user.to_string(), domain.to_string()) + } else { + Err(()) + } + } + + /// Gets the `user` part of this Human Readable Name + pub fn user(&self) -> &str { + &self.user + } + + /// Gets the `domain` part of this Human Readable Name + pub fn domain(&self) -> &str { + &self.domain + } +} + +// Serialized per the requirements for inclusion in a BOLT 12 `invoice_request` +impl Writeable for HumanReadableName { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + (self.user.len() as u8).write(writer)?; + writer.write_all(&self.user.as_bytes())?; + (self.domain.len() as u8).write(writer)?; + writer.write_all(&self.domain.as_bytes()) + } +} + +impl Readable for HumanReadableName { + fn read(reader: &mut R) -> Result { + let mut read_bytes = [0; 255]; + + let user_len: u8 = Readable::read(reader)?; + reader.read_exact(&mut read_bytes[..user_len as usize])?; + let user_bytes: Vec = read_bytes[..user_len as usize].into(); + let user = match String::from_utf8(user_bytes) { + Ok(user) => user, + Err(_) => return Err(DecodeError::InvalidValue), + }; + + let domain_len: u8 = Readable::read(reader)?; + reader.read_exact(&mut read_bytes[..domain_len as usize])?; + let domain_bytes: Vec = read_bytes[..domain_len as usize].into(); + let domain = match String::from_utf8(domain_bytes) { + Ok(domain) => domain, + Err(_) => return Err(DecodeError::InvalidValue), + }; + + HumanReadableName::new(user, domain).map_err(|()| DecodeError::InvalidValue) + } +} diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index b847f9eeb..21ce044e3 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1490,6 +1490,16 @@ impl Hostname { pub fn len(&self) -> u8 { (&self.0).len() as u8 } + + /// Check if the chars in `s` are allowed to be included in a [`Hostname`]. + pub(crate) fn str_is_valid_hostname(s: &str) -> bool { + s.len() <= 255 && + s.chars().all(|c| + c.is_ascii_alphanumeric() || + c == '.' || + c == '-' + ) + } } impl core::fmt::Display for Hostname { @@ -1525,11 +1535,7 @@ impl TryFrom for Hostname { type Error = (); fn try_from(s: String) -> Result { - if s.len() <= 255 && s.chars().all(|c| - c.is_ascii_alphanumeric() || - c == '.' || - c == '-' - ) { + if Hostname::str_is_valid_hostname(&s) { Ok(Hostname(s)) } else { Err(())