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