[{"data":1,"prerenderedAt":88},["ShallowReactive",2],{"tutorial-sicurezza-avanzata-in-flutter-code-obfuscation-e-certificate-pinning":3,"comments-tutorial-sicurezza-avanzata-in-flutter-code-obfuscation-e-certificate-pinning":87},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":34},93,"Sicurezza avanzata in Flutter: code obfuscation e certificate pinning","sicurezza-avanzata-in-flutter-code-obfuscation-e-certificate-pinning","Guida avanzata per mettere in sicurezza un'app Flutter in produzione: build offuscate con --obfuscate e symbolize, hardening R8\u002FProGuard, certificate pinning SPKI con Dio, rotazione dei pin e verifica con un proxy MITM.","Quando un'app Flutter esce dalla fase di sviluppo ed entra in produzione, il codice compilato e il traffico di rete diventano superfici d'attacco reali: il binario può essere decompilato per estrarre endpoint, chiavi e logica di business, mentre il traffico HTTPS può essere intercettato da un proxy MITM con una CA installata sul dispositivo (scenario banale su device rooted, emulatori o reti aziendali).\n\nIn questo tutorial affrontiamo due contromisure complementari e spesso richieste dagli audit di sicurezza:\n\n- **Code obfuscation** dell'AOT snapshot Dart, con gestione corretta dei file di simboli per poter comunque leggere gli stack trace di produzione.\n- **Certificate pinning** basato su hash SHA-256 della **SubjectPublicKeyInfo (SPKI)**, implementato correttamente con Dio (attenzione: `badCertificateCallback` **non** basta) e con strategia di rotazione dei pin.\n\n> Prerequisiti: familiarità con build release Android\u002FiOS, Gradle, `dio`, e nozioni base di TLS\u002FX.509. Il tutorial presuppone Flutter 3.x e Dio 5.x.\n\nAlla fine avrai una pipeline di build hardenizzata, un client HTTP che rifiuta qualunque certificato non pinnato e una procedura di verifica con `mitmproxy`.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F31f0148e-d123-4d7c-9521-01b152b8eb6d.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1564843055511-9cb87d8eaf22?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODkyNzM5NDh8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Maccy","https:\u002F\u002Funsplash.com\u002F@jizhidexiaohailang","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fturned-on-iphone-6-rIqrq0HzTdA","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=hvUxPS7YBow",{"youtube_id":17,"duration_seconds":18,"published_at":19},"hvUxPS7YBow",166,"2026-09-13T09:52:18+00:00","advanced",55,"3.x (Dio 5.x)","published","2026-09-13T04:32:28+00:00","Flutter: obfuscation e certificate pinning avanzati","Guida avanzata alla sicurezza delle app Flutter: build offuscate, R8, pin SPKI con Dio, rotazione dei pin e verifica con proxy MITM.",{"id":28,"name":29,"slug":30},3,"Best practice","best-practice",{"id":32,"name":33},1,"Flutter Bot",[35,43,51,58,65,72,80],{"id":36,"position":32,"title":37,"body":38,"code_snippet":39,"code_language":40,"expected_result":41,"demo_url":42,"video_url":42},619,"Definire il modello di minaccia e la checklist di hardening","Prima di scrivere codice, stabilisci **cosa** stai proteggendo e **da chi**. L'obfuscation e il pinning non rendono l'app inviolabile: alzano il costo dell'attacco e bloccano gli attacchi opportunistici.\n\n**Cosa NON risolvono:**\n- Un segreto hardcoded resta estraibile: `--dart-define` finisce comunque nello snapshot. Le chiavi che danno accesso a risorse critiche vanno tenute **server-side**.\n- Il pinning non protegge da un attaccante che controlla il device e può patchare il binario (per quello servono soluzioni RASP).\n\n**Checklist minima per una release:**\n\n| Area | Azione |\n|---|---|\n| Binario | `--obfuscate --split-debug-info`, simboli archiviati fuori dal repo |\n| Android | `minifyEnabled`\u002F`shrinkResources`, `debuggable false`, no `usesCleartextTraffic` |\n| iOS | Bitcode\u002Fstrip simboli, `NSAllowsArbitraryLoads` assente |\n| Rete | Solo HTTPS + TLS 1.2\u002F1.3, certificate pinning con pin di backup |\n| Dati | Token in `flutter_secure_storage`, niente PII nei log |\n| Log | `debugPrint` no-op in release, nessun dump di header `Authorization` |\n\nCrea un file `SECURITY.md` nel repo con le decisioni prese e i responsabili della rotazione dei pin: sarà il documento che l'auditor ti chiederà.\n\nUn accorgimento immediato è azzerare i log in release, così da non lasciare tracce di endpoint e payload nel logcat.","\u002F\u002F lib\u002Fcore\u002Fbootstrap.dart\nimport 'package:flutter\u002Ffoundation.dart';\n\nvoid configureLogging() {\n  if (kReleaseMode) {\n    \u002F\u002F Disattiva completamente debugPrint nelle build di produzione.\n    debugPrint = (String? message, {int? wrapWidth}) {};\n  }\n}\n\n\u002F\u002F main.dart\n\u002F\u002F void main() {\n\u002F\u002F   configureLogging();\n\u002F\u002F   runApp(const MyApp());\n\u002F\u002F }","dart","Hai una checklist scritta e, in release, nessun output di debugPrint visibile in logcat o nei log di sistema iOS.",null,{"id":44,"position":45,"title":46,"body":47,"code_snippet":48,"code_language":49,"expected_result":50,"demo_url":42,"video_url":42},620,2,"Compilare con obfuscation e gestire i file di simboli","Flutter offusca i nomi di classi, funzioni e librerie dell'AOT snapshot Dart tramite il flag `--obfuscate`, che **deve** essere accompagnato da `--split-debug-info`: senza quest'ultimo il comando fallisce, perché i simboli originali vanno estratti in un file separato.\n\nPunti chiave:\n- I simboli sono **per architettura e per build**: `app.android-arm64.symbols`, `app.ios-arm64.symbols`, ecc.\n- Vanno archiviati come artefatto di CI (o su uno storage privato) insieme al **numero di build**: senza di essi gli stack trace di Crashlytics\u002FSentry sono illeggibili.\n- **Mai** committarli nel repository o distribuirli con l'app.\n- L'obfuscation rompe il codice che si basa sui nomi a runtime: `MyType.toString()`, `runtimeType.toString()`, switch su nomi di enum ottenuti via reflection-like. Verifica serializzatori e router che usano `Type` come chiave.\n\nDopo il crash, usa `flutter symbolize` per risalire allo stack trace originale.","# Android (App Bundle)\nflutter build appbundle --release \\\n  --obfuscate \\\n  --split-debug-info=build\u002Fsymbols\u002Fandroid\u002F1.4.2+142 \\\n  --dart-define=ENV=prod\n\n# iOS\nflutter build ipa --release \\\n  --obfuscate \\\n  --split-debug-info=build\u002Fsymbols\u002Fios\u002F1.4.2+142\n\n# Verifica che i simboli siano stati generati\nls -la build\u002Fsymbols\u002Fandroid\u002F1.4.2+142\n# app.android-arm64.symbols  app.android-arm.symbols  app.android-x64.symbols\n\n# Decodifica di uno stack trace offuscato salvato in crash.txt\nflutter symbolize \\\n  -i crash.txt \\\n  -d build\u002Fsymbols\u002Fandroid\u002F1.4.2+142\u002Fapp.android-arm64.symbols","bash","Le build release contengono simboli Dart offuscati; i file .symbols sono archiviati per versione e `flutter symbolize` restituisce stack trace leggibili.",{"id":52,"position":28,"title":53,"body":54,"code_snippet":55,"code_language":56,"expected_result":57,"demo_url":42,"video_url":42},621,"Hardening del layer nativo: R8\u002FProGuard su Android e strip su iOS","L'obfuscation di Dart non tocca il codice Java\u002FKotlin dei plugin né il wrapper dell'app. Su Android abilita **R8** con shrinking delle risorse e regole di keep mirate; ricorda che l'engine Flutter e i plugin che usano reflection (Firebase, Gson, ecc.) richiedono regole esplicite, altrimenti otterrai `ClassNotFoundException` solo in release.\n\nAggiungi in `android\u002Fapp\u002Fbuild.gradle` (o `.kts`) la configurazione del buildType `release` e crea `android\u002Fapp\u002Fproguard-rules.pro`.\n\nSu iOS, in Xcode verifica nel target Runner:\n- **Strip Debug Symbols During Copy** = Yes (Release)\n- **Deployment Postprocessing** = Yes\n- `dSYM` caricati sul crash reporter, mai nell'IPA distribuito\n\nInfine, su Android, blocca il traffico in chiaro a livello di piattaforma: nel `AndroidManifest.xml` di release imposta `android:usesCleartextTraffic=\"false\"` e punta a una `network_security_config` dedicata (la riprendiamo nello step 6).","\u002F\u002F android\u002Fapp\u002Fbuild.gradle\nandroid {\n    buildTypes {\n        release {\n            signingConfig signingConfigs.release\n            minifyEnabled true\n            shrinkResources true\n            debuggable false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),\n                          'proguard-rules.pro'\n        }\n    }\n}\n\n\u002F\u002F android\u002Fapp\u002Fproguard-rules.pro\n\u002F\u002F -keep class io.flutter.app.** { *; }\n\u002F\u002F -keep class io.flutter.plugin.**  { *; }\n\u002F\u002F -keep class io.flutter.embedding.** { *; }\n\u002F\u002F -dontwarn io.flutter.embedding.**\n\u002F\u002F # rimuove le chiamate a Log.* dal bytecode\n\u002F\u002F -assumenosideeffects class android.util.Log {\n\u002F\u002F     public static *** d(...);\n\u002F\u002F     public static *** v(...);\n\u002F\u002F     public static *** i(...);\n\u002F\u002F }","groovy","L'APK\u002FAAB di release è minificato e non debuggabile; l'app si avvia correttamente e i plugin nativi continuano a funzionare (da verificare sempre su device reale, non solo in debug).",{"id":59,"position":60,"title":61,"body":62,"code_snippet":63,"code_language":49,"expected_result":64,"demo_url":42,"video_url":42},622,4,"Estrarre i pin SPKI SHA-256 dal certificato del server","Il pinning corretto non si basa sull'hash dell'intero certificato (che cambia a ogni rinnovo) ma sull'hash della **SubjectPublicKeyInfo**: se il server rinnova il certificato mantenendo la stessa chiave (o la stessa CSR), il pin resta valido.\n\nStrategia consigliata:\n1. **Pin primario**: SPKI del certificato leaf attualmente in uso.\n2. **Pin di backup**: SPKI di una chiave di riserva già generata, oppure dell'**intermediate CA** del provider. Senza backup, un rinnovo con chiave nuova manda offline tutte le app installate.\n3. Documenta la data di scadenza e imposta un reminder: i pin vanno ruotati con una release **prima** della scadenza.\n\nEstrai i valori con OpenSSL. Lo stesso comando applicato alla catena completa ti dà anche il pin dell'intermediate.","# Pin SPKI del certificato leaf\nopenssl s_client -servername api.example.com -connect api.example.com:443 \u003C \u002Fdev\u002Fnull 2>\u002Fdev\u002Fnull \\\n  | openssl x509 -pubkey -noout \\\n  | openssl pkey -pubin -outform der \\\n  | openssl dgst -sha256 -binary \\\n  | openssl enc -base64\n# => YLh1dUR9y6Kja30RrAn7JKnbQG\u002FuEtLMkBgFF2Fuihg=\n\n# Scarica l'intera catena per estrarre anche l'intermediate (pin di backup)\nopenssl s_client -showcerts -servername api.example.com \\\n  -connect api.example.com:443 \u003C \u002Fdev\u002Fnull 2>\u002Fdev\u002Fnull > chain.pem\n\n# Controlla la scadenza del leaf\nopenssl x509 -in chain.pem -noout -enddate","Hai almeno due stringhe base64 (pin primario e pin di backup) e la data di scadenza del certificato annotata nel SECURITY.md.",{"id":66,"position":67,"title":68,"body":69,"code_snippet":70,"code_language":40,"expected_result":71,"demo_url":42,"video_url":42},623,5,"Implementare il pinning SPKI con Dio (senza cadere nella trappola di badCertificateCallback)","**L'errore più diffuso**: implementare il pinning dentro `HttpClient.badCertificateCallback`. Quel callback viene invocato **solo quando la validazione standard fallisce**. Se l'attaccante ha installato la propria CA nello store di sistema (o l'app usa una CA aziendale), il certificato è \"valido\", il callback non viene mai chiamato e il pinning non entra in gioco.\n\nLa soluzione con Dio 5 è `IOHttpClientAdapter.validateCertificate`, invocato su **ogni** connessione dopo l'handshake. Implementiamo:\n\n- parsing ASN.1 del DER del certificato per estrarre la SPKI (`asn1lib`),\n- calcolo SHA-256 + base64 (`crypto`),\n- confronto con un set di pin per host, con **deny-by-default** per gli host non configurati.\n\nDipendenze: `dio`, `asn1lib`, `crypto`.\n\n> Nota architetturale: il pinning va applicato a **tutti** i client che escono dall'app. `Image.network`, `cached_network_image` e le WebView usano client HTTP diversi dal tuo `Dio`: se scaricano risorse sensibili vanno pinnati separatamente (per `Image.network` puoi impostare un `HttpOverrides` globale).","\u002F\u002F lib\u002Fcore\u002Fnetwork\u002Fspki_pinner.dart\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:asn1lib\u002Fasn1lib.dart';\nimport 'package:crypto\u002Fcrypto.dart';\nimport 'package:dio\u002Fdio.dart';\nimport 'package:dio\u002Fio.dart';\n\nclass SpkiPinner {\n  const SpkiPinner(this.pinsByHost);\n\n  \u002F\u002F\u002F host -> insieme di digest SHA-256(SPKI) in base64.\n  final Map\u003CString, Set\u003CString>> pinsByHost;\n\n  bool validate(X509Certificate? cert, String host, int port) {\n    if (cert == null) return false;\n    final pins = pinsByHost[host];\n    if (pins == null || pins.isEmpty) return false; \u002F\u002F deny-by-default\n    final fingerprint = spkiSha256Base64(cert.der);\n    return fingerprint != null && pins.contains(fingerprint);\n  }\n\n  \u002F\u002F\u002F Estrae la SubjectPublicKeyInfo dal DER X.509 e ne calcola SHA-256\u002Fbase64.\n  static String? spkiSha256Base64(Uint8List der) {\n    try {\n      final certificate = ASN1Parser(der).nextObject() as ASN1Sequence;\n      final tbs = certificate.elements.first as ASN1Sequence;\n      \u002F\u002F Il campo version [0] EXPLICIT e' opzionale: sposta gli indici.\n      final hasVersion = tbs.elements.first.tag == 0xA0;\n      final spki = tbs.elements[hasVersion ? 6 : 5];\n      return base64.encode(sha256.convert(spki.encodedBytes).bytes);\n    } catch (_) {\n      return null; \u002F\u002F parsing fallito => certificato rifiutato\n    }\n  }\n}\n\n\u002F\u002F lib\u002Fcore\u002Fnetwork\u002Fdio_factory.dart\nDio buildPinnedDio({required bool enablePinning}) {\n  const pinner = SpkiPinner({\n    'api.example.com': {\n      'YLh1dUR9y6Kja30RrAn7JKnbQG\u002FuEtLMkBgFF2Fuihg=', \u002F\u002F primario (leaf)\n      'Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=', \u002F\u002F backup (intermediate)\n    },\n  });\n\n  final dio = Dio(BaseOptions(\n    baseUrl: 'https:\u002F\u002Fapi.example.com',\n    connectTimeout: const Duration(seconds: 10),\n  ));\n\n  dio.httpClientAdapter = IOHttpClientAdapter(\n    createHttpClient: () {\n      final client = HttpClient(context: SecurityContext(withTrustedRoots: true));\n      \u002F\u002F Nessuna deroga: i certificati non validi restano rifiutati.\n      client.badCertificateCallback = (cert, host, port) => false;\n      return client;\n    },\n    \u002F\u002F Chiamato SEMPRE, anche quando la catena e' considerata valida.\n    validateCertificate: enablePinning ? pinner.validate : null,\n  );\n\n  return dio;\n}","Le chiamate verso api.example.com funzionano normalmente; se il certificato presentato non corrisponde a nessun pin, Dio lancia una DioException di tipo connectionError\u002FbadCertificate e la richiesta non parte.",{"id":73,"position":74,"title":75,"body":76,"code_snippet":77,"code_language":78,"expected_result":79,"demo_url":42,"video_url":42},624,6,"Rotazione dei pin, kill-switch e pinning a livello di piattaforma","Un pinning mal gestito è un **incidente di produzione**: alla scadenza del certificato tutte le installazioni smettono di funzionare e l'unica via d'uscita è pubblicare una release sugli store (con tempi di review e adozione non controllabili).\n\nStrategie da adottare **insieme**:\n\n1. **Sempre almeno due pin** (chiave corrente + chiave di backup già emessa).\n2. **Configurazione per ambiente**: pinning attivo solo in `prod`; in `dev`\u002F`staging` disattivato per poter ispezionare il traffico con Charles\u002Fmitmproxy.\n3. **Scadenza dei pin lato app**: se la build è più vecchia di N mesi, mostra un blocco \"aggiorna l'app\" invece di fallire silenziosamente.\n4. **Pinning nativo su Android** tramite `network_security_config.xml`: copre anche le richieste dei plugin nativi e supporta `expiration`, dopo la quale Android ignora i pin (fail-open controllato) evitando il brick dell'app.\n\nSotto trovi la configurazione Android e il wiring per ambiente lato Dart.","\u003C!-- android\u002Fapp\u002Fsrc\u002Fmain\u002Fres\u002Fxml\u002Fnetwork_security_config.xml -->\n\u003C?xml version=\"1.0\" encoding=\"utf-8\"?>\n\u003Cnetwork-security-config>\n    \u003Cbase-config cleartextTrafficPermitted=\"false\">\n        \u003Ctrust-anchors>\n            \u003Ccertificates src=\"system\" \u002F>\n        \u003C\u002Ftrust-anchors>\n    \u003C\u002Fbase-config>\n\n    \u003Cdomain-config cleartextTrafficPermitted=\"false\">\n        \u003Cdomain includeSubdomains=\"true\">api.example.com\u003C\u002Fdomain>\n        \u003C!-- Dopo expiration i pin vengono ignorati: evita il brick delle vecchie build -->\n        \u003Cpin-set expiration=\"2026-06-30\">\n            \u003Cpin digest=\"SHA-256\">YLh1dUR9y6Kja30RrAn7JKnbQG\u002FuEtLMkBgFF2Fuihg=\u003C\u002Fpin>\n            \u003Cpin digest=\"SHA-256\">Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=\u003C\u002Fpin>\n        \u003C\u002Fpin-set>\n    \u003C\u002Fdomain-config>\n\u003C\u002Fnetwork-security-config>\n\n\u003C!-- AndroidManifest.xml (application) -->\n\u003C!-- android:networkSecurityConfig=\"@xml\u002Fnetwork_security_config\" -->\n\u003C!-- android:usesCleartextTraffic=\"false\" -->","xml","Su Android il pinning è attivo anche per il traffico dei plugin nativi, con una data di scadenza esplicita; in dev\u002Fstaging il traffico resta ispezionabile tramite proxy.",{"id":81,"position":82,"title":83,"body":84,"code_snippet":85,"code_language":40,"expected_result":86,"demo_url":42,"video_url":42},625,7,"Verificare che tutto funzioni: test automatici e attacco MITM simulato","L'ultimo passo, quello che quasi tutti saltano: **dimostrare** che il pinning blocca davvero un MITM e che l'obfuscation è attiva.\n\n**A) Test unitario sul calcolo del pin.** Salva un certificato reale come fixture PEM\u002FDER in `test\u002Ffixtures\u002F` e verifica che `SpkiSha256Base64` produca esattamente il valore restituito da OpenSSL: se una futura versione di `asn1lib` cambia comportamento, il test lo intercetta.\n\n**B) Test d'attacco con mitmproxy.**\n1. Avvia `mitmproxy` in modalità regular sulla tua macchina.\n2. Configura il proxy sul device\u002Femulatore e **installa la CA di mitmproxy nello store di sistema** (su Android 7+ serve un emulatore con root o una `network_security_config` di debug).\n3. Esegui la build **di produzione** dell'app: tutte le chiamate verso l'host pinnato devono fallire, mentre su una build `dev` (pinning disattivato) devono passare e comparire in mitmproxy.\n\nSe con la CA installata vedi il traffico in chiaro nella build di produzione, il pinning non è attivo (tipico sintomo dell'implementazione con solo `badCertificateCallback`).\n\n**C) Verifica dell'obfuscation.** Estrai le stringhe dallo snapshot AOT e controlla che i nomi delle tue classi Dart non compaiano.","\u002F\u002F test\u002Fspki_pinner_test.dart\nimport 'dart:io';\nimport 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:myapp\u002Fcore\u002Fnetwork\u002Fspki_pinner.dart';\n\nvoid main() {\n  test('calcola il pin SPKI atteso dal certificato di produzione', () {\n    final der = File('test\u002Ffixtures\u002Fapi_example_com.der').readAsBytesSync();\n    expect(\n      SpkiPinner.spkiSha256Base64(der),\n      'YLh1dUR9y6Kja30RrAn7JKnbQG\u002FuEtLMkBgFF2Fuihg=',\n    );\n  });\n\n  test('rifiuta host non configurati (deny-by-default)', () {\n    const pinner = SpkiPinner({'api.example.com': {'AAAA'}});\n    expect(pinner.validate(null, 'evil.example.com', 443), isFalse);\n  });\n}\n\n\u002F\u002F --- Verifiche da terminale ---\n\u002F\u002F mitmproxy --mode regular --listen-port 8080\n\u002F\u002F\n\u002F\u002F Obfuscation: i nomi delle classi Dart non devono comparire\n\u002F\u002F unzip -o app-release.apk -d out\u002F\n\u002F\u002F strings out\u002Flib\u002Farm64-v8a\u002Flibapp.so | grep -i \"AuthRepositoryImpl\" | head\n\u002F\u002F (output atteso: vuoto)","I test unitari passano, la build di produzione non riesce a comunicare attraverso il proxy MITM e i nomi delle classi Dart non sono più presenti in libapp.so.",[],1789295420172]