fix: Onion v3 parse/format order and render lowercase
What changed, and why it matters
This commit fixes how rust-lightning reads and writes Tor onion v3 network addresses. Previously the code treated the 56-character onion address as if its bytes were arranged in the wrong order: it put the version and checksum at the front and the public key at the back, which is the opposite of the real Tor specification. It also printed onion addresses in uppercase base32 instead of lowercase. The fix reorders parsing and formatting to match the official Tor layout (pubkey first, then checksum, then version) and forces lowercase output. Because of the old bug, a valid real-world onion address would have been parsed into the wrong internal fields, and addresses produced by the library would not have been valid Tor addresses.
Review whether any persisted onion addresses, peer announcements, or logs produced before this patch need re-parsing, because old formatted addresses were invalid. Consider adding explicit checksum verification and version-range validation during parsing to detect corrupt or malicious onion addresses. Backport to release branches that expose OnionV3 socket parsing.
Security signals we found
Incorrect parsing of network address format
Non-conformant serialization of network address format
Spec-mismatch in Tor Onion v3 address encoding
Potential interoperability / connectivity failure with Tor peers
No explicit validation of checksum or version value
Evidence from the diff
The patch corrects OnionV3 serialization/deserialization in lightning/src/ln/msgs.rs. The Tor rendezvous spec encodes a v3 onion address as base32 of 35 bytes: 32-byte ed25519 pubkey, 2-byte big-endian checksum, 1-byte version. The old code assumed layout [version][checksum_be][pubkey], so parse_onion_address sliced onion[0] as version, onion[1..3] as checksum, and onion[3..35] as pubkey. The Display impl concatenated version || checksum || pubkey and emitted uppercase base32. The new code slices 0..32 for the pubkey, 32..34 for the checksum, and 34 for the version, and Display now emits pubkey || checksum || version in lowercase base32. Tests were updated with a real spec-correct vector and a round-trip assertion.
Changed components
lightning/src/ln/msgs.rsSocketAddress::OnionV3parse_onion_addressimpl Display for SocketAddressInspect captured patch +37 / −34
diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs
index db5353e..fd806a2 100644
--- a/lightning/src/ln/msgs.rs
+++ b/lightning/src/ln/msgs.rs
@@ -1273,27 +1273,27 @@ impl std::net::ToSocketAddrs for SocketAddress {
pub fn parse_onion_address(
host: &str, port: u16,
) -> Result<SocketAddress, SocketAddressParseError> {
- if host.ends_with(".onion") {
- let domain = &host[..host.len() - ".onion".len()];
- if domain.len() != 56 {
- return Err(SocketAddressParseError::InvalidOnionV3);
- }
- let onion = base32::Alphabet::RFC4648 { padding: false }
- .decode(&domain)
- .map_err(|_| SocketAddressParseError::InvalidOnionV3)?;
- if onion.len() != 35 {
- return Err(SocketAddressParseError::InvalidOnionV3);
- }
- let version = onion[0];
- let first_checksum_flag = onion[1];
- let second_checksum_flag = onion[2];
- let mut ed25519_pubkey = [0; 32];
- ed25519_pubkey.copy_from_slice(&onion[3..35]);
- let checksum = u16::from_be_bytes([first_checksum_flag, second_checksum_flag]);
- return Ok(SocketAddress::OnionV3 { ed25519_pubkey, checksum, version, port });
- } else {
+ if !host.ends_with(".onion") {
return Err(SocketAddressParseError::InvalidInput);
}
+ let domain = &host[..host.len() - ".onion".len()];
+ if domain.len() != 56 {
+ return Err(SocketAddressParseError::InvalidOnionV3);
+ }
+ let onion = base32::Alphabet::RFC4648 { padding: false }
+ .decode(domain)
+ .map_err(|_| SocketAddressParseError::InvalidOnionV3)?;
+ if onion.len() != 35 {
+ return Err(SocketAddressParseError::InvalidOnionV3);
+ }
+
+ let mut ed25519_pubkey = [0u8; 32];
+ ed25519_pubkey.copy_from_slice(&onion[0..32]);
+
+ let checksum = u16::from_be_bytes([onion[32], onion[33]]);
+ let version = onion[34];
+
+ Ok(SocketAddress::OnionV3 { ed25519_pubkey, checksum, version, port })
}
impl Display for SocketAddress {
@@ -1313,10 +1313,13 @@ impl Display for SocketAddress {
version,
port,
} => {
- let [first_checksum_flag, second_checksum_flag] = checksum.to_be_bytes();
- let mut addr = vec![*version, first_checksum_flag, second_checksum_flag];
+ let mut addr = Vec::with_capacity(35);
addr.extend_from_slice(ed25519_pubkey);
- let onion = base32::Alphabet::RFC4648 { padding: false }.encode(&addr);
+ let [c0, c1] = checksum.to_be_bytes();
+ addr.push(c0);
+ addr.push(c1);
+ addr.push(*version);
+ let onion = base32::Alphabet::RFC4648 { padding: false }.encode(&addr).to_lowercase();
write!(f, "{}.onion:{}", onion, port)?
},
SocketAddress::Hostname { hostname, port } => write!(f, "{}:{}", hostname, port)?,
@@ -6666,21 +6669,21 @@ mod tests {
let onion_v3 = SocketAddress::OnionV3 {
ed25519_pubkey: [
- 37, 24, 75, 5, 25, 73, 117, 194, 139, 102, 182, 107, 4, 105, 247, 246, 85, 111,
- 177, 172, 49, 137, 167, 155, 64, 221, 163, 47, 31, 33, 71, 3,
+ 121, 188, 198, 37, 24, 75, 5, 25, 73, 117, 194, 139, 102, 182, 107, 4, 105, 247,
+ 246, 85, 111, 177, 172, 49, 137, 167, 155, 64, 221, 163, 47, 31,
],
- checksum: 48326,
- version: 121,
+ checksum: 8519,
+ version: 3,
port: 1234,
};
- assert_eq!(
- onion_v3,
- SocketAddress::from_str(
- "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:1234"
- )
- .unwrap()
- );
- assert_eq!(onion_v3, SocketAddress::from_str(&onion_v3.to_string()).unwrap());
+ let onion_v3_str = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:1234";
+ let parsed = SocketAddress::from_str(onion_v3_str).unwrap();
+ assert_eq!(onion_v3, parsed);
+ assert_eq!(onion_v3_str, parsed.to_string());
+ match parsed {
+ SocketAddress::OnionV3 { version, .. } => assert_eq!(version, 3),
+ _ => panic!("expected OnionV3"),
+ }
assert_eq!(
Err(SocketAddressParseError::InvalidOnionV3),
Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.