1. Тема дня

Сегодня строим безопасный путь удалённой команды:

text
транспорт MQTT/CAN/RS-485
→ декодирование
→ проверка формата и длины
→ аутентификация сообщения
→ защита от повторного воспроизведения
→ авторизация операции
→ проверка локальных ограничений
→ очередь задачи-владельца
→ выполнение
→ сохранение результата
→ подтверждение

Главная мысль: TLS защищает канал, но команда всё равно должна иметь собственную идентичность, защиту от повторов и строго определённую семантику выполнения.

2. Зачем это нужно в твоих проектах

Через MQTT устройство может получать команды:

text
переподключить EC25
изменить APN
изменить MQTT broker
запустить OTA
изменить фильтр входов
перезапустить устройство
включить/выключить нагрузку

Даже при TLS остаются риски:

text
broker скомпрометирован;
учётная запись publisher украдена;
старое сообщение повторно доставлено;
retained-команда пришла после reboot;
QoS повторил операцию;
сервер отправил команду не тому устройству.

CAN CRC и Modbus CRC не являются аутентификацией. Они защищают от случайных ошибок, но не от узла, способного сформировать корректный кадр.

3. Теория

8.3.1 3.1. Свойства команды

text
Аутентичность:
  команду создал доверенный отправитель.
Целостность:
  поля не изменены.
Свежесть:
  это не повтор старого трафика.
Авторизация:
  отправителю разрешена эта операция.
Идемпотентность:
  повторная доставка не создаёт повторный побочный эффект.
Аудируемость:
  можно понять, что пришло и почему принято/отклонено.

8.3.2 3.2. Формат команды

c
typedef enum {
    CMD_NONE = 0,
    CMD_MODEM_RESET,
    CMD_MQTT_RECONNECT,
    CMD_SET_LOG_LEVEL,
    CMD_SET_IR_DUTY,
    CMD_CONFIG_STAGE,
    CMD_CONFIG_COMMIT,
    CMD_OTA_START,
    CMD_SYSTEM_REBOOT,
} command_type_t;
typedef struct {
    uint8_t protocol_version;
    uint32_t issuer_id;
    uint32_t key_id;
    uint32_t target_device_id;
    uint64_t command_id;
    uint64_t sequence;
    uint64_t issued_utc_s;
    uint64_t expires_utc_s;
    uint64_t session_id;
    uint64_t nonce;
    command_type_t type;
    const uint8_t *payload;
    uint16_t payload_length;
    const uint8_t *auth_tag;
    uint16_t auth_tag_length;
} secure_command_view_t;

command_id нужен для идемпотентности, sequence — для anti-replay, target_device_id — чтобы команда не исполнилась на другом устройстве.

8.3.3 3.3. Canonical serialization

Нельзя подписывать C-структуру как лежит в памяти: padding, endianness, enum size, указатели и неинициализированные байты делают формат нестабильным. Нужно подписывать канонический массив байтов:

text
protocol_version
issuer_id big-endian
key_id big-endian
target_device_id big-endian
command_id big-endian
sequence big-endian
issued_at big-endian
expires_at big-endian
session_id big-endian
nonce big-endian
command_type big-endian
payload_length big-endian
payload bytes

8.3.4 3.4. HMAC-SHA256

text
tag = HMAC-SHA256(key, canonical_command)

Constant-time сравнение:

c
static bool constant_time_equal(const uint8_t *a,
                                const uint8_t *b,
                                size_t length)
{
    uint8_t difference = 0;
    for (size_t i = 0; i < length; i++) {
        difference |= (uint8_t)(a[i] ^ b[i]);
    }
    return difference == 0;
}

Сначала проверь длину tag.

8.3.5 3.5. HMAC или цифровая подпись

HMAC быстрее и проще, но устройство знает секрет, пригодный для создания MAC. Цифровая подпись позволяет устройству хранить только public key, а private signing key остаётся на сервере. Практично:

text
MQTT server commands:
  подпись server key или per-device HMAC
локальная RS-485:
  per-link HMAC
CAN с малым payload:
  gateway-authenticated session или CAN FD

8.3.6 3.6. Anti-replay

Старая команда с правильным MAC всё ещё валидна криптографически. Нужен sequence/nonce/session. Скользящее окно:

c
typedef struct {
    uint64_t highest_accepted_sequence;
    uint64_t replay_bitmap;
} replay_window_t;

Важно: replay state обновляется только после успешной проверки MAC/signature.

8.3.7 3.7. Идемпотентность

Предпочитай команды состояния:

text
SET_OUTPUT ON
SET_LOG_LEVEL WARN
SET_IR_DUTY 30%

