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 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(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

  1. 如何使用 OpenSSL 建立開發測試用途的自簽憑證 (Self-Signed Certificate)