Dubrazec standard, revision 3, wire protocol
The layout of the card, its memories and its electrical figures are on the CARD-ROM page. This document is the protocol: frame format, the twelve commands in their encoding, the authentication sequence, how a record is sealed, and the status words a reader must handle.
Frames
Anticollision and the half-duplex block protocol are ISO 14443-3 Type A, unchanged. Above them the standard defines its own frame rather than ISO 14443-4, because the deadline at a gate does not survive the block chaining negotiation. A frame is at most 264 bytes over the coil and 1,032 over the pads.
| Offset | Length | Field | Note |
|---|---|---|---|
| 0 | 1 | CMD | Command code, 0x01 to 0x0C. |
| 1 | 1 | FLG | Bit 0 session open, bit 1 contact interface, bit 2 more frames follow, bits 3 to 7 zero. |
| 2 | 2 | LEN | Payload length, big endian. |
| 4 | LEN | Payload | Command arguments, or the reply. |
| 4+LEN | 2 | SW | Status word. Present in replies only. |
| 6+LEN | 2 | CRC | CRC-16 as in ISO 14443-3, over the whole frame. |
Commands
| Code | Command | Payload out | Payload back | Typical |
|---|---|---|---|---|
| 0x01 | SELECT | none | revision, region sizes, capability bits | 12 ms |
| 0x02 | READ_ID | none | 256 B ROM block | 28 ms |
| 0x03 | AUTH | key ref, reader nonce, 8 B | card nonce and MAC, 24 B | 46 ms |
| 0x04 | READ_OPT | offset, length | bytes | 9 ms |
| 0x05 | WRITE_OPT | offset, bytes | none | 22 ms |
| 0x06 | READ_REC | from index, count, max 64 | records, 32 B each | 18 ms |
| 0x07 | APPEND_REC | record, 32 B | index written | 62 ms |
| 0x08 | MERGE | source UUID, session token | count copied | 340 ms |
| 0x09 | EXPIRE | cut-off stamp | count dropped | 38 ms |
| 0x0A | STATUS | none | wear counters, bad blocks, record count | 11 ms |
| 0x0B | READ_FREE | offset, length | bytes | 14 ms |
| 0x0C | WRITE_FREE | offset, bytes | none | 26 ms |
WRITE_OPT, WRITE_FREE and MERGE return 0x6982 over the coil in every build. A card in a pocket cannot be written to by a reader held near it.
Authentication
reader -> card SELECT
card -> reader rev=04 rom=0100 eep=2000 nand=200000 caps=0x0F
reader -> card READ_ID
card -> reader uuid=04:9C:2E:7B:41:80:00 issuer=... sig=Ed25519(...)
reader checks sig against the issuer key it holds
K = AES-128-CMAC(Kmaster, uuid || 0x01) per-card key
reader -> card AUTH keyref=00 rndR=8B
card -> reader rndC=8B macC=AES-CMAC(K, rndR || rndC)[0..7]
reader -> card AUTH keyref=00 macR=AES-CMAC(K, rndC || rndR)[0..7]
card -> reader SW=9000 session open, SK = AES-CMAC(K, rndR || rndC)
The session key seals every frame that follows until the field drops or 0x0F is sent. A reader that fails the second pass gets 0x6300 and three attempts before the card refuses the key reference for 200 ms, which is long enough to make a search pointless at a gate and short enough that a person who taps twice notices nothing.
Records
32 bytes, big endian throughout 0 u32 stamp minutes since 2000-01-01 00:00 local standard time 4 u64 station station identifier, see /v1/stations 12 u64 section pair of station identifiers, 32 bits each, or zero 20 u8 count 1 to 9 21 u8 kind 0 gate 1 machine 2 aboard 3 merge 4 issue 22 u16 operator cooperative identifier 24 u64 seal AES-CMAC(Knet, bytes 0..23 || uuid)[0..7]
The seal is taken under the network key, not the per-card key, so a record written in Iosmein is checkable by a Fluid gate that holds the Iosmein network key and by nothing else. A gate that cannot check a seal counts the record and marks it unverified in its own log; it never refuses passage over it.
A record with kind 3 puts the UUID of the card it came from in the section field, and the count field says how many records followed it.
Status words
| SW | Meaning | What a gate does |
|---|---|---|
| 0x9000 | Done. | Continues. |
| 0x6300 | Authentication failed. | Retries once, then shows the panel that names the card and refuses passage. |
| 0x6581 | NAND write failed after retries. | Opens anyway and logs the card as unwritable, so the counter can replace it. |
| 0x6700 | Bad length. | Firmware fault. Logged and reported. |
| 0x6982 | Refused on this interface. | Only over the coil, for the three write commands. |
| 0x6A82 | Region not present. | Card of an older revision. Falls back to READ_ID and the count is taken without a section. |
| 0x6A84 | No space, all blocks worn. | Opens, and the panel asks the holder to take a new card at the machine. |
| 0x6F00 | Card lost the field mid-command. | Nothing is committed. The journal replays on the next presentation. |
Test vectors for diversification, both authentication passes and the record seal are in vectors.txt here, and in the tree at source.nrf.rof/conformance/vectors. A build that reproduces them is a build the conformance suite will run against.