openaliro
Aliro reader: UWB/CCC core and ESP32-S3/C5/C6 port
Loading...
Searching...
No Matches
aliro_device.h
1// Aliro initiator (User-Device) session layer: the device-side counterpart of
2// aliro_reader.c. Drives the credential-auth handshake from the phone/fob role —
3// parses the reader's AUTH0/AUTH1/EXCHANGE commands, runs the mirror-image key
4// schedule (ECDH, the two ECDSA transcripts, the §8.3.1.13 salt), and produces
5// the sealed responses. The result is the same 32-byte URSK the reader derives.
6/*
7 * Copyright (c) 2026 asxeem
8 * SPDX-License-Identifier: ISC
9 *
10 * aliro_device — the initiator half of the Aliro Access Protocol. Reuses the
11 * direction-symmetric crypto (aliro_crypto.c) and EC primitives (aliro_prim.h);
12 * the only genuinely new logic is the inverse codec (aliro_device_apdu) and the
13 * device view of the two AES-256-GCM channels (opposite seal/open direction to
14 * the reader). Compiled by the device build + host tests only.
15 *
16 * Provenance: clean-room, mirrored from the reader flow in aliro_reader.c.
17 */
18#pragma once
19
20#include <stddef.h>
21#include <stdint.h>
22
23#include "aliro_crypto.h" /* ALIRO_KEY_BLOCK_LEN + channel/BleSK derivation */
24#include "aliro_device_apdu.h"
25
26#ifdef __cplusplus
27extern "C" {
28#endif
29
30/* Device view of an Aliro AES-256-GCM channel. The reader's aliro_secchan seals
31 * on direction 0 and opens on direction 1; the device is the mirror — it OPENS
32 * reader->device traffic (direction 0, key s0) and SEALS device->reader traffic
33 * (direction 1, key s1). Both per-direction counters start at 1 (§8.3.1.13). */
34struct aliro_dev_secchan {
35 uint8_t s0[32]; /* reader->device key (block split S0) */
36 uint8_t s1[32]; /* device->reader key (block split S1) */
37 uint32_t ctr_r2d;
38 uint32_t ctr_d2r;
39};
40
41void aliro_dev_secchan_init(struct aliro_dev_secchan *sc, const uint8_t s0[32],
42 const uint8_t s1[32]);
43/* Open an inbound reader->device message (direction 0, key s0). Advances ctr_r2d
44 * on success; returns <0 on a GCM tag mismatch (hard auth failure). */
45int aliro_dev_secchan_open(struct aliro_dev_secchan *sc, const uint8_t *ct, size_t ct_len,
46 const uint8_t tag[16], uint8_t *pt);
47/* Seal an outbound device->reader message (direction 1, key s1). Advances
48 * ctr_d2r; writes ct (ct_len == pt_len) and the 16-byte tag. Returns 0. */
49int aliro_dev_secchan_seal(struct aliro_dev_secchan *sc, const uint8_t *pt, size_t pt_len,
50 uint8_t *ct, uint8_t tag[16]);
51
52/* ---- device BleSK ranging channel (mirror of the reader's sc_ble) ----
53 *
54 * The UWB ranging-setup traffic (Reader-Status AP-Completed, Initiate-Ranging,
55 * M1-M4, notifications) rides one BleSK-keyed AES-256-GCM channel — the same
56 * construction as the AP channel above, but each SDU carries a 4-byte
57 * [proto][id][len_be16] header that is authenticated as GCM AAD and whose wire
58 * length field is payload+16 (the reader's aliro_msg_seal/open framing). We
59 * reuse struct aliro_dev_secchan: s0 = BleSKReader (device OPENS, direction 0),
60 * s1 = BleSKDevice (device SEALS, direction 1). */
61
62/* Initialise the device BleSK channel from the 160-byte key block and the
63 * versions salt (reader_supported_versions || selected_version; 01 00 01 00 for
64 * v1.0-only). Derives BleSKReader/BleSKDevice via aliro_crypto_derive_ble_keys;
65 * both counters start at 1. Returns 0, or <0 if the HKDF derivation fails. */
66int aliro_dev_blesk_init(struct aliro_dev_secchan *ch, const uint8_t block[ALIRO_KEY_BLOCK_LEN],
67 const uint8_t *versions_salt, size_t salt_len);
68
69/* Open a reader-sealed BleSK SDU: wire = [proto][id][len_be16][ct||tag] with
70 * len_be16 = payload_len + 16. Authenticates the 4-byte header as AAD, opens on
71 * direction 0 (key s0 = BleSKReader), writes plain = [proto][id][payload_len_be16]
72 * [payload] and sets *plain_len = 4 + payload_len. Advances ctr_r2d; returns 0 on
73 * success, <0 on a tag/length failure (hard auth failure). */
74int aliro_dev_ble_open(struct aliro_dev_secchan *ch, const uint8_t *wire, size_t wire_len,
75 uint8_t *plain, size_t plain_cap, size_t *plain_len);
76
77/* Seal a device->reader BleSK SDU (inverse of aliro_dev_ble_open): plain =
78 * [proto][id][payload_len_be16][payload] (the header length must equal the
79 * payload length); writes wire = [proto][id][(payload_len+16)_be16][ct||tag] on
80 * direction 1 (key s1 = BleSKDevice), header as AAD, and sets *wire_len. Advances
81 * ctr_d2r; returns 0 or <0. Byte-compatible with the reader's aliro_msg_open. */
82int aliro_dev_ble_seal(struct aliro_dev_secchan *ch, const uint8_t *plain, size_t plain_len,
83 uint8_t *wire, size_t wire_cap, size_t *wire_len);
84
85/* Seal an AUTH0 fast-phase cryptogram (§8.3.1.11): the byte-exact mirror of the
86 * reader's aliro_crypto_verify_cryptogram. AES-256-GCM under CryptogramSK, a
87 * 12-byte all-zero IV, no AAD; out = encrypted_payload(plain_len) || 16-byte tag.
88 * out must hold plain_len + 16 bytes. Returns 0 on success. */
89int aliro_dev_seal_cryptogram(const uint8_t cryptogram_sk[32], const uint8_t *plain,
90 size_t plain_len, uint8_t *out);
91
92/* Standard-path session derivation, factored EC-free (the caller supplies the
93 * ECDH shared X). Builds z, the §8.3.1.13 SESSION salt and the 160-byte block,
94 * then the device AP channel (s0/s1 = block split S0/S1) and the URSK. Every
95 * input mirrors the reader's on_auth1_response. block_out, if non-NULL, receives
96 * the 160-byte block (the BleSK at offset 96 lives there and nowhere else, so a
97 * caller that needs the ranging channel must ask for it). Returns 0 on success. */
98int aliro_device_derive_session(const uint8_t shared_x[32], const uint8_t txid[16],
99 const uint8_t reader_group_x[32], const uint8_t reader_eph_x[32],
100 const uint8_t reader_id[32], uint8_t exp_phase, const uint8_t *a5,
101 size_t a5n, const uint8_t device_eph_x[32],
102 struct aliro_dev_secchan *sc, uint8_t ursk[32],
103 uint8_t block_out[ALIRO_KEY_BLOCK_LEN]);
104
105/* ---- full initiator state machine (uses EC via aliro_prim) ---- */
106
107/* Cap on the BleSK salt: reader_supported_versions || selected_version, and the
108 * readers bound their advertised list at 8 (ALIRO_MAX_VERSIONS, aliro_ble.c), so
109 * 2 * (8 + 1) bytes is the most §11.8.1 can ask for. */
110#define ALIRO_DEV_BLESK_SALT_MAX 18u
111
112enum aliro_device_phase {
113 ALIRO_DEV_IDLE = 0,
114 ALIRO_DEV_SENT_AUTH0_RESP,
115 ALIRO_DEV_SENT_AUTH1_RESP,
116 ALIRO_DEV_ESTABLISHED,
117 ALIRO_DEV_FAILED,
118};
119
126 /* identity + the reader this device is provisioned to talk to */
127 uint8_t cred_priv[32]; /* Access Credential private scalar */
128 uint8_t cred_pub[65]; /* = pub(cred_priv), presented in AUTH1Response */
129 uint8_t reader_id[32]; /* expected reader identifier */
130 uint8_t reader_verif_pub[65]; /* reader group verification key */
131 uint8_t reader_group_x[32]; /* = reader_verif_pub.x, salt field 1 */
132
133 /* per-transaction */
134 uint8_t dev_eph_priv[32];
135 uint8_t dev_eph_pub[65];
136 uint8_t reader_eph_pub[65];
137 uint8_t txid[16];
138 uint8_t exp_phase;
139 uint16_t version;
140 uint8_t ursk[32];
141 struct aliro_dev_secchan sc; /* Access-Protocol channel (S0/S1), from AUTH1 */
142 struct aliro_dev_secchan sc_ble; /* BleSK ranging channel, from the same block */
143
144 /* §11.8.1 BleSK salt = reader_supported_versions || selected_version. It is a
145 * property of the PEER, not of us: our ESP32 reader publishes {0x0100} alone
146 * (salt 01 00 01 00) while the nRF publishes {0x0100, 0x0009} (salt
147 * 01 00 00 09 01 00, measured on air 2026-07-25), so it cannot be a constant.
148 * aliro_device_init installs the single-version v1.0 default; any transport
149 * that has really read the peer's GATT list MUST override it via
150 * aliro_device_set_blesk_salt, or the ranging channel derives a key the peer
151 * does not share and the first sealed SDU fails as a GCM tag mismatch. */
152 uint8_t blesk_salt[ALIRO_DEV_BLESK_SALT_MAX];
153 size_t blesk_salt_len;
154
155 const uint8_t *a5; /* 0xA5 proprietary-info TLV for the salt (CSA v1.0 default) */
156 size_t a5n;
157
158 enum aliro_device_phase phase;
159};
160
161/* Initialise a device: derive cred_pub from cred_priv, latch the expected reader
162 * identity + verification key (reader_group_x = its X), set the CSA v1.0 default
163 * 0xA5 salt TLV. Returns 0 on success, <0 if the EC public-key recovery fails. */
164int aliro_device_init(struct aliro_device *d, const uint8_t cred_priv[32],
165 const uint8_t reader_id[32], const uint8_t reader_verif_pub[65]);
166
167/* Install the peer's real BleSK salt, overriding the v1.0 default. Build it with
168 * aliro_ble_central_blesk_salt from the versions the GATT reader-SPSM READ
169 * actually returned. Must be called before AUTH1, which is where the ranging
170 * channel is derived. Returns 0, or -1 on an empty/odd/oversized salt. */
171int aliro_device_set_blesk_salt(struct aliro_device *d, const uint8_t *salt, size_t len);
172
173/* Feed one inbound Access-Protocol command payload (the bytes inside the BLE
174 * envelope: an ISO7816 APDU) and produce the response payload (<TLV|ct||tag> SW).
175 * Advances d->phase. Returns 0 on success, <0 on any parse/crypto/auth failure
176 * (d->phase set to ALIRO_DEV_FAILED). */
177int aliro_device_on_command(struct aliro_device *d, const uint8_t *ap_payload, size_t len,
178 uint8_t *resp, size_t cap, size_t *resp_len);
179
180#ifdef __cplusplus
181}
182#endif
Aliro device: access credential (private scalar and public point), reader identity and verification k...
Definition aliro_device.h:125