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