Hello! Siamo ancora vivi e operativi
il prototipo funziona, ma ora ci stiamo concentrando sul backend che logga gli accessi, che inevitabilmente ci servirà e al momento non abbiamo.
Il dibattito è aperto su come comunicare tra il gate e il backend. Avevamo in origine pensato a MQTT come transport, ma è un po’ macchinoso perché bisogna autenticare i client, e in più ha bisogno di un server dedicato raggiungibile su un indirizzo fisso (scomodo per “piccole” applicazioni in cui il server è usato solo per configurare i gate e non si ha bisogno del log di accesso).
Cosa abbiamo (più o meno deciso)
…quindi abbiamo pensato a Websocket. Websocket è carino, moderno, supportato da ESP-IDF, ampiamente supportato in python, ed in particolare su FastAPI.
Su websocket possiamo montare jsonrpc come RPC per chiamare i metodi sul server. Questo è comodo perché c’è già fastapi_websocket_rpc e perché dal lato ESP32 non è uno sforzo elevatissimo. Ho già fatto girare JSON for modern C++ su keycardaccess, per cui si tratta di aggiungere il codice che converte una chiamata in JSON e lo spedisce. Questo codice in realtà già esiste, in buona parte, perché per configurare keycard access ho scritto un modulino che legge la signature di un metodo, e serializza tutti gli argomenti con template magic (questo viene al momento usato sia per configurare il gate dal keymaker via NFC, sia per definire una shell per controllare il keymaker).
L’idea era di trasmettere il payload JSON in formato binario, invece che testuale, usando CBOR. Questo vuol dire meno memoria usata nell’ESP32, ed è già supportato dalla libreria JSON che ho linkato sopra.
Il pacchetto fastapi_websocket_rpc
ha un hook che consente di inserire una classe speciale per (de)serializzare il payload, insieme ad un metodo on_connect
che possiamo usare per fare operazioni di setup (e.g. scambio chiave).
Cosa dobbiamo decidere
Veniamo dunque al punto chiave. Come proteggiamo il websocket? Ci serve
- mutua autenticazione del server e del client. Server e client, a meno del primo enrollment, conoscono le rispettive chiavi pubbliche (Ed25519) usate nel programmare le carte.
- confidenzialità dei dati trasmessi (vengono trasmesse regole di accesso e dati di ingresso)
- autenticazione dei dati trasmessi, e in particolare, questa deve avvenire rispetto alle chiavi menzionate sopra
- integrità dei dati ovviamente
- non-ripetibilità (e.g. simulare un accesso avvenuto in un secondo momento, oppure rollback di vecchie regole come replay)
- integrità temporale: questo non è un aspetto relativo al web socket, ma ovviamente ogni messaggio dovrà contenere un timestamp firmato dal momento che la data di ricezione potrebbe essere posteriore.
Soluzioni esistenti (non necessariamente per websocket)
In alcune di queste cose, in particolare client authentication, il web in generale lascia un po’ a desiderare secondo me (motivo per cui da MQTT e HTTP siamo poi approdati a websocket, e personalmente già pensavo ad un socket TCP). I bearer token non vanno su websocket, a meno che non ti chiami kubernetes, e come i cookie e le sessioni richiedono comunque un altro endpoint e session management dal lato server. Ad ogni modo, token tipo JWT mi sembrano terribilmente complicati per questo tipo di applicazione. Su questa pagina comunque riepilogano diverse maniere di usare autenticazioni standard per http su websocket.
In termini invece di firmare il contenuto, c’è HTTP message authentication, è solo una draft (assurdo che nel 2024 non puoi firmare un messaggio http), non è per websocket, e comunque non supporta Ed25519. “Firmare JSON” è un po’ come firmare XML, lo devi canonicalizzare prima, secondo me non è una buona idea. Piuttosto firmiamo il blob CBOR.
Usare client certificate come autenticazione potrebbe essere un’idea, ma i certificati SSL in generale (anche lato server) hanno innumerevoli complicazioni. Innanzitutto bisogna mantenere tutta la chain of trust, erogarli in maniera sicura, tenendo presente che il backend comunque gira in locale per cui verosimilmente non avrà un chiaro nome DNS. E poi bisogna accettarli, il che significa trasmetterli comunque in qualche maniera da/all’ESP32, trovare il modo di visualizzarli e confermarli, e poi alla fine ruotarli. Big nope secondo me, considerato che uno dei vantaggi di Ed25519 era la dimensione delle chiavi (32B vs 4KB) che con i vincoli di spazio e RAM dell’ESP32 è molto pratico.
Tutto questo, quando di fatto possediamo già due coppie di chiavi Ed25519, l’equivalente una passkey.
Qualche proposta
Secondo me, aggiungiamo un layer di sicurezza tra il blob binario CBOR e il websocket.
Questo potrebbe essere:
-
richiediamo SSL sul websocket, e usiamo la chiave privata Ed25519 del gate/server per firmare tutti i messaggi. Attenzione che qualsiasi garanzia svanisce dietro all’SSL: tra un potenziale reverse proxy e il backend, i messaggi sono in chiaro. Oltretutto bisogna far accettare all’ESP32 un certificato SSL che sarà quasi sicuramente self-signed.
-
ce ne freghiamo di SSL che ci sia o meno e sfruttiamo le chiavi Ed25519 che già abbiamo. Tutte le garanzie di cui sopra sono automatiche se mettiamo i messaggi dentro al secretstream di libsodium (XChaCha20 + Poly1305). Questo lavora con una chiave simmetrica che possiamo ottenere:
- da un segreto comune che possediamo già (
pk1*sk2 == pk2*sk1
), differenziandolo in qualche modo, ad esempio scambiando dei bit random scelti da client e server. Questo implicitamente autentica entrambi.
- effettuando uno scambio di chiavi effimere, che funziona come nel punto precedente, ma fa l’hash del segreto invece di differenziare una chiave. Questo poi richiede di autenticare client e server però, per cui bisogna aggiungere una challenge
Altro?