❮  Contrôleurs Entrixy

Spécification du protocole socket

Tout ce qu'il faut pour ajouter le mode en ligne d'Entrixy à votre propre contrôleur : les messages WebSocket, le chiffrement de bout en bout et les vecteurs de test.

Contrairement à BLE, un contrôleur socket garde une connexion permanente au serveur Entrixy par WebSocket et peut être ouvert depuis n'importe où. Le mode de base est entièrement décrit par cette spécification. Pour le chiffrement de bout en bout, où le serveur ne peut pas forger une ouverture, voir §5–7. L'obtention d'un device_key pour la production en série est traitée au §10. Référence : esp32-ws-example (MIT).

1. Transport et enveloppe des messages

Le micrologiciel de référence pour l'ESP n'épingle pas le certificat du serveur par défaut. En production, épinglez-le par hôte ou par autorité de certification ; le matériel est fourni sur demande (hello@entrixy.com).

2. Connexion et authentification

Le contrôleur se présente dans la première trame :

→ device_hello
{ "type":"device_hello",
  "device_key":    "<32 hex>",
  "device_secret": "<32 hex>",
  "e2ee":          true|false }        // whether a valid ownerSecret exists (§5)

device_key/device_secret — les deux moitiés hexadécimales de 32 caractères du code de l'appareil (avec un ENTX-DEV:préfixe facultatif). Le serveur vérifie sha256(device_secret) par rapport au hachage par device_key.

❮  device_ok  { "type":"device_ok", "hb_interval":300 }   // success; hb_interval in s (10..3600)
← error      { "type":"error", "reason":"auth" }         // failure → the server closes

3. Ouverture (mode de base, sans E2EE)

❮  device_command
{ "type":"device_command", "action":"open", "command_id":"<id>", "number_id":<n> }
  // action:"close" — for a bistable drive

Le contrôleur envoie une impulsion au relais (1000 ms par défaut) et répond :

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

level hors de la liste blanche est traité comme info.

4. Heartbeat et détection de vie

Le serveur ne ping pas les appareils — ce fan-out ne passe pas à l'échelle. C'est l'appareil qui maintient la connexion en vie :

→ { "type":"ping" }    ← { "type":"pong" }     // the device sends this every ~30 s

Si un appareil reste muet plus de 90 s environ, le serveur le déconnecte. Un ping venu du serveur, l'appareil y répond par pong. Pour une motorisation bistable, il existe un heartbeat de l'état de l'objet : un device_status sans command_id avec position toutes les hb_interval secondes. Le serveur peut la modifier : ❮  { "type":"hb_interval", "seconds":<N> }.

5. E2EE : provisionnement de l'ownerSecret

Le chiffrement de bout en bout garantit que le serveur ne peut pas forger une ouverture — la commande est signée par l'application. ownerSecret (32 octets) n'est pas inscrit dans le micrologiciel : c'est l'application du propriétaire qui le livre par le port série USB:

PROVISION <64 hex>   → store the ownerSecret in NVS, return the fingerprint
WIPE                  → erase it (basic mode)
STATUS                → show the fingerprint
fingerprint = HMAC-SHA256(ownerSecret, "fp")[0..3]  (hex)

Après PROVISION ou WIPE, le contrôleur se reconnecte avec un e2ee mis à jour. Un contrôleur provisionné rejette un simple device_command — seul le §6 s'applique.

6. E2EE : ouverture par défi-réponse

Avec e2ee=true au lieu de device_command on procède par défi-réponse. La fraîcheur vient d'un nonce à usage unique, pas du temps :

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
QuitokenVérification
Propriétairevideproof == HMAC-SHA256(ownerSecret, nonce)[0..15]
Invité3 octetsvérifier owner_sig, dériver guest_key, vérifier proof
token (3B) = [guest_did 2B LE][perms 1B]                      // a capability, no 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]

Les champs binaires sont en base64. Le nonce fait 16 octets et les signatures sont tronquées à 16. Le salt=null de HKDF vaut 32 octets nuls.

7. Révoquer un invité

❮  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]

Le contrôleur ne l'applique que lorsque version est supérieur à celui qui est stocké — c'est monotone, si bien que le serveur ne peut ni le forger ni rétablir l'accès.

8. Primitives cryptographiques

La dérivation de guest_key et les signatures sont identiques à la branche invité de BLE — la cryptographie est réutilisée.

9. Vecteurs de test (E2EE)

Vecteurs à réponse connue. Les champs binaires sont donnés en hexadécimal et en base64 ; les trames transportent du base64.

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

Ouverture propriétaire (jeton vide)

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. Obtention d'un device_key (pour les OEM)

Pour que le serveur reconnaisse un appareil et sache à qui il appartient, il faut la paire device_key/device_secret, connue du serveur et liée au compte du propriétaire.

Une implémentation fonctionnelle des deux côtés est esp32-ws-example (MIT). Pour compiler .bin — voir configurateur dans le navigateur. Comment cela s'inscrit dans l'architecture ouverte : /open.