From 7d5086c69a53f631310437c4bd90c2b0f680dfbb Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Sun, 8 Mar 2026 14:50:24 +0200 Subject: [PATCH 1/3] x509/certificate: cache issuer getter result Store the Python Name object in a PyOnceLock so that repeated accesses avoid re-running parse_name, which allocates a full Python Name object tree from ASN.1 on every call. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Alexander Bokovoy --- src/rust/src/pkcs7.rs | 1 + src/rust/src/x509/certificate.rs | 13 +++++++++++-- src/rust/src/x509/ocsp_resp.rs | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index 06814f777d50..33dbe29b994b 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -768,6 +768,7 @@ where x509::certificate::Certificate { raw: raw_cert, cached_extensions: pyo3::sync::PyOnceLock::new(), + cached_issuer: pyo3::sync::PyOnceLock::new(), }, )?)?; diff --git a/src/rust/src/x509/certificate.rs b/src/rust/src/x509/certificate.rs index d0bb651f053e..7bfc78ff1a93 100644 --- a/src/rust/src/x509/certificate.rs +++ b/src/rust/src/x509/certificate.rs @@ -41,6 +41,7 @@ self_cell::self_cell!( pub(crate) struct Certificate { pub(crate) raw: OwnedCertificate, pub(crate) cached_extensions: pyo3::sync::PyOnceLock>, + pub(crate) cached_issuer: pyo3::sync::PyOnceLock>, } #[pyo3::pymethods] @@ -138,8 +139,15 @@ impl Certificate { #[getter] fn issuer<'p>(&self, py: pyo3::Python<'p>) -> pyo3::PyResult> { - Ok(x509::parse_name(py, self.raw.borrow_dependent().issuer()) - .map_err(|e| e.add_location(asn1::ParseLocation::Field("issuer")))?) + Ok(self + .cached_issuer + .get_or_try_init(py, || { + x509::parse_name(py, self.raw.borrow_dependent().issuer()) + .map_err(|e| e.add_location(asn1::ParseLocation::Field("issuer"))) + .map(|v| v.unbind()) + })? + .bind(py) + .clone()) } #[getter] @@ -441,6 +449,7 @@ pub(crate) fn load_der_x509_certificate( Ok(Certificate { raw, cached_extensions: pyo3::sync::PyOnceLock::new(), + cached_issuer: pyo3::sync::PyOnceLock::new(), }) } diff --git a/src/rust/src/x509/ocsp_resp.rs b/src/rust/src/x509/ocsp_resp.rs index cab6320fc922..4322f3710f56 100644 --- a/src/rust/src/x509/ocsp_resp.rs +++ b/src/rust/src/x509/ocsp_resp.rs @@ -265,6 +265,7 @@ impl OCSPResponse { x509::certificate::Certificate { raw: raw_cert, cached_extensions: pyo3::sync::PyOnceLock::new(), + cached_issuer: pyo3::sync::PyOnceLock::new(), }, )?)?; } From cd7bcb4ae9d3611b1e94e25c90f8a1ca316f65b8 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Sun, 8 Mar 2026 14:52:07 +0200 Subject: [PATCH 2/3] x509/certificate: cache subject getter result Store the Python Name object in a PyOnceLock so that repeated accesses avoid re-running parse_name, which allocates a full Python Name object tree from ASN.1 on every call. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Alexander Bokovoy --- src/rust/src/pkcs7.rs | 1 + src/rust/src/x509/certificate.rs | 13 +++++++++++-- src/rust/src/x509/ocsp_resp.rs | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs index 33dbe29b994b..05f3bec6565a 100644 --- a/src/rust/src/pkcs7.rs +++ b/src/rust/src/pkcs7.rs @@ -769,6 +769,7 @@ where raw: raw_cert, cached_extensions: pyo3::sync::PyOnceLock::new(), cached_issuer: pyo3::sync::PyOnceLock::new(), + cached_subject: pyo3::sync::PyOnceLock::new(), }, )?)?; diff --git a/src/rust/src/x509/certificate.rs b/src/rust/src/x509/certificate.rs index 7bfc78ff1a93..15a33beb01e1 100644 --- a/src/rust/src/x509/certificate.rs +++ b/src/rust/src/x509/certificate.rs @@ -42,6 +42,7 @@ pub(crate) struct Certificate { pub(crate) raw: OwnedCertificate, pub(crate) cached_extensions: pyo3::sync::PyOnceLock>, pub(crate) cached_issuer: pyo3::sync::PyOnceLock>, + pub(crate) cached_subject: pyo3::sync::PyOnceLock>, } #[pyo3::pymethods] @@ -152,8 +153,15 @@ impl Certificate { #[getter] fn subject<'p>(&self, py: pyo3::Python<'p>) -> pyo3::PyResult> { - Ok(x509::parse_name(py, self.raw.borrow_dependent().subject()) - .map_err(|e| e.add_location(asn1::ParseLocation::Field("subject")))?) + Ok(self + .cached_subject + .get_or_try_init(py, || { + x509::parse_name(py, self.raw.borrow_dependent().subject()) + .map_err(|e| e.add_location(asn1::ParseLocation::Field("subject"))) + .map(|v| v.unbind()) + })? + .bind(py) + .clone()) } #[getter] @@ -450,6 +458,7 @@ pub(crate) fn load_der_x509_certificate( raw, cached_extensions: pyo3::sync::PyOnceLock::new(), cached_issuer: pyo3::sync::PyOnceLock::new(), + cached_subject: pyo3::sync::PyOnceLock::new(), }) } diff --git a/src/rust/src/x509/ocsp_resp.rs b/src/rust/src/x509/ocsp_resp.rs index 4322f3710f56..6acb0970f85b 100644 --- a/src/rust/src/x509/ocsp_resp.rs +++ b/src/rust/src/x509/ocsp_resp.rs @@ -266,6 +266,7 @@ impl OCSPResponse { raw: raw_cert, cached_extensions: pyo3::sync::PyOnceLock::new(), cached_issuer: pyo3::sync::PyOnceLock::new(), + cached_subject: pyo3::sync::PyOnceLock::new(), }, )?)?; } From 09260bac2732fbbd7d68591cc9c9f11cfd967d70 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Sun, 8 Mar 2026 09:31:35 +0200 Subject: [PATCH 3/3] tests/x509: fix test_country_jurisdiction_country_too_long after subject caching The test assumed cert.subject re-parses the Name on every call, so it checked each too-long-country warning in its own pytest.warns block. After subject caching, parse_name runs only once (on the first access) and emits both COUNTRY_NAME and JURISDICTION_COUNTRY_NAME warnings in a single call. Subsequent accesses return the cached Name object without re-parsing, so the second block saw no warnings. Merge both assertions into a single pytest.warns block, which correctly captures all warnings emitted during the first (and only) parse. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Alexander Bokovoy --- tests/x509/test_x509.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py index 3be4a3162171..5d69e5358eae 100644 --- a/tests/x509/test_x509.py +++ b/tests/x509/test_x509.py @@ -1028,6 +1028,10 @@ def test_country_jurisdiction_country_too_long(self, backend): os.path.join("x509", "custom", "bad_country.pem"), x509.load_pem_x509_certificate, ) + # Both warnings are emitted during the first parse_name call (when + # cert.subject is first accessed); subsequent accesses return the + # cached Name object without re-parsing, so both checks must live + # inside a single pytest.warns block. with pytest.warns(UserWarning): assert ( cert.subject.get_attributes_for_oid(x509.NameOID.COUNTRY_NAME)[ @@ -1035,8 +1039,6 @@ def test_country_jurisdiction_country_too_long(self, backend): ].value == "too long" ) - - with pytest.warns(UserWarning): assert ( cert.subject.get_attributes_for_oid( x509.NameOID.JURISDICTION_COUNTRY_NAME