diff --git a/src/dnssec.rs b/src/dnssec.rs index b336284..3df2504 100644 --- a/src/dnssec.rs +++ b/src/dnssec.rs @@ -5,6 +5,7 @@ use log::{debug, trace}; use ring::digest; use ring::signature; +use crate::buffer::BytePacketBuffer; use crate::cache::{DnsCache, DnssecStatus}; use crate::packet::DnsPacket; use crate::question::QueryType; @@ -720,22 +721,33 @@ pub fn verify_ds(ds: &DnsRecord, dnskey: &DnsRecord, owner: &str) -> bool { // -- Canonical wire format -- +/// Encode a DNS name in canonical wire form per RFC 4034 §6.2: +/// uncompressed, with all ASCII letters lowercased. +/// +/// Delegates label parsing and RFC 1035 §5.1 escape handling to +/// `BytePacketBuffer::write_qname`, then post-processes the emitted bytes +/// to lowercase label bodies (length bytes stay untouched). This keeps +/// the escape logic in exactly one place — see #55. pub fn name_to_wire(name: &str) -> Vec { - let mut wire = Vec::with_capacity(name.len() + 2); - if name == "." || name.is_empty() { - wire.push(0); - return wire; - } - for label in name.split('.') { - if label.is_empty() { - continue; + let mut buf = BytePacketBuffer::new(); + buf.write_qname(name) + .expect("DNSSEC canonical form: name must be a well-formed DNS name"); + let mut wire = buf.filled().to_vec(); + + let mut i = 0; + while i < wire.len() { + let label_len = wire[i] as usize; + if label_len == 0 { + break; } - wire.push(label.len() as u8); - for &b in label.as_bytes() { - wire.push(b.to_ascii_lowercase()); + i += 1; + let end = i + label_len; + for b in &mut wire[i..end] { + *b = b.to_ascii_lowercase(); } + i = end; } - wire.push(0); + wire } @@ -1475,6 +1487,23 @@ mod tests { ); } + #[test] + fn name_to_wire_escaped_dot_in_label_is_not_a_separator() { + // `exa\.mple.com` is two labels: `exa.mple` (8 bytes including the 0x2E) and `com`. + let wire = name_to_wire("exa\\.mple.com"); + assert_eq!( + wire, + vec![8, b'e', b'x', b'a', b'.', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0] + ); + } + + #[test] + fn name_to_wire_decimal_escape_is_lowercased() { + // `\065` is the byte 0x41 ('A'), which canonical form must lowercase to 'a'. + let wire = name_to_wire("\\065bc.com"); + assert_eq!(wire, vec![3, b'a', b'b', b'c', 3, b'c', b'o', b'm', 0]); + } + #[test] fn parent_zone_cases() { assert_eq!(parent_zone("example.com"), "com");