Вместо опасных действий:

text
TOGGLE_OUTPUT
INCREMENT_COUNTER
PULSE_OUTPUT

Command result cache:

c
typedef enum {
    CMD_RESULT_NONE = 0,
    CMD_RESULT_IN_PROGRESS,
    CMD_RESULT_SUCCESS,
    CMD_RESULT_REJECTED,
    CMD_RESULT_FAILED,
} command_result_code_t;
typedef struct {
    uint64_t command_id;
    command_result_code_t result;
    int32_t error_code;
    uint32_t result_generation;
    uint64_t completed_mono_us;
} command_result_entry_t;

При повторе command_id операция не выполняется заново, а возвращается сохранённый результат.

8.3.8 3.8. Authorization и local safety interlocks

Аутентификация отвечает “кто отправил?”, авторизация — “что ему разрешено?”. Примеры локальных ограничений:

text
IR duty не выше безопасного максимума;
OTA только от основного питания;
factory reset только при физической кнопке;
смена GPIO polarity только в service mode;
watchdog нельзя полностью отключить удалённо.

4. Типичные ошибки

text
1. Считать QoS 2 гарантией однократного физического действия.
2. Использовать retained-команду REBOOT.
3. Подписывать C-структуру.
4. Проверять timestamp, но не sequence.
5. Обновлять replay counter до проверки MAC.
6. Один HMAC key на весь парк.
7. Сравнивать MAC через переменную длину.
8. Использовать memcmp() для security tag.
9. Выполнять команду в MQTT callback.
10. Сначала выполнять, затем сохранять command_id.
11. Считать CRC Modbus/CAN аутентификацией.
12. Позволить удалённо отключить все защиты.

5. Практическое задание на 30-60 минут

Создай SECURE_COMMAND_POLICY.md:

markdown
# Secure command policy
1. Transport security does not replace message authentication.
2. Every command has target_device_id, command_id and sequence.
3. Commands are authenticated before replay state is changed.
4. Imperative MQTT commands are never retained.
5. Duplicate command_id never repeats a side effect.
6. Authorization is checked separately from authentication.
7. Local safety checks may reject an authenticated command.
8. Critical command results are persisted before ACK.
9. Keys are unique per device or verification uses a server public key.
10. Security failures use rate-limited diagnostics.

Core API:

c
typedef enum {
    SEC_CMD_OK = 0,
    SEC_CMD_BAD_FORMAT,
    SEC_CMD_WRONG_TARGET,
    SEC_CMD_UNKNOWN_KEY,
    SEC_CMD_BAD_AUTH,
    SEC_CMD_REPLAY,
    SEC_CMD_EXPIRED,
    SEC_CMD_UNAUTHORIZED,
    SEC_CMD_INVALID_PARAMS,
    SEC_CMD_DUPLICATE,
} secure_command_status_t;
typedef struct {
    uint32_t device_id;
    uint64_t current_session_id;
    uint64_t current_utc_s;
    bool utc_valid;
    replay_window_t replay;
} secure_command_context_t;

Unit-тесты:

text
валидная команда;
бит payload изменён;
повтор sequence;
повтор command_id;
старый session_id;
expired;
unknown key_id;
unauthorized;
нулевая длина MAC;
payload length больше буфера.

6. Что почитать дальше

  • Mbed TLS HMAC/SHA256.
  • ESP-IDF Security Overview.
  • MQTT QoS/retained semantics.

Короткий итог

text
untrusted transport payload
→ bounded decoder
→ canonical representation
→ HMAC/signature verification
→ target/session check
→ anti-replay
→ authorization
→ local safety validation
→ idempotency lookup
→ owner-task execution
→ persistent result
→ authenticated response

Задание

Валидная command доставлена повторно после потери acknowledgment. Определите result lookup и недостаточность transport QoS для одного physical side effect.

Критерии самопроверки: Описать durable execution/result boundary и interrupted-operation recovery. Не обещать exactly-once physical effects только по cache или QoS.

Показать ответ автора

Аутентифицировать и проверить command, определить command_id и проверить сохранённый result/state до execution. Вернуть существующий result для completed duplicate. Delivery semantics сами не задают однократное physical execution.

Задание

Valid MAC packet адресован другому device, повторяет old sequence или требует locally unsafe operation. Определите validation order.

Критерии самопроверки: Отделить authentication, freshness, authorization и local safety; записать bounded rejection evidence без keys.

Показать ответ автора

Выполнить bounded decoding и canonical authentication, затем target/session, replay policy, authorization и local safety до owner-task execution. Invalid MAC не меняет replay state; rejected commands не создают forbidden side effect.