How to fix a TLS certificate error
Short answer: A TLS certificate error has three causes: the certificate has expired, it doesn’t cover the hostname you connected to, or its chain doesn’t root at a CA your client trusts. One openssl s_client command tells you which; the fix is on the server or in the trust store, never in turning verification off.
What the error looks like in Rust
Section titled “What the error looks like in Rust”Four runs of a program that calls reqwest::get() and prints the error chain. rustc 1.99.0, reqwest 0.13.5 with default features, macOS:
--- https://badssl.com/ ---status: 200 OK
--- https://expired.badssl.com/ ---error: error sending request for url (https://expired.badssl.com/)caused by: client error (Connect)caused by: invalid peer certificate: Other(OtherError("“*.badssl.com” certificate is expired: -67818"))
--- https://self-signed.badssl.com/ ---error: error sending request for url (https://self-signed.badssl.com/)caused by: client error (Connect)caused by: invalid peer certificate: Other(OtherError("“*.badssl.com” certificate is not trusted: -67843"))
--- https://wrong.host.badssl.com/ ---error: error sending request for url (https://wrong.host.badssl.com/)caused by: client error (Connect)caused by: invalid peer certificate: NotValidForNameAll four end in invalid peer certificate; the last line names the cause: “expired” and “not trusted” are the first and third, NotValidForName the second.
Telling the three causes apart
Section titled “Telling the three causes apart”badssl.com keeps public hosts that fail in exactly these ways, and every output below is reproducible:
for host in badssl.com expired.badssl.com self-signed.badssl.com untrusted-root.badssl.com; do echo "--- $host ---" echo | openssl s_client -connect $host:443 -servername $host 2>/dev/null \ | grep -E "subject=|issuer=|Verify return code"done--- badssl.com ---subject=/CN=*.badssl.comissuer=/C=US/O=Let's Encrypt/CN=YR1 Verify return code: 0 (ok)--- expired.badssl.com ---subject=/OU=Domain Control Validated/OU=PositiveSSL Wildcard/CN=*.badssl.comissuer=/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA Verify return code: 10 (certificate has expired)--- self-signed.badssl.com ---subject=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=*.badssl.comissuer=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=*.badssl.com Verify return code: 18 (self signed certificate)--- untrusted-root.badssl.com ---subject=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=*.badssl.comissuer=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Untrusted Root Certificate Authority Verify return code: 19 (self signed certificate in certificate chain)The last line of each: 0 (ok) on the healthy host, one distinct failure per cause. Code 10 is an expired certificate, code 18 one that signs itself, code 19 a chain ending at a root your trust store has never heard of: a private CA, or a corporate proxy’s.
One cause hides here: /usr/bin/openssl on macOS is LibreSSL, and its s_client verifies the chain but never the name (-verify_hostname is an unknown option). curl checks it:
curl -sS -o /dev/null https://wrong.host.badssl.com/curl: (60) SSL: no alternative certificate subject name matches target host name 'wrong.host.badssl.com'More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could notestablish a secure connection to it. To learn more about this situation andhow to fix it, please visit the web page mentioned above.The names a certificate does cover are in its SAN:
echo | openssl s_client -connect wrong.host.badssl.com:443 -servername wrong.host.badssl.com 2>/dev/null \ | openssl x509 -noout -text | grep -A1 "Subject Alternative Name" X509v3 Subject Alternative Name: DNS:*.badssl.com, DNS:badssl.com*.badssl.com covers one label. wrong.host.badssl.com is two, so nothing on that list matches.
The fixes
Section titled “The fixes”Expired. The only fix is on the server:
echo | openssl s_client -connect expired.badssl.com:443 -servername expired.badssl.com 2>/dev/null \ | openssl x509 -noout -enddatenotAfter=Apr 12 23:59:59 2015 GMTRenew it; no client-side setting makes an expired certificate acceptable.
Self-signed, or a private root. Trust the CA. -CAfile shows what that does to the result:
echo | openssl s_client -connect self-signed.badssl.com:443 -servername self-signed.badssl.com 2>/dev/null \ | openssl x509 -out self-signed.pemecho | openssl s_client -connect self-signed.badssl.com:443 -servername self-signed.badssl.com \ -CAfile self-signed.pem 2>/dev/null | grep "Verify return code" Verify return code: 0 (ok)The connection that returned 18 now returns 0 (ok). In Rust, tls_certs_merge() adds a root alongside the built-in ones, tls_certs_only() replaces them; add_root_certificate is the deprecated spelling in 0.13. On macOS a root can also live in the system keychain: the default features verify through Security Framework, the source of the -67818 and -67843 above. A certificate pulled off the server proves nothing, though: the root must arrive by a route you already trust.
Wrong hostname. Connect with a name the certificate covers, or get the server a certificate whose SAN lists the right names. The URL’s host is what gets checked, and what goes out as SNI:
curl -sS -o /dev/null https://badssl.com/; echo "exit=$?"exit=0Why not to turn verification off
Section titled “Why not to turn verification off”The escape hatch is tls_danger_accept_invalid_certs(true) on the client builder (danger_accept_invalid_certs before 0.13, deprecated since). Its own documentation warns that with it on, any certificate for any site is trusted, expired ones included. That is not theoretical: the path from your program to the server runs through equipment you don’t control, and a client that accepts any certificate can be handed a forged one. Two details make it worse: the flag lives on the client, so it quietly covers every request that client makes, and the hostname check is a separate switch (tls_danger_accept_invalid_hostnames), so the pair ends up in a shared helper and stays. Install the root CA instead: the error goes away and the check survives.
- How to make a GET request: where this error first shows up.
- How to retry a failed request: which failures deserve a retry; this one doesn’t.
- How to parse a URL: the previous article, and the source of the hostname a certificate must cover.
The next article in this chapter is about sending email from a Rust program.