bytebeamio / rumqtt

The MQTT ecosystem in rust
Apache License 2.0
1.64k stars 252 forks source link

Password authentication is not constant-time #796

Closed abonander closed 7 months ago

abonander commented 9 months ago

While reading the code for how rumqttd handles password authentication, the following code snippet jumped out at me:

https://github.com/bytebeamio/rumqtt/blob/c719181bb1e3c41a691a7827bfafd393b004e88c/rumqttd/src/link/remote.rs#L247-L249

This comparison is linear in respect to both the number of users as well as the length of both the username and password, which would theoretically allow an attacker to brute-force a login using a timing attack, sending random inputs and timing how long it takes for rumqttd to validate them to estimate how many characters are correct in the username and/or password.

A HashMap lookup for the username would eliminate some of that timing information, and a constant-time comparison for the password would make a timing attack on a complete login no more efficient than brute-force.

Looking closer, it appears that pairs in that snippet is already a HashMap, so I'm curious as to why a lookup using Iterator::any() was chosen.

This may not be considered high-priority, especially since authentication is pluggable, but hardening against this kind of attack would mean added value for production users. I didn't see another issue about this so I just wanted to make sure you were aware of it.

swanandx commented 9 months ago

Hey, thanks for reporting the issue!

Timing attack in this scenario doesn't seem much practically possible, consider delta in time will be negligible. But we shouldn't neglect it either.

I'm curious as to why a lookup using Iterator::any() was chosen

Can't remember, but there doesn't seem any reason to use it, simple HashMap loopup with get() should great.

Would you be interested in opening PR with the fix?

Thank you! πŸ˜πŸš€

abonander commented 9 months ago

Timing attack in this scenario doesn't seem much practically possible, consider delta in time will be negligible.

Timing attacks are generally statistical in nature. Even if the difference between two comparisons is a few nanoseconds, over a large number of samples that difference can be teased out.

Would you be interested in opening PR with the fix?

Unfortunately I have my own work to focus on, but this seems like a task that would be good for a first-time contributor.

swanandx commented 9 months ago

Unfortunately I have my own work to focus on, but this seems like a task that would be good for a first-time contributor

No worries! Will mark this issue as good-first-issue, the only change required will be using something like:

 !pairs.get(username).is_some_and(|&pass| pass == password)

Again, thanks for pointing it out πŸ˜„

FSMaxB commented 9 months ago

No worries! Will mark this issue as good-first-issue, the only change required will be using something like:

Not quite, you need to use a constant time comparison function like https://crates.io/crates/constant_time_eq (note: I haven't checked whether that package is good or not, just an example).

abonander commented 9 months ago

The subtle crate is pretty good and maintained by cryptography experts, which is why I linked to it in the OP:

use subtle::ConstantTimeEq;

!pairs.get(username).is_some_and(|pass| pass.as_bytes().ct_eq(password.as_bytes()).into())