❮  Entrixy-Controller

Spezifikation des Socket-Protokolls

Alles, was Sie brauchen, um Ihrem eigenen Controller den Online-Modus von Entrixy zu geben: die WebSocket-Nachrichten, Ende-zu-Ende-Verschlüsselung und Testvektoren.

Anders als BLEhält ein Socket-Controller eine dauerhafte Verbindung zum Entrixy-Server über WebSocket und lässt sich von überall öffnen. Der Grundmodus ist in dieser Spezifikation vollständig beschrieben. Zur Ende-zu-Ende-Verschlüsselung, bei der der Server eine Öffnung nicht fälschen kann, siehe §5–7. Der Bezug eines device_key für die Serienfertigung ist in §10 beschrieben. Referenz: esp32-ws-example (MIT).

1. Transport und Nachrichtenhülle

Die Referenz-Firmware für den ESP pinnt das Serverzertifikat standardmäßig nicht. Pinnen Sie es im Produktivbetrieb nach Host oder CA; Material gibt es auf Anfrage (hello@entrixy.com).

2. Verbindung und Authentifizierung

Der Controller stellt sich im ersten Frame vor:

→ 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 — die beiden 32-stelligen Hex-Hälften des Gerätecodes (mit optionalem ENTX-DEV:Präfix). Der Server prüft sha256(device_secret) gegen den Hash nach 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. Öffnen (Grundmodus, ohne E2EE)

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

Der Controller gibt einen Impuls auf das Relais (standardmäßig 1000 ms) und antwortet:

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

level außerhalb der Whitelist wird behandelt als info.

4. Heartbeat und Lebendigkeit

Der Server pingt nicht die Geräte — das skaliert nicht. Das Gerät hält die Verbindung selbst am Leben:

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

Schweigt ein Gerät länger als etwa 90 s, trennt der Server die Verbindung. Ein ping vom Server beantwortet das Gerät mit pong. Für einen bistabilen Antrieb gibt es einen Heartbeat des Objektzustands: ein spontanes device_status ohne command_id mit position alle hb_interval Sekunden. Der Server kann das ändern: ❮  { "type":"hb_interval", "seconds":<N> }.

5. E2EE: Bereitstellung des ownerSecret

Die Ende-zu-Ende-Verschlüsselung stellt sicher, dass der Server eine Öffnung nicht fälschen kann — den Befehl signiert die App. ownerSecret (32 Byte) steckt nicht in der Firmware: Die App des Besitzers liefert es über die serielle USB-Schnittstelle:

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)

Nach PROVISION oder WIPE verbindet sich der Controller neu mit aktualisiertem e2ee. Ein provisionierter Controller weist ein einfaches device_command zurück — es gilt nur §6.

6. E2EE: Öffnen per Challenge-Response

Mit e2ee=true statt device_command läuft ein Challenge-Response ab. Die Frische stammt von einer einmaligen Nonce, nicht von der Zeit:

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
WertokenPrüfung
Besitzerleerproof == HMAC-SHA256(ownerSecret, nonce)[0..15]
Gast3 Byteprüfe owner_sig, leite ab guest_key, prüfe 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]

Binäre Felder sind base64. Die Nonce ist 16 Byte lang, Signaturen werden auf 16 gekürzt. Das HKDF- salt=null ist 32 Nullbytes.

7. Einen Gast entziehen

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

Der Controller wendet es nur an, wenn version größer ist als der gespeicherte — monoton, sodass der Server ihn weder fälschen noch den Zugang zurückholen kann.

8. Kryptografische Primitive

Die Ableitung von guest_key und die Signaturen sind identisch mit dem Gastzweig von BLE — die Kryptografie wird wiederverwendet.

9. Testvektoren (E2EE)

Vektoren mit bekannten Ergebnissen. Binäre Felder sind sowohl hexadezimal als auch in base64 angegeben; in den Frames steht base64.

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

Owner Fire (leeres 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. Bezug eines device_key (für OEMs)

Damit der Server ein Gerät erkennt und weiß, wem es gehört, braucht es das Paar device_key/device_secret, das dem Server bekannt und an das Konto des Besitzers gebunden ist.

Eine funktionierende Umsetzung beider Seiten ist esp32-ws-example (MIT). Um .bin — siehe Konfigurator im Browser. Wie sich das in die offene Architektur einfügt: /open.