← 9. The API · Contents
Phone reference¶
How a phone presents a credential to the badgeless reader: over Bluetooth Low Energy, as an NFC card, or as a QR code on its screen. This page is the contract between the two sides that implement it, the test wallet app and the reader's firmware, and neither of them exists yet. Both are written against it.
The phone only ever presents. It never learns whether the door opened: the reader says it with its light and its buzzer, exactly as it does for a card, and the controller decides as it does for any other frame.
| Phone | Reader | Credential type | Frame on the bus | |
|---|---|---|---|---|
| Bluetooth | writes a token over GATT | Pico 2 W radio | mobileId | BLE:<token> |
| NFC | answers as a card (HCE) | PN532 | badgeNumber | NFC:<token> |
| QR code | shows it on screen | GM861S | qrCode | QR:<value> |
| PIN code | shows it, a person types it | keypad | pinCode | PIN:<digits>, assembled by the controller |
The reader stamps the technology¶
The phone sends the value alone. The reader knows which of its parts read it and puts the prefix in front, so a QR code printed on paper and the same code on a phone read the same, and a phone in NFC mode presents a badge exactly as a card would.
The prefixed text goes to the controller as an ordinary osdp_RAW reply, format code 0, its bytes the UTF-8 of the text. The controller turns those bytes into hex text like any other card, and the Badge, PIN, QR or phone, by prefix format (db/migrations/0012_reader_formats.sql) recognises the prefix in hex and decodes the rest with hexText. See 8. The dashboard for what that value node does.
A plain card on the PN532 answers with its UID only. The reader sends that UID's bytes with no prefix, which the same format reads as a badge number, as raw as the default format reads one.
Tokens¶
A token is the credential value as it is stored: FRODO-PHONE, BDG-0001.
- UTF-8, 1 to 64 bytes. Longer is refused, not cut.
- Sent as it is, without a terminator, padding or length prefix.
- The same token is a passage every time it is presented. Nothing on this page stops a recorded token from being replayed (see Not covered yet).
Bluetooth Low Energy¶
The reader is the GATT peripheral and advertises. The phone is the central: it scans, connects, writes its token and disconnects. This is the way round commercial readers work, and it leaves the phone free of the advertising support Android devices do not all share.
UUIDs¶
Every UUID shares the base 14edXXXX-a1dc-411c-8133-24fe0b950a89.
| What | UUID | Properties |
|---|---|---|
| wardn service | 14ed0001-a1dc-411c-8133-24fe0b950a89 | |
| Token | 14ed0002-a1dc-411c-8133-24fe0b950a89 | Write (with response) |
Advertising¶
- The advertising data carries the flags and the wardn service UUID in its complete list of 128-bit services. That is what a phone filters its scan on.
- The scan response carries the complete local name
wardn-<address>, the reader's OSDP address in decimal:wardn-3. The name does not fit next to a 128-bit UUID in 31 bytes once the address has three digits, which is why it is not in the advertising data. - The reader accepts one connection at a time and stops advertising while it holds one.
A presentation¶
sequenceDiagram
participant P as Phone
participant R as Reader (Pico 2 W)
participant C as Controller
P->>R: connect
P->>R: exchange MTU (at least 67)
P->>R: write Token "FRODO-PHONE"
R->>P: disconnect
R->>C: osdp_RAW "BLE:FRODO-PHONE" - The phone connects and asks for an ATT MTU of at least 67, so that a 64-byte token fits in one write. A reader that ends up with less refuses a token that does not fit rather than taking half of it.
- It writes the token to Token, with response. The response says the reader took the token, nothing more.
- The reader disconnects and sends
BLE:<token>on the bus at its next poll. From there it is a card read like any other.
A reader drops a connection that has written nothing after 5 seconds.
Write errors¶
| ATT error | When |
|---|---|
0x0D Invalid Attribute Value Length | empty, or longer than 64 bytes |
0x80 (application) | not valid UTF-8 |
0x81 (application) | a token was already written on this connection |
NFC¶
The phone answers as an ISO-DEP card through Android's host card emulation. Android gives such a card a random UID on every tap, so the reader must not use the UID once the application below has answered.
AID¶
F0 77 61 72 64 6E 01: F0 for a proprietary application, wardn in ASCII, and version 01.
On Android, the HostApduService declares it in the other category, not payment.
A presentation¶
sequenceDiagram
participant R as Reader (PN532)
participant P as Phone
R->>P: select ISO 14443-A target, RATS
R->>P: SELECT AID F0 77 61 72 64 6E 01
P->>R: "BDG-0001" 90 00 - The PN532 selects the target. A target whose SAK says it speaks ISO-DEP (bit
0x20) gets the SELECT below. Any other is a plain card: read its UID and stop there. - The reader sends SELECT by name:
00 A4 04 00 07 F0 77 61 72 64 6E 01 00. - The phone answers with the token and
90 00, in the same response. One exchange is all a tap has time for. - The reader sends
NFC:<token>on the bus.
| Response | Meaning | What the reader sends |
|---|---|---|
token, 90 00 | a persona is selected and has a badge number | NFC:<token> |
69 85 | no persona selected, or it has no badge number | nothing |
6A 82 | not a wardn phone: the AID is not there | the UID, as a plain card |
QR code¶
The phone shows the value of the selected persona's qrCode credential, nothing more. The scanner reports what it reads, and the reader adds QR:.
Without a reader¶
Until a badgeless reader is on the bench, the app can send a frame straight to the controller's simulated port, a TCP line of the form bus:<address>:<frame>. The frame is the hex text a real reader's bytes would have become on the bus, so it goes through the same format:
bus:15:424C453A46524F444F2D50484F4E45
That is BLE:FRODO-PHONE at the seeded badgeless reader of the Postern gate. A PIN is the one exception, in clear text, the way the controller assembles it: bus:15:PIN:4821.
The test wallet app¶
mobile/wardn-wallet/ is the phone side of this page: an Android app, Kotlin and Compose, minSdk 26. It is a test tool, not a product, and it holds no link to the cloud.
- In the dashboard, Users, then Test wallet. Pick the people to carry, then Export test wallet. In the app, Scan that code. The wallet stays on the phone until the next scan.
- Pick a persona: a person of the workspace, or one of two strangers whose credentials the workspace has never seen.
- Each of its credentials is a card with one way to present it: hold the phone to the reader for a badge, Show QR code or Show PIN full screen, or Send over Bluetooth for a mobile ID. Simulate sends it instead to the simulation port (
SIMULATION_LISTEN) of a reader's controller, picked among the readers whose format reads that type, as the frame that reader would send. The activity log marks that passage simulated. - On each card, every door of the wallet and what it should answer to that credential right now. The rules are those of
decideinedge/wardn-domain/src/decision.rs, door modes included, after one more: a door none of whose readers decodes the type refuses it, unless it is unlocked. The app cannot know about a door with nothing wired to it, which the controller refuses.
A simulated read goes to the address the controller last reported. A controller of the dev stack reports its Docker address, which the phone cannot reach: turn on Send everything to this host in the settings, with the computer's address and EDGE_READER_PORT.
cd mobile/wardn-wallet
./gradlew :app:testDebugUnitTest :app:lintDebug # the checks
./gradlew :app:installDebug # onto a phone plugged in over USB
It needs JDK 21 or newer, the Android SDK with platform 37.2, and local.properties naming the SDK (sdk.dir=…), which Android Studio writes on first open. None of this is in CI.
Test vectors¶
| Phone sends | Bytes on the bus | Format reads |
|---|---|---|
BLE FRODO-PHONE | 424C453A46524F444F2D50484F4E45 | mobileId FRODO-PHONE |
NFC BDG-0001 | 4E46433A4244472D30303031 | badgeNumber BDG-0001 |
QR GATE-0001 | 51523A474154452D30303031 | qrCode GATE-0001 |
a plain card, UID 04A2FF10 | 04A2FF10 | badgeNumber 04A2FF10 |
The same vectors are the format's tests, in edge/wardn-domain/src/format.rs and backend/src/common/seed-check.spec.ts.
Not covered yet¶
- Replay. A token is fixed, so anyone who recorded one can send it again. The answer is a challenge: the reader sends a random nonce and the phone signs it. The controller stores only hashes of credentials and could not check a signature made with a shared secret, so this needs a key pair per phone, its public key held as the credential. That is a new credential type and goes through 10. Security first.
- Pairing. The GATT link is neither paired nor encrypted, which is what a fixed token allows anyway.
- iOS. An app may emulate an NFC card there only with an entitlement Apple grants case by case, which a test tool is not going to get. The Bluetooth half would work.