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:
Diffstat (limited to 'src/btfaddr.cpp')
-rw-r--r--src/btfaddr.cpp263
1 files changed, 263 insertions, 0 deletions
diff --git a/src/btfaddr.cpp b/src/btfaddr.cpp
new file mode 100644
index 0000000..6edcc91
--- /dev/null
+++ b/src/btfaddr.cpp
@@ -0,0 +1,263 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// Bitflash `.btf` self-certifying addresses + service descriptors.
5// Ported from the Itzal hidden.rs design. See btfaddr.h.
6
7#include "btfaddr.h"
8
9#include <openssl/sha.h>
10// See the note in nostr.cpp: the makefiles pass -DSECP256K1_STATIC too.
11#ifndef SECP256K1_STATIC
12#define SECP256K1_STATIC
13#endif
14#include <secp256k1.h>
15#include <secp256k1_schnorrsig.h>
16#include <secp256k1_extrakeys.h>
17#include <nlohmann/json.hpp>
18
19#include <cstring>
20#include <algorithm>
21#include <cctype>
22
23using json = nlohmann::json;
24
25namespace btf
26{
27
28const char* TLD = ".btf";
29
30static const char* B32 = "abcdefghijklmnopqrstuvwxyz234567";
31
32std::string Base32Encode(const unsigned char* data, size_t n)
33{
34 std::string out;
35 out.reserve(n * 8 / 5 + 1);
36 uint64_t buf = 0;
37 uint32_t bits = 0;
38 for (size_t i = 0; i < n; i++)
39 {
40 buf = (buf << 8) | data[i];
41 bits += 8;
42 while (bits >= 5)
43 {
44 bits -= 5;
45 out.push_back(B32[(buf >> bits) & 0x1f]);
46 }
47 buf &= (bits == 0) ? 0 : ((1ULL << bits) - 1);
48 }
49 if (bits > 0)
50 out.push_back(B32[(buf << (5 - bits)) & 0x1f]);
51 return out;
52}
53
54bool Base32Decode(const std::string& s, std::vector<unsigned char>& out)
55{
56 out.clear();
57 out.reserve(s.size() * 5 / 8);
58 uint64_t buf = 0;
59 uint32_t bits = 0;
60 for (unsigned char c : s)
61 {
62 uint64_t val;
63 if (c >= 'a' && c <= 'z') val = c - 'a';
64 else if (c >= 'A' && c <= 'Z') val = c - 'A';
65 else if (c >= '2' && c <= '7') val = c - '2' + 26;
66 else return false;
67 buf = (buf << 5) | val;
68 bits += 5;
69 if (bits >= 8)
70 {
71 bits -= 8;
72 out.push_back((unsigned char)((buf >> bits) & 0xff));
73 }
74 buf &= (bits == 0) ? 0 : ((1ULL << bits) - 1);
75 }
76 return true;
77}
78
79static std::string ToLowerTrim(const std::string& in)
80{
81 std::string s = in;
82 // trim whitespace
83 size_t a = s.find_first_not_of(" \t\r\n");
84 size_t b = s.find_last_not_of(" \t\r\n");
85 if (a == std::string::npos) return std::string();
86 s = s.substr(a, b - a + 1);
87 // strip trailing dots (fully-qualified hostname form)
88 while (!s.empty() && s.back() == '.') s.pop_back();
89 for (char& c : s) c = (char)std::tolower((unsigned char)c);
90 return s;
91}
92
93// Two-byte checksum, domain-separated so it can't collide with any other hash use.
94static void Checksum(const unsigned char pubkey[32], unsigned char out[2])
95{
96 static const char* DS = ".btf-checksum-v1";
97 SHA256_CTX ctx;
98 SHA256_Init(&ctx);
99 SHA256_Update(&ctx, (const unsigned char*)DS, strlen(DS));
100 SHA256_Update(&ctx, pubkey, 32);
101 unsigned char d[32];
102 SHA256_Final(d, &ctx);
103 out[0] = d[0];
104 out[1] = d[1];
105}
106
107std::string Address(const unsigned char pubkey[32])
108{
109 unsigned char raw[34];
110 memcpy(raw, pubkey, 32);
111 Checksum(pubkey, raw + 32);
112 return Base32Encode(raw, 34) + TLD;
113}
114
115bool IsBtf(const std::string& host)
116{
117 std::string s = ToLowerTrim(host);
118 std::string tld = TLD;
119 if (s.size() < tld.size()) return false;
120 return s.compare(s.size() - tld.size(), tld.size(), tld) == 0;
121}
122
123bool ParseAddress(const std::string& addr, unsigned char pubkeyOut[32])
124{
125 std::string s = ToLowerTrim(addr);
126 std::string tld = TLD;
127 if (s.size() < tld.size()) return false;
128 if (s.compare(s.size() - tld.size(), tld.size(), tld) != 0) return false;
129 std::string label = s.substr(0, s.size() - tld.size());
130
131 std::vector<unsigned char> raw;
132 if (!Base32Decode(label, raw)) return false;
133 if (raw.size() < 34) return false;
134
135 unsigned char pk[32];
136 memcpy(pk, &raw[0], 32);
137 unsigned char sum[2];
138 Checksum(pk, sum);
139 if (raw[32] != sum[0] || raw[33] != sum[1]) return false;
140
141 memcpy(pubkeyOut, pk, 32);
142 return true;
143}
144
145
146// ---- descriptor ------------------------------------------------------------
147
148static std::string HexEncode(const unsigned char* p, size_t n)
149{
150 static const char* h = "0123456789abcdef";
151 std::string s;
152 s.reserve(n * 2);
153 for (size_t i = 0; i < n; i++) { s += h[p[i] >> 4]; s += h[p[i] & 0xf]; }
154 return s;
155}
156
157static bool HexDecode(const std::string& s, unsigned char* out, size_t n)
158{
159 if (s.size() != n * 2) return false;
160 for (size_t i = 0; i < n; i++)
161 {
162 unsigned int b;
163 if (sscanf(s.c_str() + i * 2, "%2x", &b) != 1) return false;
164 out[i] = (unsigned char)b;
165 }
166 return true;
167}
168
169// Digest signed over the descriptor fields (domain-separated).
170static void DescriptorDigest(const unsigned char pubkey[32], const std::string& enc,
171 const std::string& meeting_node, uint64_t created,
172 unsigned char out[32])
173{
174 static const char* DS = ".btf-descriptor-v2";
175 SHA256_CTX ctx;
176 SHA256_Init(&ctx);
177 SHA256_Update(&ctx, (const unsigned char*)DS, strlen(DS));
178 SHA256_Update(&ctx, pubkey, 32);
179 SHA256_Update(&ctx, (const unsigned char*)enc.data(), enc.size());
180 SHA256_Update(&ctx, (const unsigned char*)meeting_node.data(), meeting_node.size());
181 unsigned char be[8];
182 for (int i = 0; i < 8; i++) be[i] = (unsigned char)((created >> (8 * (7 - i))) & 0xff);
183 SHA256_Update(&ctx, be, 8);
184 SHA256_Final(out, &ctx);
185}
186
187std::string SignDescriptor(void* ctxv, const unsigned char seckey[32],
188 const std::string& enc, const std::string& meeting_node,
189 uint64_t created)
190{
191 secp256k1_context* ctx = (secp256k1_context*)ctxv;
192 secp256k1_keypair kp;
193 if (!secp256k1_keypair_create(ctx, &kp, seckey)) return std::string();
194 secp256k1_xonly_pubkey xpub;
195 if (!secp256k1_keypair_xonly_pub(ctx, &xpub, NULL, &kp)) return std::string();
196 unsigned char pubkey[32];
197 secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xpub);
198
199 unsigned char digest[32];
200 DescriptorDigest(pubkey, enc, meeting_node, created, digest);
201
202 unsigned char sig[64];
203 // 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();
205
206 json v = json::object();
207 v["v"] = 2;
208 v["pubkey"] = HexEncode(pubkey, 32);
209 v["enc"] = enc;
210 v["meeting_node"] = meeting_node;
211 v["created"] = created;
212 v["sig"] = HexEncode(sig, 64);
213 return v.dump();
214}
215
216bool VerifyDescriptor(void* ctxv, const std::string& jsonStr,
217 const unsigned char expect_pubkey[32], Descriptor& out)
218{
219 secp256k1_context* ctx = (secp256k1_context*)ctxv;
220 // Descriptors arrive from untrusted peers (via Nostr relays). Guard the whole
221 // parse: a malformed or wrong-typed field must be rejected, never throw.
222 try
223 {
224 json v = json::parse(jsonStr);
225 if (!v.is_object()) return false;
226 if (!v.contains("pubkey") || !v.contains("enc") || !v.contains("meeting_node")
227 || !v.contains("created") || !v.contains("sig")) return false;
228 // Reject wrong field types before extracting (json::get would otherwise throw).
229 if (!v["pubkey"].is_string() || !v["enc"].is_string() ||
230 !v["meeting_node"].is_string() || !v["sig"].is_string() ||
231 !v["created"].is_number_unsigned()) return false;
232
233 unsigned char pubkey[32];
234 if (!HexDecode(v["pubkey"].get<std::string>(), pubkey, 32)) return false;
235 if (memcmp(pubkey, expect_pubkey, 32) != 0) return false; // descriptor is for a different key
236
237 std::string enc = v["enc"].get<std::string>();
238 std::string meeting_node = v["meeting_node"].get<std::string>();
239 uint64_t created = v["created"].get<uint64_t>();
240
241 unsigned char sig[64];
242 if (!HexDecode(v["sig"].get<std::string>(), sig, 64)) return false;
243
244 unsigned char digest[32];
245 DescriptorDigest(expect_pubkey, enc, meeting_node, created, digest);
246
247 secp256k1_xonly_pubkey xpub;
248 if (!secp256k1_xonly_pubkey_parse(ctx, &xpub, expect_pubkey)) return false;
249 if (!secp256k1_schnorrsig_verify(ctx, sig, digest, 32, &xpub)) return false;
250
251 memcpy(out.pubkey, expect_pubkey, 32);
252 out.enc = enc;
253 out.meeting_node = meeting_node;
254 out.created = created;
255 return true;
256 }
257 catch (...)
258 {
259 return false; // malformed / wrong-typed descriptor -> reject, don't crash
260 }
261}
262
263} // namespace btf