From 1bfe6321215bd68001725dfd54512fd1a0cb765f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 7 Jun 2026 00:53:40 +0000 Subject: [PATCH 1/2] Implement __hash__ for ASN.1 wrapper types Adds __hash__ to PrintableString, IA5String, UTCTime, GeneralizedTime, and BitString, delegating to the hash of the wrapped Python value (and for BitString, the (data, padding_bits) tuple) so that equal values hash equally. https://claude.ai/code/session_01XYJLrH5WSNkJbgNd19jPtf --- CHANGELOG.rst | 5 ++ .../bindings/_rust/declarative_asn1.pyi | 5 ++ src/rust/src/declarative_asn1/types.rs | 22 +++++++++ tests/hazmat/asn1/test_api.py | 49 +++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3d285b638ae2..a9c4eebfa310 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -30,6 +30,11 @@ Changelog :class:`~cryptography.x509.CertificateSigningRequest`, and :class:`~cryptography.x509.CertificateRevocationList` as field types in :doc:`/hazmat/asn1/index` structures. +* :class:`~cryptography.hazmat.asn1.PrintableString`, + :class:`~cryptography.hazmat.asn1.IA5String`, + :class:`~cryptography.hazmat.asn1.UTCTime`, + :class:`~cryptography.hazmat.asn1.GeneralizedTime`, and + :class:`~cryptography.hazmat.asn1.BitString` are now hashable. .. _v48-0-0: diff --git a/src/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi b/src/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi index e499eb5c846e..f24aaea1f3e9 100644 --- a/src/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi +++ b/src/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi @@ -81,30 +81,35 @@ class PrintableString: def __new__(cls, inner: str) -> PrintableString: ... def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... def as_str(self) -> str: ... class IA5String: def __new__(cls, inner: str) -> IA5String: ... def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... def as_str(self) -> str: ... class UTCTime: def __new__(cls, inner: datetime.datetime) -> UTCTime: ... def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... def as_datetime(self) -> datetime.datetime: ... class GeneralizedTime: def __new__(cls, inner: datetime.datetime) -> GeneralizedTime: ... def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... def as_datetime(self) -> datetime.datetime: ... class BitString: def __new__(cls, data: bytes, padding_bits: int) -> BitString: ... def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... def as_bytes(self) -> bytes: ... def padding_bits(self) -> int: ... diff --git a/src/rust/src/declarative_asn1/types.rs b/src/rust/src/declarative_asn1/types.rs index 4efbe75a849d..7c9cb3afc26d 100644 --- a/src/rust/src/declarative_asn1/types.rs +++ b/src/rust/src/declarative_asn1/types.rs @@ -254,6 +254,10 @@ impl PrintableString { (**self.inner.bind(py)).eq(other.inner.bind(py)) } + fn __hash__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + (**self.inner.bind(py)).hash() + } + pub fn __repr__<'py>( &self, py: pyo3::Python<'py>, @@ -301,6 +305,10 @@ impl IA5String { (**self.inner.bind(py)).eq(other.inner.bind(py)) } + fn __hash__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + (**self.inner.bind(py)).hash() + } + pub fn __repr__<'py>( &self, py: pyo3::Python<'py>, @@ -359,6 +367,10 @@ impl UtcTime { (**self.inner.bind(py)).eq(other.inner.bind(py)) } + fn __hash__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + (**self.inner.bind(py)).hash() + } + pub fn __repr__<'py>( &self, py: pyo3::Python<'py>, @@ -404,6 +416,10 @@ impl GeneralizedTime { (**self.inner.bind(py)).eq(other.inner.bind(py)) } + fn __hash__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + (**self.inner.bind(py)).hash() + } + pub fn __repr__<'py>( &self, py: pyo3::Python<'py>, @@ -453,6 +469,12 @@ impl BitString { && self.padding_bits == other.padding_bits) } + fn __hash__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult { + (self.data.bind(py), self.padding_bits) + .into_pyobject(py)? + .hash() + } + pub fn __repr__<'py>( &self, py: pyo3::Python<'py>, diff --git a/tests/hazmat/asn1/test_api.py b/tests/hazmat/asn1/test_api.py index 63294768fad8..4eb2bc48b0e8 100644 --- a/tests/hazmat/asn1/test_api.py +++ b/tests/hazmat/asn1/test_api.py @@ -29,6 +29,14 @@ def test_invalid_printable_string(self) -> None: with pytest.raises(ValueError, match="invalid PrintableString: café"): asn1.PrintableString("café") + def test_hash_printable_string(self) -> None: + assert hash(asn1.PrintableString("MyString")) == hash( + asn1.PrintableString("MyString") + ) + assert hash(asn1.PrintableString("MyString")) != hash( + asn1.PrintableString("OtherString") + ) + def test_repr_ia5_string(self) -> None: my_string = "MyString" assert repr(asn1.IA5String(my_string)) == f"IA5String({my_string!r})" @@ -41,6 +49,14 @@ def test_invalid_ia5_string(self) -> None: with pytest.raises(ValueError, match="invalid IA5String: café"): asn1.IA5String("café") + def test_hash_ia5_string(self) -> None: + assert hash(asn1.IA5String("MyString")) == hash( + asn1.IA5String("MyString") + ) + assert hash(asn1.IA5String("MyString")) != hash( + asn1.IA5String("OtherString") + ) + def test_utc_time_as_datetime(self) -> None: dt = datetime.datetime( 2000, 1, 1, 10, 10, 10, tzinfo=datetime.timezone.utc @@ -53,6 +69,16 @@ def test_repr_utc_time(self) -> None: ) assert repr(asn1.UTCTime(dt)) == f"UTCTime({dt!r})" + def test_hash_utc_time(self) -> None: + dt = datetime.datetime( + 2000, 1, 1, 10, 10, 10, tzinfo=datetime.timezone.utc + ) + other_dt = datetime.datetime( + 2001, 1, 1, 10, 10, 10, tzinfo=datetime.timezone.utc + ) + assert hash(asn1.UTCTime(dt)) == hash(asn1.UTCTime(dt)) + assert hash(asn1.UTCTime(dt)) != hash(asn1.UTCTime(other_dt)) + def test_invalid_utc_time(self) -> None: with pytest.raises( ValueError, @@ -107,6 +133,18 @@ def test_repr_generalized_time(self) -> None: ) assert repr(asn1.GeneralizedTime(dt)) == f"GeneralizedTime({dt!r})" + def test_hash_generalized_time(self) -> None: + dt = datetime.datetime( + 2000, 1, 1, 10, 10, 10, 300000, tzinfo=datetime.timezone.utc + ) + other_dt = datetime.datetime( + 2001, 1, 1, 10, 10, 10, 300000, tzinfo=datetime.timezone.utc + ) + assert hash(asn1.GeneralizedTime(dt)) == hash(asn1.GeneralizedTime(dt)) + assert hash(asn1.GeneralizedTime(dt)) != hash( + asn1.GeneralizedTime(other_dt) + ) + def test_invalid_generalized_time(self) -> None: with pytest.raises( ValueError, @@ -129,6 +167,17 @@ def test_repr_bitstring(self) -> None: == f"BitString(data={data!r}, padding_bits=2)" ) + def test_hash_bitstring(self) -> None: + assert hash(asn1.BitString(b"\x01\x02\x30", 2)) == hash( + asn1.BitString(b"\x01\x02\x30", 2) + ) + assert hash(asn1.BitString(b"\x01\x02\x30", 2)) != hash( + asn1.BitString(b"\x01\x02\x40", 2) + ) + assert hash(asn1.BitString(b"\x01\x02\x30", 2)) != hash( + asn1.BitString(b"\x01\x02\x30", 3) + ) + def test_invalid_bitstring(self) -> None: with pytest.raises( ValueError, From 1df35a3b097f534f6d7749e4dd45d10ef10e1708 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 7 Jun 2026 00:54:38 +0000 Subject: [PATCH 2/2] Remove changelog entry https://claude.ai/code/session_01XYJLrH5WSNkJbgNd19jPtf --- CHANGELOG.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a9c4eebfa310..3d285b638ae2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -30,11 +30,6 @@ Changelog :class:`~cryptography.x509.CertificateSigningRequest`, and :class:`~cryptography.x509.CertificateRevocationList` as field types in :doc:`/hazmat/asn1/index` structures. -* :class:`~cryptography.hazmat.asn1.PrintableString`, - :class:`~cryptography.hazmat.asn1.IA5String`, - :class:`~cryptography.hazmat.asn1.UTCTime`, - :class:`~cryptography.hazmat.asn1.GeneralizedTime`, and - :class:`~cryptography.hazmat.asn1.BitString` are now hashable. .. _v48-0-0: