3

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

Niki Yoshiuchi
  • 16,883
  • 1
  • 35
  • 44

2 Answers2

9

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.

AndiDog
  • 68,631
  • 21
  • 159
  • 205
  • Thanks, this is essentially what I have ended up doing. – Niki Yoshiuchi Apr 03 '20 at 21:00
  • hey @AndiDog, my app behaves different depending on if i add `onBadCertificate` or not...without it, i get: `CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate` and with it, i get: `SSLV3_ALERT_BAD_CERTIFICATE`...both for the same `.perm` file....any clue what can be wrong? – codeKiller May 04 '20 at 10:57
  • Debug the alert in Wireshark or in the logs? Since you see a difference, I assume the desired effect actually works, but you possibly still got a server-side alert for some other reason (e.g. bad client certificate). – AndiDog May 07 '20 at 08:40
  • @JamesTan No. But which Flutter platform runs node? Are you cross-compiling server-side Dart code to node.js? Then maybe https://stackoverflow.com/questions/10888610/ignore-invalid-self-signed-ssl-certificate-in-node-js-with-https-request helps. – AndiDog Jun 02 '20 at 07:57
  • sorry i was using nodejs as client side, not flutter. havent try that reference, but i got this working: https://stackoverflow.com/questions/62108009/grpc-node-client-allow-signed-certificate – James Tan Jun 10 '20 at 18:26
0

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',
    ),
  ),
);
driedler
  • 3,750
  • 33
  • 26