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.