upleb.uk

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

summaryrefslogtreecommitdiff
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
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.
-rw-r--r--src/btfaddr.cpp40
-rw-r--r--src/test_btfaddr.cpp19
2 files changed, 41 insertions, 18 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);
diff --git a/src/test_btfaddr.cpp b/src/test_btfaddr.cpp
index 8de426e..9d33d63 100644
--- a/src/test_btfaddr.cpp
+++ b/src/test_btfaddr.cpp
@@ -133,12 +133,23 @@ int main()
133 std::string d3 = btf::SignDescriptor(ctx, sk, ENC_HEX, "node-1", 5, ONION); 133 std::string d3 = btf::SignDescriptor(ctx, sk, ENC_HEX, "node-1", 5, ONION);
134 json j3 = json::parse(d3); 134 json j3 = json::parse(d3);
135 j3["onion"] = "bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcde.onion:8433"; 135 j3["onion"] = "bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcde.onion:8433";
136 CHECK(btf::VerifyDescriptor(ctx, j3.dump(), pk, o) && o.onion.empty(), 136 CHECK(!btf::VerifyDescriptor(ctx, j3.dump(), pk, o),
137 "tampered onion ignored rather than trusted"); 137 "tampered onion rejects the descriptor");
138 json j4 = json::parse(d3); 138 json j4 = json::parse(d3);
139 j4["sig3"] = std::string(128, '0'); 139 j4["sig3"] = std::string(128, '0');
140 CHECK(btf::VerifyDescriptor(ctx, j4.dump(), pk, o) && o.onion.empty(), 140 CHECK(!btf::VerifyDescriptor(ctx, j4.dump(), pk, o),
141 "bad onion signature ignored rather than trusted"); 141 "bad onion signature rejects the descriptor");
142 json j5 = json::parse(d3);
143 j5.erase("sig3");
144 CHECK(!btf::VerifyDescriptor(ctx, j5.dump(), pk, o),
145 "unsigned onion rejects the descriptor");
146 // Removing both fields still verifies: the result is indistinguishable
147 // from a peer that never had an onion endpoint. That residual strip
148 // needs the onion bound into the primary sig, an upstream change.
149 json j6 = json::parse(d3);
150 j6.erase("onion"); j6.erase("sig3");
151 CHECK(btf::VerifyDescriptor(ctx, j6.dump(), pk, o) && o.onion.empty(),
152 "full strip indistinguishable from no-onion (documented residual)");
142 } 153 }
143 154
144 printf("full_resolve_chain_from_address\n"); 155 printf("full_resolve_chain_from_address\n");