2021 年 9 月 30 日后,Flutter 在 Android 7 CERTIFICATE_VERIFY_FAILED 上使用 LetsEncrypt SSL 证书

Bak*_*ker 26 android ssl-certificate flutter lets-encrypt

2021 年 9 月 30 日之后,在旧版 Android 7 设备上使用 Let's Encrypt SSL 证书向网站发出 https get/post 请求失败,并出现以下错误:

HandshakeException: Handshake error in client (OS Error: CERTIFICATE_VERIFY_FAILED: certificate has expired(handshake.cc:354))
Run Code Online (Sandbox Code Playgroud)

此错误不会发生在较新的 Android 或 Apple 设备上。

为什么旧的 Android 手机会突然出现此错误?

我该如何解决这个问题?

Bak*_*ker 30

解决方案

在 Flutter 中,为了再次在旧设备上与 Let's Encrypt SSL 保护的网站建立 SSL https 连接,我们可以通过对象(来自 dart 本机通信库)提供 Let's Encrypt 的可信证书SecurityContextdart:io HttpClient我们可以直接使用该证书来进行 https get/post调用,或者如果我们使用流行的 pub.dev 包,HttpClient我们可以提供针对 Flutter/Dart的定制。package:http IOClient

例子

这是一个 Flutter 单元测试,它创建了一个dart:io HttpClient带有SecurityContext提供给它的 Let's Encrypt 根证书的 a 。然后,它HttpClient被提供给package:http IOClient哪个实现的Client接口,并且可以用于所有通常的get调用post等。

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

import 'package:test/test.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';

void main() {
  const sslUrl = 'https://valid-isrgrootx1.letsencrypt.org/';

  /// From dart:io, create a HttpClient with a trusted certificate [cert]
  /// added to SecurityContext.
  /// Wrapped in try catch in case the certificate is already trusted by
  /// device/os, which will cause an exception to be thrown.
  HttpClient customHttpClient({String cert}) {
    SecurityContext context = SecurityContext.defaultContext;

    try {
      if (cert != null) {
        Uint8List bytes = utf8.encode(cert);
        context.setTrustedCertificatesBytes(bytes);
        print('createHttpClient() - cert added!');
      }
    } on TlsException catch (e) {
      if (e?.osError?.message != null &&
          e.osError.message.contains('CERT_ALREADY_IN_HASH_TABLE')) {
        print('createHttpClient() - cert already trusted! Skipping.');
      } else {
        print('createHttpClient().setTrustedCertificateBytes EXCEPTION: $e');
        rethrow;
      }
    }

    return new HttpClient(context: context);
  }

  /// Use package:http Client with our custom dart:io HttpClient with added
  /// LetsEncrypt trusted certificate
  http.Client createLEClient() {
    IOClient ioClient;
    ioClient = IOClient(customHttpClient(cert: ISRG_X1));
    return ioClient;
  }

  /// Example using a custom package:http Client
  /// that will work with devices missing LetsEncrypt
  /// ISRG Root X1 certificates, like old Android 7 devices.
  test('HTTP client to LetsEncrypt SSL website', () async {
    http.Client _client = createLEClient();
    http.Response _response = await _client.get(sslUrl);
    print(_response.body);
    expect(_response.statusCode, 200);
    _client.close(); // remember to close client as per https://pub.dev/packages/http
  });
}

/// This is LetsEncrypt's self-signed trusted root certificate authority
/// certificate, issued under common name: ISRG Root X1 (Internet Security
/// Research Group).  Used in handshakes to negotiate a Transport Layer Security
/// connection between endpoints.  This certificate is missing from older devices
/// that don't get OS updates such as Android 7 and older.  But, we can supply
/// this certificate manually to our HttpClient via SecurityContext so it can be
/// used when connecting to URLs protected by LetsEncrypt SSL certificates.
/// PEM format LE self-signed cert from here: https://letsencrypt.org/certificates/
const String ISRG_X1 = """-----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-----""";
Run Code Online (Sandbox Code Playgroud)

由于此单元测试是在具有ISRG Root X1 证书的台式机/笔记本电脑上运行的,因此它可能不是很有趣/有用。获得更新的系统将安装此证书颁发机构 (CA) 证书,并且“应该”在验证 Let's Encrypt SSL 证书的“信任链”时没有问题。

但在没有 ISRG 根 X1 证书且永远不会有的旧设备上,使用上述两个函数,LE的CA 证书(ISRG 根 X1)丢失时,我们可以与 Let's Encrypt SSL 保护的互联网资源建立 https/TLS 连接。customHttpClient()createLEClient()

为什么会发生这种情况

Let's Encrypt SSL 证书是通过数字签名信任 (DST)(一个较早且完善的证书颁发机构 (CA))的交叉签名创建/颁发的。

由广泛信任的 CA 进行交叉签名意味着 Let's Ecrypt 的 (LE) SSL 证书从第一天(大约 5 年前)起就被几乎所有应用程序和设备视为合法。

DST 用于交叉签署 LE 证书的证书已于 2021 年 9 月 30 日到期。这意味着 LE 证书的“信任链”不再被某些旧设备接受。

有多种解决方案可以解决此问题,这只是一种不需要最终用户干预的方法。

为什么这会影响 Android 7.1.1 之前的 Flutter

(这是我的猜测...)Dart VM(因此,Flutter)使用BoringSSL 库,这是 OpenSSL 的 Google 分支。

当找到任何匹配的信任链、无效(即过期)或其他情况时,Dart VM 中的 BoringSSL 将停止搜索有效的信任链。Google 的 Dart 团队在 6 月份遇到了这个问题(不是因为 Let's Encrypt 的 DST 交叉签名过期,而是因为类似的问题),并于 8 月 26 日为其创建了补丁。该补丁可能会随 Dart 2.15 一起推出。当该版本的 Dart 被引入 Flutter 时,我希望/猜测这个补丁能够解决这个问题。

更多信息

LE 的 DST 根证书过期的背景

有关 DST 过期和 LE 的证书链接帮助的更多背景信息

Let's Encrypt 有一个持续的大型线程,用于解决 DST 根证书过期引起的问题


tah*_*amv 27

谢谢@esty92和@3xecutor,

从https://letsencrypt.org/certs/lets-encrypt-r3.pem下载证书

将根证书值放入 asset/ca 中:

您还可以在 asset/ca 中创建一个具有此名称的文件,并复制此证书 init。

文件名“lets-encrypt-r3.pem”

-----BEGIN CERTIFICATE-----
MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw
WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP
R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx
sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm
NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg
Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG
/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC
AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB
Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA
FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw
AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw
Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB
gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W
PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl
ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz
CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm
lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4
avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2
yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O
yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids
hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+
HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv
MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX
nLRbwHOoq7hHwg==
-----END CERTIFICATE-----
Run Code Online (Sandbox Code Playgroud)

然后将 asset/ca 路径添加到您的 pubspec.yaml 中,如下所示

 flutter:
  uses-material-design: true
  assets:
     .
     .
     .
    - assets/ca/
Run Code Online (Sandbox Code Playgroud)

然后只需将其添加到您的 main.dart 中:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  ByteData data = await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem');
  SecurityContext.defaultContext.setTrustedCertificatesBytes(data.buffer.asUint8List());

  runApp(MyApp());
}
Run Code Online (Sandbox Code Playgroud)

这个方法解决了我的问题,它适用于版本低于7.1.1的android。