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.