aliro_reader.c
Aliro reader engine: drives the Access Protocol (AUTH0/AUTH1/EXCHANGE) handshake over BLE…
Overview
Aliro reader engine: drives the Access Protocol (AUTH0/AUTH1/EXCHANGE) handshake over BLE, manages reader identity and credential trust provisioning in NVS, and arms UWB ranging once a session is authenticated. Maintains a fixed-size table of per-connection sessions tracking transaction phase and secure-channel state, and exposes start/attach entry points for both standalone and Matter-attached BLE transports, plus provisioning and diagnostic APIs used by Matter commissioning and the bench console.
depends on aliro_ble.h aliro_crypto.h aliro_lab.h aliro_lat.h aliro_prim.h aliro_prov.h aliro_reader.h aliro_rssi_gate.h aliro_stepup.h aliro_apdu.h aliro_ranging.h woz_log.h woz_port.h
flowchart TD aliro_reader_authenticated_credential --> load_provisioning
API
Fstatic void compute_reader_group_x(void)
Recover the reader group key X from the provisioned signingKey. Call after any mutation of s_id. Leaves s_have_group_x=false (and logs) on failure.
aliro_reader_provision_clear, aliro_reader_provision_identity, load_provisioningFstatic const char *phase_str(enum txn_phase p)
Returns a human-readable name for a transaction phase enum value, or "?" for an unrecognized value.
on_disconnected, transaction_feedCstatic struct aliro_session
One credential-auth transaction, keyed by BLE connection handle. Holds the reader's ephemeral keypair and the transcript inputs (txid, device pubkey, Z) that derive the two secure channels and the URSK, so everything a transaction needs between AUTH0 and ranging setup lives here. Cleared on disconnect; ALIRO_MAX_SESSIONS of them are statically allocated.
Fstatic struct aliro_session *session_find(uint16_t conn_handle)
Finds the active session matching the given BLE connection handle. Returns a pointer to the matching session, or NULL if no active session has that conn_handle.
aliro_reader_rssi_sample, on_data, on_disconnectedFstatic struct aliro_session *session_alloc(uint16_t conn_handle)
Allocates and returns the first inactive slot in the fixed-size session table for a new connection, initializing it to phase PH_IDLE. Returns NULL if all ALIRO_MAX_SESSIONS slots are already active.
on_connectedFstatic void load_provisioning(void)
Loads the reader's provisioning state (identity, trust anchors) from NVS into the module-level s_id/s_trust, lazily creating the provisioning mutex on first call. Idempotent: does nothing on subsequent calls once s_loaded is set. Logs whether a dev-default or real identity was loaded and its source (NVS vs. dev default), then recomputes the reader group X coordinate.
aliro_reader_authenticated_credential, aliro_reader_prov_print, aliro_reader_provision_add_trust, aliro_reader_provision_clear, aliro_reader_provision_identity, aliro_reader_stepup_arm, aliro_reader_stepup_status, aliro_reader_trust_clear · calls compute_reader_group_xFstatic int send_ap_command(uint16_t conn, uint8_t ins, const uint8_t *tlv, size_t len)
Frame + send an Access-Protocol command: wrap the command TLV in an ISO7816 APDU (ins selects AUTH0/AUTH1/EXCHANGE), then a BLE Access frame (type=ACCESS, opcode=AP_OP_COMMAND). The command byte lives in the APDU INS, NOT the BLE opcode — the phone rejects a raw TLV under opcode=INS.
on_auth0_response, send_exchange, start_authFstatic int send_ap_raw(uint16_t conn, const uint8_t *apdu, size_t len)
Frame + send a fully-formed ISO7816 APDU (ENVELOPE / GET RESPONSE already carry their own CLA/INS, so they must NOT go through aliro_apdu_wrap). Same BLE Access frame as send_ap_command: [type=ACCESS][op=COMMAND][len_be16][apdu].
on_stepup_response, stepup_send_requestFstatic void start_auth(struct aliro_session *s)
Kick the reader-driven access protocol: ephemeral keys + txid -> AUTH0.
transaction_feed · calls send_ap_commandFstatic int init_ble_channel(struct aliro_session *s, const uint8_t block[ALIRO_KEY_BLOCK_LEN])
Derive + init the BleSK ranging channel (§11.8.1) off a 160-byte key block; BleSK sits at offset 96 in both the standard and the fast block. Salt = reader_supported_versions || user_device_selected_version — we advertise and select v1.0 only, so salt = 01 00 01 00. Its own counters (fresh from 1) live in s->sc_ble and carry across AP-Completed + M1..M4. Returns 0 on success.
on_auth1_response, try_fast_authFstatic void send_exchange(struct aliro_session *s)
Seal + send the EXCHANGE URSK-ready trigger; both auth paths land here once the secure channels + URSK are up. The AP then waits for the EXCHANGE response: §11.1.1 requires Reader-Status-AP-Completed (BleSK-sealed) after EXCHANGE succeeds, otherwise the device stalls and drops (URSK_Unavailable); on_exchange_response drives that + ranging.
on_auth1_response, try_fast_auth · calls send_ap_commandFstatic int try_fast_auth(struct aliro_session *s, const struct aliro_auth0_response *r)
Expedited-fast trial (§8.3.1.10-.12): the cryptogram proves the phone holds a Kpersistent agreed in an earlier standard phase with one of our trusted credentials. Trial-derive the fast block under each stored Kpersistent and try the AES-GCM open; a match authenticates the session with no ECDH, no signatures and no AUTH1 round-trip, and identifies the credential for the unlock attribution. Returns 0 when the session was consumed (EXCHANGE sent, or a hard failure); -1 when nothing matched and the caller should continue with the standard phase.
on_auth0_response · calls init_ble_channel, send_exchangeFstatic void on_auth0_response(struct aliro_session *s, const uint8_t *pl, size_t len)
Handles an inbound AUTH0Response: strips the APDU status word, parses the device's ephemeral public key, performs ECDH with the reader's ephemeral private key, derives the KDF intermediate z, signs the reader-usage transcript, and sends AUTH1. On any failure (short/malformed APDU, parse failure, ECDH failure, signing failure) sets s->phase to PH_FAILED and returns without sending. On success sets s->phase to PH_SENT_AUTH1 after sending the AUTH1 command. Logs (does not fail on) an unexpected status word other than 0x9000.
transaction_feed · calls send_ap_command, try_fast_authFstatic void on_auth1_response(struct aliro_session *s, const uint8_t *pl, size_t len)
Handles an inbound AUTH1Response: derives the AP and BLE-ranging secure channel keys and URSK from the ECDH intermediate, decrypts and parses the response, verifies the device's signature, checks trust, and sends EXCHANGE. Requires the reader group X coordinate to already be available (s_have_group_x); fails otherwise. Derives the session salt and 160-byte key block, splits it into the AP channel keys and URSK, and separately derives the BLE ranging-channel keys from the block's BleSK segment using a versions-based salt; both secure channels are initialized with counters starting at 1. Decrypts the AUTH1Response body via AES-GCM and fails on tag mismatch (indicating a key/counter/framing error), oversized ciphertext, or parse failure. Verifies the device's signature over the device-usage transcript using the presented device public key if available, else the device's ephemeral public key; a bad signature fails the session. Records the presented credential key under s_prov_lock and checks it against the trust store: an untrusted key fails the session unless the reader identity is the dev default (which accepts and warns). On success, seals and sends the EXCHANGE command, sets s->phase to PH_SENT_EXCHANGE, and logs the derived URSK; on any failure path sets s->phase to PH_FAILED and returns without sending EXCHANGE.
transaction_feed · calls init_ble_channel, send_exchangeFstatic void complete_ap_and_range(struct aliro_session *s)
Send Reader-Status-AP-Completed (BleSK-sealed) and arm the ranging engine; the transaction ends in PH_ESTABLISHED. Shared by the normal EXCHANGE path and the step-up path (which runs it only after the DeviceResponse is collected).
aliro_reader_rssi_sample, gated_complete_ap · calls reader_status_sendFstatic void gated_complete_ap(struct aliro_session *s)
Run complete_ap_and_range only once the RSSI power gate allows the UWB radio: the device does not initiate ranging until it receives AP-Completed (the comment above k_ap_completed_plain), so holding that one message here keeps the DW3000 dark while the phone is still tens of metres out. The gate opening (aliro_reader_rssi_sample) completes the AP; a phone that gives up meanwhile simply reconnects on approach and re-runs the fast auth. Direct call when the gate is compiled out.
on_exchange_response, on_stepup_response, stepup_send_request · calls complete_ap_and_rangeFstatic void stepup_send_request(struct aliro_session *s)
Build the Access-Document DeviceRequest, seal it into a SessionData message on the StepUpSK channel, and send it in an ENVELOPE APDU (§8.4). On any build/seal failure fall back to completing the AP so the unlock is never blocked.
on_exchange_response · calls gated_complete_ap, send_ap_rawFstatic void stepup_submit_job(struct aliro_session *s)
Hand the collected SessionData response + StepUpSK keys to the background worker so the parse/verify runs off the BLE-host task (never in the ranging arm window). No issuer trust store is provisioned in this reference build, so the verifier selects by x5chain if present and otherwise records "issuer key not found"; the verdict is logged only. A trusted wall clock is not wired (time_valid = 0), so a TimeVerificationRequired document is recorded as time-unverified.
on_stepup_responseFstatic void on_stepup_response(struct aliro_session *s, const uint8_t *pl, size_t len)
Collect the DeviceResponse across ENVELOPE / GET RESPONSE (ISO7816 61XX chaining) before completing the AP. The worker verifies it afterwards.
transaction_feed · calls gated_complete_ap, send_ap_raw, stepup_submit_jobFstatic void on_exchange_response(struct aliro_session *s, const uint8_t *pl, size_t len)
Handle the EXCHANGE response, then complete the AP and arm ranging. The body is an AP (proto-0) response on the ExpeditedSK channel: <ct || 16B tag> SW1SW2.
transaction_feed · calls gated_complete_ap, stepup_send_requestFstatic void reader_status_send(struct aliro_session *s, bool unsecured)
Reader Status Changed (Aliro transaction step 23): the reader->phone grant/relock confirmation that fires the iPhone Wallet unlock animation. proto-2 (Notification) message-id 0x02, one State Attribute (id 0x00, len 2) = [OperationSource, ReaderStateByte]. OperationSource 0x04 = this user device in the BLE+UWB Aliro flow; ReaderStateByte Unsecured 0x01 = granted (animate), Secured 0x00 = relocked. The 65-byte access-credential public key is NOT serialized (the reference uses it only to select which connection to notify). Plaintext the BleSK channel then seals: [02][02][00 04][00 02 04 <state>]. Runs on the BLE-host task (posted via aliro_ble_post_reader_status) so it serializes with the other sc_ble seals.
aliro_reader_rssi_sample, complete_ap_and_range, reader_status_send_on_hostFvoid aliro_reader_notify_unlock(bool unsecured)
Sends a Reader-Status BLE notification reporting the lock's unsecured/secured state to the connected device. unsecured is true if the reader/lock is currently unsecured (unlocked), false if secured.
Fbool aliro_reader_session_active(void)
Reports whether any peer currently holds an established Aliro session. Returns true if at least one session slot is active and in the established phase.
Fbool aliro_reader_authenticated_credential(uint8_t out[ALIRO_CRED_PUB_LEN])
Copies the credential public key that most recently passed the trust check into out. Returns true if a credential has authenticated since boot (out written), false otherwise (out untouched). Safe to call from any task.
load_provisioningFstatic size_t capture_a5_tlv(const uint8_t *pl, size_t pl_len, uint8_t *out, size_t cap)
Scan an op-0x05 Initiate-Access-Protocol payload for the phone's 0xA5 proprietary-information TLV (short-form BER length; the A5 value is small) and copy the whole TLV (tag+len+value) into out. Returns the stored length, or 0 if no well-formed 0xA5 TLV fits.
transaction_feedFstatic void transaction_feed(struct aliro_session *s, const uint8_t *data, uint16_t len)
Consume one inbound Aliro transaction SDU.
on_data · calls capture_a5_tlv, on_auth0_response, on_auth1_response, on_exchange_response, on_stepup_response, phase_str, start_authFvoid aliro_reader_rssi_sample(uint16_t conn_handle, int8_t rssi_dbm)
Feeds one connection-RSSI sample into the session's ranging power gate and acts on the resulting transition: gate opening completes a held AP (starts ranging); gate closing on an established session tears ranging down and drops the link (the phone re-runs the fast auth on its next approach). Runs on the BLE-host task, same as every other session touch point.
complete_ap_and_range, reader_status_send, session_findFstatic void on_connected(uint16_t conn_handle)
BLE connection-established callback: allocates a session slot for the new connection. Logs an error and returns without effect if no free session slot is available.
session_allocFstatic void on_disconnected(uint16_t conn_handle)
BLE disconnection callback: marks the connection's session inactive (if one exists) and stops any UWB ranging associated with the connection. Logs the session's message count and final transaction phase before deactivating it.
phase_str, session_find, spare_eph_refillFstatic void on_data(uint16_t conn_handle, const uint8_t *data, uint16_t len)
BLE data-received callback: looks up the session for conn_handle and feeds the data into its transaction state machine. Logs a warning and drops the data if no active session exists for conn_handle.
session_find, transaction_feedFstatic struct aliro_ble_config make_ble_cfg(void)
The reader's BLE transport config: advertised versions/features + the transaction transport callbacks. Shared by the standalone + attached starts.
aliro_reader_ble_prepare, aliro_reader_startFstatic int reader_engine_init(void)
crypto + provisioning load + UWB ranging setup, shared by both start paths.
aliro_reader_start, aliro_reader_start_attached · calls load_provisioning, spare_eph_refillFint aliro_reader_start(void)
Starts the Aliro reader: initializes the engine (crypto, provisioning, UWB ranging) and brings up the BLE transport using the default advertising config. Returns 0 on success; returns -1 if engine initialization fails, or the underlying aliro_ble_start result otherwise.
make_ble_cfg, reader_engine_initFconst void *aliro_reader_ble_prepare(void)
Prepares the BLE transport and returns the Aliro GATT service definition for external registration, without starting the transport. Returns NULL if aliro_ble_prepare fails; on success returns the pointer from aliro_ble_service_def(), owned by the BLE layer.
make_ble_cfgFint aliro_reader_start_attached(void)
Starts the Aliro reader in "attached" transport mode: initializes the engine, applies provisioned resolvable advertising parameters if a real GRK is present, then starts the attached BLE transport. Unlike aliro_reader_start, this applies GRK-based advertising params (group/subgroup ID from reader_id, GRK) before starting, when the reader has already been provisioned; falls back to unresolvable advertising if no GRK is set yet. Returns 0 on success; returns -1 if engine initialization fails, or the underlying aliro_ble_start_attached result otherwise.
reader_engine_initFvoid aliro_reader_refresh_adv(void)
Refreshes the BLE advertisement to include the resolvable service data once a real GroupResolvingKey (GRK) is available. Handles the case where Matter provisioning (SetAliroReaderConfig) lands after advertising has already started with only the bare 0xFFF2 UUID (dev default, all-zero GRK), which the phone cannot resolve. No-ops if the GRK in s_id is still all-zero. On a nonzero GRK, derives the two-byte subgroup ID from reader_id[16..17] and calls aliro_ble_set_adv_params + aliro_ble_readvertise to make the reader approach-resolvable.
Fvoid aliro_reader_prov_print(void)
Print the reader's provisioning state (identity, trust anchors, last presented credential) to the console for diagnostics. Loads provisioning first, then snapshots the shared state under s_prov_lock before printing so UART I/O does not hold the lock during the BLE task's trust check.
load_provisioningFint aliro_reader_trust_last(void)
Add the most recently presented credential's public key to the trust store and persist it. Returns 1 if no credential has been presented yet or it is already trusted (nothing persisted), -1 if the store is full or the NVS write fails (in-memory trust store left unchanged on failure), 0 if newly added and committed.
load_provisioningFint aliro_reader_trust_clear(void)
Empty the trust store and persist the empty store, keeping the reader identity. Returns 1 if the store was already empty (nothing persisted), -1 if the NVS write fails (in-memory trust store left unchanged), 0 if cleared and committed. Every re-pair mints a fresh credential and nothing evicts the old ones, so the store reaches ALIRO_TRUST_MAX and refuses the key currently being presented. A Matter factory reset does not touch this namespace, so without this the only way out is erasing NVS.
load_provisioningFvoid aliro_reader_stepup_arm(void)
Arm a one-shot Access-Document request (see aliro_reader.h). No-op with a note when the reader was built without CONFIG_WOZ_ALIRO_STEPUP.
load_provisioningFvoid aliro_reader_stepup_status(void)
Print the armed state and the most recent verification verdict (see aliro_reader.h).
load_provisioningFint aliro_reader_provision_identity(const uint8_t reader_id[ALIRO_READER_ID_LEN], const uint8_t sign_priv[ALIRO_READER_PRIV_LEN], const uint8_t grk[ALIRO_GRK_LEN])
Store a Matter-provisioned reader identity (reader ID, signing private key, GRK), keeping any trust anchors already present, and persist it to NVS. Returns -1 if the NVS write fails, in which case in-memory identity (s_id) is unchanged; returns 0 on success, after which the reader group key salt is recomputed via compute_reader_group_x since the signing key changed.
compute_reader_group_x, load_provisioningFint aliro_reader_provision_add_trust(const uint8_t cred_pub[ALIRO_CRED_PUB_LEN])
Add a Matter-provisioned credential public key to the reader's trust store and persist it. Returns 0 if newly added and stored, 1 if the credential was already trusted (nothing persisted), -1 if the store is full, cred_pub is not a valid P-256 point, or the NVS write fails. On failure the in-memory trust store (s_trust) is left unchanged.
load_provisioningFint aliro_reader_provision_clear(void)
Revert the reader's provisioning to the default dev identity and empty trust store, and persist that state to NVS. Returns -1 if the NVS write fails, in which case in-memory state is unchanged; returns 0 on success, after which the reader group key salt is recomputed via compute_reader_group_x.
compute_reader_group_x, load_provisioningUndocumented (2)
spare_eph_refill, reader_status_send_on_host