Skip to content

Commit be29462

Browse files
authored
Fix _hashlib.compare_digest to reject non-ASCII strings (#7280)
Add non-ASCII string check to _hashlib.compare_digest, matching the behavior of _operator._compare_digest. When both arguments are strings, non-ASCII characters now correctly raise TypeError. Also replace the non-constant-time == comparison with constant_time_eq for proper timing-attack resistance, and return PyResult<bool> instead of PyResult<PyObjectRef>.
1 parent 7b7c7a1 commit be29462

File tree

4 files changed

+19
-22
lines changed

4 files changed

+19
-22
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_hmac.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,19 +1484,11 @@ def test_compare_digest_func(self):
14841484
else:
14851485
self.assertIs(self.compare_digest, operator_compare_digest)
14861486

1487-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: TypeError not raised by compare_digest
1488-
def test_exceptions(self):
1489-
return super().test_exceptions()
1490-
14911487

14921488
@hashlib_helper.requires_hashlib()
14931489
class OpenSSLCompareDigestTestCase(CompareDigestMixin, unittest.TestCase):
14941490
compare_digest = openssl_compare_digest
14951491

1496-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: TypeError not raised by compare_digest
1497-
def test_exceptions(self):
1498-
return super().test_exceptions()
1499-
15001492

15011493
class OperatorCompareDigestTestCase(CompareDigestMixin, unittest.TestCase):
15021494
compare_digest = operator_compare_digest

crates/stdlib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ sha3 = "0.10.1"
6969
blake2 = "0.10.4"
7070
hmac = "0.12"
7171
pbkdf2 = { version = "0.12", features = ["hmac"] }
72+
constant_time_eq = { workspace = true }
7273

7374
## unicode stuff
7475
unicode_names2 = { workspace = true }

crates/stdlib/src/hashlib.rs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ pub mod _hashlib {
1414
PyBaseExceptionRef, PyBytes, PyFrozenSet, PyStr, PyTypeRef, PyUtf8StrRef, PyValueError,
1515
},
1616
class::StaticType,
17-
convert::ToPyObject,
1817
function::{ArgBytesLike, ArgStrOrBytesLike, FuncArgs, OptionalArg},
1918
types::{Constructor, Representable},
2019
};
@@ -724,22 +723,26 @@ pub mod _hashlib {
724723
a: ArgStrOrBytesLike,
725724
b: ArgStrOrBytesLike,
726725
vm: &VirtualMachine,
727-
) -> PyResult<PyObjectRef> {
728-
const fn is_str(arg: &ArgStrOrBytesLike) -> bool {
729-
matches!(arg, ArgStrOrBytesLike::Str(_))
730-
}
731-
732-
if is_str(&a) != is_str(&b) {
733-
return Err(vm.new_type_error(format!(
726+
) -> PyResult<bool> {
727+
use constant_time_eq::constant_time_eq;
728+
729+
match (&a, &b) {
730+
(ArgStrOrBytesLike::Str(a), ArgStrOrBytesLike::Str(b)) => {
731+
if !a.isascii() || !b.isascii() {
732+
return Err(vm.new_type_error(
733+
"comparing strings with non-ASCII characters is not supported",
734+
));
735+
}
736+
Ok(constant_time_eq(a.as_bytes(), b.as_bytes()))
737+
}
738+
(ArgStrOrBytesLike::Buf(a), ArgStrOrBytesLike::Buf(b)) => {
739+
Ok(a.with_ref(|a| b.with_ref(|b| constant_time_eq(a, b))))
740+
}
741+
_ => Err(vm.new_type_error(format!(
734742
"a bytes-like object is required, not '{}'",
735743
b.as_object().class().name()
736-
)));
744+
))),
737745
}
738-
739-
let a_hash = a.borrow_bytes().to_vec();
740-
let b_hash = b.borrow_bytes().to_vec();
741-
742-
Ok((a_hash == b_hash).to_pyobject(vm))
743746
}
744747

745748
#[derive(FromArgs, Debug)]

0 commit comments

Comments
 (0)