❮  Контроллеры Entrixy

Спецификация socket-протокола

Всё необходимое, чтобы встроить поддержку Entrixy «Internet» в свой контроллер: описание WebSocket-сообщений, E2EE и тест-векторы.

В отличие от BLE, socket-контроллер постоянно соединён с сервером Entrixy по WebSocket и открывается из любой точки. Базовый режим реализуется полностью по этой спеке. Для E2EE (сервер не может подделать открытие) — §5–7. Получение device_key для серийного производства — §10. Референс: esp32-ws-example (MIT).

1. Транспорт и конверт сообщений

Референс-прошивка ESP по умолчанию не пиннит сертификат сервера. Для продакшена — пиньте по хосту/CA; материал по запросу (hello@entrixy.com).

2. Подключение и аутентификация

Первым кадром контроллер представляется:

→ device_hello
{ "type":"device_hello",
  "device_key":    "<32 hex>",
  "device_secret": "<32 hex>",
  "e2ee":          true|false }        // есть ли валидный ownerSecret (§5)

device_key/device_secret — две 32-символьные hex-половины «кода устройства» (опц. префикс ENTX-DEV:). Сервер сверяет sha256(device_secret) с хэшем по device_key.

❮  device_ok  { "type":"device_ok", "hb_interval":300 }   // успех; hb_interval сек (10..3600)
← error      { "type":"error", "reason":"auth" }         // провал → сервер закрывает

3. Открытие (базовый режим, без E2EE)

❮  device_command
{ "type":"device_command", "action":"open", "command_id":"<id>", "number_id":<n> }
  // action:"close" — для бистабильного привода

Контроллер даёт импульс на реле (по умолчанию 1000 мс) и отвечает:

→ device_status
{ "type":"device_status", "command_id":"<id>",
  "level":"success"|"warning"|"danger"|"info",
  "message":"...", "final":true
  [, "position":"open"|"closed"|"unknown" ] }

level вне белого списка → трактуется как info.

4. Heartbeat и живость

Сервер не пингует устройства (fan-out не масштабируется). Живость держит устройство:

→ { "type":"ping" }    ← { "type":"pong" }     // устройство шлёт каждые ~30 с

Молчащее дольше ~90 с устройство сервер отключает. Обратный ping от сервера → устройство отвечает pong. Для бистабильного привода — object-state heartbeat: спонтанный device_status без command_id с position каждые hb_interval сек. Сервер может переустановить: ❮  { "type":"hb_interval", "seconds":<N> }.

5. E2EE: провизионинг ownerSecret

E2EE делает так, что сервер не может подделать открытие — команду подписывает приложение. ownerSecret (32 байта) в прошивку не зашивается: приложение владельца передаёт его по USB-serial:

PROVISION <64 hex>   → сохранить ownerSecret в NVS, вернуть fingerprint
WIPE                  → стереть (базовый режим)
STATUS                → показать fingerprint
fingerprint = HMAC-SHA256(ownerSecret, "fp")[0..3]  (hex)

После PROVISION/WIPE контроллер переподключается с обновлённым e2ee. Провизенный контроллер отвергает обычный device_command — только §6.

6. E2EE: challenge-response открытие

При e2ee=true вместо device_command — challenge-response. Свежесть даёт одноразовый nonce, не время:

1. ← sock_challenge_req  { "type":"sock_challenge_req", "command_id":"<id>" }
2. → sock_challenge      { "type":"sock_challenge", "command_id":"<id>", "nonce":"<b64 16B>" }
3. ← sock_fire           { "type":"sock_fire", "command_id","number_id",
                           "token":"<b64>", "owner_sig":"<b64>", "guest_id":<n>,
                           "nonce":"<b64>", "proof":"<b64>" }
4. → device_status success
КтоtokenПроверка
Владелецпустойproof == HMAC-SHA256(ownerSecret, nonce)[0..15]
Гость3 байтасверить owner_sig, вывести guest_key, сверить proof
token (3B) = [guest_did 2B LE][perms 1B]                      // capability, без TTL
owner_sig  = HMAC-SHA256(ownerSecret, token)[0..15]
guest_key  = HKDF-SHA256(salt=null, ikm=ownerSecret, info="guest"||guest_did(2B LE), L=32)
proof      = HMAC-SHA256(guest_key, nonce)[0..15]

Бинарные поля — base64. nonce 16 байт, подписи усечены до 16. HKDF salt=null = 32 нулевых байта.

7. Отзыв гостя

❮  sock_revoke  { "type":"sock_revoke", "number_id","guest_id","version","revoked", "sig":"<b64>" }
→ sock_revoke_ack
sig = HMAC-SHA256(ownerSecret, "rev" || guest_id(2B LE) || version(4B LE) || revoked(1B))[0..15]

Контроллер применяет только при version больше сохранённого (монотонно — сервер не подделает и не воскресит доступ).

8. Крипто-примитивы

Деривация guest_key и подписи идентичны гостевой ветке BLE — крипто переиспользуется.

9. Тест-векторы (E2EE)

Known-answer векторы. Бинарные поля даны и hex, и base64 (в кадрах — base64).

ownerSecret (32B)  = a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf
fingerprint        = b358977a   = HMAC(ownerSecret,"fp")[0..3]
nonce (16B)        = 000102030405060708090a0b0c0d0e0f   b64 = AAECAwQFBgcICQoLDA0ODw==

Owner fire (token пустой)

proof (16B) = 3fc0619c684a8261d06c1501ae4e726a   = HMAC(ownerSecret, nonce)[0..15]
proof b64   = P8BhnGhKgmHQbBUBrk5yag==

Guest fire (guest_did=0x0042, perms=0x01)

token (3B)      = 420001                              b64 = QgAB
owner_sig (16B) = 850a9f31fb708b176f4ed62a43acf0ad   = HMAC(ownerSecret, token)[0..15]  b64 = hQqfMftwixdvTtYqQ6zwrQ==
guest_key (32B) = 614da70a34890a807a25f7c5f271e530434c478f144370a2b9efaf0c58cd85c1
  = HKDF(salt=null, ownerSecret, info="guest"||did_LE, L=32)
proof (16B)     = a8a383ae56ac8a0182df2a275e220e83   = HMAC(guest_key, nonce)[0..15]  b64 = qKODrlasigGC3yonXiIOgw==

Revoke (guest_id=0x0042, version=3, revoked=1)

sig input = "rev"||guest_id_LE(2)||version_LE(4)||revoked(1) = 72657642000300000001
sig (16B) = 9433b96ddbb599f6c72bf17c0c9825fe   b64 = lDO5bdu1mfbHK/F8DJgl/g==

10. Получение device_key (для OEM)

Чтобы сервер узнавал устройство и понимал, чьи это ворота, нужна пара device_key/device_secret, известная серверу и привязанная к аккаунту владельца.

Рабочая реализация обеих сторон — esp32-ws-example (MIT). Собрать .bin — в браузерном конфигураторе. Как это в открытой архитектуре — /open.