Skip to content
Select theme

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.

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: NotValidForName

All four end in invalid peer certificate; the last line names the cause: “expired” and “not trusted” are the first and third, NotValidForName the second.

badssl.com keeps public hosts that fail in exactly these ways, and every output below is reproducible:

Terminal window
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.com
issuer=/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.com
issuer=/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.com
issuer=/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.com
issuer=/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:

Terminal window
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 not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

The names a certificate does cover are in its SAN:

Terminal window
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.

Expired. The only fix is on the server:

Terminal window
echo | openssl s_client -connect expired.badssl.com:443 -servername expired.badssl.com 2>/dev/null \
| openssl x509 -noout -enddate
notAfter=Apr 12 23:59:59 2015 GMT

Renew 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:

Terminal window
echo | openssl s_client -connect self-signed.badssl.com:443 -servername self-signed.badssl.com 2>/dev/null \
| openssl x509 -out self-signed.pem
echo | 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:

Terminal window
curl -sS -o /dev/null https://badssl.com/; echo "exit=$?"
exit=0

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.

The next article in this chapter is about sending email from a Rust program.