diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-06 01:53:03 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-06 01:53:03 +0000 |
| commit | ef9f06d43fc6577c15df2256abbb25b20d4227b8 (patch) | |
| tree | 7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /src/btfaddr.cpp | |
| parent | be63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff) | |
| parent | ceae3d076b5ad11ee84729ae43f14e9312896646 (diff) | |
Bring main onto the line this project actually ships (incl. the 1.2.23 compactblock port)
Closes #6, #7, #8, #11, #35, #34, #29, #28, #27, and #16
See merge request !16
Diffstat (limited to 'src/btfaddr.cpp')
| -rw-r--r-- | src/btfaddr.cpp | 114 |
1 files changed, 107 insertions, 7 deletions
diff --git a/src/btfaddr.cpp b/src/btfaddr.cpp index 6edcc91..502220e 100644 --- a/src/btfaddr.cpp +++ b/src/btfaddr.cpp | |||
| @@ -19,6 +19,8 @@ | |||
| 19 | #include <cstring> | 19 | #include <cstring> |
| 20 | #include <algorithm> | 20 | #include <algorithm> |
| 21 | #include <cctype> | 21 | #include <cctype> |
| 22 | #include <cerrno> | ||
| 23 | #include <cstdlib> | ||
| 22 | 24 | ||
| 23 | using json = nlohmann::json; | 25 | using json = nlohmann::json; |
| 24 | 26 | ||
| @@ -166,10 +168,47 @@ static bool HexDecode(const std::string& s, unsigned char* out, size_t n) | |||
| 166 | return true; | 168 | return true; |
| 167 | } | 169 | } |
| 168 | 170 | ||
| 171 | bool NormalizeOnionEndpoint(const std::string& in, std::string& out) | ||
| 172 | { | ||
| 173 | out.clear(); | ||
| 174 | std::string s = ToLowerTrim(in); | ||
| 175 | size_t colon = s.rfind(':'); | ||
| 176 | if (colon == std::string::npos || colon == 0 || colon + 1 >= s.size()) | ||
| 177 | return false; | ||
| 178 | |||
| 179 | std::string host = s.substr(0, colon); | ||
| 180 | std::string port = s.substr(colon + 1); | ||
| 181 | while (!host.empty() && host.back() == '.') host.pop_back(); | ||
| 182 | static const char* suffix = ".onion"; | ||
| 183 | const size_t suffixLen = strlen(suffix); | ||
| 184 | if (host.size() != 56 + suffixLen) | ||
| 185 | return false; | ||
| 186 | if (host.compare(host.size() - suffixLen, suffixLen, suffix) != 0) | ||
| 187 | return false; | ||
| 188 | for (size_t i = 0; i < 56; i++) | ||
| 189 | { | ||
| 190 | char c = host[i]; | ||
| 191 | if (!((c >= 'a' && c <= 'z') || (c >= '2' && c <= '7'))) | ||
| 192 | return false; | ||
| 193 | } | ||
| 194 | for (size_t i = 0; i < port.size(); i++) | ||
| 195 | if (!isdigit((unsigned char)port[i])) | ||
| 196 | return false; | ||
| 197 | |||
| 198 | errno = 0; | ||
| 199 | char* end = NULL; | ||
| 200 | long nPort = strtol(port.c_str(), &end, 10); | ||
| 201 | if (errno == ERANGE || end == NULL || *end != '\0' || nPort <= 0 || nPort > 65535) | ||
| 202 | return false; | ||
| 203 | |||
| 204 | out = host + ":" + port; | ||
| 205 | return true; | ||
| 206 | } | ||
| 207 | |||
| 169 | // Digest signed over the descriptor fields (domain-separated). | 208 | // Digest signed over the descriptor fields (domain-separated). |
| 170 | static void DescriptorDigest(const unsigned char pubkey[32], const std::string& enc, | 209 | static void DescriptorDigestV2(const unsigned char pubkey[32], const std::string& enc, |
| 171 | const std::string& meeting_node, uint64_t created, | 210 | const std::string& meeting_node, uint64_t created, |
| 172 | unsigned char out[32]) | 211 | unsigned char out[32]) |
| 173 | { | 212 | { |
| 174 | static const char* DS = ".btf-descriptor-v2"; | 213 | static const char* DS = ".btf-descriptor-v2"; |
| 175 | SHA256_CTX ctx; | 214 | SHA256_CTX ctx; |
| @@ -184,9 +223,27 @@ static void DescriptorDigest(const unsigned char pubkey[32], const std::string& | |||
| 184 | SHA256_Final(out, &ctx); | 223 | SHA256_Final(out, &ctx); |
| 185 | } | 224 | } |
| 186 | 225 | ||
| 226 | static void DescriptorDigestV3(const unsigned char pubkey[32], const std::string& enc, | ||
| 227 | const std::string& meeting_node, const std::string& onion, | ||
| 228 | uint64_t created, unsigned char out[32]) | ||
| 229 | { | ||
| 230 | static const char* DS = ".btf-descriptor-v3-onion"; | ||
| 231 | SHA256_CTX ctx; | ||
| 232 | SHA256_Init(&ctx); | ||
| 233 | SHA256_Update(&ctx, (const unsigned char*)DS, strlen(DS)); | ||
| 234 | SHA256_Update(&ctx, pubkey, 32); | ||
| 235 | SHA256_Update(&ctx, (const unsigned char*)enc.data(), enc.size()); | ||
| 236 | SHA256_Update(&ctx, (const unsigned char*)meeting_node.data(), meeting_node.size()); | ||
| 237 | SHA256_Update(&ctx, (const unsigned char*)onion.data(), onion.size()); | ||
| 238 | unsigned char be[8]; | ||
| 239 | for (int i = 0; i < 8; i++) be[i] = (unsigned char)((created >> (8 * (7 - i))) & 0xff); | ||
| 240 | SHA256_Update(&ctx, be, 8); | ||
| 241 | SHA256_Final(out, &ctx); | ||
| 242 | } | ||
| 243 | |||
| 187 | std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], | 244 | std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], |
| 188 | const std::string& enc, const std::string& meeting_node, | 245 | const std::string& enc, const std::string& meeting_node, |
| 189 | uint64_t created) | 246 | uint64_t created, const std::string& onion) |
| 190 | { | 247 | { |
| 191 | secp256k1_context* ctx = (secp256k1_context*)ctxv; | 248 | secp256k1_context* ctx = (secp256k1_context*)ctxv; |
| 192 | secp256k1_keypair kp; | 249 | secp256k1_keypair kp; |
| @@ -197,17 +254,28 @@ std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], | |||
| 197 | secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xpub); | 254 | secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xpub); |
| 198 | 255 | ||
| 199 | unsigned char digest[32]; | 256 | unsigned char digest[32]; |
| 200 | DescriptorDigest(pubkey, enc, meeting_node, created, digest); | 257 | DescriptorDigestV2(pubkey, enc, meeting_node, created, digest); |
| 201 | 258 | ||
| 202 | unsigned char sig[64]; | 259 | unsigned char sig[64]; |
| 203 | // Deterministic signature (NULL aux_rand), matching Itzal's no-aux-rand signing. | 260 | // Deterministic signature (NULL aux_rand), matching Itzal's no-aux-rand signing. |
| 204 | if (!secp256k1_schnorrsig_sign32(ctx, sig, digest, &kp, NULL)) return std::string(); | 261 | if (!secp256k1_schnorrsig_sign32(ctx, sig, digest, &kp, NULL)) return std::string(); |
| 205 | 262 | ||
| 206 | json v = json::object(); | 263 | json v = json::object(); |
| 207 | v["v"] = 2; | 264 | v["v"] = onion.empty() ? 2 : 3; |
| 208 | v["pubkey"] = HexEncode(pubkey, 32); | 265 | v["pubkey"] = HexEncode(pubkey, 32); |
| 209 | v["enc"] = enc; | 266 | v["enc"] = enc; |
| 210 | v["meeting_node"] = meeting_node; | 267 | v["meeting_node"] = meeting_node; |
| 268 | if (!onion.empty()) | ||
| 269 | { | ||
| 270 | std::string onionNorm; | ||
| 271 | if (!NormalizeOnionEndpoint(onion, onionNorm)) return std::string(); | ||
| 272 | unsigned char digest3[32]; | ||
| 273 | DescriptorDigestV3(pubkey, enc, meeting_node, onionNorm, created, digest3); | ||
| 274 | unsigned char sig3[64]; | ||
| 275 | if (!secp256k1_schnorrsig_sign32(ctx, sig3, digest3, &kp, NULL)) return std::string(); | ||
| 276 | v["onion"] = onionNorm; | ||
| 277 | v["sig3"] = HexEncode(sig3, 64); | ||
| 278 | } | ||
| 211 | v["created"] = created; | 279 | v["created"] = created; |
| 212 | v["sig"] = HexEncode(sig, 64); | 280 | v["sig"] = HexEncode(sig, 64); |
| 213 | return v.dump(); | 281 | return v.dump(); |
| @@ -242,15 +310,47 @@ bool VerifyDescriptor(void* ctxv, const std::string& jsonStr, | |||
| 242 | if (!HexDecode(v["sig"].get<std::string>(), sig, 64)) return false; | 310 | if (!HexDecode(v["sig"].get<std::string>(), sig, 64)) return false; |
| 243 | 311 | ||
| 244 | unsigned char digest[32]; | 312 | unsigned char digest[32]; |
| 245 | DescriptorDigest(expect_pubkey, enc, meeting_node, created, digest); | 313 | DescriptorDigestV2(expect_pubkey, enc, meeting_node, created, digest); |
| 246 | 314 | ||
| 247 | secp256k1_xonly_pubkey xpub; | 315 | secp256k1_xonly_pubkey xpub; |
| 248 | if (!secp256k1_xonly_pubkey_parse(ctx, &xpub, expect_pubkey)) return false; | 316 | if (!secp256k1_xonly_pubkey_parse(ctx, &xpub, expect_pubkey)) return false; |
| 249 | if (!secp256k1_schnorrsig_verify(ctx, sig, digest, 32, &xpub)) return false; | 317 | if (!secp256k1_schnorrsig_verify(ctx, sig, digest, 32, &xpub)) return false; |
| 250 | 318 | ||
| 319 | std::string onion; | ||
| 320 | if (v.contains("onion")) | ||
| 321 | { | ||
| 322 | // A descriptor that carries an onion endpoint must prove it. The | ||
| 323 | // endpoint is covered by sig3 and only sig3, so any failure here -- | ||
| 324 | // missing sig3, bad hex, unnormalised endpoint, bad signature -- | ||
| 325 | // means the descriptor was tampered with, and a tampered | ||
| 326 | // descriptor is rejected outright. Upstream dropped just the onion | ||
| 327 | // and accepted the rest, which let any relay convert an onion | ||
| 328 | // descriptor into a rendezvous-only one by deleting a field. | ||
| 329 | // | ||
| 330 | // Residual, by design: removing BOTH "onion" and "sig3" still | ||
| 331 | // verifies, because the result is indistinguishable from a peer | ||
| 332 | // that never had an onion endpoint. Closing that requires binding | ||
| 333 | // the onion into the primary sig (the V2 digest), which every | ||
| 334 | // upstream 1.2.18/1.2.19 verifier would reject -- an upstream | ||
| 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; | ||
| 348 | } | ||
| 349 | |||
| 251 | memcpy(out.pubkey, expect_pubkey, 32); | 350 | memcpy(out.pubkey, expect_pubkey, 32); |
| 252 | out.enc = enc; | 351 | out.enc = enc; |
| 253 | out.meeting_node = meeting_node; | 352 | out.meeting_node = meeting_node; |
| 353 | out.onion = onion; | ||
| 254 | out.created = created; | 354 | out.created = created; |
| 255 | return true; | 355 | return true; |
| 256 | } | 356 | } |