It has been long time since i used Bluetooth SPP. Infact I do not think I ever used Bluetooth SPP via firmware. I think i used an RN42 module to get UART converted to Bluetooth SPP. But I have used BLE GATT so, I will be using that to exchange data between the Door and ID device.
Recap:
The idea is simple enough: stop making people swipe a card and type a PIN at every single door. Instead, the ID card (a MAX32630FTHR + ATECC508A in your pocket) unlocks once via PIN, then silently does challenge-response crypto over Bluetooth every time you walk up to a door. If the card gets yanked off you, the IMU detects the tug and it locks itself. No PIN, no entry. For more details check the Part 1 of the series
- Identity Protocol Part 1 - Plan
- Identity Protocol - Part 2 - Django Server
- Identity Protocol - Part 3 - Unboxing and Blinking with Maxim LPSDK
- Identity Protocol - Part 4 - BLE using PAN1326B and BTstack
- Identity Protocol - Part 5 - Interfacing a Keypad
- Identity Protocol - Part 6 - Snatch Detection with the BMI160 IMU
- Identity Protocol - Part 7 - Colouring on the ICLED FeatherWing
- Identity Protocol - Part 8 - Cryptographically Sign with ATECC508 and Verify with Micro-ECC
GATT Service Definition
BTstack generates an ATT database at plaintext .gatt file. Refer: https://github.com/bluekitchen/btstack/blob/master/tool/compile_gatt.py For our purpose, I need to detect the door and ID card to be nearby, one service to write the nonce and another for the response. I did take help of ChatGPT to generate the file. If you are new to GATT - I recommend to watch this video - www.youtube.com/watch
PRIMARY_SERVICE, GAP_SERVICE
CHARACTERISTIC, GAP_DEVICE_NAME, READ, "Auth-Door"
PRIMARY_SERVICE, GATT_SERVICE
CHARACTERISTIC, GATT_DATABASE_HASH, READ,
// Identity Protocol Auth Service
PRIMARY_SERVICE, AAAAAAAA-0000-0000-0000-000000000001
// CHALLENGE: 32-byte nonce sent by the door, read by the card
CHARACTERISTIC, AAAAAAAA-0000-0000-0000-000000000002, READ | DYNAMIC,
// RESPONSE: 4-byte device_id followed by 64-byte signature written by the card
CHARACTERISTIC, AAAAAAAA-0000-0000-0000-000000000003, WRITE | DYNAMIC,
For Testing two Roles, One Source File
Both the door and the card run on identical hardware (MAX32630FTHR + PAN1326B), so the test is a single C file compiled twice. A ROLE make variable drives the split:
make ROLE=server # door firmware
make ROLE=client # card firmware
-DROLE_SERVER / -DROLE_CLIENT select which half of gatt_auth.c is compiled. The server advertises, serves CHALLENGE when ID card nearby, and verifies writes to RESPONSE. The client scans, connects, reads CHALLENGE, signs, and writes RESPONSE.

The Server Side (Door)
The server's packet handler wakes up three places: stack ready, client connected, and every ATT write. When client connected, we serve the once, when ATT writes to the signature service, we verify it.
Even though i have only one feather to function as a ID device, i made the firmware such that it can store multiple ID Cards device name and their public key to be synced from the Django server.
if (att_handle != RESPONSE_VALUE_HANDLE) return 0;
if (buffer_size != 68) {
printf("VERIFY FAIL, expected 68 bytes, got %u\n", buffer_size);
return 0;
}
uint32_t device_id = (buffer[0] << 24) | (buffer[1] << 16)
| (buffer[2] << 8) | buffer[3];
const uint8_t *sig = buffer + 4;
// Serch for the public key from the flash.
const uint8_t *pubkey = lookup_pubkey(device_id);
if (!pubkey) {
/* unknown device (not in django server, reject */
return 0;
}
// Generate the digest via the crypto chip
uint8_t digest[32];
atcac_sw_sha2_256(nonce, 32, digest);
// Verify using micro-ecc
int ok = uECC_verify(pubkey, digest, sig);
printf(ok ? "*** VERIFY PASS ***\n" : "*** VERIFY FAIL ***\n");
The Client Side (Card)
The client we make a state machine:
typedef enum {
STATE_IDLE,
STATE_SCANNING,
STATE_CONNECTING,
STATE_DISCOVER_SERVICE,
STATE_DISCOVER_CHARS,
STATE_READ_CHALLENGE,
STATE_WRITE_RESPONSE,
STATE_DONE,
} client_state_t;
On HCI state HCI_STATE_WORKING the client calls gap_start_scan() and watches for advertisements under name "Auth-Door". When it sees one, we execute gap_connect. once the state advances to STATE_DISCOVER_SERVICE and a gatt_client_discover_primary_services_by_uuid128 call goes out with the auth service UUID.
During the STATE_READ_CHALLENGE phase , the 32 byte nonce has arrived from the server, the client hashes it, packs its own device ID into the first four bytes of the response buffer, and calls atcab_sign:
uint8_t digest[32];
atcac_sw_sha2_256(challenge, 32, digest);
response_buf[0] = (CLIENT_DEVICE_ID >> 24) & 0xFF;
response_buf[1] = (CLIENT_DEVICE_ID >> 16) & 0xFF;
response_buf[2] = (CLIENT_DEVICE_ID >> 8) & 0xFF;
response_buf[3] = CLIENT_DEVICE_ID & 0xFF;
atcab_sign(0, digest, response_buf + 4);
gatt_client_write_value_of_characteristic(
handler, conn_handle, response_handle, 68, response_buf);
The device_id is the lower four bytes of the ATECC508A serial number which we saved during provisioning and locking of the chip along with it's public key. This is the same four bytes the server will look up in its static keystore to find the public key.
The ID Card's LED lights up green when connecting to the door device and the door device's LED turns green when the signing of nonce by ATEC508A is successfully verified by micro-ecc
Result
With both bugs sorted, the UART log across two boards looks like this on a clean run.
Again, i am using a PCB which i designed for a client to work like an breakout board for the ATTEC508A. Hence the blur. After a successful verification, the door devices starts readvertising and i do have to reset the ID card to re-auth again.
Final Notes
Again, thanks to BTStacks awesome documentation, this was an easy three hour work after setting up ATECC508A. There was some hold up due to typo in the micro-ecc bundled by BTStack, which i thought was my problem but later I realized it was a bug. I did write the public key manually in the lookup table for this unit test, in the final unit test, I will interface W5500 to sync the public keys to the feather board.