Flutter TlsException: Failure trusting builtin roots

Issue

I’m trying to perform SSL certificate pinning in a Flutter app using HttpClient. I have previously successfully performed pinning in a native Android app. This is the error message I receive:

E/flutter (28810): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: Exception: TlsException: Failure trusting builtin roots (OS Error: 
E/flutter (28810):      BAD_PKCS12_DATA(pkcs8_x509.c:645), errno  0)
E/flutter (28810): #0      _SecurityContext.setTrustedCertificatesBytes (dart:io-patch/secure_socket_patch.dart:233:59)
E/flutter (28810): #1      ApiProvider._clientWithPinnedCertificate (package:app/api/api.provider.dart:68:13)
E/flutter (28810): #2      ApiProvider.makePostRequest (package:app/api/api.provider.dart:45:11)
E/flutter (28810): #3      _fireAuthenticationRequestIsolate (package:app/api/repositories/auth.repository.dart:14:41)
E/flutter (28810): #4      _IsolateConfiguration.apply (package:flutter/src/foundation/_isolates_io.dart:84:34)
E/flutter (28810): #5      _spawn.<anonymous closure> (package:flutter/src/foundation/_isolates_io.dart:91:65)
E/flutter (28810): #6      _spawn.<anonymous closure> (package:flutter/src/foundation/_isolates_io.dart:90:5)
E/flutter (28810): #7      Timeline.timeSync (dart:developer/timeline.dart:163:22)
E/flutter (28810): #8      _spawn (package:flutter/src/foundation/_isolates_io.dart:88:35)
E/flutter (28810): #9      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:286:17)
E/flutter (28810): #10     _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

Upon downloading the .cer file, I’ve converted it into .pem using the following command:

openssl x509 -inform pem -in test.ca -outform der -out test.pem

I load the certificate from the assets folder like this:

const certificatePath  'assets/test.pem';
final certificateBytes  await rootBundle.load(certificatePath);
final certificate  certificateBytes.buffer.asUint8List();

This is a code of an API provider where HTTP POST request is fired

class ApiProvider {
  final String _baseURL;

  ApiProvider(String baseURL) : _baseURL  baseURL;

  Future<String> makePostRequest(
    String endpoint,
    Map<String, dynamic> body, {
    Map<String, String>? headers,
    bool setTrustedCertificate  false,
  }) async {
    final client  setTrustedCertificate
        ? _clientWithPinnedCertificate()
        : HttpClient();

    final url  Uri.https(_baseURL, endpoint);
    final response  await client.postUrl(url)
      ..headers.addAll(headers ?? <String, String>{})
      ..write(json.encode(body));
    final request  await response.close();

    if (request.statusCode < 200 || request.statusCode > 300) {
      throw ApiException(
        request.statusCode,
        request.reasonPhrase,
      );
    }
    final responseBody  await request
        .transform(const Utf8Decoder(allowMalformed: true))
        .reduce((previous, element) > previous + element);
    return responseBody;
  }

  HttpClient _clientWithPinnedCertificate() {
    final context  SecurityContext();
    context.setTrustedCertificatesBytes(
      GlobalConstants.certificate,
      password: certificatePassword,
    );
    final client  HttpClient(context: context);

    client.badCertificateCallback  (
      X509Certificate certificate,
      String host,
      int port,
    ) {
      print('Bad certificate: ${certificate.sha1} for host $host:$port');
      return false;
    };

    return client;
  }
}

Solution

After a long time playing around with various configurations, I’ve managed to find a solution, and it goes a bit deeper than I thought.

First, make sure you’re not calling rootBundle.load() on a separate isolate. I’ve been doing that, and this threw ambiguous errors.

Second, if you have a .p12 file, you don’t need the .ca (or a conversion to .pem). All you have to do is use the following code snippet

// Here, certificate is a Uint8List
final context  SecurityContext.defaultContext
    ..useCertificateChainBytes(
        certificate,
        password: /* input certificate passphrase */
    ),
    ..usePrivateKeyBytes(
        certificate,
        password: /* input certificate passphrase */
    );

final client  HttpClient(context: context);
client.badCertificateCallback  (
    X509Certificate cert,
    String host,
    int port,
) {
    // Handle certificate that can't be authenticated
    // Returning 'true' by itself is not really safe...
    return true;
};

Make sure you provide the password in both cases.

Answered By – Peter Aleksander Bizjak

Leave a Comment