1. Тема дня

Проектируем путь удаленной команды:

text
transport
→ bounded parser
→ canonical command hash
→ signature verification
→ audience check
→ anti-replay
→ capability authorization
→ runtime guards
→ persistent reservation
→ owner-task execution
→ audit trail

Главная мысль: mTLS/MQTT подтверждает канал, но устройство все равно должно проверить автора команды, адресата, свежесть, полномочия и допустимость операции сейчас.

2. Зачем это нужно

Команды могут быть опасными:

c
MODEM_RESET;
SET_CONFIG;
OTA_INSTALL;
DEVICE_REBOOT;
FACTORY_RESET;
OUTPUT_CONTROL.

Риски:

text
retained MQTT command;
QoS duplicate;
ошибка ACL broker;
скомпрометированный backend;
старое подписанное сообщение;
команда не тому устройству;
команда с истекшим сроком.

3. Теория

Command envelope

Поля:

c
protocol_context;
schema_version;
issuer_id;
key_id;
audience_type;
device_id/group/product;
command_id;
sequence;
issued_at/not_before/expires_at;
capability;
operation;
payload;
signature_algorithm;
signature.

Signature считается над canonical bytes всех полей кроме самой signature.

Domain separation

В signing input включай контекст:

text
"traffic-command-v1"

Не смешивать с OTA manifest:

text
"traffic-ota-manifest-v1"

Canonical encoding

Не подписывай произвольный JSON. Используй фиксированный бинарный формат или deterministic CBOR.

Подпись или HMAC

Предпочтительно для server commands:

text
backend хранит private command-signing key;
device хранит только public verification key.

HMAC быстрее, но устройство знает secret. Если HMAC, то key должен быть уникальным для устройства.

PSA Crypto

Для ESP-IDF 6.x новый code лучше делать через PSA Crypto. Для ECDSA P-256 signature формат в PSA обычно r || s 64 байта, а не DER, это нужно согласовать с backend.

Anti-replay

Нужны вместе:

text
command_id;
sequence;
expires_at;
replay cache;
persistent journal для side effects.

Порядок: сначала signature, потом изменение replay state. Иначе attacker может отправить большой sequence с плохой подписью и заблокировать команды.

Capability model

Не одна роль ADMIN, а конкретные capability:

text
STATUS_READ
MODEM_RESET
CONFIG_STAGE
CONFIG_COMMIT
OTA_INSTALL
DEVICE_REBOOT
FACTORY_RESET
OUTPUT_CONTROL

Runtime guards

Даже подписанная команда проверяет состояние:

text
power stable;
не идет Flash commit;
hardware revision compatible;
service mode active;
local confirmation present;
OTA image verified.

Dangerous operations

Для FACTORY_RESET и подобных нужен prepare/commit:

text
PREPARE -> token/deadline/local confirm -> COMMIT

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

  • mTLS считается полной авторизацией.
  • MQTT topic используется как единственное permission rule.
  • Нет replay protection.
  • Retained dangerous commands исполняются.
  • MQTT msg_id считается command_id.
  • Подписывается неканонический JSON.
  • Один HMAC ключ на парк.
  • Command signing private key хранится на устройстве.
  • Operation проверяется до signature.
  • Sequence хранится только в RAM для persistent operations.
  • Side effect выполняется до journal.
  • Все ключи: OTA/TLS/commands/Secure Boot - один и тот же.
  • Команда выполняется в MQTT callback.

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

Создай REMOTE_COMMAND_POLICY.md:

markdown
# Remote command policy
1. Transport security does not replace command authorization.
2. Every command has a globally unique command ID.
3. Dangerous commands are never accepted as retained MQTT messages.
4. Every signed command contains issuer, key ID and audience.
5. Commands use deterministic canonical encoding.
6. Signatures are checked before semantic execution.
7. Replay protection survives reset for persistent operations.
8. Every issuer has an explicit capability set.
9. Hardware operations run only in subsystem owner tasks.
10. Dangerous operations use prepare/commit.
11. Every decision produces an audit event.
12. Command signing keys are separate from TLS and Secure Boot keys.

Сделай host-compatible command_auth_core с mock verifier. Unit-тесты:

text
1. Valid GET_STATUS -> ACCEPTED.
2. Bad signature -> BAD_SIGNATURE.
3. Wrong device_id -> WRONG_AUDIENCE.
4. Unknown issuer -> UNKNOWN_ISSUER.
5. Missing capability -> NOT_AUTHORIZED.
6. Expired -> EXPIRED.
7. Same command_id -> DUPLICATE, side effect not called.
8. Old sequence -> REPLAY.
9. Payload too large -> BAD_FORMAT before crypto.
10. RETAIN flag for REBOOT -> REJECTED.
11. FACTORY_RESET without PREPARE -> LOCAL_CONFIRM_REQUIRED.

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

  • ESP-IDF 6.x PSA Crypto migration.
  • MQTT QoS, retained messages, DUP flag, fragmented events.
  • Capability-based authorization.
  • Persistent command journal.
Неаутентифицированная команда содержит очень большой sequence. Какой порядок защищает replay state от такой команды?

Критерии: Разделите аутентификацию канала, подлинность команды и свежесть.

Задание

Объясните, почему подписанная FACTORY_RESET требует проверок помимо подписи. Опишите design checks без исполнения операции.

Критерии самопроверки: Охватите автора, адресата, свежесть, полномочия и допустимость сейчас. Корректная подпись не даёт безусловного разрешения.

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

Проверить адресата, command_id/sequence/expiry и replay state, требуемую capability, текущие runtime guards и prepare/commit для опасной операции. Записать постоянные side effects в journal до исполнения и выполнять через сервис-владелец, а не MQTT callback.