简体   繁体   English

在 2021 年 9 月 30 日之后使用 LetsEncrypt SSL 证书在 Android 7 CERTIFICATE_VERIFY_FAILED 上颤动

[英]Flutter on Android 7 CERTIFICATE_VERIFY_FAILED with LetsEncrypt SSL cert after Sept 30, 2021

After Sept 30, 2021, https get/post requests to a website using a Let's Encrypt SSL ceritificate on an old Android 7 device were failing with this error: 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))

This error doesn't occur on newer Android nor Apple devices.在较新的 Android 或 Apple 设备上不会发生此错误。

Why did this error suddently start on old Android phones?为什么这个错误会在旧的 Android 手机上突然启动?

How can I resolve this?我该如何解决这个问题?

Solution解决方案

In Flutter, to once again make SSL https connections on older devices to Let's Encrypt SSL protected websites, we can supply Let's Encrypt's trusted certificate via SecurityContext to dart:io HttpClient object (from the dart native communications library), which we can use directly to make https get/post calls, or we can supply that customized HttpClient to Flutter/Dart package:http IOClient if we are using that popular pub.dev package .在 Flutter 中,为了再次在旧设备上建立 SSL https 连接到 Let's Encrypt SSL 保护的网站,我们可以通过SecurityContext将 Let's Encrypt 的可信证书提供给dart:io HttpClient对象(来自 dart 原生通信库),我们可以直接使用它进行 https get/post 调用,或者我们可以提供定制的HttpClient到 Flutter/Dart package:http IOClient如果我们使用流行的 pub.dev 包

Example例子

Here's a Flutter unit test which creates a dart:io HttpClient with a SecurityContext that has a Let's Encrypt root certificate supplied to it.这是一个 Flutter 单元测试,它创建了一个dart:io HttpClient和一个SecurityContext ,它提供了一个 Let's Encrypt 根证书。 Then, this HttpClient is provided to package:http IOClient which implement's the Client interface and can be used for all the usual get , post etc. calls.然后,将这个HttpClient提供给package:http IOClient ,它实现了Client接口,可用于所有常见的getpost等调用。

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;
      }
    } finally {}

    HttpClient httpClient = new HttpClient(context: context);

    return httpClient;
  }

  /// 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-----""";

Since this unit test is run on a desktop/laptop computer which has the ISRG Root X1 certificate, it's probably not very interesting/useful.由于此单元测试在具有ISRG Root X1 证书的台式机/笔记本电脑上运行,因此它可能不是很有趣/有用。 Systems which get updates will have this Certificate Authority (CA) certificate installed and "should" have no problems verifying the "chain of trust" for Let's Encrypt SSL certs.获得更新的系统将安装此证书颁发机构 (CA) 证书,并且“应该”验证 Let's Encrypt SSL 证书的“信任链”没有问题。

But on old devices which don't have the ISRG Root X1 certificate and never will, using the two functions above customHttpClient() and createLEClient() we can make https/TLS connections to Let's Encrypt SSL protected Internet resources when LE's CA cert (ISRG Root X1) is missing.但是在没有ISRG Root X1 证书并且永远不会的旧设备上,使用customHttpClient()createLEClient()上面的两个函数,我们可以建立 https/TLS 连接,当 LE 的 CA 证书(ISRG根 X1) 丢失。

Why this happened为什么会这样

Let's Encrypt SSL certificates are created/issued with a cross-sign from Digital Signature Trust (DST), an older, well-established Certificate Authority (CA). Let's Encrypt SSL 证书是通过数字签名信任 (DST) 的交叉签名创建/颁发的,这是一个较旧的、完善的证书颁发机构 (CA)。

Being cross-signed by a widely trusted CA meant Let's Ecrypt's (LE) SSL certs were accepted as legitimate by pretty much every application & device, from day 1 (roughly 5 years ago).由广泛信任的 CA 进行交叉签名意味着从第一天(大约 5 年前)开始,几乎所有应用程序和设备都接受 Let's Ecrypt (LE) SSL 证书为合法证书。

The certificate by DST used to cross-sign LE certs, expired on Sept. 30, 2021. This meant the "chain of trust" for LE certs is no longer accepted by some older devices. DST 用于对 LE 证书进行交叉签名的证书已于 2021 年 9 月 30 日到期。这意味着某些旧设备不再接受 LE 证书的“信任链”。

There are several solutions to resolving this issue and this is just one way which doesn't require intervention by the end-user.有多种解决方案可以解决此问题,这只是一种不需要最终用户干预的方法。

More Info更多信息

Background on expiration of DST root cert from LE LE 的 DST 根证书到期的背景

More background on DST expiration & cert chaining help from LE 来自 LE 的有关 DST 到期和证书链帮助的更多背景信息

Let's Encrypt has an ongoing mega-thread for the issues caused by the DST root cert expiration here Let's Encrypt 为 DST 根证书过期引起的问题提供了一个持续的超级线程

Thank you @esty92,谢谢@esty92,

Finally I even got a shorter solution:最后我什至得到了一个更短的解决方案:

Put the root certificate value in assets/isrg_x1.pem:将根证书值放在 assets/isrg_x1.pem 中:

    -----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-----

Then simply add this in your dio setup:然后只需将其添加到您的 dio 设置中:

dio = new Dio();
ByteData bytes = await rootBundle.load('assets/isrg_x1.pem');
(httpClient.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
  SecurityContext sc = new SecurityContext();
  sc.setTrustedCertificatesBytes(bytes.buffer.asUint8List());
  HttpClient httpClient = new HttpClient(context: sc);
  return httpClient;
};

But be careful, it doesn't apply to NetworkImage, etc... you need to add the certificate at the flutter app init.但是要小心,它不适用于 NetworkImage 等……您需要在 flutter app init 中添加证书。

To trust the new ISRG certificate (see the answer of @Baker and @Westy92) globally, define a class which extends HttpOverrides要在全局范围内信任新的 ISRG 证书(请参阅@Baker 和 @Westy92 的答案),请定义一个扩展HttpOverrides的类

class CertFix extends HttpOverrides {
  @override
  HttpClient createHttpClient(SecurityContext? context) {
    context ??= SecurityContext();
    context.setTrustedCertificatesBytes(ascii.encode(ISRG_X1));
    return super.createHttpClient(context);
  }
}

and add this at the beginning of main()并将其添加到main()的开头

HttpOverrides.global = CertFix();

This would make the certificate trusted in all HttpClient instances including NetworkImage , CachedNetworkImage and grpc .这将使证书在所有HttpClient实例中都受到信任,包括NetworkImageCachedNetworkImagegrpc

add this class to your main.dart将此类添加到您的 main.dart

class MyHttpOverrides extends HttpOverrides {
  @override
  HttpClient createHttpClient(SecurityContext context) {
    return super.createHttpClient(context)
      ..badCertificateCallback =
          ((X509Certificate cert, String host, int port) {
        if (host == 'your host') {
          return true;
        } else {
          return false;
        }
      });
  }
}

and

void main() {
  HttpOverrides.global = new MyHttpOverrides();
  runApp(Application());
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM