Skip to main content

_synta/
certificate.rs

1//! Python bindings for X.509 and related PKI types.
2//!
3//! Exposes [`PyCertificate`], [`PyCsr`], [`PyCrl`], and [`PyOcspResponse`] as
4//! pyo3 classes, along with OID-lookup helper functions and the
5//! ``synta.oids`` / ``synta.oids.attr`` submodules.
6//!
7//! This module is part of the `synta-python` cdylib.  Call [`register_module`]
8//! from the `#[pymodule]` entry point to expose these types to Python.
9
10use std::str::FromStr;
11use std::sync::OnceLock;
12
13use pyo3::prelude::*;
14use pyo3::types::{PyBytes, PyList, PyString};
15use pyo3::PyClass;
16
17use synta::traits::Encode;
18use synta::{Decoder, Encoding, ObjectIdentifier};
19use synta_certificate::{Certificate, Time};
20
21use crate::types::PyObjectIdentifier;
22
23/// Re-encode an ASN.1 `Element` to DER bytes, returning `None` for a missing
24/// optional field.  Used by the algorithm-parameter getters.
25fn encode_element_opt<'py>(
26    py: Python<'py>,
27    elem: Option<&synta::Element<'_>>,
28) -> PyResult<Option<Bound<'py, PyBytes>>> {
29    match elem {
30        None => Ok(None),
31        Some(e) => {
32            let mut encoder = synta::Encoder::new(Encoding::Der);
33            e.encode(&mut encoder)
34                .map_err(|err| pyo3::exceptions::PyValueError::new_err(format!("{err}")))?;
35            let bytes = encoder
36                .finish()
37                .map_err(|err| pyo3::exceptions::PyValueError::new_err(format!("{err}")))?;
38            Ok(Some(PyBytes::new(py, &bytes)))
39        }
40    }
41}
42
43/// Decode a raw DER Name SEQUENCE and format it as an RFC 4514-style string.
44fn decode_name_debug(raw: &[u8]) -> String {
45    synta_certificate::name::format_dn(raw)
46}
47
48/// Accept either a Python ``str`` (dotted-decimal OID) or an
49/// ``ObjectIdentifier`` object and return the underlying Rust value.
50fn oid_from_pyany(obj: &Bound<'_, PyAny>) -> PyResult<ObjectIdentifier> {
51    if let Ok(oid_ref) = obj.extract::<PyRef<PyObjectIdentifier>>() {
52        return Ok(oid_ref.inner.clone());
53    }
54    if let Ok(s) = obj.extract::<String>() {
55        return ObjectIdentifier::from_str(&s)
56            .map_err(|_| pyo3::exceptions::PyValueError::new_err(format!("invalid OID: {s}")));
57    }
58    Err(pyo3::exceptions::PyTypeError::new_err(
59        "oid must be a str or ObjectIdentifier",
60    ))
61}
62
63/// Shared implementation for every `from_pem` static method.
64///
65/// Decodes all PEM blocks in `data` and constructs a Python object for each
66/// by calling `make_obj(py, der_bytes)`.  The closure is responsible for
67/// parsing the DER bytes and wrapping the result as a `Bound<'py, PyAny>`;
68/// this lets the caller use concrete types (with their full `Py::new` bounds)
69/// without any generic `PyClass` constraint here.
70///
71/// Returns a single object for one block, a `list` for multiple blocks, and
72/// raises `ValueError` when no block is found.
73fn pem_blocks_to_pyobject<'py, F>(
74    py: Python<'py>,
75    data: &[u8],
76    make_obj: F,
77) -> PyResult<Bound<'py, PyAny>>
78where
79    F: for<'a> Fn(Python<'py>, Bound<'a, PyBytes>) -> PyResult<Bound<'py, PyAny>>,
80{
81    let blocks = synta_certificate::pem_blocks(data);
82    match blocks.len() {
83        0 => Err(pyo3::exceptions::PyValueError::new_err(
84            "no PEM block found in input",
85        )),
86        1 => make_obj(py, PyBytes::new(py, &blocks[0].1)),
87        _ => {
88            let list = PyList::empty(py);
89            for (_, der) in &blocks {
90                list.append(make_obj(py, PyBytes::new(py, der))?)?;
91            }
92            Ok(list.into_any())
93        }
94    }
95}
96
97/// Shared implementation for every `to_pem` static method.
98///
99/// Accepts either a single Python object of type `T` or a `list` of them,
100/// serialises each to a PEM block labelled `label`, and returns the
101/// concatenated bytes.  The `get_der` closure extracts the raw DER bytes from
102/// a Rust reference to `T`.
103///
104/// Using a closure (rather than a trait method) avoids requiring `T: PyClass`
105/// with internal PyO3 initialiser bounds while still letting the caller use
106/// concrete types.
107fn pyobject_to_pem<'py, T, D>(
108    py: Python<'py>,
109    label: &str,
110    obj_or_list: &Bound<'_, PyAny>,
111    get_der: D,
112) -> PyResult<Bound<'py, PyBytes>>
113where
114    T: PyClass,
115    D: for<'r> Fn(&'r T) -> &'r [u8],
116{
117    let mut pem: Vec<u8> = Vec::new();
118    if let Ok(list) = obj_or_list.cast::<PyList>() {
119        for item in list.iter() {
120            let bound_t = item.cast::<T>().map_err(|_| {
121                pyo3::exceptions::PyTypeError::new_err(format!(
122                    "list items must be {label} objects"
123                ))
124            })?;
125            let borrow = bound_t.borrow();
126            pem.extend_from_slice(&synta_certificate::der_to_pem(label, get_der(&borrow)));
127        }
128    } else {
129        let bound_t = obj_or_list.cast::<T>().map_err(|_| {
130            pyo3::exceptions::PyTypeError::new_err(format!(
131                "expected a {label} object or list[{label}]"
132            ))
133        })?;
134        let borrow = bound_t.borrow();
135        pem.extend_from_slice(&synta_certificate::der_to_pem(label, get_der(&borrow)));
136    }
137    Ok(PyBytes::new(py, &pem))
138}
139
140/// X.509 Certificate accessible from Python.
141///
142/// Example (inside a Python extension that called `register_module`):
143///
144/// ```python
145/// cert = Certificate.from_der(open("cert.der", "rb").read())
146/// print(cert.subject)
147/// ```
148#[pyclass(frozen, name = "Certificate")]
149pub struct PyCertificate {
150    // Holds a strong reference to the Python bytes object that backs the
151    // DER data.  The CPython refcount prevents the underlying bytes buffer
152    // from being freed for the full lifetime of this struct.  After the
153    // struct is collected by Python's GC, `_data` drops (in Rust's
154    // declaration order) and decrements the refcount; by that point no
155    // Rust code can hold a borrow of this struct, so no read of `raw`
156    // can occur through `&self` after drop begins.
157    _data: Py<PyBytes>,
158    // Raw slice pointing into `_data`'s buffer.  SAFETY: valid as long as
159    // `_data` is alive; CPython bytes objects have a fixed-address buffer
160    // that is never relocated.  Stored here so the OnceLock init closure in
161    // `cert()` can access the bytes without requiring a GIL token.
162    raw: &'static [u8],
163    // Full decoded certificate, heap-allocated and initialised lazily on first
164    // field access.  `Box` keeps the 568-byte `Certificate` off the struct so
165    // that `PyCertificate` stays within CPython's 512-byte `pymalloc`
166    // threshold (after adding the Python object header).  Allocating through
167    // pymalloc instead of the system allocator is roughly 3× faster, which is
168    // measurable at parse-only speeds.  The full recursive decode is deferred
169    // so that parse-only workloads pay only the shallow envelope-scan cost.
170    inner: OnceLock<Box<Certificate<'static>>>,
171    // Byte range within `_data` covering the complete TBSCertificate TLV.
172    tbs_range: std::ops::Range<usize>,
173    // Lazily-computed Python objects.  Each field is initialised on first
174    // Python access and returned via clone_ref on every subsequent call,
175    // avoiding repeated allocation and PyO3 string construction.
176    issuer_cache: OnceLock<Py<PyString>>,
177    subject_cache: OnceLock<Py<PyString>>,
178    signature_algorithm_cache: OnceLock<Py<PyString>>,
179    not_before_cache: OnceLock<Py<PyString>>,
180    not_after_cache: OnceLock<Py<PyString>>,
181    public_key_algorithm_cache: OnceLock<Py<PyString>>,
182    serial_number_cache: OnceLock<Py<PyAny>>,
183    signature_value_cache: OnceLock<Py<PyBytes>>,
184    public_key_cache: OnceLock<Py<PyBytes>>,
185    tbs_bytes_cache: OnceLock<Py<PyBytes>>,
186    signature_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
187    public_key_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
188    signature_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
189    public_key_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
190    extensions_der_cache: OnceLock<Option<Py<PyBytes>>>,
191    issuer_raw_der_cache: OnceLock<Py<PyBytes>>,
192    subject_raw_der_cache: OnceLock<Py<PyBytes>>,
193}
194
195impl PyCertificate {
196    /// Return the fully-decoded certificate, triggering a full recursive
197    /// `Certificate::decode()` on the first call.
198    ///
199    /// The result is cached in `inner` so subsequent calls are a single
200    /// atomic load.  Returns `Err(PyValueError)` if the full decode fails
201    /// (e.g. malformed inner content that passed the shallow envelope scan
202    /// in `from_der`).  Unlike a panic, this surfaces as a catchable Python
203    /// `ValueError` rather than an uncatchable `PanicException`.
204    fn cert(&self) -> PyResult<&Certificate<'static>> {
205        if let Some(v) = self.inner.get() {
206            return Ok(v.as_ref());
207        }
208        let mut decoder = Decoder::new(self.raw, Encoding::Der);
209        let decoded = decoder.decode().map_err(|e| {
210            pyo3::exceptions::PyValueError::new_err(format!("Certificate DER decode failed: {e}"))
211        })?;
212        let _ = self.inner.set(Box::new(decoded));
213        Ok(self.inner.get().unwrap().as_ref())
214    }
215}
216
217#[pymethods]
218impl PyCertificate {
219    /// Parse a DER-encoded X.509 certificate.
220    #[staticmethod]
221    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
222        // Store the Python bytes object directly — no copy of the DER data.
223        let py_bytes = data.unbind();
224
225        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
226        // keeps the Python bytes object alive for the lifetime of this struct.
227        // CPython's bytes objects have a fixed-address, non-relocating payload
228        // buffer (CPython has no moving GC for bytes objects).  The slice
229        // lifetime is extended to 'static; the actual safety invariants are:
230        //   (1) All reads of `raw` go through `&self` (a borrow of the struct).
231        //       No borrow of the struct can outlive the struct, so `raw` is
232        //       never read after the struct begins dropping.
233        //   (2) `raw: &'static [u8]` has no destructor (it is a fat pointer
234        //       with no heap allocation), so Rust dropping `_data` before `raw`
235        //       (fields drop in declaration order) does not cause a
236        //       use-after-free during the drop sequence itself.
237        //   (3) `inner` contains `Certificate<'static>` references into the
238        //       buffer; dropping `Box<Certificate<'static>>` frees the box
239        //       allocation but does not read through the contained &'static
240        //       slices (borrows have no destructors in Rust).
241        // CPython-only: this pattern does not hold for PyPy or GraalPy,
242        // which may relocate or compact heap objects.
243        let raw: &'static [u8] = unsafe {
244            let s = py_bytes.bind(py).as_bytes();
245            std::slice::from_raw_parts(s.as_ptr(), s.len())
246        };
247
248        // Shallow structural scan: validate the Certificate SEQUENCE envelope
249        // and locate the TBSCertificate TLV range.  This is ~4 decoder
250        // operations — roughly 10× faster than a full Certificate::decode().
251        // The full decode is deferred to the first field access via `cert()`.
252        //
253        // Certificate ::= SEQUENCE {
254        //     tbsCertificate   TBSCertificate,   -- child 0: SEQUENCE
255        //     signatureAlgorithm AlgorithmIdentifier, -- child 1: SEQUENCE
256        //     signature        BIT STRING            -- child 2
257        // }
258        let tbs_range = {
259            let mut d = Decoder::new(raw, Encoding::Der);
260            // Outer Certificate SEQUENCE tag + length.
261            d.read_tag()
262                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
263            d.read_length()
264                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
265            // TBSCertificate: record the full TLV range (tag + length + content).
266            let tbs_start = d.position();
267            d.read_tag()
268                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
269            let tbs_len = d
270                .read_length()
271                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
272            let tbs_content_len = tbs_len
273                .definite()
274                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
275            tbs_start..(d.position() + tbs_content_len)
276        };
277
278        Ok(Self {
279            _data: py_bytes,
280            raw,
281            inner: OnceLock::new(),
282            tbs_range,
283            issuer_cache: OnceLock::new(),
284            subject_cache: OnceLock::new(),
285            signature_algorithm_cache: OnceLock::new(),
286            not_before_cache: OnceLock::new(),
287            not_after_cache: OnceLock::new(),
288            public_key_algorithm_cache: OnceLock::new(),
289            serial_number_cache: OnceLock::new(),
290            signature_value_cache: OnceLock::new(),
291            public_key_cache: OnceLock::new(),
292            tbs_bytes_cache: OnceLock::new(),
293            signature_algorithm_oid_cache: OnceLock::new(),
294            public_key_algorithm_oid_cache: OnceLock::new(),
295            signature_algorithm_params_cache: OnceLock::new(),
296            public_key_algorithm_params_cache: OnceLock::new(),
297            extensions_der_cache: OnceLock::new(),
298            issuer_raw_der_cache: OnceLock::new(),
299            subject_raw_der_cache: OnceLock::new(),
300        })
301    }
302
303    /// Parse a DER-encoded X.509 certificate and perform a full RFC 5280 decode immediately.
304    ///
305    /// Unlike :meth:`from_der`, which performs only a shallow 4-operation
306    /// envelope scan and defers the full :class:`Certificate` decode to the
307    /// first field access, this method triggers the complete recursive
308    /// ``Certificate::decode()`` at construction time.
309    ///
310    /// Use this when you need all fields to be available without any
311    /// lazy-decode latency on the first getter call, or when benchmarking the
312    /// true full-parse cost that is comparable to Criterion's ``rust_typed``
313    /// numbers.
314    ///
315    /// ```python
316    /// # Full parse happens here — no deferred work on first field access.
317    /// cert = Certificate.full_from_der(der)
318    /// print(cert.issuer)   # warm path only
319    /// ```
320    #[staticmethod]
321    fn full_from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
322        let cert = Self::from_der(py, data)?;
323        // Prime the OnceLock: runs the full Certificate::decode() now.
324        cert.cert()?;
325        Ok(cert)
326    }
327
328    /// Parse a PEM-encoded X.509 certificate.
329    ///
330    /// Strips the ``-----BEGIN CERTIFICATE-----`` / ``-----END CERTIFICATE-----``
331    /// boundary lines, decodes the base64 body, and calls :meth:`from_der`.
332    /// No external dependencies are required — the decoder is implemented in
333    /// pure Rust inside ``synta-certificate``.
334    ///
335    /// Returns a single :class:`Certificate` when the input contains exactly
336    /// one PEM block.  When multiple blocks are present (e.g. a certificate
337    /// chain file), returns a :class:`list` of :class:`Certificate` objects in
338    /// the order they appear.  Raises :exc:`ValueError` if no PEM block is
339    /// found.
340    ///
341    /// ```python
342    /// # Single certificate — returns Certificate directly.
343    /// cert = Certificate.from_pem(open("cert.pem", "rb").read())
344    /// print(cert.subject)
345    ///
346    /// # Certificate chain — returns list[Certificate].
347    /// chain = Certificate.from_pem(open("chain.pem", "rb").read())
348    /// for cert in chain:
349    ///     print(cert.subject)
350    /// ```
351    #[staticmethod]
352    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
353        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
354            let obj = Self::from_der(py, bytes)?;
355            Ok(Py::new(py, obj)?.into_bound(py).into_any())
356        })
357    }
358
359    /// Convert to a ``cryptography.x509.Certificate`` (PyCA) object.
360    ///
361    /// Passes the original DER bytes directly to
362    /// ``cryptography.x509.load_der_x509_certificate``; no re-encoding is
363    /// performed.  Requires the ``cryptography`` package to be installed.
364    ///
365    /// ```python
366    /// synta_cert = synta.Certificate.from_der(der)
367    /// pyca_cert  = synta_cert.to_pyca()
368    /// # Full cryptographic operations are now available via PyCA:
369    /// pyca_cert.public_key().verify(signature, message, ...)
370    /// ```
371    fn to_pyca<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
372        let m = py.import("cryptography.x509").map_err(|_| {
373            pyo3::exceptions::PyImportError::new_err(
374                "the 'cryptography' package is required; install it with: pip install cryptography",
375            )
376        })?;
377        m.call_method1(
378            "load_der_x509_certificate",
379            (self._data.clone_ref(py).into_bound(py),),
380        )
381    }
382
383    /// Construct a ``Certificate`` from a ``cryptography.x509.Certificate``.
384    ///
385    /// Serialises the PyCA certificate to DER via
386    /// ``cert.public_bytes(Encoding.DER)`` and parses the result with
387    /// :meth:`from_der`.  Requires the ``cryptography`` package to be
388    /// installed.
389    ///
390    /// **Fast path** — if the object exposes a ``_synta_der_bytes`` attribute
391    /// whose value is a ``bytes`` object, that buffer is used directly and
392    /// the ``public_bytes()`` call is skipped entirely.  Wrapper classes that
393    /// already hold the raw DER (e.g. an ``IPACertificate`` loaded from LDAP)
394    /// can opt in by setting the attribute at construction time:
395    ///
396    /// ```python
397    /// class IPACertificate:
398    ///     def __init__(self, der: bytes):
399    ///         self._synta_der_bytes = der          # enables fast path
400    ///         self._pyca = x509.load_der_x509_certificate(der)
401    ///
402    /// # Zero re-encoding cost — DER is read from _synta_der_bytes directly:
403    /// synta_cert = synta.Certificate.from_pyca(ipa_cert)
404    /// ```
405    ///
406    /// Without the attribute the standard path is used:
407    ///
408    /// ```python
409    /// pyca_cert  = cryptography.x509.load_pem_x509_certificate(pem)
410    /// synta_cert = synta.Certificate.from_pyca(pyca_cert)
411    /// ```
412    #[staticmethod]
413    fn from_pyca(py: Python<'_>, pyca_cert: Bound<'_, PyAny>) -> PyResult<Self> {
414        // Optional fast path: if the caller has cached raw DER bytes on the
415        // object under the `_synta_der_bytes` attribute, use that directly to
416        // avoid the re-encoding cost of `public_bytes(Encoding.DER)`.  This
417        // lets wrappers like FreeIPA's `IPACertificate` opt in by setting
418        // `self._synta_der_bytes = der` at construction time.
419        if let Ok(attr) = pyca_cert.getattr("_synta_der_bytes") {
420            if let Ok(der_bytes) = attr.cast_into::<PyBytes>() {
421                return Self::from_der(py, der_bytes);
422            }
423        }
424
425        let ser = py
426            .import("cryptography.hazmat.primitives.serialization")
427            .map_err(|_| {
428                pyo3::exceptions::PyImportError::new_err(
429                    "the 'cryptography' package is required; install it with: pip install cryptography",
430                )
431            })?;
432        let der_bytes: Bound<'_, PyBytes> = pyca_cert
433            .call_method1("public_bytes", (ser.getattr("Encoding")?.getattr("DER")?,))?
434            .cast_into()?;
435        Self::from_der(py, der_bytes)
436    }
437
438    /// Serialize one certificate or a list of certificates to PEM format.
439    ///
440    /// The mirror of :meth:`from_pem`: accepts either a single
441    /// :class:`Certificate` or a :class:`list` of them, and returns a
442    /// :class:`bytes` value containing one or more
443    /// ``-----BEGIN CERTIFICATE-----`` blocks concatenated in order.
444    ///
445    /// ```python
446    /// # Single certificate:
447    /// pem = Certificate.to_pem(cert)
448    /// open("cert.pem", "wb").write(pem)
449    ///
450    /// # Certificate chain:
451    /// pem = Certificate.to_pem([leaf, intermediate, root])
452    /// open("chain.pem", "wb").write(pem)
453    /// ```
454    #[staticmethod]
455    fn to_pem<'py>(
456        py: Python<'py>,
457        obj_or_list: Bound<'_, PyAny>,
458    ) -> PyResult<Bound<'py, PyBytes>> {
459        pyobject_to_pem::<Self, _>(py, "CERTIFICATE", &obj_or_list, |c| c.raw)
460    }
461
462    /// Serial number as a Python int.
463    ///
464    /// Returns a native Python `int` regardless of size (X.509 serials can be
465    /// up to 20 bytes / 160 bits per RFC 5280).  The Python object is created
466    /// once and cached; subsequent accesses return a reference to the same object.
467    #[getter]
468    fn serial_number<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
469        // `get_or_try_init` is not stable on `OnceLock`, so build the value
470        // outside the lock if the cache is empty, then store it.
471        if let Some(cached) = self.serial_number_cache.get() {
472            return Ok(cached.clone_ref(py).into_bound(py));
473        }
474        let serial = &self.cert()?.tbs_certificate.serial_number;
475        let py_int: Py<PyAny> = if let Ok(v) = serial.as_i64() {
476            // Fast path: fits in i64 (covers traditional short serials)
477            v.into_pyobject(py)?.into_any().unbind()
478        } else if let Ok(v) = serial.as_i128() {
479            // Fits in i128 (covers up to 16-byte random serials)
480            v.into_pyobject(py)?.into_any().unbind()
481        } else {
482            // Large serial (17–20 bytes): call int.from_bytes() directly.
483            // Using call_method avoids the parse+compile overhead of py.eval()
484            // on every cold-path invocation.  `signed` is keyword-only in
485            // Python's int.from_bytes signature, so it must go in kwargs.
486            let bytes_obj = PyBytes::new(py, serial.as_bytes());
487            let kwargs = pyo3::types::PyDict::new(py);
488            kwargs.set_item(pyo3::intern!(py, "signed"), true)?;
489            py.get_type::<pyo3::types::PyInt>()
490                .call_method(
491                    pyo3::intern!(py, "from_bytes"),
492                    (bytes_obj, pyo3::intern!(py, "big")),
493                    Some(&kwargs),
494                )
495                .map(|r| r.unbind())?
496        };
497        // Ignore a racing writer; both produce the same value.
498        let _ = self.serial_number_cache.set(py_int.clone_ref(py));
499        Ok(py_int.into_bound(py))
500    }
501
502    /// Issuer distinguished name as a string.
503    #[getter]
504    fn issuer<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
505        if let Some(cached) = self.issuer_cache.get() {
506            return Ok(cached.clone_ref(py).into_bound(py));
507        }
508        let s = decode_name_debug(self.cert()?.tbs_certificate.issuer.as_bytes());
509        let py_str = PyString::new(py, &s).unbind();
510        let _ = self.issuer_cache.set(py_str.clone_ref(py));
511        Ok(py_str.into_bound(py))
512    }
513
514    /// Subject distinguished name as a string.
515    #[getter]
516    fn subject<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
517        if let Some(cached) = self.subject_cache.get() {
518            return Ok(cached.clone_ref(py).into_bound(py));
519        }
520        let s = decode_name_debug(self.cert()?.tbs_certificate.subject.as_bytes());
521        let py_str = PyString::new(py, &s).unbind();
522        let _ = self.subject_cache.set(py_str.clone_ref(py));
523        Ok(py_str.into_bound(py))
524    }
525
526    /// Signature algorithm name (e.g. "RSA", "ECDSA", "Ed25519", "ML-DSA-65"),
527    /// or the dotted OID notation for unrecognized algorithms.
528    #[getter]
529    fn signature_algorithm<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
530        if let Some(cached) = self.signature_algorithm_cache.get() {
531            return Ok(cached.clone_ref(py).into_bound(py));
532        }
533        let oid = &self.cert()?.signature_algorithm.algorithm;
534        let name = synta_certificate::identify_signature_algorithm(oid);
535        let s = if name != "Other" {
536            name.to_string()
537        } else {
538            oid.to_string()
539        };
540        let py_str = PyString::new(py, &s).unbind();
541        let _ = self.signature_algorithm_cache.set(py_str.clone_ref(py));
542        Ok(py_str.into_bound(py))
543    }
544
545    /// Raw signature bytes.
546    ///
547    /// The `bytes` object is created once on first access and the same Python
548    /// object is returned (by reference) on every subsequent call.
549    #[getter]
550    fn signature_value<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
551        if let Some(cached) = self.signature_value_cache.get() {
552            return Ok(cached.clone_ref(py).into_bound(py));
553        }
554        let py_bytes = PyBytes::new(py, self.cert()?.signature_value.as_bytes()).unbind();
555        let _ = self.signature_value_cache.set(py_bytes.clone_ref(py));
556        Ok(py_bytes.into_bound(py))
557    }
558
559    /// notBefore time as a string.
560    #[getter]
561    fn not_before<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
562        if let Some(cached) = self.not_before_cache.get() {
563            return Ok(cached.clone_ref(py).into_bound(py));
564        }
565        let s = match &self.cert()?.tbs_certificate.validity.not_before {
566            Time::UtcTime(t) => t.to_string(),
567            Time::GeneralTime(t) => t.to_string(),
568        };
569        let py_str = PyString::new(py, &s).unbind();
570        let _ = self.not_before_cache.set(py_str.clone_ref(py));
571        Ok(py_str.into_bound(py))
572    }
573
574    /// notAfter time as a string.
575    #[getter]
576    fn not_after<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
577        if let Some(cached) = self.not_after_cache.get() {
578            return Ok(cached.clone_ref(py).into_bound(py));
579        }
580        let s = match &self.cert()?.tbs_certificate.validity.not_after {
581            Time::UtcTime(t) => t.to_string(),
582            Time::GeneralTime(t) => t.to_string(),
583        };
584        let py_str = PyString::new(py, &s).unbind();
585        let _ = self.not_after_cache.set(py_str.clone_ref(py));
586        Ok(py_str.into_bound(py))
587    }
588
589    /// Subject public-key algorithm name (e.g. "RSA", "ECDSA", "Ed25519", "ML-DSA-65"),
590    /// or the dotted OID notation for unrecognized algorithms.
591    #[getter]
592    fn public_key_algorithm<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
593        if let Some(cached) = self.public_key_algorithm_cache.get() {
594            return Ok(cached.clone_ref(py).into_bound(py));
595        }
596        let oid = &self
597            .cert()?
598            .tbs_certificate
599            .subject_public_key_info
600            .algorithm
601            .algorithm;
602        let s = synta_certificate::identify_public_key_algorithm(oid)
603            .map(|s| s.to_string())
604            .unwrap_or_else(|| oid.to_string());
605        let py_str = PyString::new(py, &s).unbind();
606        let _ = self.public_key_algorithm_cache.set(py_str.clone_ref(py));
607        Ok(py_str.into_bound(py))
608    }
609
610    /// Raw subject public-key bytes.
611    ///
612    /// The `bytes` object is created once on first access and the same Python
613    /// object is returned (by reference) on every subsequent call.
614    #[getter]
615    fn public_key<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
616        if let Some(cached) = self.public_key_cache.get() {
617            return Ok(cached.clone_ref(py).into_bound(py));
618        }
619        let py_bytes = PyBytes::new(
620            py,
621            self.cert()?
622                .tbs_certificate
623                .subject_public_key_info
624                .subject_public_key
625                .as_bytes(),
626        )
627        .unbind();
628        let _ = self.public_key_cache.set(py_bytes.clone_ref(py));
629        Ok(py_bytes.into_bound(py))
630    }
631
632    /// Version field (0 = v1, 1 = v2, 2 = v3), or None if absent.
633    #[getter]
634    fn version(&self) -> PyResult<Option<i64>> {
635        Ok(self
636            .cert()?
637            .tbs_certificate
638            .version
639            .as_ref()
640            .and_then(|v| v.as_i64().ok()))
641    }
642
643    /// Complete DER encoding of this certificate (the original bytes passed to
644    /// ``from_der``).
645    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
646        self._data.clone_ref(py).into_bound(py)
647    }
648
649    /// Raw DER bytes of the TBSCertificate structure (the bytes that were
650    /// signed by the issuer's key).
651    #[getter]
652    fn tbs_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
653        self.tbs_bytes_cache
654            .get_or_init(|| {
655                let data = self._data.bind(py).as_bytes();
656                PyBytes::new(py, &data[self.tbs_range.clone()]).unbind()
657            })
658            .clone_ref(py)
659            .into_bound(py)
660    }
661
662    /// OID of the signature algorithm
663    /// (e.g. ``ObjectIdentifier("1.2.840.113549.1.1.11")`` for sha256WithRSAEncryption).
664    ///
665    /// Unlike ``signature_algorithm``, this always returns the machine-readable
666    /// OID, even for algorithms that synta does not recognise by name.
667    #[getter]
668    fn signature_algorithm_oid<'py>(
669        &self,
670        py: Python<'py>,
671    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
672        if let Some(cached) = self.signature_algorithm_oid_cache.get() {
673            return Ok(cached.clone_ref(py).into_bound(py));
674        }
675        let obj = Py::new(
676            py,
677            PyObjectIdentifier::from_oid(self.cert()?.signature_algorithm.algorithm.clone()),
678        )?;
679        let _ = self.signature_algorithm_oid_cache.set(obj.clone_ref(py));
680        Ok(obj.into_bound(py))
681    }
682
683    /// Raw DER bytes of the signature algorithm parameters, or ``None`` if
684    /// the AlgorithmIdentifier has no parameters field (e.g. Ed25519).
685    #[getter]
686    fn signature_algorithm_params<'py>(
687        &self,
688        py: Python<'py>,
689    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
690        if let Some(cached) = self.signature_algorithm_params_cache.get() {
691            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
692        }
693        let computed =
694            encode_element_opt(py, self.cert()?.signature_algorithm.parameters.as_ref())?;
695        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
696        let _ = self.signature_algorithm_params_cache.set(to_store);
697        Ok(computed)
698    }
699
700    /// OID of the subject public-key algorithm
701    /// (e.g. ``ObjectIdentifier("1.2.840.10045.2.1")`` for id-ecPublicKey).
702    #[getter]
703    fn public_key_algorithm_oid<'py>(
704        &self,
705        py: Python<'py>,
706    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
707        if let Some(cached) = self.public_key_algorithm_oid_cache.get() {
708            return Ok(cached.clone_ref(py).into_bound(py));
709        }
710        let obj = Py::new(
711            py,
712            PyObjectIdentifier::from_oid(
713                self.cert()?
714                    .tbs_certificate
715                    .subject_public_key_info
716                    .algorithm
717                    .algorithm
718                    .clone(),
719            ),
720        )?;
721        let _ = self.public_key_algorithm_oid_cache.set(obj.clone_ref(py));
722        Ok(obj.into_bound(py))
723    }
724
725    /// Raw DER bytes of the public-key algorithm parameters, or ``None``.
726    ///
727    /// For EC keys this is an OID naming the curve (e.g. secp256r1).
728    /// For RSA and Ed25519 this is typically ``None`` or a NULL element.
729    #[getter]
730    fn public_key_algorithm_params<'py>(
731        &self,
732        py: Python<'py>,
733    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
734        if let Some(cached) = self.public_key_algorithm_params_cache.get() {
735            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
736        }
737        let computed = encode_element_opt(
738            py,
739            self.cert()?
740                .tbs_certificate
741                .subject_public_key_info
742                .algorithm
743                .parameters
744                .as_ref(),
745        )?;
746        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
747        let _ = self.public_key_algorithm_params_cache.set(to_store);
748        Ok(computed)
749    }
750
751    /// Raw DER bytes of the extensions SEQUENCE OF, or ``None`` for v1/v2
752    /// certificates that carry no extensions.
753    ///
754    /// The returned bytes begin with the SEQUENCE tag (``0x30``) and contain
755    /// the full SEQUENCE OF Extension encoding.  Pass them to a ``Decoder``
756    /// to iterate the individual extensions:
757    ///
758    /// ```python
759    /// ext_der = cert.extensions_der
760    /// if ext_der:
761    ///     dec = synta.Decoder(ext_der, synta.Encoding.DER)
762    ///     exts_dec = dec.decode_sequence()
763    ///     while not exts_dec.is_empty():
764    ///         ext_tlv = exts_dec.decode_raw_tlv()
765    /// ```
766    #[getter]
767    fn extensions_der<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
768        if let Some(cached) = self.extensions_der_cache.get() {
769            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
770        }
771        let computed = self
772            .cert()?
773            .tbs_certificate
774            .extensions
775            .as_ref()
776            .map(|raw: &synta::RawDer<'_>| PyBytes::new(py, raw.as_bytes()));
777        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
778        let _ = self.extensions_der_cache.set(to_store);
779        Ok(computed)
780    }
781
782    /// Return the DER content of a named extension's value, or ``None``.
783    ///
784    /// Searches the certificate's extension list for an extension whose
785    /// ``extnID`` equals *oid* (dotted-decimal notation, e.g.
786    /// ``"2.5.29.17"`` for SubjectAltName).  When found, the bytes inside
787    /// the ``extnValue`` OCTET STRING — the DER-encoded extension-specific
788    /// structure — are returned.  Returns ``None`` when the certificate
789    /// carries no extensions or the named OID is absent.
790    ///
791    /// Example — parse SubjectAltName:
792    ///
793    /// ```python
794    ///     san_der = cert.get_extension_value_der("2.5.29.17")
795    ///     if san_der:
796    ///         dec = synta.Decoder(san_der, synta.Encoding.DER)
797    ///         san_seq = dec.decode_sequence()
798    ///         while not san_seq.is_empty():
799    ///             tag_num, tag_class, _ = san_seq.peek_tag()
800    ///             child = san_seq.decode_implicit_tag(tag_num, tag_class)
801    ///             if tag_num == 2:  # dNSName
802    ///                 print(child.remaining_bytes().decode("ascii"))
803    /// ```
804    fn get_extension_value_der<'py>(
805        &self,
806        py: Python<'py>,
807        oid: &Bound<'_, PyAny>,
808    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
809        let target = oid_from_pyany(oid)?;
810        let raw = match self.cert()?.tbs_certificate.extensions.as_ref() {
811            Some(r) => r,
812            None => return Ok(None),
813        };
814        for ext in &synta_certificate::decode_extensions(raw.as_bytes()) {
815            if ext.extn_id == target {
816                return Ok(Some(PyBytes::new(py, ext.extn_value.as_bytes())));
817            }
818        }
819        Ok(None)
820    }
821
822    /// Return the Subject Alternative Names of this certificate.
823    ///
824    /// Combines looking up the SAN extension (OID ``2.5.29.17``) and parsing
825    /// its ``GeneralName`` entries into a single call.  Returns a
826    /// :class:`list` of ``(tag_number: int, content: bytes)`` tuples in
827    /// document order; returns an empty list when the certificate carries no
828    /// SAN extension.
829    ///
830    /// Tag numbers follow RFC 5280 §4.2.1.6 — the same convention as
831    /// :func:`~synta.parse_general_names`.  Named constants for every tag are
832    /// available in :mod:`synta.general_name`:
833    ///
834    /// | :attr:`~synta.general_name.RFC822_NAME` (1) | raw IA5String bytes (e-mail) |
835    /// | :attr:`~synta.general_name.DNS_NAME` (2)    | raw IA5String bytes |
836    /// | :attr:`~synta.general_name.DIRECTORY_NAME` (4) | complete Name SEQUENCE TLV |
837    /// | :attr:`~synta.general_name.URI` (6)         | raw IA5String bytes |
838    /// | :attr:`~synta.general_name.IP_ADDRESS` (7)  | 4 bytes (IPv4) or 16 bytes (IPv6) |
839    /// | :attr:`~synta.general_name.REGISTERED_ID` (8) | raw OID value bytes |
840    ///
841    /// ```python,ignore
842    /// import ipaddress
843    /// import synta.general_name as gn
844    ///
845    /// for tag_num, content in cert.subject_alt_names():
846    ///     if tag_num == gn.DNS_NAME:
847    ///         print("DNS:", content.decode("ascii"))
848    ///     elif tag_num == gn.IP_ADDRESS:
849    ///         print("IP:", ipaddress.ip_address(content))
850    ///     elif tag_num == gn.RFC822_NAME:
851    ///         print("email:", content.decode("ascii"))
852    ///     elif tag_num == gn.DIRECTORY_NAME:
853    ///         attrs = synta.parse_name_attrs(content)
854    ///         print("DirName:", attrs)
855    ///     elif tag_num == gn.URI:
856    ///         print("URI:", content.decode("ascii"))
857    /// ```
858    fn subject_alt_names<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
859        use pyo3::types::{PyBytes, PyList, PyTuple};
860        let list = PyList::empty(py);
861        for (tag_num, content) in self.cert()?.subject_alt_names() {
862            let tuple = PyTuple::new(
863                py,
864                [
865                    tag_num.into_pyobject(py)?.into_any(),
866                    PyBytes::new(py, &content).into_any(),
867                ],
868            )?;
869            list.append(tuple)?;
870        }
871        Ok(list)
872    }
873
874    /// Raw DER bytes of the issuer Name SEQUENCE.
875    #[getter]
876    fn issuer_raw_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
877        if let Some(cached) = self.issuer_raw_der_cache.get() {
878            return Ok(cached.clone_ref(py).into_bound(py));
879        }
880        let py_bytes = PyBytes::new(py, self.cert()?.tbs_certificate.issuer.as_bytes()).unbind();
881        let _ = self.issuer_raw_der_cache.set(py_bytes.clone_ref(py));
882        Ok(py_bytes.into_bound(py))
883    }
884
885    /// Raw DER bytes of the subject Name SEQUENCE.
886    #[getter]
887    fn subject_raw_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
888        if let Some(cached) = self.subject_raw_der_cache.get() {
889            return Ok(cached.clone_ref(py).into_bound(py));
890        }
891        let py_bytes = PyBytes::new(py, self.cert()?.tbs_certificate.subject.as_bytes()).unbind();
892        let _ = self.subject_raw_der_cache.set(py_bytes.clone_ref(py));
893        Ok(py_bytes.into_bound(py))
894    }
895
896    fn __repr__(&self) -> PyResult<String> {
897        let cert = self.cert()?;
898        let serial = &cert.tbs_certificate.serial_number;
899        let serial_str = if let Ok(v) = serial.as_i64() {
900            v.to_string()
901        } else {
902            format!("<{} bytes>", serial.as_bytes().len())
903        };
904        Ok(format!(
905            "Certificate(subject={:?}, serial={})",
906            decode_name_debug(cert.tbs_certificate.subject.as_bytes()),
907            serial_str,
908        ))
909    }
910}
911
912// ── Helpers for CSR / CRL (Name is decoded eagerly, not stored as RawDer) ─────
913
914/// Re-encode a parsed `Name` to DER and format it as an RFC 4514 DN string.
915fn name_to_dn_string(name: &synta_certificate::Name<'_>) -> String {
916    let mut enc = synta::Encoder::new(synta::Encoding::Der);
917    if name.encode(&mut enc).is_err() {
918        return String::new();
919    }
920    synta_certificate::name::format_dn(&enc.finish().unwrap_or_default())
921}
922
923/// Re-encode a parsed `Name` to its DER bytes.
924fn name_to_der_bytes(name: &synta_certificate::Name<'_>) -> Vec<u8> {
925    let mut enc = synta::Encoder::new(synta::Encoding::Der);
926    if name.encode(&mut enc).is_err() {
927        return Vec::new();
928    }
929    enc.finish().unwrap_or_default()
930}
931
932/// Map an `OCSPResponseStatus` enum value to its RFC 6960 display name.
933fn ocsp_status_str(status: synta_certificate::ocsp::OCSPResponseStatus) -> &'static str {
934    use synta_certificate::ocsp::OCSPResponseStatus::*;
935    match status {
936        Successful => "successful",
937        MalformedRequest => "malformedRequest",
938        InternalError => "internalError",
939        TryLater => "tryLater",
940        SigRequired => "sigRequired",
941        Unauthorized => "unauthorized",
942    }
943}
944
945// ── PyCsr ─────────────────────────────────────────────────────────────────────
946
947/// PKCS #10 Certificate Signing Request accessible from Python.
948///
949/// ```python
950/// csr = CertificationRequest.from_der(open("req.der", "rb").read())
951/// print(csr.subject)
952/// print(csr.public_key_algorithm)
953/// ```
954#[pyclass(frozen, name = "CertificationRequest")]
955pub struct PyCsr {
956    _data: Py<PyBytes>,
957    raw: &'static [u8],
958    inner: OnceLock<Box<synta_certificate::csr::CertificationRequest<'static>>>,
959    subject_cache: OnceLock<Py<PyString>>,
960    subject_raw_der_cache: OnceLock<Py<PyBytes>>,
961    signature_algorithm_cache: OnceLock<Py<PyString>>,
962    signature_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
963    signature_cache: OnceLock<Py<PyBytes>>,
964    public_key_algorithm_cache: OnceLock<Py<PyString>>,
965    public_key_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
966    public_key_cache: OnceLock<Py<PyBytes>>,
967}
968
969impl PyCsr {
970    fn csr(&self) -> PyResult<&synta_certificate::csr::CertificationRequest<'static>> {
971        if let Some(v) = self.inner.get() {
972            return Ok(v.as_ref());
973        }
974        let mut decoder = Decoder::new(self.raw, Encoding::Der);
975        let decoded = decoder.decode().map_err(|e| {
976            pyo3::exceptions::PyValueError::new_err(format!(
977                "CertificationRequest DER decode failed: {e}"
978            ))
979        })?;
980        let _ = self.inner.set(Box::new(decoded));
981        Ok(self.inner.get().unwrap().as_ref())
982    }
983}
984
985#[pymethods]
986impl PyCsr {
987    /// Parse a DER-encoded PKCS #10 Certificate Signing Request.
988    #[staticmethod]
989    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
990        let py_bytes = data.unbind();
991        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
992        // keeps the Python bytes object alive for the lifetime of this struct.
993        // CPython's bytes objects have a fixed-address, non-relocating payload
994        // buffer (CPython has no moving GC).  The slice lifetime is extended
995        // to 'static; the actual safety invariants are:
996        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
997        //       can outlive the struct, so `raw` is never read after drop begins.
998        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
999        //       allocation), so field drop order does not cause use-after-free.
1000        //   (3) `inner` contains only borrow-typed fields; dropping
1001        //       Box<CertificationRequest<'static>> does not read through the
1002        //       contained &'static slices (borrows have no destructors).
1003        // CPython-only: does not hold for PyPy or GraalPy.
1004        let raw: &'static [u8] = unsafe {
1005            let s = py_bytes.bind(py).as_bytes();
1006            std::slice::from_raw_parts(s.as_ptr(), s.len())
1007        };
1008        // Minimal structural scan: outer SEQUENCE tag + length.
1009        {
1010            let mut d = Decoder::new(raw, Encoding::Der);
1011            d.read_tag()
1012                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1013            d.read_length()
1014                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1015        }
1016        Ok(Self {
1017            _data: py_bytes,
1018            raw,
1019            inner: OnceLock::new(),
1020            subject_cache: OnceLock::new(),
1021            subject_raw_der_cache: OnceLock::new(),
1022            signature_algorithm_cache: OnceLock::new(),
1023            signature_algorithm_oid_cache: OnceLock::new(),
1024            signature_cache: OnceLock::new(),
1025            public_key_algorithm_cache: OnceLock::new(),
1026            public_key_algorithm_oid_cache: OnceLock::new(),
1027            public_key_cache: OnceLock::new(),
1028        })
1029    }
1030
1031    /// Parse a PEM-encoded PKCS #10 Certificate Signing Request.
1032    ///
1033    /// Returns a single :class:`CertificationRequest` for one PEM block, or a
1034    /// :class:`list` of :class:`CertificationRequest` objects when multiple
1035    /// blocks are present.  Raises :exc:`ValueError` if no PEM block is found.
1036    ///
1037    /// ```python
1038    /// csr = CertificationRequest.from_pem(open("csr.pem", "rb").read())
1039    /// print(csr.subject)
1040    /// ```
1041    #[staticmethod]
1042    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
1043        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
1044            let obj = Self::from_der(py, bytes)?;
1045            Ok(Py::new(py, obj)?.into_bound(py).into_any())
1046        })
1047    }
1048
1049    /// Serialize one CSR or a list of CSRs to PEM format.
1050    ///
1051    /// ```python
1052    /// pem = CertificationRequest.to_pem(csr)
1053    /// pem = CertificationRequest.to_pem([csr1, csr2])
1054    /// ```
1055    #[staticmethod]
1056    fn to_pem<'py>(
1057        py: Python<'py>,
1058        obj_or_list: Bound<'_, PyAny>,
1059    ) -> PyResult<Bound<'py, PyBytes>> {
1060        pyobject_to_pem::<Self, _>(py, "CERTIFICATE REQUEST", &obj_or_list, |c| c.raw)
1061    }
1062
1063    /// Subject distinguished name as a string.
1064    #[getter]
1065    fn subject<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
1066        if let Some(cached) = self.subject_cache.get() {
1067            return Ok(cached.clone_ref(py).into_bound(py));
1068        }
1069        let s = name_to_dn_string(&self.csr()?.certification_request_info.subject);
1070        let py_str = PyString::new(py, &s).unbind();
1071        let _ = self.subject_cache.set(py_str.clone_ref(py));
1072        Ok(py_str.into_bound(py))
1073    }
1074
1075    /// Raw DER bytes of the subject Name SEQUENCE.
1076    #[getter]
1077    fn subject_raw_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
1078        if let Some(cached) = self.subject_raw_der_cache.get() {
1079            return Ok(cached.clone_ref(py).into_bound(py));
1080        }
1081        let bytes = name_to_der_bytes(&self.csr()?.certification_request_info.subject);
1082        let py_bytes = PyBytes::new(py, &bytes).unbind();
1083        let _ = self.subject_raw_der_cache.set(py_bytes.clone_ref(py));
1084        Ok(py_bytes.into_bound(py))
1085    }
1086
1087    /// CSR version (0 = v1 per RFC 2986).
1088    #[getter]
1089    fn version(&self) -> PyResult<i64> {
1090        Ok(self
1091            .csr()?
1092            .certification_request_info
1093            .version
1094            .as_i64()
1095            .unwrap_or(0))
1096    }
1097
1098    /// Signature algorithm name (e.g. "RSA", "ECDSA", "Ed25519").
1099    #[getter]
1100    fn signature_algorithm<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
1101        if let Some(cached) = self.signature_algorithm_cache.get() {
1102            return Ok(cached.clone_ref(py).into_bound(py));
1103        }
1104        let oid = &self.csr()?.signature_algorithm.algorithm;
1105        let name = synta_certificate::identify_signature_algorithm(oid);
1106        let s = if name != "Other" {
1107            name.to_string()
1108        } else {
1109            oid.to_string()
1110        };
1111        let py_str = PyString::new(py, &s).unbind();
1112        let _ = self.signature_algorithm_cache.set(py_str.clone_ref(py));
1113        Ok(py_str.into_bound(py))
1114    }
1115
1116    /// OID of the signature algorithm.
1117    #[getter]
1118    fn signature_algorithm_oid<'py>(
1119        &self,
1120        py: Python<'py>,
1121    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
1122        if let Some(cached) = self.signature_algorithm_oid_cache.get() {
1123            return Ok(cached.clone_ref(py).into_bound(py));
1124        }
1125        let obj = Py::new(
1126            py,
1127            PyObjectIdentifier::from_oid(self.csr()?.signature_algorithm.algorithm.clone()),
1128        )?;
1129        let _ = self.signature_algorithm_oid_cache.set(obj.clone_ref(py));
1130        Ok(obj.into_bound(py))
1131    }
1132
1133    /// Raw signature bytes.
1134    #[getter]
1135    fn signature<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
1136        if let Some(cached) = self.signature_cache.get() {
1137            return Ok(cached.clone_ref(py).into_bound(py));
1138        }
1139        let py_bytes = PyBytes::new(py, self.csr()?.signature.as_bytes()).unbind();
1140        let _ = self.signature_cache.set(py_bytes.clone_ref(py));
1141        Ok(py_bytes.into_bound(py))
1142    }
1143
1144    /// Public-key algorithm name (e.g. "RSA", "ECDSA", "Ed25519").
1145    #[getter]
1146    fn public_key_algorithm<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
1147        if let Some(cached) = self.public_key_algorithm_cache.get() {
1148            return Ok(cached.clone_ref(py).into_bound(py));
1149        }
1150        let oid = &self
1151            .csr()?
1152            .certification_request_info
1153            .subject_pkinfo
1154            .algorithm
1155            .algorithm;
1156        let s = synta_certificate::identify_public_key_algorithm(oid)
1157            .map(|s| s.to_string())
1158            .unwrap_or_else(|| oid.to_string());
1159        let py_str = PyString::new(py, &s).unbind();
1160        let _ = self.public_key_algorithm_cache.set(py_str.clone_ref(py));
1161        Ok(py_str.into_bound(py))
1162    }
1163
1164    /// OID of the subject public-key algorithm.
1165    #[getter]
1166    fn public_key_algorithm_oid<'py>(
1167        &self,
1168        py: Python<'py>,
1169    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
1170        if let Some(cached) = self.public_key_algorithm_oid_cache.get() {
1171            return Ok(cached.clone_ref(py).into_bound(py));
1172        }
1173        let obj = Py::new(
1174            py,
1175            PyObjectIdentifier::from_oid(
1176                self.csr()?
1177                    .certification_request_info
1178                    .subject_pkinfo
1179                    .algorithm
1180                    .algorithm
1181                    .clone(),
1182            ),
1183        )?;
1184        let _ = self.public_key_algorithm_oid_cache.set(obj.clone_ref(py));
1185        Ok(obj.into_bound(py))
1186    }
1187
1188    /// Raw subject public-key bytes.
1189    #[getter]
1190    fn public_key<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
1191        if let Some(cached) = self.public_key_cache.get() {
1192            return Ok(cached.clone_ref(py).into_bound(py));
1193        }
1194        let py_bytes = PyBytes::new(
1195            py,
1196            self.csr()?
1197                .certification_request_info
1198                .subject_pkinfo
1199                .subject_public_key
1200                .as_bytes(),
1201        )
1202        .unbind();
1203        let _ = self.public_key_cache.set(py_bytes.clone_ref(py));
1204        Ok(py_bytes.into_bound(py))
1205    }
1206
1207    /// Complete DER encoding of this CSR (the original bytes passed to ``from_der``).
1208    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1209        self._data.clone_ref(py).into_bound(py)
1210    }
1211
1212    fn __repr__(&self) -> PyResult<String> {
1213        Ok(format!(
1214            "CertificationRequest(subject={:?})",
1215            name_to_dn_string(&self.csr()?.certification_request_info.subject),
1216        ))
1217    }
1218}
1219
1220// ── PyCrl ─────────────────────────────────────────────────────────────────────
1221
1222/// X.509 Certificate Revocation List accessible from Python.
1223///
1224/// ```python
1225/// crl = CertificateList.from_der(open("crl.der", "rb").read())
1226/// print(crl.issuer)
1227/// print(crl.revoked_count)
1228/// ```
1229#[pyclass(frozen, name = "CertificateList")]
1230pub struct PyCrl {
1231    _data: Py<PyBytes>,
1232    raw: &'static [u8],
1233    inner: OnceLock<Box<synta_certificate::crl::CertificateList<'static>>>,
1234    issuer_cache: OnceLock<Py<PyString>>,
1235    issuer_raw_der_cache: OnceLock<Py<PyBytes>>,
1236    this_update_cache: OnceLock<Py<PyString>>,
1237    next_update_cache: OnceLock<Option<Py<PyString>>>,
1238    signature_algorithm_cache: OnceLock<Py<PyString>>,
1239    signature_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
1240    signature_value_cache: OnceLock<Py<PyBytes>>,
1241}
1242
1243impl PyCrl {
1244    fn crl(&self) -> PyResult<&synta_certificate::crl::CertificateList<'static>> {
1245        if let Some(v) = self.inner.get() {
1246            return Ok(v.as_ref());
1247        }
1248        let mut decoder = Decoder::new(self.raw, Encoding::Der);
1249        let decoded = decoder.decode().map_err(|e| {
1250            pyo3::exceptions::PyValueError::new_err(format!(
1251                "CertificateList DER decode failed: {e}"
1252            ))
1253        })?;
1254        let _ = self.inner.set(Box::new(decoded));
1255        Ok(self.inner.get().unwrap().as_ref())
1256    }
1257}
1258
1259#[pymethods]
1260impl PyCrl {
1261    /// Parse a DER-encoded X.509 Certificate Revocation List.
1262    #[staticmethod]
1263    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
1264        let py_bytes = data.unbind();
1265        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
1266        // keeps the Python bytes object alive for the lifetime of this struct.
1267        // CPython's bytes objects have a fixed-address, non-relocating payload
1268        // buffer (CPython has no moving GC).  The slice lifetime is extended
1269        // to 'static; the actual safety invariants are:
1270        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
1271        //       can outlive the struct, so `raw` is never read after drop begins.
1272        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
1273        //       allocation), so field drop order does not cause use-after-free.
1274        //   (3) `inner` contains only borrow-typed fields; dropping
1275        //       Box<CertificateList<'static>> does not read through the
1276        //       contained &'static slices (borrows have no destructors).
1277        // CPython-only: does not hold for PyPy or GraalPy.
1278        let raw: &'static [u8] = unsafe {
1279            let s = py_bytes.bind(py).as_bytes();
1280            std::slice::from_raw_parts(s.as_ptr(), s.len())
1281        };
1282        {
1283            let mut d = Decoder::new(raw, Encoding::Der);
1284            d.read_tag()
1285                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1286            d.read_length()
1287                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1288        }
1289        Ok(Self {
1290            _data: py_bytes,
1291            raw,
1292            inner: OnceLock::new(),
1293            issuer_cache: OnceLock::new(),
1294            issuer_raw_der_cache: OnceLock::new(),
1295            this_update_cache: OnceLock::new(),
1296            next_update_cache: OnceLock::new(),
1297            signature_algorithm_cache: OnceLock::new(),
1298            signature_algorithm_oid_cache: OnceLock::new(),
1299            signature_value_cache: OnceLock::new(),
1300        })
1301    }
1302
1303    /// Parse a PEM-encoded X.509 Certificate Revocation List.
1304    ///
1305    /// Returns a single :class:`CertificateList` for one PEM block, or a
1306    /// :class:`list` of :class:`CertificateList` objects when multiple blocks
1307    /// are present.  Raises :exc:`ValueError` if no PEM block is found.
1308    ///
1309    /// ```python
1310    /// crl = CertificateList.from_pem(open("crl.pem", "rb").read())
1311    /// print(crl.issuer)
1312    /// ```
1313    #[staticmethod]
1314    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
1315        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
1316            let obj = Self::from_der(py, bytes)?;
1317            Ok(Py::new(py, obj)?.into_bound(py).into_any())
1318        })
1319    }
1320
1321    /// Serialize one CRL or a list of CRLs to PEM format.
1322    ///
1323    /// ```python
1324    /// pem = CertificateList.to_pem(crl)
1325    /// pem = CertificateList.to_pem([crl1, crl2])
1326    /// ```
1327    #[staticmethod]
1328    fn to_pem<'py>(
1329        py: Python<'py>,
1330        obj_or_list: Bound<'_, PyAny>,
1331    ) -> PyResult<Bound<'py, PyBytes>> {
1332        pyobject_to_pem::<Self, _>(py, "X509 CRL", &obj_or_list, |c| c.raw)
1333    }
1334
1335    /// Issuer distinguished name as a string.
1336    #[getter]
1337    fn issuer<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
1338        if let Some(cached) = self.issuer_cache.get() {
1339            return Ok(cached.clone_ref(py).into_bound(py));
1340        }
1341        let s = name_to_dn_string(&self.crl()?.tbs_cert_list.issuer);
1342        let py_str = PyString::new(py, &s).unbind();
1343        let _ = self.issuer_cache.set(py_str.clone_ref(py));
1344        Ok(py_str.into_bound(py))
1345    }
1346
1347    /// Raw DER bytes of the issuer Name SEQUENCE.
1348    #[getter]
1349    fn issuer_raw_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
1350        if let Some(cached) = self.issuer_raw_der_cache.get() {
1351            return Ok(cached.clone_ref(py).into_bound(py));
1352        }
1353        let bytes = name_to_der_bytes(&self.crl()?.tbs_cert_list.issuer);
1354        let py_bytes = PyBytes::new(py, &bytes).unbind();
1355        let _ = self.issuer_raw_der_cache.set(py_bytes.clone_ref(py));
1356        Ok(py_bytes.into_bound(py))
1357    }
1358
1359    /// thisUpdate time as a string.
1360    #[getter]
1361    fn this_update<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
1362        if let Some(cached) = self.this_update_cache.get() {
1363            return Ok(cached.clone_ref(py).into_bound(py));
1364        }
1365        let s = match &self.crl()?.tbs_cert_list.this_update {
1366            Time::UtcTime(t) => t.to_string(),
1367            Time::GeneralTime(t) => t.to_string(),
1368        };
1369        let py_str = PyString::new(py, &s).unbind();
1370        let _ = self.this_update_cache.set(py_str.clone_ref(py));
1371        Ok(py_str.into_bound(py))
1372    }
1373
1374    /// nextUpdate time as a string, or ``None`` if absent.
1375    #[getter]
1376    fn next_update<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyString>>> {
1377        if let Some(cached) = self.next_update_cache.get() {
1378            return Ok(cached.as_ref().map(|s| s.clone_ref(py).into_bound(py)));
1379        }
1380        let computed: Option<Bound<'py, PyString>> =
1381            self.crl()?.tbs_cert_list.next_update.as_ref().map(|t| {
1382                let s = match t {
1383                    Time::UtcTime(t) => t.to_string(),
1384                    Time::GeneralTime(t) => t.to_string(),
1385                };
1386                PyString::new(py, &s)
1387            });
1388        let to_store = computed.as_ref().map(|s| s.as_unbound().clone_ref(py));
1389        let _ = self.next_update_cache.set(to_store);
1390        Ok(computed)
1391    }
1392
1393    /// Signature algorithm name (e.g. "RSA", "ECDSA").
1394    #[getter]
1395    fn signature_algorithm<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
1396        if let Some(cached) = self.signature_algorithm_cache.get() {
1397            return Ok(cached.clone_ref(py).into_bound(py));
1398        }
1399        let oid = &self.crl()?.signature_algorithm.algorithm;
1400        let name = synta_certificate::identify_signature_algorithm(oid);
1401        let s = if name != "Other" {
1402            name.to_string()
1403        } else {
1404            oid.to_string()
1405        };
1406        let py_str = PyString::new(py, &s).unbind();
1407        let _ = self.signature_algorithm_cache.set(py_str.clone_ref(py));
1408        Ok(py_str.into_bound(py))
1409    }
1410
1411    /// OID of the signature algorithm.
1412    #[getter]
1413    fn signature_algorithm_oid<'py>(
1414        &self,
1415        py: Python<'py>,
1416    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
1417        if let Some(cached) = self.signature_algorithm_oid_cache.get() {
1418            return Ok(cached.clone_ref(py).into_bound(py));
1419        }
1420        let obj = Py::new(
1421            py,
1422            PyObjectIdentifier::from_oid(self.crl()?.signature_algorithm.algorithm.clone()),
1423        )?;
1424        let _ = self.signature_algorithm_oid_cache.set(obj.clone_ref(py));
1425        Ok(obj.into_bound(py))
1426    }
1427
1428    /// Raw signature bytes.
1429    #[getter]
1430    fn signature_value<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
1431        if let Some(cached) = self.signature_value_cache.get() {
1432            return Ok(cached.clone_ref(py).into_bound(py));
1433        }
1434        let py_bytes = PyBytes::new(py, self.crl()?.signature_value.as_bytes()).unbind();
1435        let _ = self.signature_value_cache.set(py_bytes.clone_ref(py));
1436        Ok(py_bytes.into_bound(py))
1437    }
1438
1439    /// Number of revoked certificates in this CRL (0 if no revoked entries).
1440    #[getter]
1441    fn revoked_count(&self) -> PyResult<usize> {
1442        Ok(self
1443            .crl()?
1444            .tbs_cert_list
1445            .revoked_certificates
1446            .as_ref()
1447            .map(|v| v.len())
1448            .unwrap_or(0))
1449    }
1450
1451    /// Complete DER encoding of this CRL (the original bytes passed to ``from_der``).
1452    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1453        self._data.clone_ref(py).into_bound(py)
1454    }
1455
1456    fn __repr__(&self) -> PyResult<String> {
1457        let crl = self.crl()?;
1458        Ok(format!(
1459            "CertificateList(issuer={:?}, revoked_count={})",
1460            name_to_dn_string(&crl.tbs_cert_list.issuer),
1461            crl.tbs_cert_list
1462                .revoked_certificates
1463                .as_ref()
1464                .map(|v| v.len())
1465                .unwrap_or(0),
1466        ))
1467    }
1468}
1469
1470// ── PyOcspResponse ────────────────────────────────────────────────────────────
1471
1472/// OCSP Response (RFC 6960) accessible from Python.
1473///
1474/// ```python
1475/// resp = OCSPResponse.from_der(open("resp.der", "rb").read())
1476/// print(resp.status)
1477/// if resp.response_bytes:
1478///     basic = resp.response_bytes  # DER of BasicOCSPResponse
1479/// ```
1480#[pyclass(frozen, name = "OCSPResponse")]
1481pub struct PyOcspResponse {
1482    _data: Py<PyBytes>,
1483    raw: &'static [u8],
1484    inner: OnceLock<Box<synta_certificate::ocsp::OCSPResponse<'static>>>,
1485    status_cache: OnceLock<Py<PyString>>,
1486    response_type_oid_cache: OnceLock<Option<Py<PyObjectIdentifier>>>,
1487    response_bytes_cache: OnceLock<Option<Py<PyBytes>>>,
1488}
1489
1490impl PyOcspResponse {
1491    fn ocsp(&self) -> PyResult<&synta_certificate::ocsp::OCSPResponse<'static>> {
1492        if let Some(v) = self.inner.get() {
1493            return Ok(v.as_ref());
1494        }
1495        let mut decoder = Decoder::new(self.raw, Encoding::Der);
1496        let decoded = decoder.decode().map_err(|e| {
1497            pyo3::exceptions::PyValueError::new_err(format!("OCSPResponse DER decode failed: {e}"))
1498        })?;
1499        let _ = self.inner.set(Box::new(decoded));
1500        Ok(self.inner.get().unwrap().as_ref())
1501    }
1502}
1503
1504#[pymethods]
1505impl PyOcspResponse {
1506    /// Parse a DER-encoded OCSP Response.
1507    #[staticmethod]
1508    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
1509        let py_bytes = data.unbind();
1510        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
1511        // keeps the Python bytes object alive for the lifetime of this struct.
1512        // CPython's bytes objects have a fixed-address, non-relocating payload
1513        // buffer (CPython has no moving GC).  The slice lifetime is extended
1514        // to 'static; the actual safety invariants are:
1515        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
1516        //       can outlive the struct, so `raw` is never read after drop begins.
1517        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
1518        //       allocation), so field drop order does not cause use-after-free.
1519        //   (3) `inner` contains only borrow-typed fields; dropping
1520        //       Box<OCSPResponse<'static>> does not read through the contained
1521        //       &'static slices (borrows have no destructors).
1522        // CPython-only: does not hold for PyPy or GraalPy.
1523        let raw: &'static [u8] = unsafe {
1524            let s = py_bytes.bind(py).as_bytes();
1525            std::slice::from_raw_parts(s.as_ptr(), s.len())
1526        };
1527        {
1528            let mut d = Decoder::new(raw, Encoding::Der);
1529            d.read_tag()
1530                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1531            d.read_length()
1532                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1533        }
1534        Ok(Self {
1535            _data: py_bytes,
1536            raw,
1537            inner: OnceLock::new(),
1538            status_cache: OnceLock::new(),
1539            response_type_oid_cache: OnceLock::new(),
1540            response_bytes_cache: OnceLock::new(),
1541        })
1542    }
1543
1544    /// Parse a PEM-encoded OCSP Response.
1545    ///
1546    /// Returns a single :class:`OCSPResponse` for one PEM block, or a
1547    /// :class:`list` of :class:`OCSPResponse` objects when multiple blocks are
1548    /// present.  Raises :exc:`ValueError` if no PEM block is found.
1549    ///
1550    /// ```python
1551    /// resp = OCSPResponse.from_pem(open("ocsp.pem", "rb").read())
1552    /// print(resp.status)
1553    /// ```
1554    #[staticmethod]
1555    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
1556        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
1557            let obj = Self::from_der(py, bytes)?;
1558            Ok(Py::new(py, obj)?.into_bound(py).into_any())
1559        })
1560    }
1561
1562    /// Serialize one OCSP response or a list of them to PEM format.
1563    ///
1564    /// ```python
1565    /// pem = OCSPResponse.to_pem(resp)
1566    /// pem = OCSPResponse.to_pem([resp1, resp2])
1567    /// ```
1568    #[staticmethod]
1569    fn to_pem<'py>(
1570        py: Python<'py>,
1571        obj_or_list: Bound<'_, PyAny>,
1572    ) -> PyResult<Bound<'py, PyBytes>> {
1573        pyobject_to_pem::<Self, _>(py, "OCSP RESPONSE", &obj_or_list, |c| c.raw)
1574    }
1575
1576    /// Response status string (e.g. ``"successful"``, ``"tryLater"``).
1577    #[getter]
1578    fn status<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
1579        if let Some(cached) = self.status_cache.get() {
1580            return Ok(cached.clone_ref(py).into_bound(py));
1581        }
1582        let s = ocsp_status_str(self.ocsp()?.response_status);
1583        let py_str = PyString::new(py, s).unbind();
1584        let _ = self.status_cache.set(py_str.clone_ref(py));
1585        Ok(py_str.into_bound(py))
1586    }
1587
1588    /// Dotted-decimal OID of the response type, or ``None`` for error responses
1589    /// that carry no ``responseBytes`` (e.g. ``tryLater``).
1590    ///
1591    /// For successful responses this is typically the id-pkix-ocsp-basic OID
1592    /// (``"1.3.6.1.5.5.7.48.1.1"``).
1593    #[getter]
1594    fn response_type_oid<'py>(
1595        &self,
1596        py: Python<'py>,
1597    ) -> PyResult<Option<Bound<'py, PyObjectIdentifier>>> {
1598        if let Some(cached) = self.response_type_oid_cache.get() {
1599            return Ok(cached.as_ref().map(|o| o.clone_ref(py).into_bound(py)));
1600        }
1601        let computed: Option<Py<PyObjectIdentifier>> = self
1602            .ocsp()?
1603            .response_bytes
1604            .as_ref()
1605            .map(|rb| Py::new(py, PyObjectIdentifier::from_oid(rb.response_type.clone())))
1606            .transpose()?;
1607        let _ = self
1608            .response_type_oid_cache
1609            .set(computed.as_ref().map(|o| o.clone_ref(py)));
1610        Ok(computed.map(|o| o.into_bound(py)))
1611    }
1612
1613    /// Raw DER bytes of the embedded response (the OCTET STRING content of
1614    /// ``ResponseBytes``), or ``None`` for error responses.
1615    ///
1616    /// For id-pkix-ocsp-basic responses, these bytes are a DER-encoded
1617    /// ``BasicOCSPResponse``.
1618    #[getter]
1619    fn response_bytes<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
1620        if let Some(cached) = self.response_bytes_cache.get() {
1621            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
1622        }
1623        let computed: Option<Bound<'py, PyBytes>> = self
1624            .ocsp()?
1625            .response_bytes
1626            .as_ref()
1627            .map(|rb| PyBytes::new(py, rb.response.as_bytes()));
1628        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
1629        let _ = self.response_bytes_cache.set(to_store);
1630        Ok(computed)
1631    }
1632
1633    /// Complete DER encoding of this OCSP response (the original bytes passed to ``from_der``).
1634    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1635        self._data.clone_ref(py).into_bound(py)
1636    }
1637
1638    fn __repr__(&self) -> PyResult<String> {
1639        Ok(format!(
1640            "OCSPResponse(status={})",
1641            ocsp_status_str(self.ocsp()?.response_status),
1642        ))
1643    }
1644}
1645
1646// ── PKCS#7 / PKCS#12 free functions ─────────────────────────────────────────
1647
1648/// Build a `PyList[Certificate]` from a `Vec<Vec<u8>>` of DER-encoded certs.
1649fn der_certs_to_pylist<'py>(
1650    py: Python<'py>,
1651    der_certs: Vec<Vec<u8>>,
1652) -> PyResult<Bound<'py, PyList>> {
1653    let list = PyList::empty(py);
1654    for der in der_certs {
1655        let py_bytes = PyBytes::new(py, &der);
1656        let cert = PyCertificate::from_der(py, py_bytes)?;
1657        list.append(Py::new(py, cert)?.into_bound(py))?;
1658    }
1659    Ok(list)
1660}
1661
1662/// Extract X.509 certificates from a PKCS#7 SignedData blob (DER or BER).
1663///
1664/// Accepts raw DER or BER input; BER indefinite-length encodings (common in
1665/// PKCS#7 files produced by older tools) are handled transparently.  Returns
1666/// a list of :class:`Certificate` objects in document order.
1667///
1668/// Raises :exc:`ValueError` on any ASN.1 structural error or if the input
1669/// is not id-signedData.
1670///
1671/// ```python,ignore
1672/// data = open("bundle.p7b", "rb").read()
1673/// certs = synta.load_der_pkcs7_certificates(data)
1674/// for cert in certs:
1675///     print(cert.subject)
1676/// ```
1677#[pyfunction]
1678pub fn load_der_pkcs7_certificates<'py>(
1679    py: Python<'py>,
1680    data: &[u8],
1681) -> PyResult<Bound<'py, PyList>> {
1682    let der_certs = synta_certificate::certs_from_pkcs7(data)
1683        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1684    der_certs_to_pylist(py, der_certs)
1685}
1686
1687/// Extract X.509 certificates from a PEM-encoded PKCS#7 SignedData blob.
1688///
1689/// Decodes PEM block(s) in ``data`` (any label is accepted: ``PKCS7``,
1690/// ``CMS``, ``CERTIFICATE``, etc.) then calls
1691/// :func:`load_der_pkcs7_certificates` on each DER payload.  All certificates
1692/// across all blocks are returned as a single flat list.
1693///
1694/// Raises :exc:`ValueError` if no PEM block is found or if any block fails
1695/// to parse as PKCS#7 SignedData.
1696///
1697/// ```python,ignore
1698/// data = open("bundle.pem", "rb").read()
1699/// certs = synta.load_pem_pkcs7_certificates(data)
1700/// for cert in certs:
1701///     print(cert.subject)
1702/// ```
1703#[pyfunction]
1704pub fn load_pem_pkcs7_certificates<'py>(
1705    py: Python<'py>,
1706    data: &[u8],
1707) -> PyResult<Bound<'py, PyList>> {
1708    let blocks = synta_certificate::pem_blocks(data);
1709    if blocks.is_empty() {
1710        return Err(pyo3::exceptions::PyValueError::new_err(
1711            "no PEM block found in input",
1712        ));
1713    }
1714    let mut all_certs: Vec<Vec<u8>> = Vec::new();
1715    for (_, der) in &blocks {
1716        let certs = synta_certificate::certs_from_pkcs7(der)
1717            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1718        all_certs.extend(certs);
1719    }
1720    der_certs_to_pylist(py, all_certs)
1721}
1722
1723/// Extract X.509 certificates from a PKCS#12 archive.
1724///
1725/// Accepts raw DER or BER input; the encoding is detected automatically.
1726/// Returns a list of :class:`Certificate` objects in document order.
1727/// Non-certificate bag types (``keyBag``, ``pkcs8ShroudedKeyBag``,
1728/// ``crlBag``, ``secretBag``) are silently skipped.
1729///
1730/// ``password`` is the archive password as raw bytes (UTF-8 without a NUL
1731/// terminator).  Pass ``b""`` or omit the argument for password-less archives.
1732///
1733/// Encrypted bags are supported when the ``openssl`` Cargo feature is enabled.
1734/// Without that feature a :exc:`ValueError` is raised on any encrypted bag.
1735///
1736/// ```python,ignore
1737/// data = open("archive.p12", "rb").read()
1738/// certs = synta.load_pkcs12_certificates(data, b"s3cr3t")
1739/// for cert in certs:
1740///     print(cert.subject)
1741/// ```
1742#[pyfunction]
1743#[pyo3(signature = (data, password = None))]
1744pub fn load_pkcs12_certificates<'py>(
1745    py: Python<'py>,
1746    data: &[u8],
1747    password: Option<&[u8]>,
1748) -> PyResult<Bound<'py, PyList>> {
1749    let pw = password.unwrap_or(b"");
1750    #[cfg(feature = "openssl")]
1751    let der_certs =
1752        synta_certificate::certs_from_pkcs12(data, pw, &synta_certificate::OpensslDecryptor)
1753            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1754    #[cfg(not(feature = "openssl"))]
1755    let der_certs = synta_certificate::certs_from_pkcs12(data, pw, &synta_certificate::NoCrypto)
1756        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1757    der_certs_to_pylist(py, der_certs)
1758}
1759
1760/// Extract PKCS#8 private keys from a PKCS#12 archive.
1761///
1762/// Returns a :class:`list` of :class:`bytes` objects, one per private key
1763/// found in ``keyBag`` (unencrypted) and ``pkcs8ShroudedKeyBag`` (encrypted,
1764/// decrypted when the ``openssl`` Cargo feature is enabled and ``password``
1765/// is given) entries.  Each element is a DER-encoded ``OneAsymmetricKey``
1766/// (PKCS#8) structure, suitable for passing to a cryptography library.
1767///
1768/// ``certBag``, ``crlBag``, ``secretBag`` and any unknown bag types are
1769/// silently skipped.
1770///
1771/// ```python,ignore
1772/// data = open("archive.p12", "rb").read()
1773/// keys = synta.load_pkcs12_keys(data, b"s3cr3t")
1774/// for key_der in keys:
1775///     from cryptography.hazmat.primitives.serialization import load_der_private_key
1776///     key = load_der_private_key(key_der, None)
1777/// ```
1778#[pyfunction]
1779#[pyo3(signature = (data, password = None))]
1780pub fn load_pkcs12_keys<'py>(
1781    py: Python<'py>,
1782    data: &[u8],
1783    password: Option<&[u8]>,
1784) -> PyResult<Bound<'py, PyList>> {
1785    let pw = password.unwrap_or(b"");
1786    #[cfg(feature = "openssl")]
1787    let der_keys =
1788        synta_certificate::keys_from_pkcs12(data, pw, &synta_certificate::OpensslDecryptor)
1789            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1790    #[cfg(not(feature = "openssl"))]
1791    let der_keys = synta_certificate::keys_from_pkcs12(data, pw, &synta_certificate::NoCrypto)
1792        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1793    let list = PyList::empty(py);
1794    for der in der_keys {
1795        list.append(PyBytes::new(py, &der))?;
1796    }
1797    Ok(list)
1798}
1799
1800/// Extract both certificates and private keys from a PKCS#12 archive.
1801///
1802/// Returns a 2-tuple ``(certs, keys)`` where:
1803///
1804/// - ``certs`` is a :class:`list` of :class:`Certificate` objects.
1805/// - ``keys`` is a :class:`list` of :class:`bytes`, each a DER-encoded
1806///   PKCS#8 ``OneAsymmetricKey`` structure.
1807///
1808/// Encrypted bags are decrypted with ``password`` when the ``openssl``
1809/// Cargo feature is enabled; without it, encrypted bags are silently
1810/// skipped and a :exc:`ValueError` is raised only on structural errors.
1811/// Pass ``b""`` or omit ``password`` for password-less archives.
1812///
1813/// Use :func:`load_pkcs12_certificates` if you only need certificates, or
1814/// :func:`load_pkcs12_keys` if you only need keys.
1815///
1816/// ```python,ignore
1817/// data = open("archive.p12", "rb").read()
1818/// certs, keys = synta.load_pkcs12(data, b"s3cr3t")
1819/// for cert in certs:
1820///     print(cert.subject)
1821/// for key_der in keys:
1822///     from cryptography.hazmat.primitives.serialization import load_der_private_key
1823///     key = load_der_private_key(key_der, None)
1824/// ```
1825#[pyfunction]
1826#[pyo3(signature = (data, password = None))]
1827pub fn load_pkcs12<'py>(
1828    py: Python<'py>,
1829    data: &[u8],
1830    password: Option<&[u8]>,
1831) -> PyResult<Bound<'py, pyo3::types::PyTuple>> {
1832    let pw = password.unwrap_or(b"");
1833    #[cfg(feature = "openssl")]
1834    let pki = synta_certificate::pki_from_pkcs12(data, pw, &synta_certificate::OpensslDecryptor)
1835        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1836    #[cfg(not(feature = "openssl"))]
1837    let pki = synta_certificate::pki_from_pkcs12(data, pw, &synta_certificate::NoCrypto)
1838        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1839    let certs = der_certs_to_pylist(py, pki.certs)?;
1840    let keys = PyList::empty(py);
1841    for der in pki.keys {
1842        keys.append(PyBytes::new(py, &der))?;
1843    }
1844    pyo3::types::PyTuple::new(py, [certs.into_any(), keys.into_any()])
1845}
1846
1847/// Auto-detect the encoding of ``data`` and return every PKI object as a
1848/// ``(label, der_bytes)`` tuple, in document order.
1849///
1850/// Supported formats: PEM (any label), PKCS#7 / CMS SignedData (DER or BER),
1851/// PKCS#12 PFX (DER or BER), and raw DER.
1852///
1853/// **Labels:**
1854///
1855/// - PEM blocks whose payload is a PKCS#7 ``ContentInfo`` (detected by
1856///   structure, not label) are expanded: the embedded certificates are
1857///   extracted and each is returned as ``("CERTIFICATE", der)``.
1858/// - All other PEM block types pass through with their original label
1859///   (e.g. ``"CERTIFICATE"``, ``"PRIVATE KEY"``).  Malformed PEM blocks
1860///   (bad base64, truncated header) are silently skipped.
1861/// - Binary PKCS#7 and raw DER yield ``"CERTIFICATE"`` labels.
1862/// - Binary PKCS#12 yields ``"CERTIFICATE"`` for ``certBag`` entries and
1863///   ``"PRIVATE KEY"`` for ``keyBag`` / ``pkcs8ShroudedKeyBag`` entries.
1864///
1865/// **Decryption:**
1866///
1867/// ``password`` applies only to PKCS#12 archives.  When ``password`` is
1868/// given **and** the library was built with the ``openssl`` Cargo feature,
1869/// encrypted SafeBags are decrypted.  Otherwise encrypted bags are silently
1870/// skipped; unencrypted certificates in the same archive are still returned.
1871///
1872/// :raises ValueError: For any structural failure:
1873///
1874///   - A PKCS#7 block (binary or PEM-wrapped) has malformed DER, or its
1875///     ``contentType`` OID is not ``id-signedData``.
1876///   - The PKCS#12 input has a structural ASN.1 error.
1877///   - ``password`` was supplied with the ``openssl`` feature enabled and
1878///     decryption of an encrypted SafeBag failed (e.g. wrong password,
1879///     unsupported cipher).
1880///   - The PKCS#12 authSafe uses an unsupported ``ContentInfo`` type.
1881///
1882/// :raises TypeError: If ``data`` is not :class:`bytes`, or ``password``
1883///   is not :class:`bytes` or ``None``.
1884///
1885/// ```python,ignore
1886/// data = open("bundle.p7b", "rb").read()
1887/// blocks = synta.read_pki_blocks(data)
1888/// for label, der in blocks:
1889///     print(f"{label}: {len(der)} bytes")
1890///
1891/// # PKCS#12 with encryption
1892/// data = open("archive.p12", "rb").read()
1893/// blocks = synta.read_pki_blocks(data, b"s3cr3t")
1894/// ```
1895#[pyfunction]
1896#[pyo3(signature = (data, password = None))]
1897pub fn read_pki_blocks<'py>(
1898    py: Python<'py>,
1899    data: &[u8],
1900    password: Option<&[u8]>,
1901) -> PyResult<Bound<'py, PyList>> {
1902    let pw = password.unwrap_or(b"");
1903    #[cfg(feature = "openssl")]
1904    let blocks = if password.is_some() {
1905        synta_certificate::read_pki_blocks(
1906            data,
1907            pw,
1908            Some(&synta_certificate::OpensslDecryptor as &dyn synta_certificate::PkiDecryptor),
1909        )
1910    } else {
1911        synta_certificate::read_pki_blocks(data, pw, None::<&dyn synta_certificate::PkiDecryptor>)
1912    };
1913    #[cfg(not(feature = "openssl"))]
1914    let blocks =
1915        synta_certificate::read_pki_blocks(data, pw, None::<&dyn synta_certificate::PkiDecryptor>);
1916    let blocks = blocks.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1917    let list = PyList::empty(py);
1918    for (label, der) in blocks {
1919        let tuple = pyo3::types::PyTuple::new(
1920            py,
1921            [
1922                PyString::new(py, &label).into_any(),
1923                PyBytes::new(py, &der).into_any(),
1924            ],
1925        )?;
1926        list.append(tuple)?;
1927    }
1928    Ok(list)
1929}
1930
1931// ── PyKEMRecipientInfo ────────────────────────────────────────────────────────
1932
1933/// KEM Recipient Info (RFC 9629) accessible from Python.
1934///
1935/// ``KEMRecipientInfo`` carries a quantum-safe KEM-encapsulated content-encryption
1936/// key inside a CMS ``EnvelopedData`` structure.  It is carried as an
1937/// ``OtherRecipientInfo`` alternative identified by ``id-ori-kem``.
1938///
1939/// ```python,ignore
1940/// kemri = KEMRecipientInfo.from_der(raw)
1941/// print(kemri.kem_algorithm_oid)  # e.g. ObjectIdentifier("1.3.6.1.4.1.22554.5.6.1")
1942/// key_bytes = kemri.encrypted_key
1943/// ```
1944#[pyclass(frozen, name = "KEMRecipientInfo")]
1945pub struct PyKEMRecipientInfo {
1946    _data: Py<PyBytes>,
1947    raw: &'static [u8],
1948    inner: OnceLock<Box<synta_certificate::cms_kem_types::KEMRecipientInfo<'static>>>,
1949    // OID caches
1950    kem_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
1951    kdf_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
1952    key_encryption_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
1953    // Optional DER params caches
1954    kem_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
1955    kdf_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
1956    key_encryption_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
1957    // Byte field caches
1958    recipient_id_cache: OnceLock<Py<PyBytes>>,
1959    kem_ciphertext_cache: OnceLock<Py<PyBytes>>,
1960    encrypted_key_cache: OnceLock<Py<PyBytes>>,
1961    // Optional byte field caches
1962    ukm_cache: OnceLock<Option<Py<PyBytes>>>,
1963}
1964
1965impl PyKEMRecipientInfo {
1966    fn kemri(&self) -> PyResult<&synta_certificate::cms_kem_types::KEMRecipientInfo<'static>> {
1967        if let Some(v) = self.inner.get() {
1968            return Ok(v.as_ref());
1969        }
1970        let mut decoder = Decoder::new(self.raw, Encoding::Der);
1971        let decoded = decoder.decode().map_err(|e| {
1972            pyo3::exceptions::PyValueError::new_err(format!(
1973                "KEMRecipientInfo DER decode failed: {e}"
1974            ))
1975        })?;
1976        let _ = self.inner.set(Box::new(decoded));
1977        Ok(self.inner.get().unwrap().as_ref())
1978    }
1979}
1980
1981#[pymethods]
1982impl PyKEMRecipientInfo {
1983    /// Parse a DER-encoded ``KEMRecipientInfo`` SEQUENCE.
1984    #[staticmethod]
1985    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
1986        let py_bytes = data.unbind();
1987        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
1988        // keeps the Python bytes object alive for the lifetime of this struct.
1989        // CPython's bytes objects have a fixed-address, non-relocating payload
1990        // buffer (CPython has no moving GC).  The slice lifetime is extended
1991        // to 'static; the actual safety invariants are:
1992        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
1993        //       can outlive the struct, so `raw` is never read after drop begins.
1994        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
1995        //       allocation), so field drop order does not cause use-after-free.
1996        //   (3) `inner` contains only borrow-typed fields in the decoded type;
1997        //       dropping the Box does not read through the contained &'static
1998        //       slices (borrows have no destructors in Rust).
1999        // CPython-only: does not hold for PyPy or GraalPy.
2000        let raw: &'static [u8] = unsafe {
2001            let s = py_bytes.bind(py).as_bytes();
2002            std::slice::from_raw_parts(s.as_ptr(), s.len())
2003        };
2004        {
2005            let mut d = Decoder::new(raw, Encoding::Der);
2006            d.read_tag()
2007                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2008            d.read_length()
2009                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2010        }
2011        Ok(Self {
2012            _data: py_bytes,
2013            raw,
2014            inner: OnceLock::new(),
2015            kem_algorithm_oid_cache: OnceLock::new(),
2016            kdf_algorithm_oid_cache: OnceLock::new(),
2017            key_encryption_algorithm_oid_cache: OnceLock::new(),
2018            kem_algorithm_params_cache: OnceLock::new(),
2019            kdf_algorithm_params_cache: OnceLock::new(),
2020            key_encryption_algorithm_params_cache: OnceLock::new(),
2021            recipient_id_cache: OnceLock::new(),
2022            kem_ciphertext_cache: OnceLock::new(),
2023            encrypted_key_cache: OnceLock::new(),
2024            ukm_cache: OnceLock::new(),
2025        })
2026    }
2027
2028    /// Complete DER encoding of this ``KEMRecipientInfo``
2029    /// (the original bytes passed to ``from_der``).
2030    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
2031        self._data.clone_ref(py).into_bound(py)
2032    }
2033
2034    /// CMS version (always ``0`` for KEMRecipientInfo per RFC 9629).
2035    #[getter]
2036    fn version(&self) -> PyResult<i64> {
2037        Ok(self.kemri()?.version.as_i64().unwrap_or(0))
2038    }
2039
2040    /// Raw DER bytes of the ``RecipientIdentifier`` CHOICE field.
2041    ///
2042    /// Decode with :class:`Decoder` to distinguish ``issuerAndSerialNumber``
2043    /// (SEQUENCE) from ``subjectKeyIdentifier`` (context tag ``[0]``).
2044    #[getter]
2045    fn recipient_id<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
2046        if let Some(cached) = self.recipient_id_cache.get() {
2047            return Ok(cached.clone_ref(py).into_bound(py));
2048        }
2049        let py_bytes = PyBytes::new(py, self.kemri()?.rid.as_bytes()).unbind();
2050        let _ = self.recipient_id_cache.set(py_bytes.clone_ref(py));
2051        Ok(py_bytes.into_bound(py))
2052    }
2053
2054    /// OID of the KEM algorithm (the ``kem`` field).
2055    ///
2056    /// For ML-KEM-768 this is ``2.16.840.1.101.3.4.4.2``.
2057    #[getter]
2058    fn kem_algorithm_oid<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
2059        if let Some(cached) = self.kem_algorithm_oid_cache.get() {
2060            return Ok(cached.clone_ref(py).into_bound(py));
2061        }
2062        let obj = Py::new(
2063            py,
2064            PyObjectIdentifier::from_oid(self.kemri()?.kem.algorithm.clone()),
2065        )?;
2066        let _ = self.kem_algorithm_oid_cache.set(obj.clone_ref(py));
2067        Ok(obj.into_bound(py))
2068    }
2069
2070    /// Raw DER bytes of the KEM algorithm parameters, or ``None``.
2071    #[getter]
2072    fn kem_algorithm_params<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
2073        if let Some(cached) = self.kem_algorithm_params_cache.get() {
2074            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
2075        }
2076        let computed = encode_element_opt(py, self.kemri()?.kem.parameters.as_ref())?;
2077        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
2078        let _ = self.kem_algorithm_params_cache.set(to_store);
2079        Ok(computed)
2080    }
2081
2082    /// Raw bytes of the KEM ciphertext (``kemct`` field, the encapsulated key).
2083    #[getter]
2084    fn kem_ciphertext<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
2085        if let Some(cached) = self.kem_ciphertext_cache.get() {
2086            return Ok(cached.clone_ref(py).into_bound(py));
2087        }
2088        let py_bytes = PyBytes::new(py, self.kemri()?.kemct.as_bytes()).unbind();
2089        let _ = self.kem_ciphertext_cache.set(py_bytes.clone_ref(py));
2090        Ok(py_bytes.into_bound(py))
2091    }
2092
2093    /// OID of the key-derivation algorithm (the ``kdf`` field).
2094    #[getter]
2095    fn kdf_algorithm_oid<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
2096        if let Some(cached) = self.kdf_algorithm_oid_cache.get() {
2097            return Ok(cached.clone_ref(py).into_bound(py));
2098        }
2099        let obj = Py::new(
2100            py,
2101            PyObjectIdentifier::from_oid(self.kemri()?.kdf.algorithm.clone()),
2102        )?;
2103        let _ = self.kdf_algorithm_oid_cache.set(obj.clone_ref(py));
2104        Ok(obj.into_bound(py))
2105    }
2106
2107    /// Raw DER bytes of the KDF algorithm parameters, or ``None``.
2108    #[getter]
2109    fn kdf_algorithm_params<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
2110        if let Some(cached) = self.kdf_algorithm_params_cache.get() {
2111            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
2112        }
2113        let computed = encode_element_opt(py, self.kemri()?.kdf.parameters.as_ref())?;
2114        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
2115        let _ = self.kdf_algorithm_params_cache.set(to_store);
2116        Ok(computed)
2117    }
2118
2119    /// KEK (key-encryption key) length in bytes (``kekLength`` field, range 1..65535).
2120    #[getter]
2121    fn kek_length(&self) -> PyResult<i64> {
2122        Ok(self.kemri()?.kek_length.as_i64().unwrap_or(0))
2123    }
2124
2125    /// User keying material bytes (``ukm`` field), or ``None`` if absent.
2126    #[getter]
2127    fn ukm<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
2128        if let Some(cached) = self.ukm_cache.get() {
2129            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
2130        }
2131        let computed = self
2132            .kemri()?
2133            .ukm
2134            .as_ref()
2135            .map(|u| PyBytes::new(py, u.as_bytes()));
2136        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
2137        let _ = self.ukm_cache.set(to_store);
2138        Ok(computed)
2139    }
2140
2141    /// OID of the key-encryption (key-wrap) algorithm (the ``wrap`` field).
2142    #[getter]
2143    fn key_encryption_algorithm_oid<'py>(
2144        &self,
2145        py: Python<'py>,
2146    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
2147        if let Some(cached) = self.key_encryption_algorithm_oid_cache.get() {
2148            return Ok(cached.clone_ref(py).into_bound(py));
2149        }
2150        let obj = Py::new(
2151            py,
2152            PyObjectIdentifier::from_oid(self.kemri()?.wrap.algorithm.clone()),
2153        )?;
2154        let _ = self
2155            .key_encryption_algorithm_oid_cache
2156            .set(obj.clone_ref(py));
2157        Ok(obj.into_bound(py))
2158    }
2159
2160    /// Raw DER bytes of the key-encryption algorithm parameters, or ``None``.
2161    #[getter]
2162    fn key_encryption_algorithm_params<'py>(
2163        &self,
2164        py: Python<'py>,
2165    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
2166        if let Some(cached) = self.key_encryption_algorithm_params_cache.get() {
2167            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
2168        }
2169        let computed = encode_element_opt(py, self.kemri()?.wrap.parameters.as_ref())?;
2170        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
2171        let _ = self.key_encryption_algorithm_params_cache.set(to_store);
2172        Ok(computed)
2173    }
2174
2175    /// Encrypted content-encryption key bytes (the ``encryptedKey`` OCTET STRING).
2176    #[getter]
2177    fn encrypted_key<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
2178        if let Some(cached) = self.encrypted_key_cache.get() {
2179            return Ok(cached.clone_ref(py).into_bound(py));
2180        }
2181        let py_bytes = PyBytes::new(py, self.kemri()?.encrypted_key.as_bytes()).unbind();
2182        let _ = self.encrypted_key_cache.set(py_bytes.clone_ref(py));
2183        Ok(py_bytes.into_bound(py))
2184    }
2185
2186    fn __repr__(&self) -> PyResult<String> {
2187        let kemri = self.kemri()?;
2188        Ok(format!(
2189            "KEMRecipientInfo(kem={}, kdf={}, kek_length={})",
2190            kemri.kem.algorithm,
2191            kemri.kdf.algorithm,
2192            kemri.kek_length.as_i64().unwrap_or(0),
2193        ))
2194    }
2195}
2196
2197// ── PyCMSORIforKEMOtherInfo ───────────────────────────────────────────────────
2198
2199/// CMS ORI KEM Other Info (RFC 9629 §6.2) accessible from Python.
2200///
2201/// ``CMSORIforKEMOtherInfo`` is used as the ``otherInfo`` input to the KDF
2202/// when deriving a KEK from a KEM shared secret.  It binds the key-encryption
2203/// algorithm, KEK length, and optional UKM to the KDF computation.
2204///
2205/// ```python,ignore
2206/// info = CMSORIforKEMOtherInfo.from_der(raw)
2207/// print(info.kek_length)      # e.g. 32
2208/// print(info.key_encryption_algorithm_oid)
2209/// ```
2210#[pyclass(frozen, name = "CMSORIforKEMOtherInfo")]
2211pub struct PyCMSORIforKEMOtherInfo {
2212    _data: Py<PyBytes>,
2213    raw: &'static [u8],
2214    inner: OnceLock<Box<synta_certificate::cms_kem_types::CMSORIforKEMOtherInfo<'static>>>,
2215    key_encryption_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
2216    key_encryption_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
2217    ukm_cache: OnceLock<Option<Py<PyBytes>>>,
2218}
2219
2220impl PyCMSORIforKEMOtherInfo {
2221    fn info(&self) -> PyResult<&synta_certificate::cms_kem_types::CMSORIforKEMOtherInfo<'static>> {
2222        if let Some(v) = self.inner.get() {
2223            return Ok(v.as_ref());
2224        }
2225        let mut decoder = Decoder::new(self.raw, Encoding::Der);
2226        let decoded = decoder.decode().map_err(|e| {
2227            pyo3::exceptions::PyValueError::new_err(format!(
2228                "CMSORIforKEMOtherInfo DER decode failed: {e}"
2229            ))
2230        })?;
2231        let _ = self.inner.set(Box::new(decoded));
2232        Ok(self.inner.get().unwrap().as_ref())
2233    }
2234}
2235
2236#[pymethods]
2237impl PyCMSORIforKEMOtherInfo {
2238    /// Parse a DER-encoded ``CMSORIforKEMOtherInfo`` SEQUENCE.
2239    #[staticmethod]
2240    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
2241        let py_bytes = data.unbind();
2242        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
2243        // keeps the Python bytes object alive for the lifetime of this struct.
2244        // CPython's bytes objects have a fixed-address, non-relocating payload
2245        // buffer (CPython has no moving GC).  The slice lifetime is extended
2246        // to 'static; the actual safety invariants are:
2247        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
2248        //       can outlive the struct, so `raw` is never read after drop begins.
2249        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
2250        //       allocation), so field drop order does not cause use-after-free.
2251        //   (3) `inner` contains only borrow-typed fields in the decoded type;
2252        //       dropping the Box does not read through the contained &'static
2253        //       slices (borrows have no destructors in Rust).
2254        // CPython-only: does not hold for PyPy or GraalPy.
2255        let raw: &'static [u8] = unsafe {
2256            let s = py_bytes.bind(py).as_bytes();
2257            std::slice::from_raw_parts(s.as_ptr(), s.len())
2258        };
2259        {
2260            let mut d = Decoder::new(raw, Encoding::Der);
2261            d.read_tag()
2262                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2263            d.read_length()
2264                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2265        }
2266        Ok(Self {
2267            _data: py_bytes,
2268            raw,
2269            inner: OnceLock::new(),
2270            key_encryption_algorithm_oid_cache: OnceLock::new(),
2271            key_encryption_algorithm_params_cache: OnceLock::new(),
2272            ukm_cache: OnceLock::new(),
2273        })
2274    }
2275
2276    /// Complete DER encoding of this ``CMSORIforKEMOtherInfo``
2277    /// (the original bytes passed to ``from_der``).
2278    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
2279        self._data.clone_ref(py).into_bound(py)
2280    }
2281
2282    /// OID of the key-encryption (key-wrap) algorithm.
2283    #[getter]
2284    fn key_encryption_algorithm_oid<'py>(
2285        &self,
2286        py: Python<'py>,
2287    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
2288        if let Some(cached) = self.key_encryption_algorithm_oid_cache.get() {
2289            return Ok(cached.clone_ref(py).into_bound(py));
2290        }
2291        let obj = Py::new(
2292            py,
2293            PyObjectIdentifier::from_oid(self.info()?.wrap.algorithm.clone()),
2294        )?;
2295        let _ = self
2296            .key_encryption_algorithm_oid_cache
2297            .set(obj.clone_ref(py));
2298        Ok(obj.into_bound(py))
2299    }
2300
2301    /// Raw DER bytes of the key-encryption algorithm parameters, or ``None``.
2302    #[getter]
2303    fn key_encryption_algorithm_params<'py>(
2304        &self,
2305        py: Python<'py>,
2306    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
2307        if let Some(cached) = self.key_encryption_algorithm_params_cache.get() {
2308            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
2309        }
2310        let computed = encode_element_opt(py, self.info()?.wrap.parameters.as_ref())?;
2311        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
2312        let _ = self.key_encryption_algorithm_params_cache.set(to_store);
2313        Ok(computed)
2314    }
2315
2316    /// KEK length in bytes (``kekLength`` field, range 1..65535).
2317    #[getter]
2318    fn kek_length(&self) -> PyResult<i64> {
2319        Ok(self.info()?.kek_length.as_i64().unwrap_or(0))
2320    }
2321
2322    /// User keying material bytes (``ukm`` field), or ``None`` if absent.
2323    #[getter]
2324    fn ukm<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
2325        if let Some(cached) = self.ukm_cache.get() {
2326            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
2327        }
2328        let computed = self
2329            .info()?
2330            .ukm
2331            .as_ref()
2332            .map(|u| PyBytes::new(py, u.as_bytes()));
2333        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
2334        let _ = self.ukm_cache.set(to_store);
2335        Ok(computed)
2336    }
2337
2338    fn __repr__(&self) -> PyResult<String> {
2339        let info = self.info()?;
2340        Ok(format!(
2341            "CMSORIforKEMOtherInfo(wrap={}, kek_length={})",
2342            info.wrap.algorithm,
2343            info.kek_length.as_i64().unwrap_or(0),
2344        ))
2345    }
2346}
2347
2348// Build and register the ``synta.cms`` submodule.
2349
2350// ── PyContentInfo ─────────────────────────────────────────────────────────────
2351
2352/// CMS ContentInfo (RFC 5652 §3) accessible from Python.
2353///
2354/// ``ContentInfo`` is the outermost CMS envelope.  It names the content type
2355/// via an OID and carries the content itself (e.g. a ``SignedData`` or
2356/// ``EnvelopedData`` SEQUENCE) as an opaque DER blob.
2357///
2358/// ```python,ignore
2359/// ci = ContentInfo.from_der(raw)
2360/// if ci.content_type_oid == synta.cms.ID_SIGNED_DATA:
2361///     signed_data_der = ci.content   # raw DER of the [0] EXPLICIT wrapper
2362/// ```
2363#[pyclass(frozen, name = "ContentInfo")]
2364pub struct PyContentInfo {
2365    _data: Py<PyBytes>,
2366    raw: &'static [u8],
2367    inner: OnceLock<Box<synta_certificate::pkcs7_types::ContentInfo<'static>>>,
2368    content_type_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
2369    content_cache: OnceLock<Py<PyBytes>>,
2370}
2371
2372impl PyContentInfo {
2373    fn content_info(&self) -> PyResult<&synta_certificate::pkcs7_types::ContentInfo<'static>> {
2374        if let Some(v) = self.inner.get() {
2375            return Ok(v.as_ref());
2376        }
2377        // Use BER so that indefinite-length PKCS#7 wrappers are accepted.
2378        let mut decoder = Decoder::new(self.raw, Encoding::Ber);
2379        let decoded = decoder.decode().map_err(|e| {
2380            pyo3::exceptions::PyValueError::new_err(format!("ContentInfo BER decode failed: {e}"))
2381        })?;
2382        let _ = self.inner.set(Box::new(decoded));
2383        Ok(self.inner.get().unwrap().as_ref())
2384    }
2385}
2386
2387#[pymethods]
2388impl PyContentInfo {
2389    /// Parse a DER- or BER-encoded CMS ``ContentInfo`` SEQUENCE.
2390    ///
2391    /// BER indefinite-length encodings (common in PKCS#7 files produced by
2392    /// older tools and OpenSSL) are accepted in addition to strict DER.
2393    /// Raises :exc:`ValueError` if the outer SEQUENCE envelope is malformed.
2394    #[staticmethod]
2395    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
2396        let py_bytes = data.unbind();
2397        let raw: &'static [u8] = unsafe {
2398            let s = py_bytes.bind(py).as_bytes();
2399            std::slice::from_raw_parts(s.as_ptr(), s.len())
2400        };
2401        {
2402            let mut d = Decoder::new(raw, Encoding::Ber);
2403            d.read_tag()
2404                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2405            d.read_length()
2406                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2407        }
2408        Ok(Self {
2409            _data: py_bytes,
2410            raw,
2411            inner: OnceLock::new(),
2412            content_type_oid_cache: OnceLock::new(),
2413            content_cache: OnceLock::new(),
2414        })
2415    }
2416
2417    /// Return the original bytes passed to :meth:`from_der`.
2418    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
2419        self._data.clone_ref(py).into_bound(py)
2420    }
2421
2422    /// OID identifying the content type
2423    /// (e.g. :data:`ID_SIGNED_DATA`, :data:`ID_ENVELOPED_DATA`).
2424    #[getter]
2425    fn content_type_oid<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
2426        if let Some(cached) = self.content_type_oid_cache.get() {
2427            return Ok(cached.clone_ref(py).into_bound(py));
2428        }
2429        let obj = Py::new(
2430            py,
2431            PyObjectIdentifier::from_oid(self.content_info()?.content_type.clone()),
2432        )?;
2433        let _ = self.content_type_oid_cache.set(obj.clone_ref(py));
2434        Ok(obj.into_bound(py))
2435    }
2436
2437    /// Raw DER bytes of the ``content`` field.
2438    ///
2439    /// This is the entire ``[0] EXPLICIT`` TLV; callers that need the inner
2440    /// value (e.g. the ``SignedData`` SEQUENCE) should strip the context tag
2441    /// and length with a :class:`~synta.Decoder`.
2442    #[getter]
2443    fn content<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
2444        if let Some(cached) = self.content_cache.get() {
2445            return Ok(cached.clone_ref(py).into_bound(py));
2446        }
2447        let py_bytes = PyBytes::new(py, self.content_info()?.content.as_bytes()).unbind();
2448        let _ = self.content_cache.set(py_bytes.clone_ref(py));
2449        Ok(py_bytes.into_bound(py))
2450    }
2451
2452    fn __repr__(&self) -> PyResult<String> {
2453        Ok(format!(
2454            "ContentInfo(content_type={})",
2455            self.content_info()?.content_type,
2456        ))
2457    }
2458}
2459
2460// ── PyIssuerAndSerialNumber ───────────────────────────────────────────────────
2461
2462/// CMS IssuerAndSerialNumber (RFC 5652 §10.2.4) accessible from Python.
2463///
2464/// Identifies a recipient or signer by the issuer distinguished name and
2465/// certificate serial number of their certificate.  Used as one alternative
2466/// of the ``RecipientIdentifier`` CHOICE inside :class:`KEMRecipientInfo`.
2467///
2468/// ```python,ignore
2469/// ias = IssuerAndSerialNumber.from_der(raw)
2470/// print(ias.issuer)          # "CN=My CA,O=Example,C=US"
2471/// print(ias.serial_number)   # 12345678
2472/// ```
2473#[pyclass(frozen, name = "IssuerAndSerialNumber")]
2474pub struct PyIssuerAndSerialNumber {
2475    _data: Py<PyBytes>,
2476    raw: &'static [u8],
2477    inner: OnceLock<Box<synta_certificate::cms_2010_types::IssuerAndSerialNumber<'static>>>,
2478    issuer_cache: OnceLock<Py<PyString>>,
2479    issuer_raw_der_cache: OnceLock<Py<PyBytes>>,
2480    serial_number_cache: OnceLock<Py<PyAny>>,
2481}
2482
2483impl PyIssuerAndSerialNumber {
2484    fn ias(&self) -> PyResult<&synta_certificate::cms_2010_types::IssuerAndSerialNumber<'static>> {
2485        if let Some(v) = self.inner.get() {
2486            return Ok(v.as_ref());
2487        }
2488        let mut decoder = Decoder::new(self.raw, Encoding::Der);
2489        let decoded = decoder.decode().map_err(|e| {
2490            pyo3::exceptions::PyValueError::new_err(format!(
2491                "IssuerAndSerialNumber DER decode failed: {e}"
2492            ))
2493        })?;
2494        let _ = self.inner.set(Box::new(decoded));
2495        Ok(self.inner.get().unwrap().as_ref())
2496    }
2497}
2498
2499#[pymethods]
2500impl PyIssuerAndSerialNumber {
2501    /// Parse a DER-encoded ``IssuerAndSerialNumber`` SEQUENCE.
2502    /// Raises :exc:`ValueError` if the outer SEQUENCE envelope is malformed.
2503    #[staticmethod]
2504    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
2505        let py_bytes = data.unbind();
2506        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
2507        // keeps the Python bytes object alive for the lifetime of this struct.
2508        // CPython's bytes objects have a fixed-address, non-relocating payload
2509        // buffer (CPython has no moving GC).  The slice lifetime is extended
2510        // to 'static; the actual safety invariants are:
2511        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
2512        //       can outlive the struct, so `raw` is never read after drop begins.
2513        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
2514        //       allocation), so field drop order does not cause use-after-free.
2515        //   (3) `inner` contains only borrow-typed fields in the decoded type;
2516        //       dropping the Box does not read through the contained &'static
2517        //       slices (borrows have no destructors in Rust).
2518        // CPython-only: does not hold for PyPy or GraalPy.
2519        let raw: &'static [u8] = unsafe {
2520            let s = py_bytes.bind(py).as_bytes();
2521            std::slice::from_raw_parts(s.as_ptr(), s.len())
2522        };
2523        {
2524            let mut d = Decoder::new(raw, Encoding::Der);
2525            d.read_tag()
2526                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2527            d.read_length()
2528                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2529        }
2530        Ok(Self {
2531            _data: py_bytes,
2532            raw,
2533            inner: OnceLock::new(),
2534            issuer_cache: OnceLock::new(),
2535            issuer_raw_der_cache: OnceLock::new(),
2536            serial_number_cache: OnceLock::new(),
2537        })
2538    }
2539
2540    /// Return the original bytes passed to :meth:`from_der`.
2541    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
2542        self._data.clone_ref(py).into_bound(py)
2543    }
2544
2545    /// Issuer distinguished name as an RFC 4514-style string.
2546    #[getter]
2547    fn issuer<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
2548        if let Some(cached) = self.issuer_cache.get() {
2549            return Ok(cached.clone_ref(py).into_bound(py));
2550        }
2551        let s = name_to_dn_string(&self.ias()?.issuer);
2552        let py_str = PyString::new(py, &s).unbind();
2553        let _ = self.issuer_cache.set(py_str.clone_ref(py));
2554        Ok(py_str.into_bound(py))
2555    }
2556
2557    /// Raw DER bytes of the issuer ``Name`` SEQUENCE.
2558    #[getter]
2559    fn issuer_raw_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
2560        if let Some(cached) = self.issuer_raw_der_cache.get() {
2561            return Ok(cached.clone_ref(py).into_bound(py));
2562        }
2563        let bytes = name_to_der_bytes(&self.ias()?.issuer);
2564        let py_bytes = PyBytes::new(py, &bytes).unbind();
2565        let _ = self.issuer_raw_der_cache.set(py_bytes.clone_ref(py));
2566        Ok(py_bytes.into_bound(py))
2567    }
2568
2569    /// Certificate serial number as a Python ``int``.
2570    ///
2571    /// Returns a Python ``int`` for serials that fit in 128 bits; for larger
2572    /// values calls ``int.from_bytes(raw, "big", signed=True)``.
2573    #[getter]
2574    fn serial_number<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
2575        if let Some(cached) = self.serial_number_cache.get() {
2576            return Ok(cached.clone_ref(py).into_bound(py));
2577        }
2578        let serial = &self.ias()?.serial_number;
2579        let py_int: Py<PyAny> = if let Ok(v) = serial.as_i64() {
2580            v.into_pyobject(py)?.into_any().unbind()
2581        } else if let Ok(v) = serial.as_i128() {
2582            v.into_pyobject(py)?.into_any().unbind()
2583        } else {
2584            let bytes_obj = PyBytes::new(py, serial.as_bytes());
2585            let kwargs = pyo3::types::PyDict::new(py);
2586            kwargs.set_item(pyo3::intern!(py, "signed"), true)?;
2587            py.get_type::<pyo3::types::PyInt>()
2588                .call_method(
2589                    pyo3::intern!(py, "from_bytes"),
2590                    (bytes_obj, pyo3::intern!(py, "big")),
2591                    Some(&kwargs),
2592                )
2593                .map(|r| r.unbind())?
2594        };
2595        let _ = self.serial_number_cache.set(py_int.clone_ref(py));
2596        Ok(py_int.into_bound(py))
2597    }
2598
2599    fn __repr__(&self) -> PyResult<String> {
2600        let ias = self.ias()?;
2601        let serial = &ias.serial_number;
2602        Ok(format!(
2603            "IssuerAndSerialNumber(issuer={:?}, serial={})",
2604            name_to_dn_string(&ias.issuer),
2605            serial
2606                .as_i64()
2607                .map(|v| v.to_string())
2608                .unwrap_or_else(|_| format!("<{} bytes>", serial.as_bytes().len())),
2609        ))
2610    }
2611}
2612
2613// ── PySignedData ──────────────────────────────────────────────────────────────
2614
2615/// CMS ``SignedData`` (RFC 5652 §5) accessible from Python.
2616///
2617/// Obtained by parsing the inner content of a :class:`ContentInfo` whose
2618/// ``content_type_oid`` equals :data:`~synta.cms.ID_SIGNED_DATA`.  Strip the
2619/// ``[0] EXPLICIT`` wrapper from ``ci.content`` before calling
2620/// :meth:`from_der`.
2621///
2622/// ```python,ignore
2623/// ci = ContentInfo.from_der(raw)
2624/// sd = SignedData.from_der(ci.content[4:])  # strip [0] EXPLICIT tag+len
2625/// print(sd.version)
2626/// for si in sd.signer_infos:
2627///     print(si.digest_algorithm_oid)
2628/// ```
2629#[pyclass(frozen, name = "SignedData")]
2630pub struct PySignedData {
2631    _data: Py<PyBytes>,
2632    raw: &'static [u8],
2633    inner: OnceLock<Box<synta_certificate::cms_rfc5652_types::SignedData<'static>>>,
2634    encap_content_type_cache: OnceLock<Py<PyObjectIdentifier>>,
2635    encap_content_cache: OnceLock<Option<Py<PyBytes>>>,
2636    certificates_cache: OnceLock<Option<Py<PyBytes>>>,
2637    crls_cache: OnceLock<Option<Py<PyBytes>>>,
2638    signer_infos_cache: OnceLock<Py<PyList>>,
2639}
2640
2641impl PySignedData {
2642    fn signed_data(&self) -> PyResult<&synta_certificate::cms_rfc5652_types::SignedData<'static>> {
2643        if let Some(v) = self.inner.get() {
2644            return Ok(v.as_ref());
2645        }
2646        let mut decoder = Decoder::new(self.raw, Encoding::Ber);
2647        let decoded = decoder.decode().map_err(|e| {
2648            pyo3::exceptions::PyValueError::new_err(format!("SignedData BER decode failed: {e}"))
2649        })?;
2650        let _ = self.inner.set(Box::new(decoded));
2651        Ok(self.inner.get().unwrap().as_ref())
2652    }
2653}
2654
2655#[pymethods]
2656impl PySignedData {
2657    /// Parse a DER- or BER-encoded CMS ``SignedData`` SEQUENCE.
2658    #[staticmethod]
2659    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
2660        let py_bytes = data.unbind();
2661        let raw: &'static [u8] = unsafe {
2662            let s = py_bytes.bind(py).as_bytes();
2663            std::slice::from_raw_parts(s.as_ptr(), s.len())
2664        };
2665        {
2666            let mut d = Decoder::new(raw, Encoding::Ber);
2667            d.read_tag()
2668                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2669            d.read_length()
2670                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2671        }
2672        Ok(Self {
2673            _data: py_bytes,
2674            raw,
2675            inner: OnceLock::new(),
2676            encap_content_type_cache: OnceLock::new(),
2677            encap_content_cache: OnceLock::new(),
2678            certificates_cache: OnceLock::new(),
2679            crls_cache: OnceLock::new(),
2680            signer_infos_cache: OnceLock::new(),
2681        })
2682    }
2683
2684    /// Return the original bytes passed to :meth:`from_der`.
2685    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
2686        self._data.clone_ref(py).into_bound(py)
2687    }
2688
2689    /// CMS version integer.
2690    #[getter]
2691    fn version(&self) -> PyResult<i64> {
2692        Ok(self.signed_data()?.version.as_i64().unwrap_or(0))
2693    }
2694
2695    /// OID identifying the encapsulated content type.
2696    #[getter]
2697    fn encap_content_type<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
2698        if let Some(cached) = self.encap_content_type_cache.get() {
2699            return Ok(cached.clone_ref(py).into_bound(py));
2700        }
2701        let obj = Py::new(
2702            py,
2703            PyObjectIdentifier::from_oid(
2704                self.signed_data()?
2705                    .encap_content_info
2706                    .e_content_type
2707                    .clone(),
2708            ),
2709        )?;
2710        let _ = self.encap_content_type_cache.set(obj.clone_ref(py));
2711        Ok(obj.into_bound(py))
2712    }
2713
2714    /// Raw bytes of the encapsulated content OCTET STRING value, or ``None``.
2715    #[getter]
2716    fn encap_content<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
2717        if let Some(cached) = self.encap_content_cache.get() {
2718            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
2719        }
2720        let computed = self
2721            .signed_data()?
2722            .encap_content_info
2723            .e_content
2724            .as_ref()
2725            .map(|c| PyBytes::new(py, c.as_bytes()));
2726        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
2727        let _ = self.encap_content_cache.set(to_store);
2728        Ok(computed)
2729    }
2730
2731    /// Raw content bytes of the ``certificates`` field (IMPLICIT ``[0]``), or ``None``.
2732    ///
2733    /// These are the **value** bytes captured during IMPLICIT decoding (no
2734    /// context-tag prefix).  Feed them to a :class:`~synta.Decoder` to iterate
2735    /// the individual certificate TLVs.
2736    #[getter]
2737    fn certificates<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
2738        if let Some(cached) = self.certificates_cache.get() {
2739            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
2740        }
2741        let computed = self
2742            .signed_data()?
2743            .certificates
2744            .as_ref()
2745            .map(|c| PyBytes::new(py, c.as_bytes()));
2746        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
2747        let _ = self.certificates_cache.set(to_store);
2748        Ok(computed)
2749    }
2750
2751    /// Raw content bytes of the ``crls`` field (IMPLICIT ``[1]``), or ``None``.
2752    #[getter]
2753    fn crls<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
2754        if let Some(cached) = self.crls_cache.get() {
2755            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
2756        }
2757        let computed = self
2758            .signed_data()?
2759            .crls
2760            .as_ref()
2761            .map(|c| PyBytes::new(py, c.as_bytes()));
2762        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
2763        let _ = self.crls_cache.set(to_store);
2764        Ok(computed)
2765    }
2766
2767    /// List of :class:`SignerInfo` objects (one per signer).
2768    #[getter]
2769    fn signer_infos<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
2770        if let Some(cached) = self.signer_infos_cache.get() {
2771            return Ok(cached.clone_ref(py).into_bound(py));
2772        }
2773        let list = PyList::empty(py);
2774        for si in self.signed_data()?.signer_infos.elements() {
2775            let mut enc = synta::Encoder::new(Encoding::Der);
2776            si.encode(&mut enc)
2777                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2778            let der = enc
2779                .finish()
2780                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2781            let pybytes = PyBytes::new(py, &der).unbind();
2782            // SAFETY: `pybytes` is a freshly-allocated PyBytes that owns the
2783            // `der` bytes.  CPython bytes have a fixed-address payload buffer.
2784            // `pybytes` is moved into `PySignerInfo::_data`, which holds a
2785            // strong reference for the lifetime of that struct.  `raw_static`
2786            // (no destructor) is stored in `PySignerInfo::raw`.  No read of
2787            // `raw_static` can occur after `_data` is dropped (through `&self`
2788            // borrows that cannot outlive the struct).
2789            // CPython-only: does not hold for PyPy or GraalPy.
2790            let raw_static: &'static [u8] = unsafe {
2791                let s = pybytes.bind(py).as_bytes();
2792                std::slice::from_raw_parts(s.as_ptr(), s.len())
2793            };
2794            let si_obj = Py::new(
2795                py,
2796                PySignerInfo {
2797                    _data: pybytes,
2798                    raw: raw_static,
2799                    inner: OnceLock::new(),
2800                    sid_cache: OnceLock::new(),
2801                    digest_algorithm_oid_cache: OnceLock::new(),
2802                    digest_algorithm_params_cache: OnceLock::new(),
2803                    signature_algorithm_oid_cache: OnceLock::new(),
2804                    signature_algorithm_params_cache: OnceLock::new(),
2805                    signature_cache: OnceLock::new(),
2806                    signed_attrs_cache: OnceLock::new(),
2807                    unsigned_attrs_cache: OnceLock::new(),
2808                },
2809            )?;
2810            list.append(si_obj.into_bound(py))?;
2811        }
2812        let list_unbound = list.unbind();
2813        let _ = self.signer_infos_cache.set(list_unbound.clone_ref(py));
2814        Ok(list_unbound.into_bound(py))
2815    }
2816
2817    fn __repr__(&self) -> PyResult<String> {
2818        let sd = self.signed_data()?;
2819        Ok(format!(
2820            "SignedData(version={}, signer_count={})",
2821            sd.version.as_i64().unwrap_or(0),
2822            sd.signer_infos.len(),
2823        ))
2824    }
2825}
2826
2827// ── PySignerInfo ──────────────────────────────────────────────────────────────
2828
2829/// CMS ``SignerInfo`` (RFC 5652 §5.3) accessible from Python.
2830///
2831/// Represents one signer's information within a :class:`SignedData`.
2832/// Retrieve instances via :attr:`SignedData.signer_infos` or construct
2833/// directly with :meth:`from_der`.
2834///
2835/// ```python,ignore
2836/// for si in sd.signer_infos:
2837///     print(si.digest_algorithm_oid)
2838///     print(si.signature_algorithm_oid)
2839/// ```
2840#[pyclass(frozen, name = "SignerInfo")]
2841pub struct PySignerInfo {
2842    _data: Py<PyBytes>,
2843    raw: &'static [u8],
2844    inner: OnceLock<Box<synta_certificate::cms_rfc5652_types::SignerInfo<'static>>>,
2845    sid_cache: OnceLock<Py<PyBytes>>,
2846    digest_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
2847    digest_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
2848    signature_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
2849    signature_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
2850    signature_cache: OnceLock<Py<PyBytes>>,
2851    signed_attrs_cache: OnceLock<Option<Py<PyBytes>>>,
2852    unsigned_attrs_cache: OnceLock<Option<Py<PyBytes>>>,
2853}
2854
2855impl PySignerInfo {
2856    fn signer_info(&self) -> PyResult<&synta_certificate::cms_rfc5652_types::SignerInfo<'static>> {
2857        if let Some(v) = self.inner.get() {
2858            return Ok(v.as_ref());
2859        }
2860        let mut decoder = Decoder::new(self.raw, Encoding::Der);
2861        let decoded = decoder.decode().map_err(|e| {
2862            pyo3::exceptions::PyValueError::new_err(format!("SignerInfo DER decode failed: {e}"))
2863        })?;
2864        let _ = self.inner.set(Box::new(decoded));
2865        Ok(self.inner.get().unwrap().as_ref())
2866    }
2867}
2868
2869#[pymethods]
2870impl PySignerInfo {
2871    /// Parse a DER-encoded CMS ``SignerInfo`` SEQUENCE.
2872    #[staticmethod]
2873    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
2874        let py_bytes = data.unbind();
2875        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
2876        // keeps the Python bytes object alive for the lifetime of this struct.
2877        // CPython's bytes objects have a fixed-address, non-relocating payload
2878        // buffer (CPython has no moving GC).  The slice lifetime is extended
2879        // to 'static; the actual safety invariants are:
2880        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
2881        //       can outlive the struct, so `raw` is never read after drop begins.
2882        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
2883        //       allocation), so field drop order does not cause use-after-free.
2884        //   (3) `inner` contains only borrow-typed fields in the decoded type;
2885        //       dropping the Box does not read through the contained &'static
2886        //       slices (borrows have no destructors in Rust).
2887        // CPython-only: does not hold for PyPy or GraalPy.
2888        let raw: &'static [u8] = unsafe {
2889            let s = py_bytes.bind(py).as_bytes();
2890            std::slice::from_raw_parts(s.as_ptr(), s.len())
2891        };
2892        {
2893            let mut d = Decoder::new(raw, Encoding::Der);
2894            d.read_tag()
2895                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2896            d.read_length()
2897                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
2898        }
2899        Ok(Self {
2900            _data: py_bytes,
2901            raw,
2902            inner: OnceLock::new(),
2903            sid_cache: OnceLock::new(),
2904            digest_algorithm_oid_cache: OnceLock::new(),
2905            digest_algorithm_params_cache: OnceLock::new(),
2906            signature_algorithm_oid_cache: OnceLock::new(),
2907            signature_algorithm_params_cache: OnceLock::new(),
2908            signature_cache: OnceLock::new(),
2909            signed_attrs_cache: OnceLock::new(),
2910            unsigned_attrs_cache: OnceLock::new(),
2911        })
2912    }
2913
2914    /// Return the original bytes passed to :meth:`from_der`.
2915    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
2916        self._data.clone_ref(py).into_bound(py)
2917    }
2918
2919    /// CMS version integer.
2920    #[getter]
2921    fn version(&self) -> PyResult<i64> {
2922        Ok(self.signer_info()?.version.as_i64().unwrap_or(0))
2923    }
2924
2925    /// Raw TLV bytes of the ``SignerIdentifier`` CHOICE field.
2926    ///
2927    /// Peek the tag byte to distinguish ``issuerAndSerialNumber`` (SEQUENCE,
2928    /// tag ``0x30``) from ``subjectKeyIdentifier`` (context ``[0]``, tag
2929    /// ``0x80``).
2930    #[getter]
2931    fn sid<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
2932        if let Some(cached) = self.sid_cache.get() {
2933            return Ok(cached.clone_ref(py).into_bound(py));
2934        }
2935        let py_bytes = PyBytes::new(py, self.signer_info()?.sid.as_bytes()).unbind();
2936        let _ = self.sid_cache.set(py_bytes.clone_ref(py));
2937        Ok(py_bytes.into_bound(py))
2938    }
2939
2940    /// OID of the message-digest algorithm.
2941    #[getter]
2942    fn digest_algorithm_oid<'py>(
2943        &self,
2944        py: Python<'py>,
2945    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
2946        if let Some(cached) = self.digest_algorithm_oid_cache.get() {
2947            return Ok(cached.clone_ref(py).into_bound(py));
2948        }
2949        let obj = Py::new(
2950            py,
2951            PyObjectIdentifier::from_oid(self.signer_info()?.digest_algorithm.algorithm.clone()),
2952        )?;
2953        let _ = self.digest_algorithm_oid_cache.set(obj.clone_ref(py));
2954        Ok(obj.into_bound(py))
2955    }
2956
2957    /// Raw DER bytes of the digest-algorithm parameters, or ``None``.
2958    #[getter]
2959    fn digest_algorithm_params<'py>(
2960        &self,
2961        py: Python<'py>,
2962    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
2963        if let Some(cached) = self.digest_algorithm_params_cache.get() {
2964            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
2965        }
2966        let computed =
2967            encode_element_opt(py, self.signer_info()?.digest_algorithm.parameters.as_ref())?;
2968        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
2969        let _ = self.digest_algorithm_params_cache.set(to_store);
2970        Ok(computed)
2971    }
2972
2973    /// OID of the signature algorithm.
2974    #[getter]
2975    fn signature_algorithm_oid<'py>(
2976        &self,
2977        py: Python<'py>,
2978    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
2979        if let Some(cached) = self.signature_algorithm_oid_cache.get() {
2980            return Ok(cached.clone_ref(py).into_bound(py));
2981        }
2982        let obj = Py::new(
2983            py,
2984            PyObjectIdentifier::from_oid(self.signer_info()?.signature_algorithm.algorithm.clone()),
2985        )?;
2986        let _ = self.signature_algorithm_oid_cache.set(obj.clone_ref(py));
2987        Ok(obj.into_bound(py))
2988    }
2989
2990    /// Raw DER bytes of the signature-algorithm parameters, or ``None``.
2991    #[getter]
2992    fn signature_algorithm_params<'py>(
2993        &self,
2994        py: Python<'py>,
2995    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
2996        if let Some(cached) = self.signature_algorithm_params_cache.get() {
2997            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
2998        }
2999        let computed = encode_element_opt(
3000            py,
3001            self.signer_info()?.signature_algorithm.parameters.as_ref(),
3002        )?;
3003        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3004        let _ = self.signature_algorithm_params_cache.set(to_store);
3005        Ok(computed)
3006    }
3007
3008    /// Raw bytes of the signature value.
3009    #[getter]
3010    fn signature<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
3011        if let Some(cached) = self.signature_cache.get() {
3012            return Ok(cached.clone_ref(py).into_bound(py));
3013        }
3014        let py_bytes = PyBytes::new(py, self.signer_info()?.signature.as_bytes()).unbind();
3015        let _ = self.signature_cache.set(py_bytes.clone_ref(py));
3016        Ok(py_bytes.into_bound(py))
3017    }
3018
3019    /// Raw content bytes of the ``signedAttrs`` field (IMPLICIT ``[0]``), or ``None``.
3020    ///
3021    /// To verify the signature, replace the leading context-specific tag byte
3022    /// with the SET tag ``0x31`` before hashing.
3023    #[getter]
3024    fn signed_attrs<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
3025        if let Some(cached) = self.signed_attrs_cache.get() {
3026            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3027        }
3028        let computed = self
3029            .signer_info()?
3030            .signed_attrs
3031            .as_ref()
3032            .map(|a| PyBytes::new(py, a.as_bytes()));
3033        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3034        let _ = self.signed_attrs_cache.set(to_store);
3035        Ok(computed)
3036    }
3037
3038    /// Raw content bytes of the ``unsignedAttrs`` field (IMPLICIT ``[1]``), or ``None``.
3039    #[getter]
3040    fn unsigned_attrs<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
3041        if let Some(cached) = self.unsigned_attrs_cache.get() {
3042            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3043        }
3044        let computed = self
3045            .signer_info()?
3046            .unsigned_attrs
3047            .as_ref()
3048            .map(|a| PyBytes::new(py, a.as_bytes()));
3049        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3050        let _ = self.unsigned_attrs_cache.set(to_store);
3051        Ok(computed)
3052    }
3053
3054    fn __repr__(&self) -> PyResult<String> {
3055        let si = self.signer_info()?;
3056        Ok(format!(
3057            "SignerInfo(version={}, digest_algorithm={})",
3058            si.version.as_i64().unwrap_or(0),
3059            si.digest_algorithm.algorithm,
3060        ))
3061    }
3062}
3063
3064// ── PyEnvelopedData ───────────────────────────────────────────────────────────
3065
3066/// CMS ``EnvelopedData`` (RFC 5652 §6) accessible from Python.
3067///
3068/// Obtained by parsing the inner content of a :class:`ContentInfo` whose
3069/// ``content_type_oid`` equals :data:`~synta.cms.ID_ENVELOPED_DATA`.
3070#[pyclass(frozen, name = "EnvelopedData")]
3071pub struct PyEnvelopedData {
3072    _data: Py<PyBytes>,
3073    raw: &'static [u8],
3074    inner: OnceLock<Box<synta_certificate::cms_rfc5652_types::EnvelopedData<'static>>>,
3075    originator_info_cache: OnceLock<Option<Py<PyBytes>>>,
3076    recipient_infos_cache: OnceLock<Py<PyBytes>>,
3077    content_type_cache: OnceLock<Py<PyObjectIdentifier>>,
3078    content_encryption_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
3079    content_encryption_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
3080    encrypted_content_cache: OnceLock<Option<Py<PyBytes>>>,
3081    unprotected_attrs_cache: OnceLock<Option<Py<PyBytes>>>,
3082}
3083
3084impl PyEnvelopedData {
3085    fn enveloped_data(
3086        &self,
3087    ) -> PyResult<&synta_certificate::cms_rfc5652_types::EnvelopedData<'static>> {
3088        if let Some(v) = self.inner.get() {
3089            return Ok(v.as_ref());
3090        }
3091        let mut decoder = Decoder::new(self.raw, Encoding::Ber);
3092        let decoded = decoder.decode().map_err(|e| {
3093            pyo3::exceptions::PyValueError::new_err(format!("EnvelopedData BER decode failed: {e}"))
3094        })?;
3095        let _ = self.inner.set(Box::new(decoded));
3096        Ok(self.inner.get().unwrap().as_ref())
3097    }
3098}
3099
3100#[pymethods]
3101impl PyEnvelopedData {
3102    /// Parse a DER- or BER-encoded CMS ``EnvelopedData`` SEQUENCE.
3103    #[staticmethod]
3104    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
3105        let py_bytes = data.unbind();
3106        let raw: &'static [u8] = unsafe {
3107            let s = py_bytes.bind(py).as_bytes();
3108            std::slice::from_raw_parts(s.as_ptr(), s.len())
3109        };
3110        {
3111            let mut d = Decoder::new(raw, Encoding::Ber);
3112            d.read_tag()
3113                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3114            d.read_length()
3115                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3116        }
3117        Ok(Self {
3118            _data: py_bytes,
3119            raw,
3120            inner: OnceLock::new(),
3121            originator_info_cache: OnceLock::new(),
3122            recipient_infos_cache: OnceLock::new(),
3123            content_type_cache: OnceLock::new(),
3124            content_encryption_algorithm_oid_cache: OnceLock::new(),
3125            content_encryption_algorithm_params_cache: OnceLock::new(),
3126            encrypted_content_cache: OnceLock::new(),
3127            unprotected_attrs_cache: OnceLock::new(),
3128        })
3129    }
3130
3131    /// Return the original bytes passed to :meth:`from_der`.
3132    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
3133        self._data.clone_ref(py).into_bound(py)
3134    }
3135
3136    /// CMS version integer.
3137    #[getter]
3138    fn version(&self) -> PyResult<i64> {
3139        Ok(self.enveloped_data()?.version.as_i64().unwrap_or(0))
3140    }
3141
3142    /// DER-encoded ``OriginatorInfo`` SEQUENCE, or ``None`` if absent.
3143    #[getter]
3144    fn originator_info<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
3145        if let Some(cached) = self.originator_info_cache.get() {
3146            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3147        }
3148        let computed = match &self.enveloped_data()?.originator_info {
3149            None => None,
3150            Some(oi) => {
3151                let mut enc = synta::Encoder::new(Encoding::Der);
3152                oi.encode(&mut enc)
3153                    .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3154                let bytes = enc
3155                    .finish()
3156                    .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3157                Some(PyBytes::new(py, &bytes))
3158            }
3159        };
3160        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3161        let _ = self.originator_info_cache.set(to_store);
3162        Ok(computed)
3163    }
3164
3165    /// Raw TLV bytes of the ``RecipientInfos`` SET field.
3166    #[getter]
3167    fn recipient_infos<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
3168        if let Some(cached) = self.recipient_infos_cache.get() {
3169            return Ok(cached.clone_ref(py).into_bound(py));
3170        }
3171        let py_bytes = PyBytes::new(py, self.enveloped_data()?.recipient_infos.as_bytes()).unbind();
3172        let _ = self.recipient_infos_cache.set(py_bytes.clone_ref(py));
3173        Ok(py_bytes.into_bound(py))
3174    }
3175
3176    /// OID identifying the encrypted content type.
3177    #[getter]
3178    fn content_type<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
3179        if let Some(cached) = self.content_type_cache.get() {
3180            return Ok(cached.clone_ref(py).into_bound(py));
3181        }
3182        let obj = Py::new(
3183            py,
3184            PyObjectIdentifier::from_oid(
3185                self.enveloped_data()?
3186                    .encrypted_content_info
3187                    .content_type
3188                    .clone(),
3189            ),
3190        )?;
3191        let _ = self.content_type_cache.set(obj.clone_ref(py));
3192        Ok(obj.into_bound(py))
3193    }
3194
3195    /// OID of the content-encryption algorithm.
3196    #[getter]
3197    fn content_encryption_algorithm_oid<'py>(
3198        &self,
3199        py: Python<'py>,
3200    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
3201        if let Some(cached) = self.content_encryption_algorithm_oid_cache.get() {
3202            return Ok(cached.clone_ref(py).into_bound(py));
3203        }
3204        let obj = Py::new(
3205            py,
3206            PyObjectIdentifier::from_oid(
3207                self.enveloped_data()?
3208                    .encrypted_content_info
3209                    .content_encryption_algorithm
3210                    .algorithm
3211                    .clone(),
3212            ),
3213        )?;
3214        let _ = self
3215            .content_encryption_algorithm_oid_cache
3216            .set(obj.clone_ref(py));
3217        Ok(obj.into_bound(py))
3218    }
3219
3220    /// Raw DER bytes of the content-encryption algorithm parameters, or ``None``.
3221    #[getter]
3222    fn content_encryption_algorithm_params<'py>(
3223        &self,
3224        py: Python<'py>,
3225    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
3226        if let Some(cached) = self.content_encryption_algorithm_params_cache.get() {
3227            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3228        }
3229        let computed = encode_element_opt(
3230            py,
3231            self.enveloped_data()?
3232                .encrypted_content_info
3233                .content_encryption_algorithm
3234                .parameters
3235                .as_ref(),
3236        )?;
3237        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3238        let _ = self.content_encryption_algorithm_params_cache.set(to_store);
3239        Ok(computed)
3240    }
3241
3242    /// Raw bytes of the encrypted content OCTET STRING value (IMPLICIT ``[0]``),
3243    /// or ``None`` if absent.
3244    #[getter]
3245    fn encrypted_content<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
3246        if let Some(cached) = self.encrypted_content_cache.get() {
3247            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3248        }
3249        let computed = self
3250            .enveloped_data()?
3251            .encrypted_content_info
3252            .encrypted_content
3253            .as_ref()
3254            .map(|c| PyBytes::new(py, c.as_bytes()));
3255        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3256        let _ = self.encrypted_content_cache.set(to_store);
3257        Ok(computed)
3258    }
3259
3260    /// Raw content bytes of the ``unprotectedAttrs`` field (IMPLICIT ``[1]``),
3261    /// or ``None`` if absent.
3262    #[getter]
3263    fn unprotected_attrs<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
3264        if let Some(cached) = self.unprotected_attrs_cache.get() {
3265            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3266        }
3267        let computed = self
3268            .enveloped_data()?
3269            .unprotected_attrs
3270            .as_ref()
3271            .map(|a| PyBytes::new(py, a.as_bytes()));
3272        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3273        let _ = self.unprotected_attrs_cache.set(to_store);
3274        Ok(computed)
3275    }
3276
3277    fn __repr__(&self) -> PyResult<String> {
3278        Ok(format!(
3279            "EnvelopedData(version={})",
3280            self.enveloped_data()?.version.as_i64().unwrap_or(0),
3281        ))
3282    }
3283}
3284
3285// ── PyEncryptedData ───────────────────────────────────────────────────────────
3286
3287/// CMS ``EncryptedData`` (RFC 5652 §8) accessible from Python.
3288///
3289/// Obtained by parsing the inner content of a :class:`ContentInfo` whose
3290/// ``content_type_oid`` equals :data:`~synta.cms.ID_ENCRYPTED_DATA`.
3291#[pyclass(frozen, name = "EncryptedData")]
3292pub struct PyEncryptedData {
3293    _data: Py<PyBytes>,
3294    raw: &'static [u8],
3295    inner: OnceLock<Box<synta_certificate::cms_rfc5652_types::EncryptedData<'static>>>,
3296    content_type_cache: OnceLock<Py<PyObjectIdentifier>>,
3297    content_encryption_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
3298    content_encryption_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
3299    encrypted_content_cache: OnceLock<Option<Py<PyBytes>>>,
3300    unprotected_attrs_cache: OnceLock<Option<Py<PyBytes>>>,
3301}
3302
3303impl PyEncryptedData {
3304    fn encrypted_data(
3305        &self,
3306    ) -> PyResult<&synta_certificate::cms_rfc5652_types::EncryptedData<'static>> {
3307        if let Some(v) = self.inner.get() {
3308            return Ok(v.as_ref());
3309        }
3310        let mut decoder = Decoder::new(self.raw, Encoding::Ber);
3311        let decoded = decoder.decode().map_err(|e| {
3312            pyo3::exceptions::PyValueError::new_err(format!("EncryptedData BER decode failed: {e}"))
3313        })?;
3314        let _ = self.inner.set(Box::new(decoded));
3315        Ok(self.inner.get().unwrap().as_ref())
3316    }
3317}
3318
3319#[pymethods]
3320impl PyEncryptedData {
3321    /// Parse a DER- or BER-encoded CMS ``EncryptedData`` SEQUENCE.
3322    #[staticmethod]
3323    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
3324        let py_bytes = data.unbind();
3325        let raw: &'static [u8] = unsafe {
3326            let s = py_bytes.bind(py).as_bytes();
3327            std::slice::from_raw_parts(s.as_ptr(), s.len())
3328        };
3329        {
3330            let mut d = Decoder::new(raw, Encoding::Ber);
3331            d.read_tag()
3332                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3333            d.read_length()
3334                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3335        }
3336        Ok(Self {
3337            _data: py_bytes,
3338            raw,
3339            inner: OnceLock::new(),
3340            content_type_cache: OnceLock::new(),
3341            content_encryption_algorithm_oid_cache: OnceLock::new(),
3342            content_encryption_algorithm_params_cache: OnceLock::new(),
3343            encrypted_content_cache: OnceLock::new(),
3344            unprotected_attrs_cache: OnceLock::new(),
3345        })
3346    }
3347
3348    /// Return the original bytes passed to :meth:`from_der`.
3349    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
3350        self._data.clone_ref(py).into_bound(py)
3351    }
3352
3353    /// CMS version integer.
3354    #[getter]
3355    fn version(&self) -> PyResult<i64> {
3356        Ok(self.encrypted_data()?.version.as_i64().unwrap_or(0))
3357    }
3358
3359    /// OID identifying the encrypted content type.
3360    #[getter]
3361    fn content_type<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
3362        if let Some(cached) = self.content_type_cache.get() {
3363            return Ok(cached.clone_ref(py).into_bound(py));
3364        }
3365        let obj = Py::new(
3366            py,
3367            PyObjectIdentifier::from_oid(
3368                self.encrypted_data()?
3369                    .encrypted_content_info
3370                    .content_type
3371                    .clone(),
3372            ),
3373        )?;
3374        let _ = self.content_type_cache.set(obj.clone_ref(py));
3375        Ok(obj.into_bound(py))
3376    }
3377
3378    /// OID of the content-encryption algorithm.
3379    #[getter]
3380    fn content_encryption_algorithm_oid<'py>(
3381        &self,
3382        py: Python<'py>,
3383    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
3384        if let Some(cached) = self.content_encryption_algorithm_oid_cache.get() {
3385            return Ok(cached.clone_ref(py).into_bound(py));
3386        }
3387        let obj = Py::new(
3388            py,
3389            PyObjectIdentifier::from_oid(
3390                self.encrypted_data()?
3391                    .encrypted_content_info
3392                    .content_encryption_algorithm
3393                    .algorithm
3394                    .clone(),
3395            ),
3396        )?;
3397        let _ = self
3398            .content_encryption_algorithm_oid_cache
3399            .set(obj.clone_ref(py));
3400        Ok(obj.into_bound(py))
3401    }
3402
3403    /// Raw DER bytes of the content-encryption algorithm parameters, or ``None``.
3404    #[getter]
3405    fn content_encryption_algorithm_params<'py>(
3406        &self,
3407        py: Python<'py>,
3408    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
3409        if let Some(cached) = self.content_encryption_algorithm_params_cache.get() {
3410            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3411        }
3412        let computed = encode_element_opt(
3413            py,
3414            self.encrypted_data()?
3415                .encrypted_content_info
3416                .content_encryption_algorithm
3417                .parameters
3418                .as_ref(),
3419        )?;
3420        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3421        let _ = self.content_encryption_algorithm_params_cache.set(to_store);
3422        Ok(computed)
3423    }
3424
3425    /// Raw bytes of the encrypted content OCTET STRING value (IMPLICIT ``[0]``),
3426    /// or ``None`` if absent.
3427    #[getter]
3428    fn encrypted_content<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
3429        if let Some(cached) = self.encrypted_content_cache.get() {
3430            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3431        }
3432        let computed = self
3433            .encrypted_data()?
3434            .encrypted_content_info
3435            .encrypted_content
3436            .as_ref()
3437            .map(|c| PyBytes::new(py, c.as_bytes()));
3438        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3439        let _ = self.encrypted_content_cache.set(to_store);
3440        Ok(computed)
3441    }
3442
3443    /// Raw content bytes of the ``unprotectedAttrs`` field (IMPLICIT ``[1]``),
3444    /// or ``None`` if absent.
3445    #[getter]
3446    fn unprotected_attrs<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
3447        if let Some(cached) = self.unprotected_attrs_cache.get() {
3448            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3449        }
3450        let computed = self
3451            .encrypted_data()?
3452            .unprotected_attrs
3453            .as_ref()
3454            .map(|a| PyBytes::new(py, a.as_bytes()));
3455        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3456        let _ = self.unprotected_attrs_cache.set(to_store);
3457        Ok(computed)
3458    }
3459
3460    /// Build a new :class:`EncryptedData` by encrypting ``plaintext`` with
3461    /// ``key``.
3462    ///
3463    /// - ``algorithm_oid``: dotted-decimal OID string or
3464    ///   :class:`~synta.ObjectIdentifier` for the content-encryption algorithm
3465    ///   (e.g. ``"2.16.840.1.101.3.4.1.2"`` for AES-128-CBC).
3466    /// - ``content_type_oid``: optional content-type OID (default:
3467    ///   ``id-data`` = ``"1.2.840.113549.1.7.1"``).
3468    ///
3469    /// A fresh random IV is generated for each call.
3470    ///
3471    /// Raises :exc:`NotImplementedError` when built without the ``openssl``
3472    /// Cargo feature.
3473    #[staticmethod]
3474    #[pyo3(signature = (plaintext, key, algorithm_oid, content_type_oid = None))]
3475    fn create(
3476        py: Python<'_>,
3477        plaintext: &[u8],
3478        key: &[u8],
3479        algorithm_oid: &Bound<'_, PyAny>,
3480        content_type_oid: Option<&Bound<'_, PyAny>>,
3481    ) -> PyResult<Self> {
3482        let enc_alg_oid = oid_from_pyany(algorithm_oid)?;
3483
3484        let ct_oid = match content_type_oid {
3485            Some(obj) => oid_from_pyany(obj)?,
3486            None => synta::ObjectIdentifier::new(synta_certificate::pkcs7_types::ID_DATA)
3487                .expect("id-data is a valid OID"),
3488        };
3489
3490        #[cfg(feature = "openssl")]
3491        {
3492            use synta_certificate::CmsEncryptor as _;
3493            let der = synta_certificate::OpensslEncryptor
3494                .create_encrypted_data(
3495                    ct_oid.components(),
3496                    enc_alg_oid.components(),
3497                    plaintext,
3498                    key,
3499                )
3500                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3501            let py_bytes = PyBytes::new(py, &der).unbind();
3502            Self::from_der(py, py_bytes.into_bound(py))
3503        }
3504
3505        #[cfg(not(feature = "openssl"))]
3506        {
3507            let _ = (py, plaintext, key, enc_alg_oid, ct_oid);
3508            Err(pyo3::exceptions::PyNotImplementedError::new_err(
3509                "CMS encryption requires the 'openssl' feature; \
3510                 rebuild synta-python with --features openssl",
3511            ))
3512        }
3513    }
3514
3515    /// Decrypt the encrypted content using a raw symmetric key.
3516    ///
3517    /// ``key`` must be the raw symmetric key bytes matching the
3518    /// content-encryption algorithm (e.g. 16 bytes for AES-128-CBC,
3519    /// 32 bytes for AES-256-CBC).
3520    ///
3521    /// Raises :exc:`NotImplementedError` when built without the
3522    /// ``openssl`` Cargo feature.
3523    fn decrypt<'py>(&self, py: Python<'py>, key: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
3524        #[cfg(feature = "openssl")]
3525        {
3526            use synta_certificate::CmsDecryptor as _;
3527            let ed = self.encrypted_data()?;
3528            let mut enc = synta::Encoder::new(synta::Encoding::Der);
3529            ed.encrypted_content_info
3530                .content_encryption_algorithm
3531                .encode(&mut enc)
3532                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3533            let algorithm_der = enc
3534                .finish()
3535                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3536            let ciphertext = ed
3537                .encrypted_content_info
3538                .encrypted_content
3539                .as_ref()
3540                .ok_or_else(|| {
3541                    pyo3::exceptions::PyValueError::new_err(
3542                        "EncryptedData has no encryptedContent field",
3543                    )
3544                })?
3545                .as_bytes();
3546            let plaintext = synta_certificate::OpensslDecryptor
3547                .decrypt(&algorithm_der, ciphertext, key)
3548                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3549            Ok(PyBytes::new(py, &plaintext))
3550        }
3551        #[cfg(not(feature = "openssl"))]
3552        {
3553            let _ = (py, key);
3554            Err(pyo3::exceptions::PyNotImplementedError::new_err(
3555                "CMS decryption requires the 'openssl' feature; \
3556                 rebuild synta-python with --features openssl",
3557            ))
3558        }
3559    }
3560
3561    fn __repr__(&self) -> PyResult<String> {
3562        Ok(format!(
3563            "EncryptedData(version={})",
3564            self.encrypted_data()?.version.as_i64().unwrap_or(0),
3565        ))
3566    }
3567}
3568
3569// ── PyDigestedData ────────────────────────────────────────────────────────────
3570
3571/// CMS ``DigestedData`` (RFC 5652 §7) accessible from Python.
3572///
3573/// Obtained by parsing the inner content of a :class:`ContentInfo` whose
3574/// ``content_type_oid`` is the id-digestedData OID.
3575#[pyclass(frozen, name = "DigestedData")]
3576pub struct PyDigestedData {
3577    _data: Py<PyBytes>,
3578    raw: &'static [u8],
3579    inner: OnceLock<Box<synta_certificate::cms_rfc5652_types::DigestedData<'static>>>,
3580    digest_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
3581    digest_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
3582    encap_content_type_cache: OnceLock<Py<PyObjectIdentifier>>,
3583    encap_content_cache: OnceLock<Option<Py<PyBytes>>>,
3584    digest_cache: OnceLock<Py<PyBytes>>,
3585}
3586
3587impl PyDigestedData {
3588    fn digested_data(
3589        &self,
3590    ) -> PyResult<&synta_certificate::cms_rfc5652_types::DigestedData<'static>> {
3591        if let Some(v) = self.inner.get() {
3592            return Ok(v.as_ref());
3593        }
3594        let mut decoder = Decoder::new(self.raw, Encoding::Ber);
3595        let decoded = decoder.decode().map_err(|e| {
3596            pyo3::exceptions::PyValueError::new_err(format!("DigestedData BER decode failed: {e}"))
3597        })?;
3598        let _ = self.inner.set(Box::new(decoded));
3599        Ok(self.inner.get().unwrap().as_ref())
3600    }
3601}
3602
3603#[pymethods]
3604impl PyDigestedData {
3605    /// Parse a DER- or BER-encoded CMS ``DigestedData`` SEQUENCE.
3606    #[staticmethod]
3607    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
3608        let py_bytes = data.unbind();
3609        let raw: &'static [u8] = unsafe {
3610            let s = py_bytes.bind(py).as_bytes();
3611            std::slice::from_raw_parts(s.as_ptr(), s.len())
3612        };
3613        {
3614            let mut d = Decoder::new(raw, Encoding::Ber);
3615            d.read_tag()
3616                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3617            d.read_length()
3618                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3619        }
3620        Ok(Self {
3621            _data: py_bytes,
3622            raw,
3623            inner: OnceLock::new(),
3624            digest_algorithm_oid_cache: OnceLock::new(),
3625            digest_algorithm_params_cache: OnceLock::new(),
3626            encap_content_type_cache: OnceLock::new(),
3627            encap_content_cache: OnceLock::new(),
3628            digest_cache: OnceLock::new(),
3629        })
3630    }
3631
3632    /// Return the original bytes passed to :meth:`from_der`.
3633    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
3634        self._data.clone_ref(py).into_bound(py)
3635    }
3636
3637    /// CMS version integer.
3638    #[getter]
3639    fn version(&self) -> PyResult<i64> {
3640        Ok(self.digested_data()?.version.as_i64().unwrap_or(0))
3641    }
3642
3643    /// OID of the digest algorithm.
3644    #[getter]
3645    fn digest_algorithm_oid<'py>(
3646        &self,
3647        py: Python<'py>,
3648    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
3649        if let Some(cached) = self.digest_algorithm_oid_cache.get() {
3650            return Ok(cached.clone_ref(py).into_bound(py));
3651        }
3652        let obj = Py::new(
3653            py,
3654            PyObjectIdentifier::from_oid(self.digested_data()?.digest_algorithm.algorithm.clone()),
3655        )?;
3656        let _ = self.digest_algorithm_oid_cache.set(obj.clone_ref(py));
3657        Ok(obj.into_bound(py))
3658    }
3659
3660    /// Raw DER bytes of the digest-algorithm parameters, or ``None``.
3661    #[getter]
3662    fn digest_algorithm_params<'py>(
3663        &self,
3664        py: Python<'py>,
3665    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
3666        if let Some(cached) = self.digest_algorithm_params_cache.get() {
3667            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3668        }
3669        let computed = encode_element_opt(
3670            py,
3671            self.digested_data()?.digest_algorithm.parameters.as_ref(),
3672        )?;
3673        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3674        let _ = self.digest_algorithm_params_cache.set(to_store);
3675        Ok(computed)
3676    }
3677
3678    /// OID identifying the encapsulated content type.
3679    #[getter]
3680    fn encap_content_type<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
3681        if let Some(cached) = self.encap_content_type_cache.get() {
3682            return Ok(cached.clone_ref(py).into_bound(py));
3683        }
3684        let obj = Py::new(
3685            py,
3686            PyObjectIdentifier::from_oid(
3687                self.digested_data()?
3688                    .encap_content_info
3689                    .e_content_type
3690                    .clone(),
3691            ),
3692        )?;
3693        let _ = self.encap_content_type_cache.set(obj.clone_ref(py));
3694        Ok(obj.into_bound(py))
3695    }
3696
3697    /// Raw bytes of the encapsulated content OCTET STRING value, or ``None``.
3698    #[getter]
3699    fn encap_content<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
3700        if let Some(cached) = self.encap_content_cache.get() {
3701            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3702        }
3703        let computed = self
3704            .digested_data()?
3705            .encap_content_info
3706            .e_content
3707            .as_ref()
3708            .map(|c| PyBytes::new(py, c.as_bytes()));
3709        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3710        let _ = self.encap_content_cache.set(to_store);
3711        Ok(computed)
3712    }
3713
3714    /// Raw bytes of the message digest (OCTET STRING value).
3715    #[getter]
3716    fn digest<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
3717        if let Some(cached) = self.digest_cache.get() {
3718            return Ok(cached.clone_ref(py).into_bound(py));
3719        }
3720        let py_bytes = PyBytes::new(py, self.digested_data()?.digest.as_bytes()).unbind();
3721        let _ = self.digest_cache.set(py_bytes.clone_ref(py));
3722        Ok(py_bytes.into_bound(py))
3723    }
3724
3725    fn __repr__(&self) -> PyResult<String> {
3726        let dd = self.digested_data()?;
3727        Ok(format!(
3728            "DigestedData(version={}, digest_algorithm={})",
3729            dd.version.as_i64().unwrap_or(0),
3730            dd.digest_algorithm.algorithm,
3731        ))
3732    }
3733}
3734
3735// ── PyAuthenticatedData ───────────────────────────────────────────────────────
3736
3737/// CMS ``AuthenticatedData`` (RFC 5652 §9) accessible from Python.
3738///
3739/// Obtained by parsing the inner content of a :class:`ContentInfo` whose
3740/// ``content_type_oid`` is the id-ct-authData OID.
3741#[pyclass(frozen, name = "AuthenticatedData")]
3742pub struct PyAuthenticatedData {
3743    _data: Py<PyBytes>,
3744    raw: &'static [u8],
3745    inner: OnceLock<Box<synta_certificate::cms_rfc5652_types::AuthenticatedData<'static>>>,
3746    originator_info_cache: OnceLock<Option<Py<PyBytes>>>,
3747    recipient_infos_cache: OnceLock<Py<PyBytes>>,
3748    mac_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
3749    mac_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
3750    digest_algorithm_oid_cache: OnceLock<Option<Py<PyObjectIdentifier>>>,
3751    digest_algorithm_params_cache: OnceLock<Option<Py<PyBytes>>>,
3752    encap_content_type_cache: OnceLock<Py<PyObjectIdentifier>>,
3753    encap_content_cache: OnceLock<Option<Py<PyBytes>>>,
3754    mac_cache: OnceLock<Py<PyBytes>>,
3755    auth_attrs_cache: OnceLock<Option<Py<PyBytes>>>,
3756    unauth_attrs_cache: OnceLock<Option<Py<PyBytes>>>,
3757}
3758
3759impl PyAuthenticatedData {
3760    fn authenticated_data(
3761        &self,
3762    ) -> PyResult<&synta_certificate::cms_rfc5652_types::AuthenticatedData<'static>> {
3763        if let Some(v) = self.inner.get() {
3764            return Ok(v.as_ref());
3765        }
3766        let mut decoder = Decoder::new(self.raw, Encoding::Ber);
3767        let decoded = decoder.decode().map_err(|e| {
3768            pyo3::exceptions::PyValueError::new_err(format!(
3769                "AuthenticatedData BER decode failed: {e}"
3770            ))
3771        })?;
3772        let _ = self.inner.set(Box::new(decoded));
3773        Ok(self.inner.get().unwrap().as_ref())
3774    }
3775}
3776
3777#[pymethods]
3778impl PyAuthenticatedData {
3779    /// Parse a DER- or BER-encoded CMS ``AuthenticatedData`` SEQUENCE.
3780    #[staticmethod]
3781    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
3782        let py_bytes = data.unbind();
3783        let raw: &'static [u8] = unsafe {
3784            let s = py_bytes.bind(py).as_bytes();
3785            std::slice::from_raw_parts(s.as_ptr(), s.len())
3786        };
3787        {
3788            let mut d = Decoder::new(raw, Encoding::Ber);
3789            d.read_tag()
3790                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3791            d.read_length()
3792                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3793        }
3794        Ok(Self {
3795            _data: py_bytes,
3796            raw,
3797            inner: OnceLock::new(),
3798            originator_info_cache: OnceLock::new(),
3799            recipient_infos_cache: OnceLock::new(),
3800            mac_algorithm_oid_cache: OnceLock::new(),
3801            mac_algorithm_params_cache: OnceLock::new(),
3802            digest_algorithm_oid_cache: OnceLock::new(),
3803            digest_algorithm_params_cache: OnceLock::new(),
3804            encap_content_type_cache: OnceLock::new(),
3805            encap_content_cache: OnceLock::new(),
3806            mac_cache: OnceLock::new(),
3807            auth_attrs_cache: OnceLock::new(),
3808            unauth_attrs_cache: OnceLock::new(),
3809        })
3810    }
3811
3812    /// Return the original bytes passed to :meth:`from_der`.
3813    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
3814        self._data.clone_ref(py).into_bound(py)
3815    }
3816
3817    /// CMS version integer.
3818    #[getter]
3819    fn version(&self) -> PyResult<i64> {
3820        Ok(self.authenticated_data()?.version.as_i64().unwrap_or(0))
3821    }
3822
3823    /// DER-encoded ``OriginatorInfo`` SEQUENCE, or ``None`` if absent.
3824    #[getter]
3825    fn originator_info<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
3826        if let Some(cached) = self.originator_info_cache.get() {
3827            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3828        }
3829        let computed = match &self.authenticated_data()?.originator_info {
3830            None => None,
3831            Some(oi) => {
3832                let mut enc = synta::Encoder::new(Encoding::Der);
3833                oi.encode(&mut enc)
3834                    .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3835                let bytes = enc
3836                    .finish()
3837                    .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
3838                Some(PyBytes::new(py, &bytes))
3839            }
3840        };
3841        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3842        let _ = self.originator_info_cache.set(to_store);
3843        Ok(computed)
3844    }
3845
3846    /// Raw TLV bytes of the ``RecipientInfos`` SET field.
3847    #[getter]
3848    fn recipient_infos<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
3849        if let Some(cached) = self.recipient_infos_cache.get() {
3850            return Ok(cached.clone_ref(py).into_bound(py));
3851        }
3852        let bytes =
3853            PyBytes::new(py, self.authenticated_data()?.recipient_infos.as_bytes()).unbind();
3854        let _ = self.recipient_infos_cache.set(bytes.clone_ref(py));
3855        Ok(bytes.into_bound(py))
3856    }
3857
3858    /// OID of the MAC algorithm.
3859    #[getter]
3860    fn mac_algorithm_oid<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
3861        if let Some(cached) = self.mac_algorithm_oid_cache.get() {
3862            return Ok(cached.clone_ref(py).into_bound(py));
3863        }
3864        let obj = Py::new(
3865            py,
3866            PyObjectIdentifier::from_oid(
3867                self.authenticated_data()?.mac_algorithm.algorithm.clone(),
3868            ),
3869        )?;
3870        let _ = self.mac_algorithm_oid_cache.set(obj.clone_ref(py));
3871        Ok(obj.into_bound(py))
3872    }
3873
3874    /// Raw DER bytes of the MAC algorithm parameters, or ``None``.
3875    #[getter]
3876    fn mac_algorithm_params<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
3877        if let Some(cached) = self.mac_algorithm_params_cache.get() {
3878            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3879        }
3880        let computed = encode_element_opt(
3881            py,
3882            self.authenticated_data()?.mac_algorithm.parameters.as_ref(),
3883        )?;
3884        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3885        let _ = self.mac_algorithm_params_cache.set(to_store);
3886        Ok(computed)
3887    }
3888
3889    /// OID of the digest algorithm (``[1] IMPLICIT``), or ``None`` if absent.
3890    #[getter]
3891    fn digest_algorithm_oid<'py>(
3892        &self,
3893        py: Python<'py>,
3894    ) -> PyResult<Option<Bound<'py, PyObjectIdentifier>>> {
3895        if let Some(cached) = self.digest_algorithm_oid_cache.get() {
3896            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3897        }
3898        let computed: Option<Py<PyObjectIdentifier>> =
3899            match &self.authenticated_data()?.digest_algorithm {
3900                None => None,
3901                Some(da) => Some(Py::new(
3902                    py,
3903                    PyObjectIdentifier::from_oid(da.algorithm.clone()),
3904                )?),
3905            };
3906        let to_store = computed.as_ref().map(|b| b.clone_ref(py));
3907        let _ = self.digest_algorithm_oid_cache.set(to_store);
3908        Ok(computed.map(|b| b.into_bound(py)))
3909    }
3910
3911    /// Raw DER bytes of the digest-algorithm parameters, or ``None``.
3912    #[getter]
3913    fn digest_algorithm_params<'py>(
3914        &self,
3915        py: Python<'py>,
3916    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
3917        if let Some(cached) = self.digest_algorithm_params_cache.get() {
3918            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3919        }
3920        let computed = encode_element_opt(
3921            py,
3922            self.authenticated_data()?
3923                .digest_algorithm
3924                .as_ref()
3925                .and_then(|da| da.parameters.as_ref()),
3926        )?;
3927        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3928        let _ = self.digest_algorithm_params_cache.set(to_store);
3929        Ok(computed)
3930    }
3931
3932    /// OID identifying the encapsulated content type.
3933    #[getter]
3934    fn encap_content_type<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
3935        if let Some(cached) = self.encap_content_type_cache.get() {
3936            return Ok(cached.clone_ref(py).into_bound(py));
3937        }
3938        let obj = Py::new(
3939            py,
3940            PyObjectIdentifier::from_oid(
3941                self.authenticated_data()?
3942                    .encap_content_info
3943                    .e_content_type
3944                    .clone(),
3945            ),
3946        )?;
3947        let _ = self.encap_content_type_cache.set(obj.clone_ref(py));
3948        Ok(obj.into_bound(py))
3949    }
3950
3951    /// Raw bytes of the encapsulated content OCTET STRING value, or ``None``.
3952    #[getter]
3953    fn encap_content<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
3954        if let Some(cached) = self.encap_content_cache.get() {
3955            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3956        }
3957        let computed = self
3958            .authenticated_data()?
3959            .encap_content_info
3960            .e_content
3961            .as_ref()
3962            .map(|c| PyBytes::new(py, c.as_bytes()));
3963        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3964        let _ = self.encap_content_cache.set(to_store);
3965        Ok(computed)
3966    }
3967
3968    /// Raw bytes of the MAC (message authentication code) value.
3969    #[getter]
3970    fn mac<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
3971        if let Some(cached) = self.mac_cache.get() {
3972            return Ok(cached.clone_ref(py).into_bound(py));
3973        }
3974        let bytes = PyBytes::new(py, self.authenticated_data()?.mac.as_bytes()).unbind();
3975        let _ = self.mac_cache.set(bytes.clone_ref(py));
3976        Ok(bytes.into_bound(py))
3977    }
3978
3979    /// Raw content bytes of the ``authAttrs`` field (IMPLICIT ``[2]``), or ``None``.
3980    ///
3981    /// To verify the MAC, replace the leading context-specific tag byte with
3982    /// the SET tag ``0x31`` before feeding to the MAC function.
3983    #[getter]
3984    fn auth_attrs<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
3985        if let Some(cached) = self.auth_attrs_cache.get() {
3986            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
3987        }
3988        let computed = self
3989            .authenticated_data()?
3990            .auth_attrs
3991            .as_ref()
3992            .map(|a| PyBytes::new(py, a.as_bytes()));
3993        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
3994        let _ = self.auth_attrs_cache.set(to_store);
3995        Ok(computed)
3996    }
3997
3998    /// Raw content bytes of the ``unauthAttrs`` field (IMPLICIT ``[3]``), or ``None``.
3999    #[getter]
4000    fn unauth_attrs<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
4001        if let Some(cached) = self.unauth_attrs_cache.get() {
4002            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
4003        }
4004        let computed = self
4005            .authenticated_data()?
4006            .unauth_attrs
4007            .as_ref()
4008            .map(|a| PyBytes::new(py, a.as_bytes()));
4009        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
4010        let _ = self.unauth_attrs_cache.set(to_store);
4011        Ok(computed)
4012    }
4013
4014    fn __repr__(&self) -> PyResult<String> {
4015        let ad = self.authenticated_data()?;
4016        Ok(format!(
4017            "AuthenticatedData(version={})",
4018            ad.version.as_i64().unwrap_or(0),
4019        ))
4020    }
4021}
4022
4023/// Exposes CMS (RFC 5652) content types, CMS-KEM (RFC 9629) types, and OID
4024/// constants.  Installs the module into ``sys.modules`` so that
4025/// ``from synta.cms import ContentInfo`` works.
4026fn register_cms_submodule(parent: &Bound<'_, PyModule>) -> PyResult<()> {
4027    let py = parent.py();
4028    let m = PyModule::new(py, "cms")?;
4029
4030    m.add_class::<PyContentInfo>()?;
4031    m.add_class::<PySignedData>()?;
4032    m.add_class::<PySignerInfo>()?;
4033    m.add_class::<PyEnvelopedData>()?;
4034    m.add_class::<PyEncryptedData>()?;
4035    m.add_class::<PyDigestedData>()?;
4036    m.add_class::<PyAuthenticatedData>()?;
4037    m.add_class::<PyIssuerAndSerialNumber>()?;
4038    m.add_class::<PyKEMRecipientInfo>()?;
4039    m.add_class::<PyCMSORIforKEMOtherInfo>()?;
4040
4041    // Content-type OIDs (RFC 5652 §14)
4042    m.add(
4043        "ID_DATA",
4044        oid_const(py, synta_certificate::pkcs7_types::ID_DATA),
4045    )?;
4046    m.add(
4047        "ID_SIGNED_DATA",
4048        oid_const(py, synta_certificate::pkcs7_types::ID_SIGNED_DATA),
4049    )?;
4050    m.add(
4051        "ID_ENVELOPED_DATA",
4052        oid_const(py, synta_certificate::pkcs7_types::ID_ENVELOPED_DATA),
4053    )?;
4054    m.add(
4055        "ID_ENCRYPTED_DATA",
4056        oid_const(py, synta_certificate::pkcs7_types::ID_ENCRYPTED_DATA),
4057    )?;
4058    m.add(
4059        "ID_DIGESTED_DATA",
4060        oid_const(py, synta_certificate::oids::CMS_DIGESTED_DATA),
4061    )?;
4062    m.add(
4063        "ID_CT_AUTH_DATA",
4064        oid_const(py, synta_certificate::oids::CMS_AUTH_DATA),
4065    )?;
4066    // OtherRecipientInfo OIDs (RFC 9629 §6.2)
4067    m.add(
4068        "ID_ORI",
4069        oid_const(py, synta_certificate::cms_kem_types::ID_ORI),
4070    )?;
4071    m.add(
4072        "ID_ORI_KEM",
4073        oid_const(py, synta_certificate::cms_kem_types::ID_ORI_KEM),
4074    )?;
4075    // Content-encryption algorithm OIDs (NIST AES, RFC 3565)
4076    m.add(
4077        "ID_AES128_CBC",
4078        oid_const(py, synta_certificate::pkcs12_types::ID_AES128_CBC),
4079    )?;
4080    m.add(
4081        "ID_AES192_CBC",
4082        oid_const(py, synta_certificate::pkcs12_types::ID_AES192_CBC),
4083    )?;
4084    m.add(
4085        "ID_AES256_CBC",
4086        oid_const(py, synta_certificate::pkcs12_types::ID_AES256_CBC),
4087    )?;
4088
4089    crate::install_submodule(
4090        parent,
4091        &m,
4092        "synta.cms",
4093        Some(
4094            "synta.cms — CMS (RFC 5652) and CMS-KEM (RFC 9629) types.\n\
4095             \n\
4096             Provides ContentInfo, SignedData, SignerInfo, EnvelopedData,\n\
4097             EncryptedData, DigestedData, AuthenticatedData,\n\
4098             IssuerAndSerialNumber, KEMRecipientInfo, CMSORIforKEMOtherInfo,\n\
4099             along with content-type and OtherRecipientInfo OID constants.",
4100        ),
4101    )
4102}
4103
4104/// Call this from a cdylib extension crate's `#[pymodule]` function:
4105///
4106/// ```rust,ignore
4107/// #[pymodule]
4108/// fn my_extension(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
4109///     synta_certificate::python::register_module(m)
4110/// }
4111/// ```
4112pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
4113    m.add_class::<PyObjectIdentifier>()?;
4114    m.add_class::<PyCertificate>()?;
4115    m.add_class::<PyCsr>()?;
4116    m.add_class::<PyCrl>()?;
4117    m.add_class::<PyOcspResponse>()?;
4118    m.add_function(wrap_pyfunction!(load_der_pkcs7_certificates, m)?)?;
4119    m.add_function(wrap_pyfunction!(load_pem_pkcs7_certificates, m)?)?;
4120    m.add_function(wrap_pyfunction!(load_pkcs12_certificates, m)?)?;
4121    m.add_function(wrap_pyfunction!(load_pkcs12_keys, m)?)?;
4122    m.add_function(wrap_pyfunction!(load_pkcs12, m)?)?;
4123    m.add_function(wrap_pyfunction!(read_pki_blocks, m)?)?;
4124    register_oids_submodule(m)?;
4125    register_cms_submodule(m)?;
4126    register_general_name_submodule(m)?;
4127    Ok(())
4128}
4129
4130// ── OID helper functions (exposed in synta.oids) ─────────────────────────────
4131//
4132// Each function accepts either a PyObjectIdentifier or a dotted-decimal str,
4133// delegating to the corresponding crate-level helper in lib.rs.
4134
4135/// Return the canonical display name for a signature algorithm OID.
4136///
4137/// Recognises RSA (PKCS #1) variants, ECDSA variants, EdDSA, ML-DSA (FIPS
4138/// 204), and SLH-DSA (FIPS 205). Returns ``"Other"`` for unknown OIDs.
4139///
4140/// :param oid: :class:`~synta.ObjectIdentifier` or dotted-decimal ``str``.
4141/// :returns: algorithm name string (one of the ``synta.oids`` name constants).
4142#[pyfunction]
4143fn identify_signature_algorithm(oid: &Bound<'_, PyAny>) -> PyResult<&'static str> {
4144    let inner = oid_from_pyany(oid)?;
4145    Ok(synta_certificate::identify_signature_algorithm(&inner))
4146}
4147
4148/// Return the canonical display name for a public-key algorithm OID, if known.
4149///
4150/// Recognises RSA, EC/ECDSA, DSA, EdDSA, ML-DSA, and ML-KEM.
4151/// Returns ``None`` for unrecognised OIDs.
4152///
4153/// :param oid: :class:`~synta.ObjectIdentifier` or dotted-decimal ``str``.
4154/// :returns: algorithm name string or ``None``.
4155#[pyfunction]
4156fn identify_public_key_algorithm(oid: &Bound<'_, PyAny>) -> PyResult<Option<&'static str>> {
4157    let inner = oid_from_pyany(oid)?;
4158    Ok(synta_certificate::identify_public_key_algorithm(&inner))
4159}
4160
4161/// Return the short ASN.1 name for a well-known EC named-curve OID.
4162///
4163/// Returns ``None`` for unrecognised curves.
4164///
4165/// :param oid: :class:`~synta.ObjectIdentifier` or dotted-decimal ``str``.
4166/// :returns: e.g. ``"prime256v1"``, ``"secp384r1"``, or ``None``.
4167#[pyfunction]
4168fn ec_curve_short_name(oid: &Bound<'_, PyAny>) -> PyResult<Option<&'static str>> {
4169    let inner = oid_from_pyany(oid)?;
4170    Ok(synta_certificate::ec_curve_short_name(inner.components()))
4171}
4172
4173/// Return the NIST name for a well-known EC named-curve OID.
4174///
4175/// Returns ``None`` for curves with no NIST name (e.g. ``secp256k1``).
4176///
4177/// :param oid: :class:`~synta.ObjectIdentifier` or dotted-decimal ``str``.
4178/// :returns: e.g. ``"P-256"``, ``"P-384"``, or ``None``.
4179#[pyfunction]
4180fn ec_curve_nist_name(oid: &Bound<'_, PyAny>) -> PyResult<Option<&'static str>> {
4181    let inner = oid_from_pyany(oid)?;
4182    Ok(synta_certificate::ec_curve_nist_name(inner.components()))
4183}
4184
4185/// Return the key size in bits for a well-known EC named-curve OID.
4186///
4187/// Returns the field size (security parameter) as reported by OpenSSL.
4188/// Returns ``None`` for unrecognised curves.
4189///
4190/// :param oid: :class:`~synta.ObjectIdentifier` or dotted-decimal ``str``.
4191/// :returns: e.g. ``256``, ``384``, ``521``, or ``None``.
4192#[pyfunction]
4193fn ec_curve_key_bits(oid: &Bound<'_, PyAny>) -> PyResult<Option<usize>> {
4194    let inner = oid_from_pyany(oid)?;
4195    Ok(synta_certificate::ec_curve_key_bits(inner.components()))
4196}
4197
4198/// Return the display name for a well-known X.509v3 extension OID.
4199///
4200/// Covers all RFC 5280 standard extensions and the Certificate Transparency
4201/// SCT extension. Returns the dotted-decimal string for unknown OIDs.
4202///
4203/// :param oid: :class:`~synta.ObjectIdentifier` or dotted-decimal ``str``.
4204/// :returns: human-readable name, e.g. ``"X509v3 Subject Alternative Name"``.
4205#[pyfunction]
4206fn extension_oid_name(oid: &Bound<'_, PyAny>) -> PyResult<String> {
4207    let inner = oid_from_pyany(oid)?;
4208    Ok(synta_certificate::extension_oid_name(&inner))
4209}
4210
4211// ── OID constants submodule ──────────────────────────────────────────────────
4212
4213/// Convert a well-known `&[u32]` component slice to a Python `ObjectIdentifier`.
4214///
4215/// Panics at module-init time if the slice is not a valid OID — every caller
4216/// passes a constant generated from the ASN.1 schema, so a panic here is a
4217/// bug in the source data.
4218fn oid_const(py: Python<'_>, components: &[u32]) -> Py<PyAny> {
4219    let inner = ObjectIdentifier::new(components)
4220        .expect("oid constant has invalid components — bug in synta-certificate");
4221    Py::new(py, PyObjectIdentifier::from_oid(inner))
4222        .expect("PyObjectIdentifier allocation failed")
4223        .into_any()
4224}
4225
4226/// Build and register the ``synta.oids`` submodule.
4227///
4228/// Also registers ``synta.oids.attr`` for DN attribute OIDs and installs both
4229/// into ``sys.modules`` so that ``from synta.oids import ...`` works.
4230fn register_oids_submodule(parent: &Bound<'_, PyModule>) -> PyResult<()> {
4231    let py = parent.py();
4232    let m = PyModule::new(py, "oids")?;
4233
4234    // ── ML-DSA (FIPS 204) ────────────────────────────────────────────────────
4235    m.add(
4236        "ML_DSA_44",
4237        oid_const(py, synta_certificate::oids::ML_DSA_44),
4238    )?;
4239    m.add(
4240        "ML_DSA_65",
4241        oid_const(py, synta_certificate::oids::ML_DSA_65),
4242    )?;
4243    m.add(
4244        "ML_DSA_87",
4245        oid_const(py, synta_certificate::oids::ML_DSA_87),
4246    )?;
4247
4248    // ── ML-KEM (FIPS 203) ────────────────────────────────────────────────────
4249    m.add(
4250        "ML_KEM_512",
4251        oid_const(py, synta_certificate::oids::ML_KEM_512),
4252    )?;
4253    m.add(
4254        "ML_KEM_768",
4255        oid_const(py, synta_certificate::oids::ML_KEM_768),
4256    )?;
4257    m.add(
4258        "ML_KEM_1024",
4259        oid_const(py, synta_certificate::oids::ML_KEM_1024),
4260    )?;
4261
4262    // ── SLH-DSA (FIPS 205) ───────────────────────────────────────────────────
4263    m.add(
4264        "SLH_DSA_SHA2_128S",
4265        oid_const(py, synta_certificate::oids::ID_SLH_DSA_SHA2_128S),
4266    )?;
4267    m.add(
4268        "SLH_DSA_SHA2_128F",
4269        oid_const(py, synta_certificate::oids::ID_SLH_DSA_SHA2_128F),
4270    )?;
4271    m.add(
4272        "SLH_DSA_SHA2_192S",
4273        oid_const(py, synta_certificate::oids::ID_SLH_DSA_SHA2_192S),
4274    )?;
4275    m.add(
4276        "SLH_DSA_SHA2_192F",
4277        oid_const(py, synta_certificate::oids::ID_SLH_DSA_SHA2_192F),
4278    )?;
4279    m.add(
4280        "SLH_DSA_SHA2_256S",
4281        oid_const(py, synta_certificate::oids::ID_SLH_DSA_SHA2_256S),
4282    )?;
4283    m.add(
4284        "SLH_DSA_SHA2_256F",
4285        oid_const(py, synta_certificate::oids::ID_SLH_DSA_SHA2_256F),
4286    )?;
4287    m.add(
4288        "SLH_DSA_SHAKE_128S",
4289        oid_const(py, synta_certificate::oids::ID_SLH_DSA_SHAKE_128S),
4290    )?;
4291    m.add(
4292        "SLH_DSA_SHAKE_128F",
4293        oid_const(py, synta_certificate::oids::ID_SLH_DSA_SHAKE_128F),
4294    )?;
4295    m.add(
4296        "SLH_DSA_SHAKE_192S",
4297        oid_const(py, synta_certificate::oids::ID_SLH_DSA_SHAKE_192S),
4298    )?;
4299    m.add(
4300        "SLH_DSA_SHAKE_192F",
4301        oid_const(py, synta_certificate::oids::ID_SLH_DSA_SHAKE_192F),
4302    )?;
4303    m.add(
4304        "SLH_DSA_SHAKE_256S",
4305        oid_const(py, synta_certificate::oids::ID_SLH_DSA_SHAKE_256S),
4306    )?;
4307    m.add(
4308        "SLH_DSA_SHAKE_256F",
4309        oid_const(py, synta_certificate::oids::ID_SLH_DSA_SHAKE_256F),
4310    )?;
4311
4312    // ── EdDSA (RFC 8410) ─────────────────────────────────────────────────────
4313    m.add("ED25519", oid_const(py, synta_certificate::oids::ED25519))?;
4314    m.add("ED448", oid_const(py, synta_certificate::oids::ED448))?;
4315
4316    // ── RSA (PKCS #1) ────────────────────────────────────────────────────────
4317    m.add(
4318        "RSA_ENCRYPTION",
4319        oid_const(py, synta_certificate::oids::RSA_ENCRYPTION),
4320    )?;
4321    m.add(
4322        "MD5_WITH_RSA",
4323        oid_const(py, synta_certificate::oids::MD5_WITH_RSA),
4324    )?;
4325    m.add(
4326        "SHA1_WITH_RSA",
4327        oid_const(py, synta_certificate::oids::SHA1_WITH_RSA),
4328    )?;
4329    m.add(
4330        "SHA256_WITH_RSA",
4331        oid_const(py, synta_certificate::oids::SHA256_WITH_RSA),
4332    )?;
4333    m.add(
4334        "SHA384_WITH_RSA",
4335        oid_const(py, synta_certificate::oids::SHA384_WITH_RSA),
4336    )?;
4337    m.add(
4338        "SHA512_WITH_RSA",
4339        oid_const(py, synta_certificate::oids::SHA512_WITH_RSA),
4340    )?;
4341    // Prefix OID covering all PKCS #1 RSA variants
4342    m.add("RSA", oid_const(py, synta_certificate::oids::RSA))?;
4343
4344    // ── ECDSA (ANSI X9.62 / RFC 5758) ────────────────────────────────────────
4345    m.add(
4346        "EC_PUBLIC_KEY",
4347        oid_const(py, synta_certificate::oids::EC_PUBLIC_KEY),
4348    )?;
4349    m.add(
4350        "ECDSA_WITH_SHA1",
4351        oid_const(py, synta_certificate::oids::ECDSA_WITH_SHA1),
4352    )?;
4353    m.add(
4354        "ECDSA_WITH_SHA256",
4355        oid_const(py, synta_certificate::oids::ECDSA_WITH_SHA256),
4356    )?;
4357    m.add(
4358        "ECDSA_WITH_SHA384",
4359        oid_const(py, synta_certificate::oids::ECDSA_WITH_SHA384),
4360    )?;
4361    m.add(
4362        "ECDSA_WITH_SHA512",
4363        oid_const(py, synta_certificate::oids::ECDSA_WITH_SHA512),
4364    )?;
4365    // Prefix OID covering all ECDSA signature variants
4366    m.add("ECDSA", oid_const(py, synta_certificate::oids::ECDSA_SIG))?;
4367
4368    // ── EC named curves (SEC 2 / RFC 5480) ───────────────────────────────────
4369    m.add(
4370        "EC_CURVE_P256",
4371        oid_const(py, synta_certificate::oids::EC_CURVE_P256),
4372    )?;
4373    m.add(
4374        "EC_CURVE_P384",
4375        oid_const(py, synta_certificate::oids::EC_CURVE_P384),
4376    )?;
4377    m.add(
4378        "EC_CURVE_P521",
4379        oid_const(py, synta_certificate::oids::EC_CURVE_P521),
4380    )?;
4381    m.add(
4382        "EC_CURVE_SECP256K1",
4383        oid_const(py, synta_certificate::oids::EC_CURVE_SECP256K1),
4384    )?;
4385
4386    // ── SHA-2 hash algorithms (FIPS 180-4) ───────────────────────────────────
4387    m.add("SHA224", oid_const(py, synta_certificate::oids::ID_SHA224))?;
4388    m.add("SHA256", oid_const(py, synta_certificate::oids::ID_SHA256))?;
4389    m.add("SHA384", oid_const(py, synta_certificate::oids::ID_SHA384))?;
4390    m.add("SHA512", oid_const(py, synta_certificate::oids::ID_SHA512))?;
4391    m.add(
4392        "SHA512_224",
4393        oid_const(py, synta_certificate::oids::ID_SHA512_224),
4394    )?;
4395    m.add(
4396        "SHA512_256",
4397        oid_const(py, synta_certificate::oids::ID_SHA512_256),
4398    )?;
4399
4400    // ── SHA-3 hash algorithms and XOFs (FIPS 202) ────────────────────────────
4401    m.add(
4402        "SHA3_224",
4403        oid_const(py, synta_certificate::oids::ID_SHA3_224),
4404    )?;
4405    m.add(
4406        "SHA3_256",
4407        oid_const(py, synta_certificate::oids::ID_SHA3_256),
4408    )?;
4409    m.add(
4410        "SHA3_384",
4411        oid_const(py, synta_certificate::oids::ID_SHA3_384),
4412    )?;
4413    m.add(
4414        "SHA3_512",
4415        oid_const(py, synta_certificate::oids::ID_SHA3_512),
4416    )?;
4417    m.add(
4418        "SHAKE128",
4419        oid_const(py, synta_certificate::oids::ID_SHAKE128),
4420    )?;
4421    m.add(
4422        "SHAKE256",
4423        oid_const(py, synta_certificate::oids::ID_SHAKE256),
4424    )?;
4425
4426    // ── X.509v3 extension OIDs (RFC 5280 / 2.5.29.*) ─────────────────────────
4427    m.add(
4428        "SUBJECT_KEY_IDENTIFIER",
4429        oid_const(py, synta_certificate::oids::SUBJECT_KEY_IDENTIFIER),
4430    )?;
4431    m.add(
4432        "KEY_USAGE",
4433        oid_const(py, synta_certificate::oids::KEY_USAGE),
4434    )?;
4435    m.add(
4436        "SUBJECT_ALT_NAME",
4437        oid_const(py, synta_certificate::oids::SUBJECT_ALT_NAME),
4438    )?;
4439    m.add(
4440        "ISSUER_ALT_NAME",
4441        oid_const(py, synta_certificate::oids::ISSUER_ALT_NAME),
4442    )?;
4443    m.add(
4444        "BASIC_CONSTRAINTS",
4445        oid_const(py, synta_certificate::oids::BASIC_CONSTRAINTS),
4446    )?;
4447    m.add(
4448        "CRL_DISTRIBUTION_POINTS",
4449        oid_const(py, synta_certificate::oids::CRL_DISTRIBUTION_POINTS),
4450    )?;
4451    m.add(
4452        "CERTIFICATE_POLICIES",
4453        oid_const(py, synta_certificate::oids::CERTIFICATE_POLICIES),
4454    )?;
4455    m.add(
4456        "AUTHORITY_KEY_IDENTIFIER",
4457        oid_const(py, synta_certificate::oids::AUTHORITY_KEY_IDENTIFIER),
4458    )?;
4459    m.add(
4460        "EXTENDED_KEY_USAGE",
4461        oid_const(py, synta_certificate::oids::EXTENDED_KEY_USAGE),
4462    )?;
4463    m.add(
4464        "AUTHORITY_INFO_ACCESS",
4465        oid_const(py, synta_certificate::oids::AUTHORITY_INFO_ACCESS),
4466    )?;
4467    m.add(
4468        "CT_PRECERT_SCTS",
4469        oid_const(py, synta_certificate::oids::CT_PRECERT_SCTS),
4470    )?;
4471
4472    // ── Extended Key Usage key-purpose OIDs (RFC 5280 §4.2.1.12) ─────────────
4473    m.add(
4474        "KP_SERVER_AUTH",
4475        oid_const(py, synta_certificate::oids::KP_SERVER_AUTH),
4476    )?;
4477    m.add(
4478        "KP_CLIENT_AUTH",
4479        oid_const(py, synta_certificate::oids::KP_CLIENT_AUTH),
4480    )?;
4481    m.add(
4482        "KP_CODE_SIGNING",
4483        oid_const(py, synta_certificate::oids::KP_CODE_SIGNING),
4484    )?;
4485    m.add(
4486        "KP_EMAIL_PROTECTION",
4487        oid_const(py, synta_certificate::oids::KP_EMAIL_PROTECTION),
4488    )?;
4489    m.add(
4490        "KP_TIME_STAMPING",
4491        oid_const(py, synta_certificate::oids::KP_TIME_STAMPING),
4492    )?;
4493    m.add(
4494        "KP_OCSP_SIGNING",
4495        oid_const(py, synta_certificate::oids::KP_OCSP_SIGNING),
4496    )?;
4497    m.add(
4498        "ANY_EXTENDED_KEY_USAGE",
4499        oid_const(py, synta_certificate::oids::ANY_EXTENDED_KEY_USAGE),
4500    )?;
4501
4502    // ── PKINIT OIDs (RFC 4556 + RFC 6112 + RFC 8636) ─────────────────────────
4503    m.add(
4504        "PKINIT_SAN",
4505        oid_const(py, synta_certificate::oids::ID_PKINIT_SAN),
4506    )?;
4507    m.add(
4508        "PKINIT_KP_CLIENT_AUTH",
4509        oid_const(py, synta_certificate::oids::ID_PKINIT_KPCLIENT_AUTH),
4510    )?;
4511    m.add(
4512        "PKINIT_KP_KDC",
4513        oid_const(py, synta_certificate::oids::ID_PKINIT_KPKDC),
4514    )?;
4515    m.add(
4516        "PKINIT_AUTH_DATA",
4517        oid_const(py, synta_certificate::oids::ID_PKINIT_AUTH_DATA),
4518    )?;
4519    m.add(
4520        "PKINIT_DHKEY_DATA",
4521        oid_const(py, synta_certificate::oids::ID_PKINIT_DHKEY_DATA),
4522    )?;
4523    m.add(
4524        "PKINIT_RKEY_DATA",
4525        oid_const(py, synta_certificate::oids::ID_PKINIT_RKEY_DATA),
4526    )?;
4527    m.add(
4528        "PKINIT_KDF",
4529        oid_const(py, synta_certificate::oids::ID_PKINIT_KDF),
4530    )?;
4531    m.add(
4532        "PKINIT_KDF_SHA1",
4533        oid_const(py, synta_certificate::oids::ID_PKINIT_KDF_AH_SHA1),
4534    )?;
4535    m.add(
4536        "PKINIT_KDF_SHA256",
4537        oid_const(py, synta_certificate::oids::ID_PKINIT_KDF_AH_SHA256),
4538    )?;
4539    m.add(
4540        "PKINIT_KDF_SHA384",
4541        oid_const(py, synta_certificate::oids::ID_PKINIT_KDF_AH_SHA384),
4542    )?;
4543    m.add(
4544        "PKINIT_KDF_SHA512",
4545        oid_const(py, synta_certificate::oids::ID_PKINIT_KDF_AH_SHA512),
4546    )?;
4547
4548    // ── Microsoft PKI OIDs (AD CS) ────────────────────────────────────────────
4549    m.add(
4550        "MS_SAN_UPN",
4551        oid_const(py, synta_certificate::oids::ID_MS_SAN_UPN),
4552    )?;
4553    m.add(
4554        "MS_CERTIFICATE_TEMPLATE_NAME",
4555        oid_const(py, synta_certificate::oids::ID_MS_CERTIFICATE_TEMPLATE_NAME),
4556    )?;
4557    m.add(
4558        "MS_CERTIFICATE_TEMPLATE",
4559        oid_const(py, synta_certificate::oids::ID_MS_CERTIFICATE_TEMPLATE),
4560    )?;
4561    m.add(
4562        "MS_KP_SMARTCARD_LOGON",
4563        oid_const(py, synta_certificate::oids::ID_MS_KP_SMARTCARD_LOGON),
4564    )?;
4565    m.add(
4566        "MS_NTDS_REPLICATION",
4567        oid_const(py, synta_certificate::oids::ID_MS_NTDS_REPLICATION),
4568    )?;
4569
4570    // ── CMS content-type OIDs (RFC 5652 §14) ─────────────────────────────────
4571    m.add("CMS_DATA", oid_const(py, synta_certificate::oids::CMS_DATA))?;
4572    m.add(
4573        "CMS_SIGNED_DATA",
4574        oid_const(py, synta_certificate::oids::CMS_SIGNED_DATA),
4575    )?;
4576    m.add(
4577        "CMS_ENVELOPED_DATA",
4578        oid_const(py, synta_certificate::oids::CMS_ENVELOPED_DATA),
4579    )?;
4580    m.add(
4581        "CMS_DIGESTED_DATA",
4582        oid_const(py, synta_certificate::oids::CMS_DIGESTED_DATA),
4583    )?;
4584    m.add(
4585        "CMS_ENCRYPTED_DATA",
4586        oid_const(py, synta_certificate::oids::CMS_ENCRYPTED_DATA),
4587    )?;
4588    m.add(
4589        "CMS_AUTH_DATA",
4590        oid_const(py, synta_certificate::oids::CMS_AUTH_DATA),
4591    )?;
4592    // ── CMS-KEM OtherRecipientInfo OIDs (RFC 9629 §6.2) ──────────────────────
4593    m.add("CMS_ORI", oid_const(py, synta_certificate::oids::CMS_ORI))?;
4594    m.add(
4595        "CMS_ORI_KEM",
4596        oid_const(py, synta_certificate::oids::CMS_ORI_KEM),
4597    )?;
4598
4599    // ── PKCS#9 attribute OIDs (RFC 2985 / RFC 5652 §11 / RFC 2986 / RFC 7292) ─
4600    m.add(
4601        "PKCS9_EMAIL_ADDRESS",
4602        oid_const(py, synta_certificate::oids::PKCS9_EMAIL_ADDRESS),
4603    )?;
4604    m.add(
4605        "PKCS9_CONTENT_TYPE",
4606        oid_const(py, synta_certificate::oids::PKCS9_CONTENT_TYPE),
4607    )?;
4608    m.add(
4609        "PKCS9_MESSAGE_DIGEST",
4610        oid_const(py, synta_certificate::oids::PKCS9_MESSAGE_DIGEST),
4611    )?;
4612    m.add(
4613        "PKCS9_SIGNING_TIME",
4614        oid_const(py, synta_certificate::oids::PKCS9_SIGNING_TIME),
4615    )?;
4616    m.add(
4617        "PKCS9_COUNTERSIGNATURE",
4618        oid_const(py, synta_certificate::oids::PKCS9_COUNTERSIGNATURE),
4619    )?;
4620    m.add(
4621        "PKCS9_CHALLENGE_PASSWORD",
4622        oid_const(py, synta_certificate::oids::PKCS9_CHALLENGE_PASSWORD),
4623    )?;
4624    m.add(
4625        "PKCS9_EXTENSION_REQUEST",
4626        oid_const(py, synta_certificate::oids::PKCS9_EXTENSION_REQUEST),
4627    )?;
4628    m.add(
4629        "PKCS9_FRIENDLY_NAME",
4630        oid_const(py, synta_certificate::oids::PKCS9_FRIENDLY_NAME),
4631    )?;
4632    m.add(
4633        "PKCS9_LOCAL_KEY_ID",
4634        oid_const(py, synta_certificate::oids::PKCS9_LOCAL_KEY_ID),
4635    )?;
4636
4637    // ── Helper functions ─────────────────────────────────────────────────────
4638    m.add_function(wrap_pyfunction!(identify_signature_algorithm, &m)?)?;
4639    m.add_function(wrap_pyfunction!(identify_public_key_algorithm, &m)?)?;
4640    m.add_function(wrap_pyfunction!(ec_curve_short_name, &m)?)?;
4641    m.add_function(wrap_pyfunction!(ec_curve_nist_name, &m)?)?;
4642    m.add_function(wrap_pyfunction!(ec_curve_key_bits, &m)?)?;
4643    m.add_function(wrap_pyfunction!(extension_oid_name, &m)?)?;
4644
4645    // ── DN attribute OID submodule ────────────────────────────────────────────
4646    let attr = PyModule::new(py, "attr")?;
4647    attr.add(
4648        "__doc__",
4649        "OIDs for X.500 Distinguished Name attribute types (RFC 4519).",
4650    )?;
4651    attr.add(
4652        "COMMON_NAME",
4653        oid_const(py, synta_certificate::oids::attr::COMMON_NAME),
4654    )?;
4655    attr.add(
4656        "COUNTRY",
4657        oid_const(py, synta_certificate::oids::attr::COUNTRY),
4658    )?;
4659    attr.add("STATE", oid_const(py, synta_certificate::oids::attr::STATE))?;
4660    attr.add(
4661        "LOCALITY",
4662        oid_const(py, synta_certificate::oids::attr::LOCALITY),
4663    )?;
4664    attr.add(
4665        "ORGANIZATION",
4666        oid_const(py, synta_certificate::oids::attr::ORGANIZATION),
4667    )?;
4668    attr.add(
4669        "ORG_UNIT",
4670        oid_const(py, synta_certificate::oids::attr::ORG_UNIT),
4671    )?;
4672    attr.add(
4673        "ORG_IDENTIFIER",
4674        oid_const(py, synta_certificate::oids::attr::ORG_IDENTIFIER),
4675    )?;
4676    attr.add(
4677        "STREET",
4678        oid_const(py, synta_certificate::oids::attr::STREET),
4679    )?;
4680    attr.add(
4681        "SURNAME",
4682        oid_const(py, synta_certificate::oids::attr::SURNAME),
4683    )?;
4684    attr.add(
4685        "GIVEN_NAME",
4686        oid_const(py, synta_certificate::oids::attr::GIVEN_NAME),
4687    )?;
4688    attr.add(
4689        "INITIALS",
4690        oid_const(py, synta_certificate::oids::attr::INITIALS),
4691    )?;
4692    attr.add("TITLE", oid_const(py, synta_certificate::oids::attr::TITLE))?;
4693    attr.add(
4694        "SERIAL_NUMBER",
4695        oid_const(py, synta_certificate::oids::attr::SERIAL_NUMBER),
4696    )?;
4697    attr.add(
4698        "EMAIL_ADDRESS",
4699        oid_const(py, synta_certificate::oids::attr::EMAIL_ADDRESS),
4700    )?;
4701    attr.add(
4702        "USER_ID",
4703        oid_const(py, synta_certificate::oids::attr::USER_ID),
4704    )?;
4705    attr.add(
4706        "DOMAIN_COMPONENT",
4707        oid_const(py, synta_certificate::oids::attr::DOMAIN_COMPONENT),
4708    )?;
4709
4710    crate::install_submodule(&m, &attr, "synta.oids.attr", None)?;
4711    crate::install_submodule(
4712        parent,
4713        &m,
4714        "synta.oids",
4715        Some("OID constants for X.509 signature, public-key, and extension algorithms."),
4716    )
4717}
4718
4719// ── GeneralName tag constants (synta.general_name) ───────────────────────────
4720
4721fn register_general_name_submodule(parent: &Bound<'_, PyModule>) -> PyResult<()> {
4722    let py = parent.py();
4723    let m = PyModule::new(py, "general_name")?;
4724
4725    // ── GeneralName alternatives ─────────────────────────────────────────────
4726    m.add("OTHER_NAME", synta_certificate::general_name::OTHER_NAME)?;
4727    m.add("RFC822_NAME", synta_certificate::general_name::RFC822_NAME)?;
4728    m.add("DNS_NAME", synta_certificate::general_name::DNS_NAME)?;
4729    m.add(
4730        "X400_ADDRESS",
4731        synta_certificate::general_name::X400_ADDRESS,
4732    )?;
4733    m.add(
4734        "DIRECTORY_NAME",
4735        synta_certificate::general_name::DIRECTORY_NAME,
4736    )?;
4737    m.add(
4738        "EDI_PARTY_NAME",
4739        synta_certificate::general_name::EDI_PARTY_NAME,
4740    )?;
4741    m.add("URI", synta_certificate::general_name::URI)?;
4742    m.add("IP_ADDRESS", synta_certificate::general_name::IP_ADDRESS)?;
4743    m.add(
4744        "REGISTERED_ID",
4745        synta_certificate::general_name::REGISTERED_ID,
4746    )?;
4747
4748    crate::install_submodule(
4749        parent,
4750        &m,
4751        "synta.general_name",
4752        Some(concat!(
4753            "Context-specific tag numbers for the ``GeneralName`` CHOICE type ",
4754            "(RFC 5280 \u{a7}4.2.1.6).\n\n",
4755            "These integer constants correspond to the first element of tuples returned by\n",
4756            ":func:`~synta.parse_general_names` and ",
4757            ":meth:`~synta.Certificate.subject_alt_names`.\n\n",
4758            "Example usage::\n\n",
4759            "    import synta.general_name as gn\n",
4760            "    for tag, content in cert.subject_alt_names():\n",
4761            "        if tag == gn.DNS_NAME:\n",
4762            "            print('DNS:', content.decode())\n",
4763            "        elif tag == gn.IP_ADDRESS:\n",
4764            "            print('IP:', content.hex())",
4765        )),
4766    )
4767}