I have a Flutter app that communicates with a server using gRPC. The server is using a self-signed certificate for TLS. I have added the certificate to my Flutter app, and this works on Android. However on iOS I get CERTIFICATE_VERIFY_FAILED error. Does iOS just not allow self-signed certificates?

I am setting up my gRPC client as follows:

    var cert = await rootBundle.load('assets/cert.crt');
    var creds = ChannelCredentials.secure(
        certificates: cert.buffer.asUint8List().toList()
    );
    var channel = ClientChannel(
        host,
        port: port,
        options: new ChannelOptions(credentials: creds));
    return GrpcClient(channel);


Solution 1: AndiDog

There doesn't seem to be an obvious solution on iOS for adding a trusted, self-signed root CA. Since production will likely have a publically trusted CA, you can work around by disabling TLS verification for development only.

Here's the relevant snippet of my full example repo:

Future<ClientChannel> makeChannel() async {
  final caCert = await rootBundle.loadString('assets/pki/ca/ca.crt');

  return ClientChannel(
    'localhost',
    port: 13100,
    options: ChannelOptions(
      credentials: ChannelCredentials.secure(
        certificates: utf8.encode(caCert),

        // --- WORKAROUND FOR SELF-SIGNED DEVELOPMENT CA ---
        onBadCertificate: (certificate, host) => host == 'localhost:13100',
      ),
    ),
  );
}

In this case, my server is listening on localhost:13100.


Solution 2: driedler

The following was adapted from: https://github.com/grpc/grpc-dart/issues/134

It allows for specifying a custom (or self-signed) CA cert, client certificates, and/or a custom domain:

import 'dart:convert';
import 'dart:io';
import 'package:grpc/grpc.dart';

class CustomChannelCredentials extends ChannelCredentials {
  final String caCert;
  final String? clientCert;
  final String? clientKey;

  CustomChannelCredentials({
    required this.caCert,
    this.clientCert,
    this.clientKey,
    String? authority, // Custom domain used by server cert
  }) : super.secure(
     authority: authority,
     onBadCertificate: (cert, host) {
        // This is a work-around for iOS, it seems self-signed certs are not being properly verified;
        return host == '<the common name used self-signed CA>';
      },      
   );

  @override
  SecurityContext get securityContext {
    final context = SecurityContext(
      withTrustedRoots: false, // We want to specify a custom CA cert
    );
    context.setTrustedCertificatesBytes(utf8.encode(caCert));
    context.setAlpnProtocols(supportedAlpnProtocols, false);

    if (clientCert != null) {
      context.useCertificateChainBytes(utf8.encode(clientCert!));
    }
    if (clientKey != null) {
      context.usePrivateKeyBytes(utf8.encode(clientKey!));
    }
    return context;
  }
}

Example usage:

final channel = ClientChannel(
 serverAddress,
  port: serverPort,
  options: ChannelOptions(
    credentials: CustomChannelCredentials(
      caCert: selfSignedCaCertPem,
      // clientCert: clientCertPem,
      // clientKey: clientKeyPem,
      authority: 'localhost',
    ),
  ),
);