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>.
This commit is contained in:
Jiseok CHOI authored and GitHub committed 2026-03-01 10:51:39 +09:00
1 parent 7b7c7a1b6e
commit be29462087
4 files changed
+18 -21

No files matched your search

Generated
+1
View File
@@ -3238,6 +3238,7 @@ dependencies = [
"bzip2",
"cfg-if",
"chrono",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"csv-core",
-8
View File
@@ -1484,19 +1484,11 @@ class HMACCompareDigestTestCase(CompareDigestMixin, unittest.TestCase):
else:
self.assertIs(self.compare_digest, operator_compare_digest)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: TypeError not raised by compare_digest
def test_exceptions(self):
return super().test_exceptions()
@hashlib_helper.requires_hashlib()
class OpenSSLCompareDigestTestCase(CompareDigestMixin, unittest.TestCase):
compare_digest = openssl_compare_digest
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: TypeError not raised by compare_digest
def test_exceptions(self):
return super().test_exceptions()
class OperatorCompareDigestTestCase(CompareDigestMixin, unittest.TestCase):
compare_digest = operator_compare_digest
+1
View File
@@ -69,6 +69,7 @@ sha3 = "0.10.1"
blake2 = "0.10.4"
hmac = "0.12"
pbkdf2 = { version = "0.12", features = ["hmac"] }
constant_time_eq = { workspace = true }
## unicode stuff
unicode_names2 = { workspace = true }
+16 -13
View File
@@ -14,7 +14,6 @@ pub mod _hashlib {
PyBaseExceptionRef, PyBytes, PyFrozenSet, PyStr, PyTypeRef, PyUtf8StrRef, PyValueError,
},
class::StaticType,
convert::ToPyObject,
function::{ArgBytesLike, ArgStrOrBytesLike, FuncArgs, OptionalArg},
types::{Constructor, Representable},
};
@@ -724,22 +723,26 @@ pub mod _hashlib {
a: ArgStrOrBytesLike,
b: ArgStrOrBytesLike,
vm: &VirtualMachine,
) -> PyResult<PyObjectRef> {
const fn is_str(arg: &ArgStrOrBytesLike) -> bool {
matches!(arg, ArgStrOrBytesLike::Str(_))
}
) -> PyResult<bool> {
use constant_time_eq::constant_time_eq;
if is_str(&a) != is_str(&b) {
return Err(vm.new_type_error(format!(
match (&a, &b) {
(ArgStrOrBytesLike::Str(a), ArgStrOrBytesLike::Str(b)) => {
if !a.isascii() || !b.isascii() {
return Err(vm.new_type_error(
"comparing strings with non-ASCII characters is not supported",
));
}
Ok(constant_time_eq(a.as_bytes(), b.as_bytes()))
}
(ArgStrOrBytesLike::Buf(a), ArgStrOrBytesLike::Buf(b)) => {
Ok(a.with_ref(|a| b.with_ref(|b| constant_time_eq(a, b))))
}
_ => Err(vm.new_type_error(format!(
"a bytes-like object is required, not '{}'",
b.as_object().class().name()
)));
))),
}
let a_hash = a.borrow_bytes().to_vec();
let b_hash = b.borrow_bytes().to_vec();
Ok((a_hash == b_hash).to_pyobject(vm))
}
#[derive(FromArgs, Debug)]