Importing a Self-signed Cert for a Rust reqwest HTTP client
I have set up a HTTP server with TLS using rust and a self-signed certification. It works fine when I import the cert and test the APIs in Postman. However, I got into trouble when writing a HTTP client using the rust library reqwest
. This is a note about the troubleshooting process.
In the following example, I am using OpenSSL to generate the self-signed cert.
Problem 1: UnsupportedCertVersion
At the beginning, I have a cert test-cert.pem
and a key test-key.pem
generated by this command:
openssl req -x509 -newkey rsa:4096 -keyout test-key.pem -out test-cert.pem -sha256 -days 3650 -nodes -subj "/C=HK/ST=Hong Kong/L=Hong Kong/O=MaisyT/OU=MaisyT/CN=MaisyT"
Note:
The key
test-key.pem
should be kept secret and only known by the HTTPS server.The cert
test-cert.pem
should be distribute to the client so that they can connect to the server using HTTPS.
The server is up and running with the cert. And I can get response from the server via HTTPS connection using Postman. Then, I try to create a reqwest
HTTP client:
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
#[tokio::test]
async fn test_client(){
let cert = "/Some/path/to/cert/test-cert.pem";
let cert = std::fs::read(cert).unwrap();
let cert = reqwest::Certificate::from_pem(&cert).expect("Fail to create cert.");
let client = reqwest::Client::builder()
.use_rustls_tls()
.add_root_certificate(cert)
.build()
.expect("Fail to build client.");
let mut map = HashMap::new();
map.insert("username", "userA");
map.insert("password", "password");
let res = client.post("https://localhost:3000/login")
.json(&map)
.send()
.await.unwrap();
println!("{res:#?}");
let data = res.json::<HashMap<String, String>>().await.unwrap();
println!("{data:#?}");
}
}
Cargo.toml
[dependencies] # Be aware of the features flag! tokio = { version = "1.0", features = ["full"] } # web client reqwest = { version = "0.12", features = ["json", "blocking", "rustls-tls"] }
It failed with the error:
called `Result::unwrap()` on an `Err` value: reqwest::Error { kind: Request, url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("localhost")), port: Some(3000), path: "/login", query: None, fragment: None }, source: Error { kind: Connect, source: Some(Custom { kind: Other, error: Custom { kind: InvalidData, error: InvalidCertificate(Other(OtherError(UnsupportedCertVersion))) } }) } }
As the error said it is a "UnsupportedCertVersion", I check for the version of the cert I'm using:
openssl x509 -in test-cert.pem -text -noout
# Certificate:
# Data:
# Version: 1 (0x0)
Turn out I should use a version 3 cert so I tried to create one.
Problem 2: Generating a Version 3 cert
I followed some instruction I found and add a -extensions v3_req
to the cert generation command.
openssl req -x509 -newkey rsa:4096 -keyout test-key.pem -out test-cert.pem -sha256 -days 3650 -nodes -subj "/C=HK/ST=Hong Kong/L=Hong Kong/O=MaisyT/OU=MaisyT/CN=MaisyT" -extensions v3_req
But it throws a error immediately: Error Loading extension section v3_req
. The online resource says my configuration is probably missing a [ v3_req ]
section. So I take a look on the "default" config I found in /System/Library/OpenSSL/openssl.cnf
(on a Mac). I got confused as I do found a [ v3_req ]
section in it.
At last, I found out that the openssl
is not using that config by default. Instead, I should include a config file explicitly with a -config
flag like this:
openssl req -x509 -newkey rsa:4096 -keyout test-key.pem -out test-cert.pem -sha256 -days 3650 -nodes -config ./mt.cnf
mt.cnf
:
[req]
prompt = no
default_md = sha256
default_bits = 2048
distinguished_name = dn
x509_extensions = v3_req
[dn]
C = HK
ST = Hong Kong
L = Hong Kong
O = MaisyT
OU = MaisyT
emailAddress = foo@bar.com
CN = MaisyT
[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
Problem 3: Invalid Host Name
But running the rust client still gave me error:
called
Result::unwrap()
on anErr
value: reqwest::Error { kind: Request, url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("localhost")), port: Some(3000), path: "/login", query: None, fragment: None }, source: Error { kind: Connect, source: Some(Custom { kind: Other, error: Custom { kind: InvalidData, error: InvalidCertificate(NotValidForName) } }) } }
NotValidForName
is some rustls
error that indicate that the URL the client call does not match the domain name in the cert. It is needed to added the domain name I am going to use to the cert by update the config.
mt.cnf
:
[req]
prompt = no
default_md = sha256
default_bits = 2048
distinguished_name = dn
x509_extensions = v3_req
[dn]
C = HK
ST = Hong Kong
L = Hong Kong
O = MaisyT
OU = MaisyT
emailAddress = foo@bar.com
CN = MaisyT
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = maisy.com
So I update my cert once more and I finally can connect to the server with the reqwest
client using HTTPS.
Reference
Subscribe to my newsletter
Read articles from Maisy Tse directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Maisy Tse
Maisy Tse
I just start my career as a programmer. So, here are some notes about what I learned.