Flutter self-signed certificate

Preface
The server in the Flutter project uses a self-signed certificate. If you directly use https request or wss request, a certificate signing error will be reported.

HandshakeException: Handshake error in client (OS Error:
I/flutter (28959): │ CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate(handshake.cc:359))

or

CERTIFICATE_VERIFY_FAILED: self signed certificate in certificate chain(handshake.cc:354))
Trust self-signed certificates
1. Ignore certificate verification
You can ignore certificate verification by following these steps. thereby accepting self-signed certificates

First create a class to override HttpOverrides

class MyHttpOverrides extends HttpOverrides {<!-- -->
  @override
  HttpClient createHttpClient(SecurityContext context) {<!-- -->
    return super.createHttpClient(context)
          ..badCertificateCallback = (X509Certificate cert, String host, int port) {<!-- -->
            //add your certificate verification logic here
            return true;
          };
  }
}

Then set MyHttpOverrides to global HttpOverrides in the main method.

HttpOverrides.global = new DevHttpOverrides();
When the system detects that there is a problem with the certificate, it will give us the opportunity to accept or reject the wrong certificate through the callback badCertificateCallback. When true is returned, it means that we accept the certificate and will continue to complete the communication request. When false is returned, it means that we do not accept it. Wrong certificate, https communication will fail.

Of course, you can also add your own certificate verification logic in badCertificateCallback. It is strongly not recommended to directly return badCertificateCallback to true in a formal environment, because this will accept all certificates, which is equivalent to the SSL encryption layer being useless and easily causing man-in-the-middle attacks.

2. Verify the server certificate using the local CA root certificate
We can place the CA certificate used by the server to generate a self-signed certificate locally, set it as a trusted certificate, and then use this certificate to verify the server certificate. code show as below:

class MyHttpOverrides extends HttpOverrides {<!-- -->
  @override
  HttpClient createHttpClient(SecurityContext context) {<!-- -->
     //Load local root certificate
      ByteData data = await rootBundle.load('assets/data/ca-cert.pem');
      final SecurityContext clientContext = SecurityContext()
     //Use this method to set the trusted root certificate
      ..setTrustedCertificatesBytes(data.buffer.asUint8List())
    return super.createHttpClient(context)
          ..badCertificateCallback = (X509Certificate cert, String host, int port) {<!-- -->
            //add your certificate verification logic here
            return false;
          };
  }
}

This approach can greatly improve security.

common problem
CERTIFICATE_VERIFY_FAILED: Hostname mismatch

By default, Flutter SSL will detect whether the CN field in the certificate subject is consistent with the host in the request URL. If they are inconsistent, this error will be reported.

If the requested URL is https://baidu.com/tanslate, then the CN field of the server’s certificate must contain Baidu.com.

Unfortunately, there is currently no corresponding API based on flutter3.0 to ignore hostname detection. What we can do is add our own verification logic to badCertificateCallback, as follows:

 ..badCertificateCallback = (X509Certificate cert, String host, int port) {
       if(host=="baidu.com")
        return true;
       else return false;
      };

2. When the local CA root certificate verifies the server certificate, when the signature of the server certificate is incorrect, it will call back to badCertificateCallback, and other certificate errors will also call back to badCertificateCallback. If you want to distinguish the causes of different certificate errors, you need to manually verify them. Certificate signature and other information.

class MyHttpOverrides extends HttpOverrides {<!-- -->
  MyHttpOverrides() {<!-- -->}
  @override
  HttpClient createHttpClient(SecurityContext? context) {<!-- -->
      ByteData data = await rootBundle.load('assets/data/ca-cert.pem');
    final SecurityContext clientContext = SecurityContext()
      ..setTrustedCertificatesBytes(caCertificate);
    return super.createHttpClient(clientContext)
      ..badCertificateCallback = (X509Certificate cert, String host, int port)
      //Verify the server certificate against the root certificate
        bool isverify = verifyCertificate(ca_cert, cert.pem);
        if (!isverify){<!-- -->
          //Signature error
          return false;
        }
        if (currentIp != host) {<!-- -->
          return false;
        } else {<!-- -->
          return true;
        }
    };
  }
}

The verifyCertificate method is as follows, using the x509 package. portal

import 'dart:convert';
import 'dart:typed_data';
import 'package:x509b/x509.dart' as x509;
import 'package:asn1lib/asn1lib.dart';

import 'flutter_log.dart';

 bool verifyCertificate(String caCert, String serverCert) {<!-- -->
  // var strX1PublicKeyInfo = "-----BEGIN CERTIFICATE-----\
SOME PUBLIC KEY\
-----END CERTIFICATE-----";
  // var strX2Certificate = "-----BEGIN CERTIFICATE-----\
SOME CERTIFICATE\
-----END CERTIFICATE-----";

  var x1PublicKey = (x509.parsePem(caCert).single as x509.X509Certificate).publicKey as x509.RsaPublicKey;
  var x2Certificate = x509.parsePem(serverCert).single as x509.X509Certificate;

  var x2CertificateDER = decodePEM(serverCert);

  var asn1Parser = ASN1Parser(x2CertificateDER);
  var seq = asn1Parser.nextObject() as ASN1Sequence;
  var tbsSequence = seq.elements[0] as ASN1Sequence;

  var signature = x509.Signature(Uint8List.fromList(x2Certificate.signatureValue!));
  var verifier = x1PublicKey.createVerifier(x509.algorithms.signing.rsa.sha256);

  return verifier.verify(tbsSequence.encodedBytes, signature);
}

Uint8List decodePEM(String pem) {<!-- -->
  var startsWith = [
    '-----BEGIN PUBLIC KEY-----',
    '-----BEGIN PRIVATE KEY-----',
    '-----BEGIN CERTIFICATE-----',
  ];
  var endsWith = [
    '-----END PUBLIC KEY-----',
    '-----END PRIVATE KEY-----',
    '-----END CERTIFICATE-----'
  ];
  pem=pem.trim();
  //HACK
  for (var s in startsWith) {<!-- -->
    if (pem.startsWith(s)) pem = pem.substring(s.length);
  }

  for (var s in endsWith) {<!-- -->
    if (pem.trim().endsWith(s)) {<!-- -->
      Log().i("certificate:substring");
      pem = pem.trim().split(s)[0];
    }
  }

  //Dart base64 decoder does not support line breaks
  pem = pem.replaceAll('\
', '');
  pem = pem.replaceAll('\r', '');
  return Uint8List.fromList(base64.decode(pem));
}

Author: Xingchenhai TT
Link: https://www.jianshu.com/p/5dfb882671a3