openaliro
Aliro reader: UWB/CCC core and ESP32-S3/C5/C6 port
Loading...
Searching...
No Matches
aliro_crypto.h
1// Aliro crypto public API: key derivation, AES-GCM secure channels, and wire message
2// seal/open framing shared by the reader and device sides of an Aliro session.
3/*
4 * Copyright (c) 2026 asxeem
5 * SPDX-License-Identifier: ISC
6 *
7 * aliro_crypto — the credential-auth cryptography for the Aliro reader: the
8 * P-256 / AES-256-GCM / SHA-256 suite and the key-derivation schedule that
9 * turns a passed credential authentication into the 32-byte URSK (the UWB
10 * ranging root) plus the secure-channel keys.
11 *
12 * This is Phase 3.1: the primitives and the key schedule, host-KAT verifiable.
13 * The transaction state machine that drives it (AUTH0/AUTH1, EXCHANGE) is 3.2;
14 * the handoff of the URSK into woz_uwb_start_aliro(cfg) is 3.3.
15 *
16 * Provenance: original. The suite is standard (mbedTLS-PSA + a portable
17 * SHA-256/KDF core). The wire/derivation facts come from the project's
18 * reverse-engineering notes, not from any vendor source.
19 */
20#pragma once
21
22#include <stddef.h>
23#include <stdint.h>
24
25#ifdef __cplusplus
26extern "C" {
27#endif
28
29#define ALIRO_URSK_LEN 32u
30#define ALIRO_KEY_BLOCK_LEN 160u /* full derived block */
31#define ALIRO_URSK_OFFSET 128u /* URSK = block[128 .. 159] */
32#define ALIRO_SESSION_KEY_LEN 32u
33#define ALIRO_SHARED_SECRET_LEN 32u /* ECDH X coordinate */
34#define ALIRO_TXID_LEN 16u
35#define ALIRO_EC_PUBX_LEN 32u /* an EC point's X coordinate */
36#define ALIRO_GCM_NONCE_LEN 12u
37#define ALIRO_GCM_TAG_LEN 16u
38
39/* interface_byte for the salt transcript (Aliro §8.3.1.13): the transport the
40 * transaction runs on. BLE for the reader's live path; NFC for the §14 example. */
41#define ALIRO_IFACE_NFC 0x5Eu
42#define ALIRO_IFACE_BLE 0xC3u
43
44/* Initialise the crypto backend (idempotent). 0 on success, negative on fail. */
45int aliro_crypto_init(void);
46
47/* Extract the 32-byte URSK from the derived 160-byte key block. */
48void aliro_crypto_ursk_from_block(const uint8_t block[ALIRO_KEY_BLOCK_LEN],
49 uint8_t ursk[ALIRO_URSK_LEN]);
50
51/*
52 * ---- Credential-auth key schedule (standard/ECDH path) -------------------
53 *
54 * Stage 1: Z = SHA-256( shared_secret(32) | 0x00000001 | txid(16) ), the
55 * single-block ANSI-X9.63 concat KDF over the raw ECDH shared secret. Z is the
56 * IKM for every stage-2 HKDF.
57 */
58void aliro_crypto_derive_z(const uint8_t shared_secret[ALIRO_SHARED_SECRET_LEN],
59 const uint8_t txid[ALIRO_TXID_LEN], uint8_t z[32]);
60
61/*
62 * Stage 2: block = HKDF-SHA256(salt, IKM=z, info=device_pub_x(32), L=160). The
63 * salt is the CreateSalt transcript (aliro_salt_build). Returns 0 on success.
64 */
65int aliro_crypto_derive_block(const uint8_t z[32], const uint8_t *salt, size_t salt_len,
66 const uint8_t device_pub_x[ALIRO_EC_PUBX_LEN],
67 uint8_t block[ALIRO_KEY_BLOCK_LEN]);
68
69/*
70 * Single 32-byte keyed derivation off z: Kpersistent (salt type 2) and the
71 * Auth1 cryptogram key (salt type 0) both use this, differing only in the salt.
72 * block = HKDF-SHA256(salt, IKM=z, info=device_pub_x, L=32). Returns 0 on ok.
73 */
74int aliro_crypto_derive_key32(const uint8_t z[32], const uint8_t *salt, size_t salt_len,
75 const uint8_t device_pub_x[ALIRO_EC_PUBX_LEN], uint8_t out[32]);
76
77/*
78 * Split the 160-byte block into the two directional session keys + URSK. The
79 * reference derives up to two optional "shared" keys (C/D) alongside; a config
80 * flag shifts which segments are the symmetric keys. with_c=1: enc=S0, dec=S1;
81 * with_c=0: enc=S1, dec=S2 (S0 unused). URSK = S4 (offset 128) either way.
82 */
83void aliro_crypto_split(const uint8_t block[ALIRO_KEY_BLOCK_LEN], int with_c,
84 uint8_t enc_key[ALIRO_SESSION_KEY_LEN],
85 uint8_t dec_key[ALIRO_SESSION_KEY_LEN], uint8_t ursk[ALIRO_URSK_LEN]);
86
87/*
88 * ---- Expedited-fast phase (§8.1.1.2, §8.3.1.10/.11/.12) ------------------
89 *
90 * The fast phase skips ECDH and the two signatures: the User Device proves it
91 * holds a Kpersistent (agreed during an earlier expedited-standard phase) by
92 * returning a cryptogram the Reader verifies by trial against each stored
93 * Kpersistent. No new derivation code is needed — the fast key material reuses
94 * the standard primitives with different inputs:
95 *
96 * Kpersistent (during the standard phase, per §8.3.1.13):
97 * aliro_crypto_derive_key32(IKM = Kdh, salt = salt_persistent, info =
98 * credential_ephemeral_pub_x) -> 32 bytes. salt_persistent is aliro_salt_build
99 * type ALIRO_SALT_KPERSISTENT, which appends the Access Credential public-key X.
100 *
101 * Fast block (per §8.3.1.12):
102 * aliro_crypto_derive_block(IKM = Kpersistent, salt = salt_fast, info =
103 * credential_ephemeral_pub_x) -> 160 bytes. salt_fast is aliro_salt_build type
104 * ALIRO_SALT_CRYPTOGRAM ("VolatileFast"), also appending the credential pub-key X.
105 *
106 * The fast block's layout differs from the standard block only in the first 96
107 * bytes: CryptogramSK@0, ExpeditedSKReader@32, ExpeditedSKDevice@64, BleSK@96,
108 * URSK@128. So BleSK (aliro_crypto_derive_ble_keys) and URSK (offset 128) come
109 * out exactly as in the standard block, and aliro_crypto_split(block, 0, ...)
110 * yields ExpeditedSKReader (enc) / ExpeditedSKDevice (dec). CryptogramSK is the
111 * leading 32 bytes.
112 */
113#define ALIRO_CRYPTOGRAM_SK_OFFSET 0u /* CryptogramSK = fast_block[0 .. 31] */
114#define ALIRO_CRYPTOGRAM_LEN 64u /* AUTH0 response tag 0x9D: enc_payload(48) || tag(16) */
115
116/* Verify an AUTH0 fast-phase cryptogram (§8.3.1.11): AES-256-GCM open of
117 * cryptogram = encrypted_payload || 16-byte tag under CryptogramSK, with a
118 * 12-byte all-zero IV and no AAD. On a tag match, writes (cryptogram_len - 16)
119 * plaintext bytes to plain_payload and returns 0; returns <0 on a mismatch,
120 * meaning this Kpersistent is not the one (the caller tries the next). The
121 * caller must size plain_payload to at least cryptogram_len - 16 bytes. */
122int aliro_crypto_verify_cryptogram(const uint8_t cryptogram_sk[ALIRO_SESSION_KEY_LEN],
123 const uint8_t *cryptogram, size_t cryptogram_len,
124 uint8_t *plain_payload);
125
126/*
127 * ---- Secure channel (AES-256-GCM, directional per-message counters) ------
128 *
129 * Nonce = 8-byte big-endian direction (0 outbound/seal, 1 inbound/open) followed
130 * by a 4-byte big-endian per-direction counter. Separate seal/open counters,
131 * start at 0, no wrap. SessionCrypto sends no AAD; the BLE channel authenticates
132 * a 4-byte AAD (caller-supplied here).
133 */
134struct aliro_secchan {
135 uint8_t enc_key[ALIRO_SESSION_KEY_LEN];
136 uint8_t dec_key[ALIRO_SESSION_KEY_LEN];
137 uint32_t enc_ctr;
138 uint32_t dec_ctr;
139};
140
141void aliro_secchan_init(struct aliro_secchan *sc, const uint8_t enc_key[ALIRO_SESSION_KEY_LEN],
142 const uint8_t dec_key[ALIRO_SESSION_KEY_LEN]);
143void aliro_crypto_gcm_nonce(uint64_t direction, uint32_t counter,
144 uint8_t nonce[ALIRO_GCM_NONCE_LEN]);
145/* Seal/open advance the matching counter on success. Return 0 on success;
146 * open returns <0 on a tag mismatch (hard auth failure) — never trust the
147 * plaintext then. */
148int aliro_secchan_seal(struct aliro_secchan *sc, const uint8_t *aad, size_t aad_len,
149 const uint8_t *pt, size_t pt_len, uint8_t *ct,
150 uint8_t tag[ALIRO_GCM_TAG_LEN]);
151int aliro_secchan_open(struct aliro_secchan *sc, const uint8_t *aad, size_t aad_len,
152 const uint8_t *ct, size_t ct_len, const uint8_t tag[ALIRO_GCM_TAG_LEN],
153 uint8_t *pt);
154
155/*
156 * ---- Aliro message security (§11.8): ranging/notification SDUs -----------
157 *
158 * Proto-1/2/3 SDUs (UWB Ranging Service M1-M4, Notification, Supplementary) ride
159 * a SEPARATE AES-256-GCM channel from the AP secure channel: BleSKReader/
160 * BleSKDevice keys (HKDF off BleSK = block offset 96), fresh per-direction
161 * counters starting at 1, and the 4-byte header (with the PLAINTEXT payload
162 * length) as AAD. Wire form: [proto][id][len_be16][encrypted_payload||16B tag],
163 * where len_be16 = plaintext length + 16. Reuse struct aliro_secchan for it
164 * (enc=BleSKReader, dec=BleSKDevice; aliro_secchan_init sets both counters to 1).
165 */
166#define ALIRO_BLESK_OFFSET 96u /* BleSK = block[96 .. 127] (§8.3.1.12/.13) */
167
168/* Derive BleSKReader + BleSKDevice from the 160-byte block per §11.8.1:
169 * HKDF-SHA256(ikm=BleSK, info="BleSKReader"/"BleSKDevice", L=32,
170 * salt = reader_supported_versions || user_device_selected_version). 0 on ok. */
171int aliro_crypto_derive_ble_keys(const uint8_t block[ALIRO_KEY_BLOCK_LEN], const uint8_t *salt,
172 size_t salt_len, uint8_t ble_reader[ALIRO_SESSION_KEY_LEN],
173 uint8_t ble_device[ALIRO_SESSION_KEY_LEN]);
174
175/* Seal an engine plaintext message [proto][id][len_plain_be16][payload] into the
176 * on-wire [proto][id][(len_plain+16)_be16][ct||tag], sealed under sc with the
177 * 4-byte plaintext-length header as AAD (§11.8.2). *wire_len set on 0-return. */
178int aliro_msg_seal(struct aliro_secchan *sc, const uint8_t *plain, size_t plain_len, uint8_t *wire,
179 size_t wire_cap, size_t *wire_len);
180
181/* Inverse of aliro_msg_seal: open a wire SDU into the engine plaintext form,
182 * verifying the tag. Returns <0 on a tag mismatch (drop the connection then). */
183int aliro_msg_open(struct aliro_secchan *sc, const uint8_t *wire, size_t wire_len, uint8_t *plain,
184 size_t plain_cap, size_t *plain_len);
185
186/*
187 * ---- CreateSalt transcript builder --------------------------------------
188 *
189 * Builds the stage-2 HKDF salt byte-exact to Aliro §8.3.1.13 (salt_volatile for
190 * the SESSION/standard type):
191 * span_s1(reader_group_identifier_key.x, 32) || label(12) || reader_id(32) ||
192 * interface_byte(1) || 0x5C || 0x02 || protocol_version(2) ||
193 * reader_value(reader ephemeral pub X, 32) || txid(16) ||
194 * flag(exp_phase_type||user_auth_policy, 2) || a5_tlv(0xA5 proprietary info).
195 * For the fast/persistent types a trailing s3opt (Access Credential public key X)
196 * follows the a5_tlv. Returns 0 on success and the assembled length in *out_len.
197 */
198enum aliro_salt_type {
199 ALIRO_SALT_CRYPTOGRAM = 0, /* label "VolatileFast" */
200 ALIRO_SALT_SESSION = 1, /* label "Volatile****" */
201 ALIRO_SALT_KPERSISTENT = 2 /* label "Persistent**" */
202};
203
204#define ALIRO_SALT_MAX 256u
205
206int aliro_salt_build(enum aliro_salt_type type, const uint8_t txid[ALIRO_TXID_LEN],
207 const uint8_t span_s1[ALIRO_EC_PUBX_LEN],
208 const uint8_t reader_value[ALIRO_EC_PUBX_LEN], const uint8_t reader_id[32],
209 uint8_t interface_byte, uint16_t proto_version, uint8_t exp_phase_type,
210 uint8_t user_auth_policy,
211 const uint8_t s3opt[ALIRO_EC_PUBX_LEN] /* NULL for type 1 */,
212 const uint8_t *a5_tlv, size_t a5_tlv_len, uint8_t *out, size_t *out_len);
213
214#ifdef __cplusplus
215}
216#endif