Make https work on gRPC in Rust: load a root certificate into the TLS config

Why is this so hard?

To send gRPC messages in Rust, one uses the tonic library. If you’re sending to an https endpoint, you’ll need to provide a ClientTlsConfig. By default, you’ll get a very distrustful one.

Here’s the error message:

the grpc server returns error (The service is currently unavailable): , detailed error message: error trying to connect: invalid peer certificate contents: invalid peer certificate: UnknownIssuer

Here’s the mitigation:

As far as I can tell, the ClientTlsConfig distrusts every certificate. It doesn’t seem to load the native certificates from the environment, like a civilized https client.

To make it trust something, give it a root certificate:

use tonic::transport::ClientTlsConfig;
use tonic::transport::Certificate;

let pem = tokio::fs::read("/etc/ssl/cert.pem").await.expect("oh no, the cert file wasn't loaded");
let cert = Certificate::from_pem(pem);

ClientTlsConfig::new().ca_certificate(cert)

I couldn’t find a better way to get a certificate than from the filesystem. I’d like to use the rustls-native-certs library for that, but no, tonic won’t let me use those certificates, and it won’t let me use a ClientConfig from rustls (except in a very old version).

Where can I find the certificate to load?

If you’re lucky, you can find a nice big root certificate installed in your OS. I found mine in /etc/ssl/cert.pem (on a Mac).

If you’re less lucky, you find a whole lot of small certificates. In my Docker container, /etc/ssl/certs/ contains 137 of them. The ClientTlsConfig only lets me load one! Seriously, tonic??

Since I only need to connect to one endpoint, I only need one root certificate, as long as it is the root certificate used by the SSL certificate at my endpoint. So, which one is that? Investigate:

openssl s_client -connect api.honeycomb.io:443 -servername localhost

Here, please replace api.honeycomb.io with the endpoint you’re going to send gRPC to.

This says CONNECTED(00000006) for a long time, and eventually prints out a lot of info about the certificate. Look for the Certificate chain section:

---
Certificate chain
 0 s:/CN=*.honeycomb.io
   i:/C=US/O=Amazon/OU=Server CA 1B/CN=Amazon
 1 s:/C=US/O=Amazon/OU=Server CA 1B/CN=Amazon
   i:/C=US/O=Amazon/CN=Amazon Root CA 1
 2 s:/C=US/O=Amazon/CN=Amazon Root CA 1
   i:/C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Services Root Certificate Authority - G2
 3 s:/C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Services Root Certificate Authority - G2
   i:/C=US/O=Starfield Technologies, Inc./OU=Starfield Class 2 Certification Authority
---

We want to load one of the certificates in this chain. Probably the last one. I looked in my filesystem:

$ ls /etc/ssl/certs/*Starfield*.pem
/etc/ssl/certs/Starfield_Class_2_CA.pem
/etc/ssl/certs/Starfield_Root_Certificate_Authority_-_G2.pem
/etc/ssl/certs/Starfield_Services_Root_Certificate_Authority_-_G2.pem

The last one looked a lot like the /CN=Starfield Services Root Certificate Authority - G2 in my certificate chain, so I used that one. It worked.

Good luck with this. I hope it gets easier in the future to use the native certs, like other languages and frameworks typically do.