❮  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. Unsere eigenen Platinen, die es sprechen, sind die NET-Reihe; die nur über Bluetooth arbeiten, sind die BLE-Reihe.

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 ESP32 und ESP8266 prüft das Serverzertifikat gegen die Let’s-Encrypt-Wurzeln (ISRG Root X1 und X2) und stellt ihre Uhr per NTP, um die Gültigkeitsdaten zu prüfen. Ein Server mit einem Zertifikat einer anderen Stelle trägt seine Wurzel in SERVER_CA in der Firmware ein. Bleibt SERVER_CA leer, verbindet sich die Firmware ohne Prüfung des Servers.

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, "ping_s":10 }   // success; hb_interval in s (10..3600), ping_s — the ping period
← error      { "type":"error", "reason":"auth" }         // failure → the server closes

Ein Controller, der über das Internet arbeitet, hält selbst die Verbindung zum Server, daher sieht der Server die IP-Adresse des Controllers und den Zeitpunkt jedes Befehls an ihn. Nur der an das Telefon des Eigentümers gebundene ESP32-Netzwerkcontroller prüft die Signatur jedes Befehls (§5–6). Die Controller auf ESP8266, Raspberry Pi und Node.js vertrauen dem Server: Er kann ihnen selbst ein Öffnen schicken.

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"|"opening"|"closing"|"unknown" ] }

level außerhalb der Whitelist wird behandelt als info. Die App zeigt message wörtlich an und schreibt sie neben dem Tippen ins Protokoll.

Der Controller meldet auch, was bei ihm selbst passiert. Ein solcher device_status hat ein leeres command_id und ein Feld event – die App schreibt ihn ins Protokoll des Besitzers.

→ { "type":"device_status", "command_id":"", "event":"button"|"limit"|"auto",
    "level":"success", "message":"Opened", "final":true, "position":"open" }
eventWann
buttondie Ausgangstaste innen wurde gedrückt; bei einem bistabilen Antrieb wechselt sie zwischen Öffnen und Schließen
limitder Endschalter: die Tür wurde von Hand bewegt, oder der Antrieb ist nicht innerhalb der im Controller eingestellten Fahrzeit dort angekommen, wohin ihn ein Druck geschickt hat (level danger, message "Did not close" oder "Did not open"). Bestätigt der Endschalter einen Druck, trägt das Ereignis "confirm":true: die App aktualisiert diesen Druck im Protokoll, statt eine Zeile hinzuzufügen
autoder Controller hat nach der eingestellten Zahl von Sekunden selbst geschlossen

Mit Endschalter meldet der Controller opening oder closing vom Impuls, bis der Endschalter die Position bestätigt. Ohne Endschalter meldet er sofort die Zielposition.

"hold":true bittet die App, die Meldung bis zur nächsten Meldung des Controllers auf der Karte zu halten; ohne das verschwindet sie nach einigen Sekunden. Die Controller setzen es für Öffnen und Closing (Stufe info, das Tor ist unterwegs) und für einen Fehler.

Die Antwort auf einen Befehl, der den Antrieb tatsächlich gestartet hat, enthält "started":true. Nur für einen solchen Druck wartet der Server auf das Ende durch den Endschalter: „Belegt“ oder „öffnet bereits“ bei einem zweiten Druck übernimmt nicht die Bewegung eines anderen.

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 ping_s seconds

Schweigt ein Gerät länger, als der Server erlaubt, trennt der Server es und zeigt es offline. Den Takt nennt der Server in device_ok.ping_s: ein Drittel dieser Zeit (DEVICE_OFFLINE_S, standardmäßig 30 s). Ein Gerät, das "ping_ctl":true in device_hello sendet, folgt ihm; andere pingen alle 30 s und gelten nach 90 s Stille als offline. Auf 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.

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>" [, "action":"close"] }
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]

Ein Schließen für einen bistabilen Antrieb trägt "action":"close" und wird über den Nonce mit den fünf angehängten ASCII-Bytes close am Ende: proof = HMAC-SHA256(key, nonce || "close")[0..15]. Dabei ist key der ownerSecret des Besitzers oder der guest_key des Gastes. Ein Öffnen wird über den reinen Nonce signiert. Der Server kann also weder ein Öffnen in ein Schließen verwandeln noch umgekehrt: Die Signatur würde nicht passen. Was zu tun ist, entscheidet der Signierende – nach der Stellung, die der Controller gemeldet hat.

Der Controller akzeptiert eine Nonce 30 Sekunden nach der Ausgabe und nur in der Verbindung, in der er sie ausgegeben hat: Ein vom Server zurückgehaltener signierter Befehl wirkt später nicht mehr.

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", "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.

Der ESP32-Netzwerkcontroller speichert bis zu 96 Entscheidungen zu Gästen. Ist die Liste voll, macht ein Eintrag „erlaubt“ Platz für einen neuen Widerruf; sind alle 96 Widerrufe, entfällt der älteste.

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==

Schließen (bistabiler Antrieb)

signed input = nonce || "close" = 000102030405060708090a0b0c0d0e0f636c6f7365
owner proof  = ef15ca41823f126de0815f84c7dc8275   = HMAC(ownerSecret, input)[0..15]  b64 = 7xXKQYI/Em3ggV+Ex9yCdQ==
guest proof  = 54f23d61766a274d8996c26e67925c32   = HMAC(guest_key, input)[0..15]    b64 = VPI9YXZqJ02JlsJuZ5JcMg==

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.