94. LoRaWAN: сеть, ключи, классы
Радио LoRa (раздел 93) доставляет байты от передатчика к приёмнику. LoRaWAN — сеть поверх этого радио. Она решает четыре задачи: - кто принимает кадры; - как сервер узнаёт прибор; - почему посторонний не прочитает и не подделает данные; - когда прибор может получить ответ.
Проверено опытом: кадр собран на переносимом C и отправлен настоящему сетевому серверу ChirpStack в контейнерах.
Как это называется в документации
Оригинал Перевод gateway, packet forwarder шлюз, пересыльщик пакетов шлюза network server, application server, join server сетевой сервер, сервер приложений, сервер подключения DevEUI, JoinEUI, DevAddr заводской номер прибора, номер сервера подключения, сетевой адрес OTAA, ABP подключение по воздуху, активация с вшитыми ключами AppKey, NwkSKey, AppSKey корневой ключ, сетевой и прикладной ключи сессии MIC подпись кадра FCnt, FPort счётчик кадров, порт MHDR, FHDR, FCtrl, FOpts заголовки кадра, управляющий байт, MAC-команды в заголовке class A, B, C классы приборов по тому, когда они слушают RX1, RX2 первое и второе окно приёма data rate (DR), ADR скорость, её автоматический подбор confirmed / unconfirmed uplink кадр вверх с подтверждением и без
94.1 Как устроена сеть
LoRaWAN — это «звезда звёзд»:
| Звено | Что делает |
|---|---|
| прибор | передаёт в эфир и ни к чему не подключён; его кадр может услышать несколько шлюзов |
| шлюз | принимает всё подряд на всех своих каналах и всех SF одновременно (раздел 93.2) и пересылает на сервер по IP, не разбирая и не расшифровывая, — в том числе чужие кадры |
| сетевой сервер | убирает дубликаты от нескольких шлюзов, проверяет подпись и счётчик, подбирает прибору скорость, выбирает шлюз для ответа |
| сервер приложений | расшифровывает данные и отдаёт их приложению — через MQTT или HTTP (разделы 87 и 88) |
Шлюз ничего не знает о приборах, поэтому шлюзы можно ставить где угодно и добавлять по мере надобности. Вся логика сети — на сервере.
94.2 Кадр
| Поле | Байт | Что внутри |
|---|---|---|
| MHDR | 1 | тип кадра: 0x40 — данные вверх без подтверждения |
| DevAddr | 4 | сетевой адрес прибора |
| FCtrl | 1 | флаги: ADR, подтверждение, длина FOpts |
| FCnt | 2 | 16 младших бит счётчика кадров |
| FOpts | 0–15 | MAC-команды сети, если есть |
| FPort | 1 | 0 — MAC-команды в данных, 1–223 — данные приложения |
| FRMPayload | сколько нужно | зашифрованные данные |
| MIC | 4 | подпись |
Без FOpts служебных байт 13 (раздел 89). Кадр опыта с 7 байтами данных
t=21.50 — 20 байт:
40 | EF CD AB 01 | 00 | 01 00 | 01 | 36 B6 EF 94 14 F5 C6 | 7A D3 2F 6B
MHDR DevAddr FCtrl FCnt FPort зашифрованные данные MIC
Адрес 01ABCDEF и счётчик записаны младшим байтом вперёд, а не
старшим, как данные Modbus и числа в заголовках TCP/IP. Путаница в
порядке байт — первая причина, по которой сервер молча не видит прибор.
94.3 Ключи и активация
В сессии LoRaWAN 1.0 два ключа, и разделены они не случайно:
- NwkSKey — сетевой. Им проверяют подпись кадра. Сетевой сервер видит заголовки, но прочитать данные не может.
- AppSKey — прикладной. Им шифруют данные. Он есть только у сервера приложений: оператор сети ваших данных не прочитает.
Прибор получает ключи одним из двух способов:
- OTAA — подключение по воздуху. В приборе хранятся DevEUI, JoinEUI и корневой ключ AppKey. Прибор отправляет запрос на подключение, получает ответ с адресом и выводит из него ключи сессии. Каждое подключение — новые ключи и счётчики с нуля.
- ABP — вшитые ключи. Адрес и ключи сессии записаны в прибор заранее, подключения нет. Это просто, но ключи не меняются, а счётчик после перезагрузки начинается с нуля, и сервер такие кадры отбросит (опыт ниже). Хранить счётчик приходится в энергонезависимой памяти.
Для серийных приборов берут OTAA, ABP оставляют для стенда и опытов. В LoRaWAN 1.1 сетевой ключ разделён на три, а в сессии 1.0, как на стенде, все три равны NwkSKey. Ключи в прошивке открытым массивом читаются из образа (разделы 43 и 77.9).
94.4 Шифрование и подпись
Данные шифруются в режиме счётчика. Для каждых 16 байт данных AES шифрует блок A_i с адресом, счётчиком кадра и номером блока, и данные складываются с результатом по xor. Подпись — первые 4 байта AES-CMAC от блока B0 и всего кадра:
/* Блоки A_i и B0: метка 0x01 или 0x49, четыре нуля, направление (0 — вверх),
* адрес и счётчик — младшим байтом вперёд, ноль, номер блока или длина */
static void lw_block(uint8_t b[16], uint8_t tag, uint32_t dev_addr, uint32_t fcnt, uint8_t last)
{
memset(b, 0, 16);
b[0] = tag;
for (int i = 0; i < 4; i++) {
b[6 + i] = (uint8_t)(dev_addr >> (8 * i));
b[10 + i] = (uint8_t)(fcnt >> (8 * i));
}
b[15] = last;
}
/* Неподтверждаемый кадр вверх: MHDR 0x40, DevAddr, FCtrl, FCnt (2 младших
* байта), FPort, зашифрованные данные (до 64 байт) и MIC. Вернёт длину или 0. */
size_t lorawan_uplink(uint8_t *out, uint32_t dev_addr, uint32_t fcnt, uint8_t fport,
const uint8_t *data, size_t n, const uint8_t nwk_skey[16], const uint8_t app_skey[16])
{
uint8_t a[16], s[16], b[16 + 9 + 64], mac[16];
size_t k = 0;
if (n > 64)
return 0;
out[k++] = 0x40; /* неподтверждаемые данные вверх */
for (int i = 0; i < 4; i++)
out[k++] = (uint8_t)(dev_addr >> (8 * i));
out[k++] = 0x00; /* FCtrl: без ADR и без FOpts */
out[k++] = (uint8_t)fcnt;
out[k++] = (uint8_t)(fcnt >> 8);
out[k++] = fport;
for (size_t i = 0; i < n; i++) { /* шифр — xor с AES(AppSKey, A_i) */
if (i % 16 == 0) {
lw_block(a, 0x01, dev_addr, fcnt, (uint8_t)(i / 16 + 1));
aes128_encrypt(app_skey, a, s);
}
out[k++] = data[i] ^ s[i % 16];
}
lw_block(b, 0x49, dev_addr, fcnt, (uint8_t)k); /* подпись — CMAC(NwkSKey, B0 | кадр) */
memcpy(b + 16, out, k);
aes128_cmac(nwk_skey, b, 16 + k, mac);
memcpy(out + k, mac, 4);
return k + 4;
}
aes128_encrypt и aes128_cmac в проверке — компактные реализации
только для опыта. Они сверены с опубликованными проверочными значениями
и с библиотекой на Python. В приборе берут библиотеку или аппаратный
блок (раздел 43). На Cortex-M3 AES занимает 184 байта стека, сборка
кадра — 208.
⚠️ Счётчик — часть шифра. Счётчик кадра входит в блок A_i, поэтому одни и те же данные с разными счётчиками шифруются по-разному. Повторите счётчик с теми же ключами — например, после перезагрузки прибора с ABP — и два кадра окажутся зашифрованы одной и той же гаммой. Сложив их по xor, посторонний получит xor двух открытых текстов без всякого ключа. Счётчик с одними ключами не должен повторяться никогда.
94.5 Опыт: настоящий сетевой сервер
Стенд. В контейнерах работают: - сетевой сервер и сервер приложений ChirpStack с базой PostgreSQL и Redis; - брокер Mosquitto; - мост шлюза.
Прибор заведён через программный интерфейс сервера (gRPC): ABP, LoRaWAN 1.0.3, EU868.
«Шлюз» — программа на C. Она заворачивает кадр так, как его пересылает пересыльщик пакетов настоящего шлюза: UDP-пакет с JSON, где указаны частота, SF, мощность, SNR и сам кадр в base64. Мост подтвердил все четыре пересылки:
| Что отправлено | Что сделал сервер |
|---|---|
счётчик 1, t=21.50 |
принял, расшифровал и опубликовал в MQTT: счётчик 1, DR5 — SF7 на 125 кГц |
| тот же кадр ещё раз — перехвачен и повторён | отбросил: «счётчик не вырос», событие в журнале прибора |
| счётчик 2, в подписи изменён один бит | отбросил: «ни одна сессия с адресом 01abcdef не дала верной подписи» |
| счётчик 2, правильный кадр | принял t=21.75: испорченный кадр счётчик не сдвинул |
Байты обоих кадров независимо пересобраны на Python и совпали.
Третья строка показывает то, чего не видно в описании кадра. Адрес прибора не уникален: старшие биты адреса — номер сети, на сами приборы остаётся меньше, и сервер вправе выдать один адрес нескольким приборам. Сервер находит все сессии с таким адресом и проверяет подпись каждой. Подпись заодно и опознаёт прибор. Кадр с неверной подписью для сервера — чужой, и приложение о нём даже не узнает.
94.6 Классы A, B, C
| Класс | Когда прибор слушает | Команда дойдёт | Питание |
|---|---|---|---|
| A | только два окна после своей передачи | при следующей передаче прибора | батарейка, годы |
| B | ещё и в окна, назначенные по маякам шлюзов | в назначенное окно | батарейка |
| C | всё время, кроме своих передач | почти сразу | от сети |
Класс A обязаны поддерживать все приборы. Окна приёма в регионе стенда: RX1 — через 1 с после конца передачи, на частоте кадра, RX2 — через 2 с на 869,525 МГц на DR0 (SF12). Это значения региона по умолчанию: сеть может задать свои, часто SF9 для RX2. При OTAA прибор получает их в ответе на подключение, при ABP их вписывают в прибор вручную. Команда для прибора класса A ждёт на сервере в очереди, пока прибор сам не выйдет на связь.
Окно открывают по таймеру от момента «передача окончена» — это DIO0 из раздела 93, — а не циклом задержки (раздел 13). Если окно открыть поздно, ответы сервера не приходят никогда, а прибор выглядит «глухим».
Точность окна по описанию LoRaWAN — ±20 мкс, поэтому таймер окна берут от кварца: LSE или HSE. LSI у STM32 ошибается на десятки процентов (раздел 12), HSI — около 1 % (раздел 6), а 1 % от секунды — это 10 мс, больше всей преамбулы SF7: 8 символов по 1,024 мс — 8,2 мс. Стеки открывают окно чуть раньше и держат его дольше, чтобы покрыть уход часов, и платят за это током приёма.
Ответ дорого стоит. Шлюз работает в полудуплексе: пока он передаёт ответ одному прибору, он не слышит никого. Кадры с подтверждением требуют ответа на каждый кадр. В сети из тысяч приборов их включают только для редких важных событий, а не для телеметрии.
94.7 ADR: скорость по SNR
Сетевой сервер помнит SNR последних кадров прибора и считает запас: лучший SNR минус порог для текущей скорости (раздел 93.2) минус запас на установку, обычно 10 дБ. Каждые 3 дБ запаса — ступень скорости вверх. Когда быстрее уже некуда, сервер снижает мощность.
| DR | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| SF на 125 кГц | 12 | 11 | 10 | 9 | 8 | 7 |
| порог SNR, дБ | −20 | −17,5 | −15 | −12,5 | −10 | −7,5 |
Пример: прибор на SF12 с лучшим SNR +3 дБ имеет запас 3 − (−20) − 10 = 13 дБ. Это 4 ступени, и сервер переводит прибор на DR4 — SF8. Пакет становится короче в несколько раз, батарейка живёт дольше, а эфир освобождается для других. ADR хорош для неподвижных приборов. У движущихся SNR скачет, поэтому ADR у них выключают и скорость задают сами.
94.8 Прибор на STM32: что хранить и что слать
- Стек. MAC-команды, окна приёма, подключение и ADR — это готовый стек LoRaWAN, обычно от производителя радиомодуля. Код раздела нужен, чтобы понимать, что внутри, и проверять.
- Что хранить. Счётчик кадров (для ABP) и сессию (для OTAA) держат в энергонезависимой памяти и переживают с ними перезагрузку. Иначе кадры отбрасываются — или, хуже, повторяется гамма шифра.
- DevNonce. Для OTAA хранят ещё номер запроса на подключение: сервер помнит использованные номера и повтор отвергает — у ChirpStack это ошибка «DevNonce has already been used». С LoRaWAN 1.0.4 DevNonce — счётчик, который обязан расти и переживать перезагрузку. Прибор, начинающий его заново после сброса, после первой же перезагрузки подключиться не сможет.
- Счётчик. В кадре только 16 младших бит. Прибор, который шлёт раз в 10 минут, проходит их за 455 суток. Сервер восстанавливает старшие биты, а стек прибора хранит полный 32-битный счётчик.
- Что слать. Данные — в двоичном виде, а не текстом и не JSON. Строка
t=21.50— 7 байт, а температура в сотых долях градуса вint16_t— 2 байта. На SF12, где пакет длится полторы секунды (раздел 89), каждый байт на счету.
94.9 Грабли
Сервер молча не видит прибор. Адрес или ключ введён не в том порядке байт. В журнале сервера написано, что ни одна сессия не дала верной подписи.
После перезагрузки прибор с ABP замолчал. Счётчик начался с нуля, а сервер ждёт больше прежнего: «счётчик не вырос».
Счётчик сбросили вместе с базой сервера, ключи оставили. Гамма повторяется, и данные можно восстановить без ключа.
Команды до прибора не доходят. Окно RX1 открыто позже, чем через 1 с: цикл задержки съехал из-за прерываний или таймер окна идёт от LSI.
После перезагрузки прибор с OTAA не подключается. DevNonce начат заново, а сервер помнит использованные номера.
Сеть захлёбывается. Все приборы шлют кадры с подтверждением, и шлюз больше передаёт, чем слушает.
Батарейка садится за месяцы вместо лет. Прибор шлёт JSON на SF12.
Ключи утекли из прошивки. Они лежали в образе открытым массивом.
94.10 Итоги
- LoRaWAN — звезда звёзд. Шлюзы прозрачны, вся логика на сетевом сервере, данные расшифровывает сервер приложений.
- Два ключа сессии: NwkSKey подписывает, AppSKey шифрует. Оператор сети данных не читает.
- Кадр — 13 служебных байт плюс данные. Адрес и счётчик записаны младшим байтом вперёд.
- Счётчик защищает от повтора и входит в шифр, поэтому повторять его с теми же ключами нельзя. На стенде ChirpStack повтор и испорченная подпись отброшены, два честных кадра приняты и расшифрованы.
- Адрес не уникален, и прибор опознаёт подпись.
- Класс A слушает два окна после передачи: 1 с и 2 с. Класс C слушает всегда и питается от сети.
- ADR переводит прибор на ступень быстрее за каждые 3 дБ запаса.
Дальше: раздел 94а «NFC-метка ST25DV» — как телефон и прошивка делят одну память: адреса на шине, пароль, почтовый ящик и прерывание по записи.