]> git.bitcoin.ninja Git - rust-lightning/commitdiff
Add a type to track `HumanReadableName`s
authorMatt Corallo <git@bluematt.me>
Sat, 13 Jul 2024 16:23:47 +0000 (16:23 +0000)
committerMatt Corallo <git@bluematt.me>
Mon, 30 Sep 2024 16:19:33 +0000 (16:19 +0000)
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.

lightning/src/onion_message/dns_resolution.rs
lightning/src/util/ser.rs

index 3d531264a82c7a5852f4b3a04519f8fe43470355..489b2ed0673a3590efed448f9a6d93332f177564 100644 (file)
@@ -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<HumanReadableName, ()> {
+               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<HumanReadableName, ()> {
+               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<W: Writer>(&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<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
+               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<u8> = 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<u8> = 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)
+       }
+}
index b847f9eebd73827e3720f0db86a8d980ba6f5ee8..21ce044e3c9fb45a7a65946b147f005c92f8aab0 100644 (file)
@@ -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<String> for Hostname {
        type Error = ();
 
        fn try_from(s: String) -> Result<Self, Self::Error> {
-               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(())