CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
ble_gpg_xsig.cpp
Go to the documentation of this file.
1
21
24#include "mod_gpg/GpgStorage.h"
25#include "mod_gpg/gpg.h"
26#include "openpgp/fingerprint.h"
27
30#include "cdc_log.h"
31#include "esp_attr.h"
32
33#include <algorithm>
34#include <cstring>
35#include <ctime>
36
37namespace cdc::mod_gpg {
38
39namespace {
40
41constexpr const char* TAG = "GPG_BLE_XSIG";
42
43// 8E2F1F30-8B5D-4D7A-9A6E-4C9D6A8B1A01 — chosen distinct from vCard's 1F20
44// service to keep the two protocols cleanly separated without overloading
45// one service definition with foreign characteristics. (CROSS_SIGNING.md
46// originally proposed reusing the vCard service UUID; the deviation is noted
47// in the doc.)
48constexpr uint8_t kSvcUuid[16] = {
49 0x01, 0x1A, 0x8B, 0x6A, 0x9D, 0x4C, 0x6E, 0x9A,
50 0x7A, 0x4D, 0x5D, 0x8B, 0x30, 0x1F, 0x2F, 0x8E,
51};
52constexpr uint8_t kRxUuid[16] = {
53 0x01, 0x1A, 0x8B, 0x6A, 0x9D, 0x4C, 0x6E, 0x9A,
54 0x7A, 0x4D, 0x5D, 0x8B, 0x31, 0x1F, 0x2F, 0x8E,
55};
56constexpr uint8_t kStatusUuid[16] = {
57 0x01, 0x1A, 0x8B, 0x6A, 0x9D, 0x4C, 0x6E, 0x9A,
58 0x7A, 0x4D, 0x5D, 0x8B, 0x32, 0x1F, 0x2F, 0x8E,
59};
60
61constexpr uint8_t kOpcWriteStart = 0x11;
62constexpr uint8_t kOpcWriteCont = 0x12;
63constexpr uint8_t kOpcWriteEnd = 0x13;
64constexpr uint8_t kOpcDataStart = 0x91;
65constexpr uint8_t kOpcDataCont = 0x92;
66constexpr uint8_t kOpcDataEnd = 0x93;
67
68constexpr uint8_t kStatusOk = 0x00;
69constexpr uint8_t kStatusBadPayload = 0x01;
70constexpr uint8_t kStatusStoreFull = 0x02;
71constexpr uint8_t kStatusInternalErr = 0x03;
72
73// Min/max payload sizes per spec.
74constexpr size_t kMinPayloadLen = 1 + 1 + 32 + 20 + 1 + 1; // Ed25519 + empty UID
75constexpr size_t kMaxPayloadLen = 1 + 1 + 64 + 20 + 1 + 63; // P-256 + max UID
76
77struct RxSession {
78 bool active = false;
79 uint16_t conn_handle = 0xFFFF;
80 size_t expected_len = 0;
81 size_t received_len = 0;
82 uint8_t buf[kMaxPayloadLen];
83};
84
85EXT_RAM_BSS_ATTR RxSession s_rx = {};
86
87bool s_initialized = false;
88uint16_t s_rx_handle = 0;
89uint16_t s_status_handle = 0;
90cdc::hal::GattCharacteristic s_chars[2] = {};
91cdc::hal::GattServiceDef s_svc = {};
92
93XsigReceivedCallback s_callback = nullptr;
94
95void resetRx() {
96 s_rx.active = false;
97 s_rx.conn_handle = 0xFFFF;
98 s_rx.expected_len = 0;
99 s_rx.received_len = 0;
100}
101
102void sendStatus(uint16_t conn_handle, uint8_t code) {
104 if (!ble || s_status_handle == 0) return;
105 const uint8_t frame[3] = {kOpcDataEnd, 0x01, code};
106 ble->sendNotification(conn_handle, s_status_handle, frame, sizeof(frame));
107}
108
109bool parsePayloadIntoKey(const uint8_t* data, size_t len, gpg_recv_key_t* out) {
110 if (!data || !out) return false;
111 if (len < kMinPayloadLen) return false;
112
113 size_t off = 0;
114 const uint8_t curve = data[off++];
115 if (curve != CDC_CURVE_ED25519 && curve != CDC_CURVE_P256) return false;
116
117 const uint8_t pubkey_len = data[off++];
118 if (curve == CDC_CURVE_ED25519 && pubkey_len != 32) return false;
119 if (curve == CDC_CURVE_P256 && pubkey_len != 64) return false;
120 if (off + pubkey_len + 20 + 1 > len) return false;
121
122 std::memset(out, 0, sizeof(*out));
123 out->curve = curve;
124 out->pubkey_len = pubkey_len;
125 std::memcpy(out->pubkey, data + off, pubkey_len);
126 off += pubkey_len;
127 std::memcpy(out->fingerprint_v4, data + off, 20);
128 off += 20;
129
130 const uint8_t uid_len = data[off++];
131 if (uid_len > 63 || off + uid_len > len) return false;
132 std::memcpy(out->user_id, data + off, uid_len);
133 out->user_id[uid_len] = '\0';
134
135 out->received_at = static_cast<uint32_t>(std::time(nullptr));
136
137 // Compute V5 fingerprint locally from the public-key material we just
138 // received. The peer may not have sent it (cross-sign protocol payload
139 // is V4-only), but we want a consistent record on this badge.
140 calculateFingerprintV5(curve, out->pubkey, out->pubkey_len,
141 out->received_at, out->fingerprint_v5);
142 return true;
143}
144
145void finalizeRx() {
146 gpg_recv_key_t key = {};
147 if (!parsePayloadIntoKey(s_rx.buf, s_rx.received_len, &key)) {
148 LOG_W(TAG, "Rejecting malformed payload (%u bytes)",
149 static_cast<unsigned>(s_rx.received_len));
150 sendStatus(s_rx.conn_handle, kStatusBadPayload);
151 resetRx();
152 return;
153 }
154 if (!GpgRecvStore::instance().addKey(key)) {
155 sendStatus(s_rx.conn_handle, kStatusStoreFull);
156 resetRx();
157 return;
158 }
159 sendStatus(s_rx.conn_handle, kStatusOk);
160 if (s_callback) s_callback(key);
161 LOG_I(TAG, "Received key from %s (%u B)", key.user_id,
162 static_cast<unsigned>(s_rx.received_len));
163 resetRx();
164}
165
166int onRxWrite(uint16_t conn_handle, uint16_t /*attr*/,
167 const uint8_t* data, uint16_t len)
168{
169 if (!data || len == 0) return 0;
170 const uint8_t opcode = data[0];
171
172 switch (opcode) {
173 case kOpcWriteStart: {
174 if (len < 3) return 0;
175 const size_t total = (static_cast<size_t>(data[1]) << 8) | data[2];
176 if (total == 0 || total > kMaxPayloadLen) {
177 sendStatus(conn_handle, kStatusBadPayload);
178 resetRx();
179 return 0;
180 }
181 s_rx.active = true;
182 s_rx.conn_handle = conn_handle;
183 s_rx.expected_len = total;
184 s_rx.received_len = 0;
185 const size_t chunk = len - 3;
186 if (chunk > 0) {
187 const size_t to_copy = std::min<size_t>(chunk, sizeof(s_rx.buf));
188 std::memcpy(s_rx.buf, data + 3, to_copy);
189 s_rx.received_len = to_copy;
190 }
191 if (s_rx.received_len >= s_rx.expected_len) finalizeRx();
192 return 0;
193 }
194 case kOpcWriteCont:
195 case kOpcWriteEnd: {
196 if (!s_rx.active || conn_handle != s_rx.conn_handle) return 0;
197 const size_t chunk = len - 1;
198 const size_t remaining = sizeof(s_rx.buf) - s_rx.received_len;
199 const size_t to_copy = std::min(chunk, remaining);
200 std::memcpy(s_rx.buf + s_rx.received_len, data + 1, to_copy);
201 s_rx.received_len += to_copy;
202 if (opcode == kOpcWriteEnd || s_rx.received_len >= s_rx.expected_len) {
203 finalizeRx();
204 }
205 return 0;
206 }
207 default:
208 return 0;
209 }
210}
211
214size_t buildOwnKeyPayload(uint8_t* out, size_t out_size) {
215 gpg_status_t status = {};
216 if (!gpg_get_status(&status)) return 0;
217
218 const uint8_t curve = status.curve;
219 const uint8_t pubkey_len = (curve == CDC_CURVE_ED25519) ? 32 : 64;
220
221 // We need the actual pubkey bytes; gpg_status_t doesn't carry them, so
222 // pull them from the SIG ECC slot directly. (This logic mirrors what
223 // gpg.cpp::se_get_pubkey does, but we want to keep this file
224 // self-contained.)
226 if (!se) return 0;
227
228 uint8_t pubkey[64] = {0};
230 if (se->eccGetPublicKey(gpg_storage_sig_slot(), pubkey, &hal_curve)
232 return 0;
233 }
234
235 const size_t uid_len = strnlen(status.user_id, sizeof(status.user_id));
236 const size_t total = 1 + 1 + pubkey_len + 20 + 1 + uid_len;
237 if (total > out_size) return 0;
238
239 size_t off = 0;
240 out[off++] = curve;
241 out[off++] = pubkey_len;
242 std::memcpy(out + off, pubkey, pubkey_len);
243 off += pubkey_len;
244 std::memcpy(out + off, status.fingerprint, 20);
245 off += 20;
246 out[off++] = static_cast<uint8_t>(uid_len);
247 std::memcpy(out + off, status.user_id, uid_len);
248 off += uid_len;
249 return off;
250}
251
252} // namespace
253
255 if (s_initialized) return true;
256
258 if (!ble) {
259 LOG_W(TAG, "BluetoothController unavailable, deferring init");
260 return false;
261 }
262
263 s_chars[0].uuid = cdc::hal::BleUuid::from128(kRxUuid);
266 s_chars[0].valueHandle = &s_rx_handle;
267 s_chars[0].onWrite = onRxWrite;
268 s_chars[0].onRead = nullptr;
269
270 s_chars[1].uuid = cdc::hal::BleUuid::from128(kStatusUuid);
273 s_chars[1].valueHandle = &s_status_handle;
274 s_chars[1].onWrite = nullptr;
275 s_chars[1].onRead = nullptr;
276
277 s_svc.uuid = cdc::hal::BleUuid::from128(kSvcUuid);
278 s_svc.characteristics = s_chars;
279 s_svc.numCharacteristics = 2;
280
281 if (!ble->registerGattService(s_svc)) {
282 LOG_W(TAG, "registerGattService failed");
283 return false;
284 }
285 s_initialized = true;
286 LOG_I(TAG, "GPG cross-sign service registered (rx=%u status=%u)",
287 static_cast<unsigned>(s_rx_handle),
288 static_cast<unsigned>(s_status_handle));
289 return true;
290}
291
293 s_callback = cb;
294}
295
298namespace {
299
300struct SendSession {
301 bool active = false;
302 bool awaiting_disc = false;
303 uint16_t conn_handle = 0xFFFF;
304 uint16_t peer_rx = 0;
305 uint8_t payload[kMaxPayloadLen];
306 size_t payload_len = 0;
307 size_t payload_off = 0;
308};
309EXT_RAM_BSS_ATTR SendSession s_tx = {};
310
313
314void cancelSend(const char* why) {
315 LOG_W(TAG, "Send aborted: %s", why ? why : "?");
317 if (ble) {
318 if (s_tx.conn_handle != 0xFFFF) ble->disconnectHandle(s_tx.conn_handle);
319 if (s_disc_token != 0xFFFF) ble->removeServiceDiscoveryCallback(s_disc_token);
320 if (s_write_token != 0xFFFF) ble->removeWriteCompleteCallback(s_write_token);
321 }
322 s_disc_token = 0xFFFF;
323 s_write_token = 0xFFFF;
324 s_tx = SendSession{};
325}
326
327bool writeNextChunk() {
329 if (!ble || !s_tx.active) return false;
330
331 const uint16_t mtu = ble->getMtu();
332 if (s_tx.payload_off == 0) {
333 // START frame: opcode + 2-byte length + chunk.
334 const size_t header = 3;
335 const size_t chunk = std::min<size_t>(mtu - header, s_tx.payload_len);
336 uint8_t buf[244];
337 if (header + chunk > sizeof(buf)) return false;
338 buf[0] = kOpcWriteStart;
339 buf[1] = (s_tx.payload_len >> 8) & 0xFF;
340 buf[2] = s_tx.payload_len & 0xFF;
341 std::memcpy(buf + 3, s_tx.payload, chunk);
342 s_tx.payload_off = chunk;
343 return ble->writeCharacteristic(s_tx.conn_handle, s_tx.peer_rx,
344 buf, header + chunk, true);
345 }
346
347 // CONT / END frames: opcode + chunk.
348 const size_t header = 1;
349 const size_t remaining = s_tx.payload_len - s_tx.payload_off;
350 const size_t chunk = std::min<size_t>(mtu - header, remaining);
351 const bool last = (chunk == remaining);
352 uint8_t buf[244];
353 if (header + chunk > sizeof(buf)) return false;
354 buf[0] = last ? kOpcWriteEnd : kOpcWriteCont;
355 std::memcpy(buf + 1, s_tx.payload + s_tx.payload_off, chunk);
356 s_tx.payload_off += chunk;
357 return ble->writeCharacteristic(s_tx.conn_handle, s_tx.peer_rx,
358 buf, header + chunk, true);
359}
360
361void onSendDiscovery(uint16_t conn_handle,
362 const cdc::hal::IBluetoothController::DiscoveredService* svc,
363 bool complete)
364{
365 if (!s_tx.active || conn_handle != s_tx.conn_handle) return;
366 if (!svc || !complete) return;
367 if (!(svc->uuid == cdc::hal::BleUuid::from128(kSvcUuid))) return;
368
369 for (uint8_t i = 0; i < svc->numCharacteristics; ++i) {
370 if (svc->characteristics[i].uuid == cdc::hal::BleUuid::from128(kRxUuid)) {
371 s_tx.peer_rx = svc->characteristics[i].valueHandle;
372 break;
373 }
374 }
375 if (s_tx.peer_rx == 0) { cancelSend("RX char not found"); return; }
376 s_tx.awaiting_disc = false;
377 writeNextChunk();
378}
379
380void onSendWriteComplete(uint16_t conn_handle, uint16_t /*attr*/, int status) {
381 if (!s_tx.active || conn_handle != s_tx.conn_handle) return;
382 if (status != 0) { cancelSend("write failed"); return; }
383 if (s_tx.payload_off >= s_tx.payload_len) {
384 LOG_I(TAG, "Send complete, %u bytes", static_cast<unsigned>(s_tx.payload_len));
385 // Leave the peer time to ACK before disconnecting.
387 if (ble) ble->disconnectHandle(s_tx.conn_handle);
388 s_tx = SendSession{};
389 s_disc_token = 0xFFFF;
390 s_write_token = 0xFFFF;
391 return;
392 }
393 writeNextChunk();
394}
395
396} // namespace
397
398bool ble_gpg_xsig_send(const uint8_t addr[6], uint8_t addr_type) {
399 if (s_tx.active) return false;
400
402 if (!ble) return false;
403
404 s_tx.payload_len = buildOwnKeyPayload(s_tx.payload, sizeof(s_tx.payload));
405 if (s_tx.payload_len == 0) return false;
406
407 s_disc_token = ble->addServiceDiscoveryCallback(onSendDiscovery);
408 s_write_token = ble->addWriteCompleteCallback(onSendWriteComplete);
409
410 s_tx.active = true;
411 s_tx.awaiting_disc = true;
412 s_tx.payload_off = 0;
413 if (!ble->connect(addr, addr_type)) {
414 cancelSend("connect failed");
415 return false;
416 }
417 // We rely on the BluetoothController's existing connection callback to
418 // fire discoverServiceByUuid; for the simple case we kick off discovery
419 // right after a small delay implicitly handled by the stack. If the
420 // controller doesn't auto-discover, the caller can extend this layer.
421 s_tx.conn_handle = 0xFFFE; // placeholder, filled by stack
422 ble->discoverServiceByUuid(s_tx.conn_handle,
424 return true;
425}
426
427} // namespace cdc::mod_gpg
static const char * TAG
uint8_t gpg_storage_sig_slot(void)
static bool s_initialized
Definition cdc_log.cpp:28
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
static GpgRecvStore & instance()
#define CDC_CURVE_ED25519
Definition fido2.h:23
#define CDC_CURVE_P256
Definition fido2.h:24
uint8_t curve
bool gpg_get_status(gpg_status_t *status)
Fills status from the OpenPGP card-application state.
Definition gpg.cpp:95
constexpr uint8_t READ_ENC
constexpr uint8_t WRITE_ENC
constexpr uint8_t NOTIFY
constexpr uint8_t READ
constexpr uint8_t WRITE
IBluetoothController * getBluetoothControllerInstance()
Returns singleton Bluetooth stub when NimBLE is unavailable.
ISecureElement * getSecureElementInstance()
Returns singleton secure-element stub instance.
bool ble_gpg_xsig_send(const uint8_t addr[6], uint8_t addr_type)
Push the badge's own public key to a peer.
bool ble_gpg_xsig_init()
Initialise the GPG cross-sign BLE endpoint.
void ble_gpg_xsig_set_received_callback(XsigReceivedCallback cb)
Install / remove the "key received" notification.
void(*)(const gpg_recv_key_t &key) XsigReceivedCallback
Callback invoked when a remote badge has finished pushing a key.
bool calculateFingerprintV5(uint8_t curve, const uint8_t *pubkey, size_t pubkey_len, uint32_t created_at, uint8_t out_fp[32])
Compute the V5 / RFC 9580 OpenPGP fingerprint (SHA-256, 32 bytes).
static BleUuid from128(const uint8_t v[16])
GattCharacteristic * characteristics
DiscoveredCharacteristic characteristics[MAX_DISCOVERED_CHARS]
One GPG public key received from another badge.
Snapshot of the current OpenPGP card-application state for UI display.
Definition gpg.h:25