Let's Encrypt fix Flutter dopo 30 Settembre 2021

Netfarm

Cosa è Let's Encrypt e perché è importante il 30 Settembre 2021

Se sviluppate applicazioni mobile è molto difficile non la conosciate, Let's Encrypt è una certification authority che automatizza gratuitamente la creazione, la validazione, il rilascio ed il rinnovo di certificati X.509 per il protocollo TLS (fonte Wikipedia).

Il progetto Let's Encrypt nasce nel 2012, ma solo nel 2015 viene generato un suo certificato radice. Nel frattempo, per far accettare i certificati da loro firmati sui vari devices, si sono avvalsi di un certificato intermedio cross-signed grazie al supporto di IdenTrust. Il supporto è stato prolungato da un accordo con Digital Signature Trust, ma il loro certificato (DST Root CA X3) scadeva il 30 settembre 2021.

I sistemi operativi recenti non hanno problemi perché già possiedono nella lista delle loro CA fidate ISRG Root X1 (CA di Let's Encrypt), tuttavia poiché molti certificati vengono ancora cross-signed da DST Root CA X3 e alcune versioni di OpenSSL (< 1.1) soffrono di un bug, il certificato potrebbe venire rifiutato.

Quando si generano i certificati Let's Encrypt si hanno due possibilità, certificato firmato solo da ISRG Root X1 (e intermedie) o quello
cross-signed da DST Root CA X3 (sinceramente non ne ho ancora capito bene l'utilità dopo il 30 settembre).

Nel primo caso abbiamo bisogno della root CA ISRG Root X1, e l'errore restituito è:

CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate

Nel secondo caso, se si usa una versione di OpenSSL con il bug, occorre disabilitare il certificato scaduto, in tal caso l'errore restituito è

CERTIFICATE_VERIFY_FAILED: certificate has expired

Sui vecchi Android (< 7.1.1) nessuna delle due cose è possibile, quindi bisogna ricorrere a dei workaround. Vediamo come farlo in Flutter.

Flutter dio

No, non è una bestemmia, è la libreria Flutter che viene usata per fare richieste di rete.

Per fortuna la libreria ci mette a disposizione delle callback e un factory per evitare di dare fiducia a qualunque certificato ed accettare solo quelli Let's Encrypt validi.

Possiamo per esempio creare una nostra classe HttpOverrides e assegnarla a HttpOverrides.global, questo ci permette di intercettare la creazione delle istanze degli HttpClient.

HttpClient espone una callback badCertificateCallback che ci permette di gestire gli errori sui certificati.

In questa callback controlleremo se il certificato errato è effettivamente DST Root CA X3 e lo accetteremo come buono. Questa callback non verrà invocata sui device che non espongono il bug di OpenSSL poiché l'altra signature del certificato sarà valida.

Questa modifica risolve il problema dei certificati cross-signed da DST Root CA X3, se invece scegliamo di generare i certificati ISRG Root X1 senza cross-signed, ci ritroveremo ISRG Root X1 come sconosciuto (in minori versioni di Android).

In questo caso basta aggiungerlo a SecurityContext.defaultContext.

La seguente classe può essere usata nei vostri progetti, ricordatevi soltanto (anche nel main), di assegnarla a HttpOverrides.global:

import 'LEHttpOverrides.dart'; ... if (Platform.isAndroid) HttpOverrides.global = LEHttpOverrides(allowExpiredDSTX3: true);

Il workaround viene effettuato solo su Android perché su iOS molte di queste callback non sono implementate e ormai la maggior parte dei devices, anche vecchi, sono aggiornati alla versione 12 di iOS che non presenta il problema.

import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

// expired
const String kSha1DstX3 = 'DAC9024F54D8F6DF94935FB1732638CA6AD77C13';

// ISRG Root X1
const String kIsrgRootX1 = '''-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----''';

String _hexconverter(Uint8List bytes) {
  final StringBuffer buffer = StringBuffer();
  for (int part in bytes) {
    buffer.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
  }
  return buffer.toString().toUpperCase();
}

class LEHttpOverrides extends HttpOverrides {
  static bool? _initRootCert;
  final bool allowExpiredDSTX3;

  static bool _addRootCert() {
    if (Platform.isAndroid) {
      final List<int> _cert = ascii.encode(kIsrgRootX1);
      SecurityContext.defaultContext.setTrustedCertificatesBytes(_cert);
    }
    return true;
  }

  LEHttpOverrides({this.allowExpiredDSTX3 = false}) {
    _initRootCert ??= _addRootCert();
  }

  @override
  HttpClient createHttpClient(SecurityContext? context) {
    final HttpClient client = super.createHttpClient(context);
    if (allowExpiredDSTX3) {
      client.badCertificateCallback =
          (X509Certificate cert, String host, int port) =>
              _hexconverter(cert.sha1) == kSha1DstX3;
    }
    return client;
  }
}
String _hexconverter(Uint8List bytes) {
  final StringBuffer buffer = new StringBuffer();
  for (int part in bytes)
    buffer.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
  return buffer.toString().toUpperCase();
}

class LEHttpOverrides extends HttpOverrides {
  static bool? _initRootCert;

  static bool _addRootCert() {
    if (Platform.isAndroid) {
      final List<int> _cert = ascii.encode(_k_isrg_root_x1);
      SecurityContext.defaultContext.setTrustedCertificatesBytes(_cert);
    }
return true; } @override HttpClient createHttpClient(SecurityContext? context) { final HttpClient client = super.createHttpClient(context); client.badCertificateCallback = (X509Certificate cert, String host, int port) => _hexconverter(cert.sha1) == _k_sha1_dst_x3; return client; } }

Repository

In alternativa potete includere il nostro repository: https://pub.dev/packages/lehttp_overrides

WebView

Per quanto riguarda i WebView siete un po' più sfortunati, il plugin webview_flutter non mette a disposizione nessuna callback. Ci sono delle alternative ma abilitano tutti i certificati indistintamente, la cosa non garantisce una grande sicurezza.