Aggiornato al 2026-09-04. Ogni riga di questa pagina è o un numero, o una cosa che potete verificare da soli. Dove non c'è niente da verificare, lo scriviamo.
Una dichiarazione datata e firmata con la stessa chiave ML-DSA-65 degli aggiornamenti: nessuna richiesta di dati, nessun ordine di consegna chiavi, nessuna backdoor imposta, nessun ordine di silenzio. Se smette di essere aggiornata, o la firma non torna, trattatelo come una risposta.
canary.txt · firma · chiave pubblica del produttore
# una volta, nella cartella dove hai scaricato i tre file:
npm i @noble/post-quantum
node -e '
const {ml_dsa65}=await import("@noble/post-quantum/ml-dsa.js"),fs=await import("fs");
const b=fs.readFileSync("canary.txt"),s=fs.readFileSync("canary.txt.sig","utf8").trim();
const pk=fs.readFileSync("vendor-pubkey.txt","utf8").trim();
console.log(ml_dsa65.verify(Buffer.from(s,"hex"),b,Buffer.from(pk,"hex")) ? "FIRMA VALIDA" : "FIRMA NON VALIDA");'
Correzione, 5 settembre 2026. Fino a oggi questa riga aveva gli argomenti
nell'ordine sbagliato (verify(pk, msg, sig)): chi seguiva le nostre
istruzioni per controllare il canarino non otteneva «FIRMA NON VALIDA», otteneva un
errore — e poteva ragionevolmente concludere che il canarino non tornasse. La firma era
sempre stata valida; era rotta l'istruzione, cioe' l'unica cosa che serve a non doverci
credere sulla parola. L'ordine giusto in @noble/post-quantum 0.6 e'
verify(firma, messaggio, chiavePubblica). Ce ne siamo accorti riscrivendo
il firmatario in Rust e provando l'interoperabilita fra le due implementazioni.
La firma è prodotta da un binario Rust (fips204) e verificata qui sopra da
una libreria JavaScript indipendente (@noble/post-quantum): due
implementazioni diverse dello stesso standard, che è l'unico modo per accorgersi se una
delle due sbaglia.
| Voce | Stato | Come lo verificate |
|---|---|---|
| Richieste di dati da autorità | 0 | Dichiarato nel canarino, firmato ML-DSA-65 e datato. |
| Richieste soddisfatte | 0 | Idem. E se anche fossimo costretti, sotto c'è l'elenco esatto di cosa potremmo consegnare. |
| Ordini di consegna chiavi | 0 | Le chiavi degli utenti non sono sul server: non esiste un file da consegnare. |
| Backdoor o codice imposto | 0 | Verificabile: la build è riproducibile dal sorgente pubblicato. |
| Binari di terze parti nell'APK | 0 · dalla v2.2.0 | Il nucleo Xray si compila da sorgente a ogni build. Prima era un .aar scaricato. |
| Build riproducibile | sì · dalla v2.1.0 | 1.662 voci su 1.662 identiche partendo dal solo archivio pubblicato (verifica automatica ogni notte). |
| Sorgente pubblicato | sì · dalla v2.1.0 | Un archivio deterministico per ogni versione, con le istruzioni di verifica dentro. |
| Firma dell'APK | RSA 4096 | Chiave di release nostra, mai uscita dal server. Impronta qui sotto. |
| Firma degli aggiornamenti | ML-DSA-65 | Post-quantistica. L'app rifiuta ogni aggiornamento la cui firma non torna. |
| Firma del canarino | ML-DSA-65 · Rust | Prodotta da un binario Rust con fips204: nessun interprete nel percorso. |
| Codice del server | Rust | Il servizio in produzione è Rust (Axum). Anche il canarino e questo rapporto ora lo sono. |
| Derivazione del PIN sul telefono | PBKDF2-HMAC-SHA512 · 600.000 | Nativo di Android, fuori dal thread dell'interfaccia. Prima era un solo SHA-512. |
| Doppio fondo (negazione plausibile) | sì · dalla v2.5.0 | Nessun verificatore del PIN esca è salvato: non esiste un dato che provi che esista. |
| Audit esterno indipendente | nessuno | Non l'abbiamo. Continuiamo a scriverlo invece di sperare che non lo chiediate. |
| Google Play Services / FCM | presente, non necessario | Le notifiche funzionano senza; il file di configurazione resta nell'APK ed è pubblico. |
| Analytics, telemetria, crash reporting | nessuno | Nessun SDK di terze parti che chiami casa. Verificabile nel sorgente. |
Non pubblichiamo il risultato finale come se fosse venuto bene al primo colpo. È andata così, e il modo in cui è andata è la parte utile: ogni tentativo ha trovato un difetto vero che nessuna dichiarazione avrebbe trovato.
| Tentativo | Differenze | Causa trovata |
|---|---|---|
| 1° | 11 voci su 1663 | I compilatori nativi incorporano il percorso assoluto dei sorgenti dentro le
.so: codice identico, librerie diverse. Risolto compilando sempre da un
percorso canonico (/lattice-src, bind mount), noi e chi verifica. |
| 2° | 3 voci su 1663 | Mancava modules/app-disguise nell'archivio: un modulo nativo locale
fuori da src/ e android/. Il progetto compilava comunque —
senza quel modulo, nove classi in meno nel dex. |
| 3° | 0 voci su 1663 | Riproducibile. APK compilato solo dall'archivio pubblicato, in un'altra cartella, con un'altra chiave di firma: identico byte per byte a quello pubblicato. |
| v2.2.0 | 0 binari importati | Il nucleo Xray non è più un .aar scaricato: si compila da sorgente
con Go e gomobile a ogni build, e due compilazioni danno lo stesso file. |
| v2.3.1 → | verifica ogni notte | Un cron ricompila l'APK pubblicato in un container pulito e pubblica il risultato in verify-status.json. Non ce lo dobbiamo ricordare noi. |
Firma APK (RSA 4096, SHA-256 del certificato) 45:11:53:AA:02:9A:67:B9:61:29:DA:64:76:27:3B:3D:9E:6F:A4:34:25:2F:CD:2C:8E:17:2B:9D:8D:AF:F1:C3 Chiave del produttore per gli aggiornamenti e per il canarino (ML-DSA-65, pubblica) 9d35f3390f4df55ab631567b00ed9628b93b9c2e1b988ab07e6000f18010da40… Nucleo Xray compilato da noi (libv2ray.aar, versione 2.1.0 — importato) 670cf11d9d10a6bb6548ac4f593acfa4339155732f6f8de4d45923f30a74deed
La chiave di firma dell'APK è cambiata con la v2.2.0: la precedente era la chiave di debug di Android, la cui password è pubblica. Chi aveva una versione precedente deve disinstallare e reinstallare: Android, giustamente, non accetta un aggiornamento firmato con una chiave diversa. Lo scriviamo qui perché è un fastidio reale, causato da un errore reale, e nasconderlo sarebbe peggio del fastidio.
Non è una certificazione. Non è un audit. È un rapporto scritto da chi gestisce il servizio, che è esattamente la persona di cui non dovreste fidarvi sulla parola. Per questo ogni riga rimanda a qualcosa che potete controllare da soli: un archivio da ricompilare, una firma da verificare, un file da confrontare. Le righe che non rimandano a niente — «nessuna richiesta ricevuta», «nessun audit esterno» — sono dichiarazioni, e come tali valgono quanto la firma che le accompagna e la data che portano. Non di più.