KeycardAccess - Controllo accessi

Sì in effetti hashare 7 byte non aiuta molto. Per questo ieri parlando con @g5pw suggerivo di aggiungere il salt_card.

Online suggeriscono di usare l’UID, ma resto sempre confuso all’idea che non sia forse meglio impostare noi un salt_card e lasciar perdere l’UID direttamente. La scheda è identificata da un valore che aggiungiamo noi così almeno brute force è escluso.

Per quanto riguarda l’ESP32, nemmeno io ho problemi con la piattaforma, e sicuramente avere la libreria per esp32 è utile non solo a noi! Rispetto all’orange pi è sicuramente più portabile.

Non sapevo l’esp avesse l’ethernet:)

Forse è meglio iniziare più ad alto livello, e poi implementare i dettagli pensando a tutta l’infrastruttura che si vuole supportare.
Ad esempio:
Vogliamo usare un server esterno centralizzato per la decisione di acesso?
Allora il circuito deve essere il più trasparente possibile a questo servizio (si occupa solo di stabilire una canale sicuro fra il tag e il server)

Vogliamo qualcosa che decida lui autonomamente secondo delle regole interne (aggiornate periodicamente da un server centrale). Quindi si complica il deployment e l’accentramento delle regole e audit.

1 Mi Piace

Secondo me the way to go è andare con la proposta 3, scegliendo K0 := KDF(UID,<pre_shared_secret>).

Per quanto riguarda la generazione del DB, io ometterei K{S,P}_c e userei solo TLS auth, tanto il database dobbiamo tenerlo in chiaro da qualche parte, quindi avere anche chiavi asimmetriche per i lettori mi sembra un overkill.

Status update

Rieccoci qui un mese e 460 commit più tardi!!

A questo punto abbiamo completato la libreria per controllare la Mifare e tutte le API sono testate :slight_smile:

Bisogna fare

  • code review
  • documentazione
  • rilasciarla per ESP32
  • pensare adesso all’implementazione dell’apriporta vero e proprio

Queste cose possono avvenire anche insieme e progressivamente.

Per chi vuole dare un’occhiata, il progetto è a Progetti / Keycard Access · GitLab

E la documentazione scritta finora la trovate a https://proj.mittelab.dev/keycard-access

1 Mi Piace

Time for an update!

Abbiamo il PN532 nella porta, che comunica con l’interno via un converter RS232. Abbiamo verificato che l’apertura della porta via relay funziona, entro breve programmo l’esp32 e dovremmo avere una base da cui partire!

2 Mi Piace

Hello! Siamo ancora vivi e operativi :slight_smile: 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?

1 Mi Piace

Grazie della summary @5p4k !

Penso che usare SSL per la protezione dei dati sia sufficiente. La CA è qualcosa che si può configurare a build time, direttamente via esp-idf. Inoltre, il pannello sarà esposto via HTTPS quindi da lì non si scappa…

La cosa più “standard” sarebbe usare mTLS. Un paio di dati che possono aiutarci a valutare se è feasible:

  • Usare mTLS con RSA è inaccettabile.
  • Tecnicamente mTLS supporta ed25519. Sarebbe da capire se possiamo usare la stessa chiave ed25519.
  • Usare mTLS in FastAPI/Python è un po’ scomodo (bisogna parlare con uvicorn direttamente)
  • Ci semplifica il tutto: una volta autenticati, siamo sicuri che stiamo parlando con una device “vera”
    • non serve proteggersi da replay attacks
    • non serve verificare firme
    • sarebbe solo da verificare che la device possa inserire solo log relativi a se stessa
    • non avere un nome DNS non è una limitazione per creare un cert, basta l’IP
  • La rotation di certificati possiamo evitarla:
    • ignoriamo la data di scadenza
    • generiamo certificati validi per 100 anni :slight_smile:
    • implementiamo ACME per esp32 (mi pare un po’ overkill)

Aggiornamento febbraio 2025

Alla fine la soluzione implementata è stata usare direttamente libSodium secret streams su TCP. Dentro c’è il payload JSON RPC codificato con CBOR. Il problema principale è la gestione di una PKI, la rotazione dei certificati, integrazione complessa tra sistemi esistenti. Queste sono tutte cose da gestire lato sysadmin, che non è l’ideale. Quindi, avendo già a disposizione le chiavi, tanto vale usare quelle. Più dettagli sulla presentazione data all’ESC 2K24, che trovate qui: slides.

Implementazione del protocollo

Indipendentemente dal canale, ci servono due implementazioni, una lato client (in C++ per ESP-IDF), ed una in python (per il server). Verrà un giorno in cui si potrà fare tutto agevolmente in Rust e servirà solo un’implementazione, ma non è questo il giorno. Eccole qui:

L’implementazione in python è stata completata in settembre 2024, mentre in novembre ho fatto quella in C++. Bonus point: compila su Linux per cui adesso abbiamo python-C++ cross testing (e ovviamente unit test su ESP32) con multi-project pipeline:

Questo però ha attivato una serie di azioni di refactoring e pulizia a cascata

Refactoring a cascata

La scelta di rendere KAProto una componente separata è stata motivata dal poterla testare più agevolmente. Avere due implementazioni in Python e C++ permette di testare l’una contro l’altra in locale. Questo significa che ho dovuto scorporare buona parte delle dipendenze di keycard-access e sollevarle in modo che keycard-access e kaproto le potessero condividere.

