bytebeamio / rumqtt

The MQTT ecosystem in rust

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Password authentication is not constant-time

abonander opened this issue · comments

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

let static_auth_verified = pairs
.iter()
.any(|(user, pass)| (user, pass) == (username, password));

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.

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! 😁🚀

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.

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 😄

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).

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