Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client certificates seem to be ignored when using rustls #903

Closed
alexhexabeam opened this issue May 6, 2020 · 9 comments · Fixed by #1852
Closed

Client certificates seem to be ignored when using rustls #903

alexhexabeam opened this issue May 6, 2020 · 9 comments · Fixed by #1852

Comments

@alexhexabeam
Copy link

#[derive(Serialize, Deserialize)]
struct TokenResponse {
    #[serde(rename = "accessToken")]
    access_token: String,
}

async fn acquire_token(url: &str) -> Result<String, MyError> {
    let cert_bytes = fs::read("combined.pem")?;
    let id = reqwest::Identity::from_pem(&cert_bytes)?;

    let ca = reqwest::Certificate::from_pem(&fs::read("ca.pem")?)?;
    let client = reqwest::ClientBuilder::new()
        .danger_accept_invalid_certs(true)
        .add_root_certificate(ca)
        .identity(id)
        .timeout(Duration::new(10, 0))
        .build()?;
    let jwt_token_response = client.post(url)
        .header("Accept", "application/json")
        .send().await?.error_for_status()?.json::<TokenResponse>().await?;

    Ok(jwt_token_response.access_token)
}

The above code errors with a 401 when it gets to the .error_for_status()?.

Below is combined.pem in this example (just a test cert, don't worry):

-----BEGIN PRIVATE KEY-----
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
AWJ/fXbHEPkeYYkLZUecST88bx8zgFk=
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIG6DCCBNCgAwIBAgIJAO3gFZSDuSlNMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlTYW4gTWF0ZW8x
EDAOBgNVBAoMB0V4YWJlYW0xEzARBgNVBAMMCmV4YWJlYW0tY2EwHhcNMjAwNTA1
MjEwMzQ1WhcNMzAwNTAzMjEwMzQ1WjBjMQswCQYDVQQGEwJVUzETMBEGA1UECAwK
Q2FsaWZvcm5pYTESMBAGA1UEBwwJU2FuIE1hdGVvMRAwDgYDVQQKDAdFeGFiZWFt
MRkwFwYDVQQDDBBjbG91ZGh1Yi11cGRhdGVyMIICIjANBgkqhkiG9w0BAQEFAAOC
Ag8AMIICCgKCAgEA0WrYuyaajjMk8A9NFHQI4a6wX57lWOjiDay9bpqYopgi5yIh
oHzKnHo2RODRg8ZAcwgk8nxin1Yyi3Q/PXf36CirZC5OeYHgZtydm7yeQBjRvhTR
qbqVI+tvk7qRmh0fPuL3eKPjM8Q2ZJbVpY8suBtfs8EtJruv2qVqgJDvPAAmXZvB
1P8iH+5kV9YpWeMPl3gw38WIe8pTAYzCCOnLOlJf6MhqYww9FkGmJpKpBAu9jZ+D
h3LOok92FAqgoxvTqityO69vmMWVo02mIIufDWggLnrWrZDpnIvAQ65sOuLRHB9/
1lYM/OKxzTcUrOuLaT3a2x5rz2DLX70DqfW/snXkXqFMDbP1iUepU8B1ldFCbMcM
vkM9TXEjTFOM4WfUi/g8cpX0VxhtS3UWBiWKRbyY0YDqv0oxl3FK/Nrzo6NPmMxb
d08tHPLJqogggBNm828DkpeVOFfQJlfvaLkWdKC909F2tuJR/YEl7tcXbuGD7bVf
gMrByvmY4e8ucTsUrkSnyXUqL598PKt7gx1fLIp6wA+/2JY5nNs1uDzn5IlN0Ihi
Xmz66o5m7s2MkQ8N2V/hkaTcZd1VwyEfpLRNGGC1ytqPkHiigWjw2vWAUYKxOKyt
RbLanMPd2rfiEkHjV2QbOptY/iFN0bahS+07Z3+mhQr9oUxv4jWAe7uhm+kCAwEA
AaOCAaMwggGfMB0GA1UdDgQWBBThR29QT4ZACUeCfurtLQCj6imwrDCBjwYDVR0j
BIGHMIGEgBSRVTpN/QJhcXJLHTMslrSnWQ1eJaFhpF8wXTELMAkGA1UEBhMCVVMx
EzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCVNhbiBNYXRlbzEQMA4GA1UE
CgwHRXhhYmVhbTETMBEGA1UEAwwKZXhhYmVhbS1jYYIJAO3gFZSDuSlDMBUGA1Ud
EgQOMAyCCmV4YWJlYW0tY2EwgZQGA1UdEQSBjDCBiYIQY2xvdWRodWItdXBkYXRl
coIFaG9zdDKCEGNsb3VkaHViLXVwZGF0ZXKCCGNsb3VkaHVigg9pcC0xMC0xMC0y
OS0yNDOCKmlwLTEwLTEwLTI5LTI0My51cy13ZXN0LTIuY29tcHV0ZS5pbnRlcm5h
bIIJbG9jYWxob3N0hwR/AAABhwQKCh3zMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/
BAQDAgWgMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG
9w0BAQsFAAOCAgEAh6dSTTy2AzXbAj2mdN1fvEfNrg+SnAGMiBcF0IFs8axPUO4O
druSQmo4dM8+i1N84gStwvSvKWErE0dJQIRhXrvTZA82/EOPrTH1YCgn60l9j/4g
5nsqXOVHNMfKmF6i9SJ1Wz0GNLFKZ8X0kg9QwZYGMsDlj3cU+VYLiPLbsOrlH/cg
qNsUhlZYU1C0nHBHKYeVY7sAGe9iJpnDQP5N8kYYat27sBfhgMtfNLY0Kev2Sd7K
PZCQBTD5l1/e8P50dNax0Zdu4tTZO53LPBe3Slm5Qvq3+H0wsGgc0oj1FFK8UXVB
yxnzXFiQ+sA8Xd5Pad3wZXMPMsN9FBBQvds+Z2BYfUm2EptLKD73Rc3jRQSLSVpS
tVeI1ziFAYncNgo6XeOGYrDKZrNZRDYi6elErdwoNypM4lO4c8LgV3J1Br/TYNUP
RkoRKJ4thanOfSXTwXZiUDNF+KwE5/yJZ+dkIDGJUe532FDEJT0o4Bt68bjq5N6l
Rop8C76CwgDuqGAhNILP3yfOaCNxk5GFPGPe4eUw/z9doW5exIX7kvTElqspEoKi
FHi6mc4MdAR/SmxdT/j6rPNxyQbBAt7cRAJLjlZDBZgkcQv+EWRQNLQjJK4EXEN2
NwKiyBI1Dl6SGGscNCZLBozvMayVVgk0DTJbei2odvV7Y0xovQrN/o8kok4=
-----END CERTIFICATE-----

Note that the certificates themselves are correct, as I used them to generate the identity.p12 file in this working native-tls example:

async fn acquire_token(url: &str) -> Result<String, MyError> {
    let p12_bytes = fs::read("identity.p12")?;
    let id = reqwest::Identity::from_pkcs12_der(&p12_bytes, "password")?;

    let ca = reqwest::Certificate::from_pem(&fs::read("ca.pem")?)?;
    let client = reqwest::ClientBuilder::new()
        .danger_accept_invalid_hostnames(true)
        .add_root_certificate(ca)
        .identity(id)
        .timeout(Duration::new(10, 0))
        .build()?;
    let jwt_token_response = client.post(url)
        .header("Accept", "application/json")
        .send().await?.error_for_status()?.json::<TokenResponse>().await?;

    Ok(jwt_token_response.access_token)
}
@kyasu1
Copy link

kyasu1 commented May 15, 2020

Hi

I have been having similar issue when using rustls and I could resolve it by adding use_rustls_tls() option to the ClientBuilder as stated here.

https://docs.rs/reqwest/0.10.4/reqwest/struct.ClientBuilder.html#method.use_rustls_tls

Hope this may help

@edevil
Copy link

edevil commented Jun 29, 2020

Do you know if it is possible to specify the whole accepted list of CAs, instead of just adding a new one, and if it is possible to specify the expected servername per TLS request instead of using danger_accept_invalid_hostnames?

@lostiniceland
Copy link

Hi

I have been having similar issue when using rustls and I could resolve it by adding use_rustls_tls() option to the ClientBuilder as stated here.

https://docs.rs/reqwest/0.10.4/reqwest/struct.ClientBuilder.html#method.use_rustls_tls

Hope this may help

Thanks. I spent hours trying to get my client-cert working and just did not see this option. I thought that rustls is automatically active when activating the feature (since event the API changes, eg from_der -> from_pem)

@eric-seppanen
Copy link
Contributor

I encountered a similar situation. I had an end-to-end unit test that created a server and a client and verified that client certificate auth works. The client was constructed like this:

let client = ClientBuilder::new()
    .resolve("test_server", server_addr)
    .identity(client_identity())
    .add_root_certificate(root_cert)
    .build()
    .unwrap();

My unit test was passing until I merged this crate into a workspace with some other crates, and then it started to fail.

What I found is that if reqwest is specified with default-features = false and features = ["rustls-tls"], then my unit test passes. If I remove default-features = false (or build in a workspace where another crate enables default features) then the test fails.

This is quite surprising, that enabling additional features can cause working code to fail.

I think what's happening is that I created an Identity using Identity::from_pem() (which requires the rustls feature), but that identity gets silently ignored if the Client defaults into a native-tls configuration.

Adding .use_rustls_tls() fixes my test failure.

Assuming I understand the problem correctly, it would be helpful if this caused an error instead of silently doing the wrong thing. If there's an incompatibility between the Identity variant and the Client backend config, perhaps that should cause ClientBuilder::build to fail instead of silently doing the wrong thing?

@seanmonstar
Copy link
Owner

Thanks for investigating! If that's indeed what the code is doing, I agree it should be fixed! Would you like to take a shot at that?

@eric-seppanen
Copy link
Contributor

Is the best fix to just add a failure to ClientBuilder::build? If yes, than I could give it a shot.

I'm not sure if there are other options, but I might wish for one of these instead:

  • .identity() could be backend-agnostic, so anything that compiles should work as expected. I'm not sure if this is possible.
  • ClientBuilder could automatically switch backends based on the Identity that was passed. Maybe that's a bit too subtle, though? It would be strange

Even if a better solution exists down the road, perhaps it's still a good first step to add the short-term fix (fail build if the backend and identity are incompatible).

@seanmonstar
Copy link
Owner

Yea, returning a builder error seems better than silently not working. If the Identity type can parse it into both formats, even better. I don't know if it can.

@cpu
Copy link
Contributor

cpu commented Aug 16, 2023

Just chiming in to say this bubbled up as an issue on the Rustls repo (rustls/rustls#1297). The produced behaviour is confusing from a user perspective and takes some time to diagnose from first principles.

Here are some more detailed reproduction steps for reqwest 0.11.18 that show both the client and server behaviour in case it's helpful:

Client reqwest program
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let buf = include_bytes!("../minica.pem").as_slice();
    let cacert = reqwest::Certificate::from_pem(&buf)?;

    let client_ident_pem = include_bytes!("../combined.pem").as_slice();
    let id = reqwest::Identity::from_pem(&client_ident_pem)?;

    let client = reqwest::ClientBuilder::new()
        .timeout(Duration::from_secs(30))
        .add_root_certificate(cacert)
        .identity(id)
        .build()?;

    client.get("https://localhost:4433/").send().await?;

    Ok(())
}
Server setup using Rustls tlsserver-mio example program:
SSLKEYLOGFILE=/tmp/tlsmioserver.key.txt cargo run --bin tlsserver-mio -- --certs ../hypertest/cert.pem --key ../hypertest/key.pem --require-auth --auth ../hypertest/minica.pem --verbose --port 4433 http
Tshark packet capture showing empty client `Certificate` handshake message:
sudo tshark -i any -n -V -o 'tls.keylog_file:/tmp/tlsmioserver.key.txt' 'port 4433' 
<snipped>
Transport Layer Security
    TLSv1.3 Record Layer: Change Cipher Spec Protocol: Change Cipher Spec
        Content Type: Change Cipher Spec (20)
        Version: TLS 1.2 (0x0303)
        Length: 1
        Change Cipher Spec Message
    TLSv1.3 Record Layer: Handshake Protocol: Certificate
        Opaque Type: Application Data (23)
        Version: TLS 1.2 (0x0303)
        Length: 25
        [Content Type: Handshake (22)]
        Handshake Protocol: Certificate
            Handshake Type: Certificate (11)
            Length: 4
            Certificate Request Context Length: 0
            Certificates Length: 0
    TLSv1.3 Record Layer: Handshake Protocol: Finished
        Opaque Type: Application Data (23)
        Version: TLS 1.2 (0x0303)
        Length: 69
        [Content Type: Handshake (22)]
        Handshake Protocol: Finished
            Handshake Type: Finished (20)
            Length: 48
            Verify Data

Notably the problematic configuration does send a Certificate handshake message in response to the server's Certificate Request, it's just empty:

        Handshake Protocol: Certificate
            Handshake Type: Certificate (11)
            Length: 4
            Certificate Request Context Length: 0
            Certificates Length: 0

As described above adding use_rustls_tls() to the builder, or disabling default features and only enabling features = ["rustls-tls"] fixes the issue.

I've also applied #1852 and confirmed it produces a helpful error message instead of sending an empty client Certificate message.

@eric-seppanen
Copy link
Contributor

eric-seppanen commented Aug 16, 2023

I think the root of the problem is that Identity commits to a particular backend, then later ClientBuilder may commit to a different backend, and then try to load that Identity, and we have a problem.

The problem only arises when multiple backends are enabled, so we could create some logic that extracts the key + certificate from the backend-A Identity and transforms it into a backend-B Identity. This seems unsatisfying; it would be nice to find a way so that the correct type is always generated and we don't have this problem. Also, there's no guarantee that the two backends support all the same cryptographic capabilities so it's possible the transformation could fail.

Another idea would be to delay parsing the key and certificate until the ClientBuilder is consuming them. In this world, Identity would only be a container for the raw key and certificate bytes, and attempting to insert them into a ClientBuilder would run that backend's parsing logic. This might mean that reqwest has to carry around extra code for managing a few different file formats (DER, PEM, PEM with multiple objects) that could otherwise have been handled by the backend. But it might be worth exploring to see how burdensome it would be.

In another universe, ClientBuilder could carry generic parameters so that ClientBuilder<RustlsFlavor> would only accept RustlsIdentity, which would make all of this a compile-time error. I'm assuming that's not in the cards, but I still wish there was a way to get to compile-time errors instead of runtime errors.

Another way to look at the problem: the naming of the current Identity constructors invites mistakes. For example, Identity::from_pem creates an Identity that can only work with a rustls ClientBuilder, but nothing about that name indicates to the caller that this is happening. To make this less surprising, either an Identity should be fully backend-agnostic, or the constructor name (and type?) should proclaim its restrictions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants