upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/btfaddr.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-08-26 22:43:41 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-26 22:43:41 +0000
commit3fe9280fc11bc4ceaff3bb21533224b7b3e88da7 (patch)
tree8179a95dbede51a2b3910b3c0bacae48c8c3b2d0 /src/btfaddr.cpp
parentbbe04dc8c96b4c4766d2771d3438233bf82687c0 (diff)
btfaddr: reject descriptors whose onion endpoint is not signed
Upstream 1.2.19 signs the onion endpoint only under the optional sig3, and VerifyDescriptor treated sig3 failure as 'drop the onion, accept the rest'. Any relay could therefore strip an onion endpoint down to a rendezvous-only descriptor (or, under /oniononly, cut the peer off) by deleting one field. Now an onion field must arrive with a valid sig3 over the V3 digest or the whole descriptor is rejected. Honest 1.2.19 writers always emit both fields, so interop is preserved. The full two-field strip still verifies -- it is indistinguishable from a peer that never had an onion -- and needs the onion bound into the primary sig, which only upstream can do without breaking every 1.2.18/1.2.19 verifier; reported as an upstream protocol defect. test_btfaddr flips the two upstream checks that enshrined the soft drop and adds unsigned-onion and full-strip cases.
Diffstat (limited to 'src/btfaddr.cpp')
-rw-r--r--src/btfaddr.cpp40
1 files changed, 26 insertions, 14 deletions
diff --git a/src/btfaddr.cpp b/src/btfaddr.cpp
index ce2ac86..502220e 100644
--- a/src/btfaddr.cpp
+++ b/src/btfaddr.cpp
@@ -319,20 +319,32 @@ bool VerifyDescriptor(void* ctxv, const std::string& jsonStr,
319 std::string onion; 319 std::string onion;
320 if (v.contains("onion")) 320 if (v.contains("onion"))
321 { 321 {
322 if (v["onion"].is_string() && v.contains("sig3") && v["sig3"].is_string()) 322 // A descriptor that carries an onion endpoint must prove it. The
323 { 323 // endpoint is covered by sig3 and only sig3, so any failure here --
324 std::string onionNorm; 324 // missing sig3, bad hex, unnormalised endpoint, bad signature --
325 std::string onionRaw = v["onion"].get<std::string>(); 325 // means the descriptor was tampered with, and a tampered
326 if (NormalizeOnionEndpoint(onionRaw, onionNorm) && onionNorm == onionRaw) 326 // descriptor is rejected outright. Upstream dropped just the onion
327 { 327 // and accepted the rest, which let any relay convert an onion
328 unsigned char sig3[64]; 328 // descriptor into a rendezvous-only one by deleting a field.
329 unsigned char digest3[32]; 329 //
330 DescriptorDigestV3(expect_pubkey, enc, meeting_node, onionNorm, created, digest3); 330 // Residual, by design: removing BOTH "onion" and "sig3" still
331 if (HexDecode(v["sig3"].get<std::string>(), sig3, 64) && 331 // verifies, because the result is indistinguishable from a peer
332 secp256k1_schnorrsig_verify(ctx, sig3, digest3, 32, &xpub)) 332 // that never had an onion endpoint. Closing that requires binding
333 onion = onionNorm; 333 // the onion into the primary sig (the V2 digest), which every
334 } 334 // upstream 1.2.18/1.2.19 verifier would reject -- an upstream
335 } 335 // protocol change, not something this fork can do unilaterally.
336 if (!v["onion"].is_string()) return false;
337 if (!v.contains("sig3") || !v["sig3"].is_string()) return false;
338 std::string onionNorm;
339 std::string onionRaw = v["onion"].get<std::string>();
340 if (!NormalizeOnionEndpoint(onionRaw, onionNorm) || onionNorm != onionRaw)
341 return false;
342 unsigned char sig3[64];
343 unsigned char digest3[32];
344 DescriptorDigestV3(expect_pubkey, enc, meeting_node, onionNorm, created, digest3);
345 if (!HexDecode(v["sig3"].get<std::string>(), sig3, 64)) return false;
346 if (!secp256k1_schnorrsig_verify(ctx, sig3, digest3, 32, &xpub)) return false;
347 onion = onionNorm;
336 } 348 }
337 349
338 memcpy(out.pubkey, expect_pubkey, 32); 350 memcpy(out.pubkey, expect_pubkey, 32);