Inoltre, questo significa anche che tutte le componenti condivise devono compilare su Linux e su ESP-IDF; i file CMake devono supportare entrambe (dal momento che il package manager di IDF non va per Linux), e devono essere utilizzabili sia tramite il package manager di IDF che come submodule (per Linux). Questo ha richiesto modifiche su

  • Mittelib:
    • migrato ad ESP-IDF
    • ora compila su Linux
    • reimplementa il sistema di logging di ESP-IDF per poter usare la stessa codebase su Linux
  • Catch2 (che usiamo per unit test) può essere utilizzato come third party dependency su IDF e Linux con lo stesso CMakeFile.
  • SpookyReporter (che usiamo per fare pretty print dei test ed evitare stack overflow su ESP-IDF[1]) compila anche su Linux e può essere usato su entrambi com submodule.
  • Nlohmann’s JSON, che usiamo sia in KeycardAccess che in KAProto
    • è stato sollevato ad una dipendenza a parte
    • può essere usato come submodule su Linux ed ESP-IDF
    • su ESP-IDF può essere usato come libreria
  • KAProto (C++):
    • implementato
    • può essere usato come submodule su Linux o libreria ESP-IDF
    • su Linux usa Mittelib via FetchContent, su IDF come libreria
    • su Linux usa libSodium via FetchContent, su IDF come libreria.
    • libSodium è stato portato in maniera compatibile per avere lo stesso codice client
    • dipende da Nlohmann JSON, Catch2, SpookyReporter come sopra
  • libSpookyAction:
    • aggiornato per usare Catch2 e SpookyReporter come sopra
    • aggiornato per compatibilità con Mittelib
    • (parzialmente) migrato tutte le macro dei log
  • keycard-access proper:
    • migrato per usare Catch2 e SpookyReporter come sopra
    • migrato tutte le macro dei log
    • ora usa KAProto, libSpooky e Mittelib aggiornate con la rispettiva versione libreria IDF
    • tutte le componenti interne sono state separate e classificate in subcomponenti, questo servirà anche in futuro per vedere cosa tenere e cosa buttare

A cui si aggiungono i cambiamenti di cui sopra:

Lo stato delle repo

Molte repo hanno readme datati e alcune delle librerie mancano di release, e la documentazione è aggiornata solo su libSpookyAction, tuttavia stiamo già pensando a come porre ordine su tutto e che aspetto “finale” potrebbe avere KeycardAccess. In particolare, a rendere più fruibile il deploy e a gestire una parte maggiore della configurazione dal backend, invece che portare in giro il keymaker.

Verso il deploy

KeycardAccess è montato, perché non è attivo? Perché non abbiamo le due cose essenziali che servono per poter effettivamente averlo in produzione:

  1. un log degli eventi di apertura
  2. un sistema per revocare l’accesso dal backend

Ora abbiamo un backend e un protocollo per comunicare, per cui bisogna provvedere all’implementazione di log e revoche.

Roadmap

I task più immediati sono tracciati qui. Per il deploy, questa è ciò che manca (solo funzionalità):

Client-side event management:

  1. avere un sistema per poter registrare gli eventi, che li persista nel non-volatile storage.
  2. aggiungere l’handler che all’apertura registra l’evento
  3. aggiungere un task che si collega al server, se riesce, e svuota la queue degli eventi quando si collega

Client-side revocation management:

  1. avere un sistema per poter registrare utenti o carte revocate.
    Questo deve essere pensato nell’ordine di idee che diventerà un vero e proprio access control, quindi magari può esprimere la revoca di utente/gruppo/carta e non semplicemente un elenco di uid revocati. Inoltre, deve persistere sul non-volatile storage.
  2. integrare nel task che si collega al server la possibilità di scaricare e integrare questa lista.
    Inizialmente potrebbe riscaricarla in maniera integrale se è stata aggiornata.
  3. verificare se la carta/utente è stata revocata prima di aprire

Server-side:

  1. servire gli endpoint necessari per i punti di cui sopra.
    Qui c’è da decidere se scorporare l’applicazione in due e farle comunicare solo via DB.
  2. definire la struttura dati per poter revocare la combinazione utente/gruppo/carta e la GUI

Focus attuale

  • Al momento, sto completando la migrazione delle funzionalità di logging da IDF a Mittelib, e aggiornando libSpookyAction concordemente.
  • Ho aggiunto l’iterabilità al wrapper NVS per supportare le strutture dati per gli eventi e la lista di revoca
  • Le API NVS di IDF 5.4 sono più complete, e al momento eravamo fermi a 5.1.2 che è vecchiotta, per cui ho aggiornato IDF su tutti i progetti.
  • Aggiornare a IDF 5.4 mi ha fatto deviare per sistemare un header include su wolfSSL; a quanto pare la versione più recente di wolfSSL non compila su IDF 5.4, sto aspettando che rilascino la prossima preview. Nel frattempo libSpooky non supporterà wolfSSL (e potremmo pensare di levarlo del tutto)

Sto pensando di fare come con il logbook del gruppo NOC, e riportare qui con regolarità il progresso.


  1. Catch non è super memory-friendly, usa iostream e carica in memoria l’intero junit.xml, ESP32 non ci sta dietro ↩︎

1 Mi Piace