Skip to main content

_synta/certificate/
pkix.rs

1//! Python bindings for PKIX types: [`PyCsr`], [`PyCrl`], [`PyOcspResponse`],
2//! and PKCS#7 / PKCS#12 helper functions.
3
4use std::sync::OnceLock;
5
6use pyo3::prelude::*;
7use pyo3::types::{PyBytes, PyList, PyString};
8
9use synta::traits::Encode;
10use synta::{Decoder, Encoding};
11use synta_certificate::Time;
12
13use super::cert::{pem_blocks_to_pyobject, pyobject_to_pem, PyCertificate};
14use crate::crypto_keys::PyPrivateKey;
15use crate::types::PyObjectIdentifier;
16
17// ── Helpers for CSR / CRL (Name is decoded eagerly, not stored as RawDer) ─────
18
19/// Re-encode a parsed `Name` to DER and format it as an RFC 4514 DN string.
20fn name_to_dn_string(name: &synta_certificate::Name<'_>) -> String {
21    let mut enc = synta::Encoder::new(synta::Encoding::Der);
22    if name.encode(&mut enc).is_err() {
23        return String::new();
24    }
25    synta_certificate::name::format_dn(&enc.finish().unwrap_or_default())
26}
27
28/// Re-encode a parsed `Name` to its DER bytes.
29fn name_to_der_bytes(name: &synta_certificate::Name<'_>) -> Vec<u8> {
30    let mut enc = synta::Encoder::new(synta::Encoding::Der);
31    if name.encode(&mut enc).is_err() {
32        return Vec::new();
33    }
34    enc.finish().unwrap_or_default()
35}
36
37/// Map an `OCSPResponseStatus` enum value to its RFC 6960 display name.
38fn ocsp_status_str(status: synta_certificate::ocsp::OCSPResponseStatus) -> &'static str {
39    use synta_certificate::ocsp::OCSPResponseStatus::*;
40    match status {
41        Successful => "successful",
42        MalformedRequest => "malformedRequest",
43        InternalError => "internalError",
44        TryLater => "tryLater",
45        SigRequired => "sigRequired",
46        Unauthorized => "unauthorized",
47    }
48}
49
50// ── PyCsr ─────────────────────────────────────────────────────────────────────
51
52/// PKCS #10 Certificate Signing Request accessible from Python.
53///
54/// ```python
55/// csr = CertificationRequest.from_der(open("req.der", "rb").read())
56/// print(csr.subject)
57/// print(csr.public_key_algorithm)
58/// ```
59#[pyclass(frozen, name = "CertificationRequest")]
60pub struct PyCsr {
61    pub(super) _data: Py<PyBytes>,
62    pub(super) raw: &'static [u8],
63    inner: OnceLock<Box<synta_certificate::csr::CertificationRequest<'static>>>,
64    subject_cache: OnceLock<Py<PyString>>,
65    subject_raw_der_cache: OnceLock<Py<PyBytes>>,
66    signature_algorithm_cache: OnceLock<Py<PyString>>,
67    signature_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
68    signature_cache: OnceLock<Py<PyBytes>>,
69    public_key_algorithm_cache: OnceLock<Py<PyString>>,
70    public_key_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
71    public_key_cache: OnceLock<Py<PyBytes>>,
72}
73
74impl PyCsr {
75    fn csr(&self) -> PyResult<&synta_certificate::csr::CertificationRequest<'static>> {
76        if let Some(v) = self.inner.get() {
77            return Ok(v.as_ref());
78        }
79        let mut decoder = Decoder::new(self.raw, Encoding::Der);
80        let decoded = decoder.decode().map_err(|e| {
81            pyo3::exceptions::PyValueError::new_err(format!(
82                "CertificationRequest DER decode failed: {e}"
83            ))
84        })?;
85        let _ = self.inner.set(Box::new(decoded));
86        Ok(self.inner.get().unwrap().as_ref())
87    }
88}
89
90#[pymethods]
91impl PyCsr {
92    /// Parse a DER-encoded PKCS #10 Certificate Signing Request.
93    #[staticmethod]
94    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
95        let py_bytes = data.unbind();
96        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
97        // keeps the Python bytes object alive for the lifetime of this struct.
98        // CPython's bytes objects have a fixed-address, non-relocating payload
99        // buffer (CPython has no moving GC).  The slice lifetime is extended
100        // to 'static; the actual safety invariants are:
101        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
102        //       can outlive the struct, so `raw` is never read after drop begins.
103        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
104        //       allocation), so field drop order does not cause use-after-free.
105        //   (3) `inner` contains only borrow-typed fields; dropping
106        //       Box<CertificationRequest<'static>> does not read through the
107        //       contained &'static slices (borrows have no destructors).
108        // CPython-only: does not hold for PyPy or GraalPy.
109        let raw: &'static [u8] = unsafe {
110            let s = py_bytes.bind(py).as_bytes();
111            std::slice::from_raw_parts(s.as_ptr(), s.len())
112        };
113        // Minimal structural scan: outer SEQUENCE tag + length.
114        {
115            let mut d = Decoder::new(raw, Encoding::Der);
116            d.read_tag()
117                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
118            d.read_length()
119                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
120        }
121        Ok(Self {
122            _data: py_bytes,
123            raw,
124            inner: OnceLock::new(),
125            subject_cache: OnceLock::new(),
126            subject_raw_der_cache: OnceLock::new(),
127            signature_algorithm_cache: OnceLock::new(),
128            signature_algorithm_oid_cache: OnceLock::new(),
129            signature_cache: OnceLock::new(),
130            public_key_algorithm_cache: OnceLock::new(),
131            public_key_algorithm_oid_cache: OnceLock::new(),
132            public_key_cache: OnceLock::new(),
133        })
134    }
135
136    /// Parse a PEM-encoded PKCS #10 Certificate Signing Request.
137    ///
138    /// Returns a single :class:`CertificationRequest` for one PEM block, or a
139    /// :class:`list` of :class:`CertificationRequest` objects when multiple
140    /// blocks are present.  Raises :exc:`ValueError` if no PEM block is found.
141    ///
142    /// ```python
143    /// csr = CertificationRequest.from_pem(open("csr.pem", "rb").read())
144    /// print(csr.subject)
145    /// ```
146    #[staticmethod]
147    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
148        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
149            let obj = Self::from_der(py, bytes)?;
150            Ok(Py::new(py, obj)?.into_bound(py).into_any())
151        })
152    }
153
154    /// Serialize one CSR or a list of CSRs to PEM format.
155    ///
156    /// ```python
157    /// pem = CertificationRequest.to_pem(csr)
158    /// pem = CertificationRequest.to_pem([csr1, csr2])
159    /// ```
160    #[staticmethod]
161    fn to_pem<'py>(
162        py: Python<'py>,
163        obj_or_list: Bound<'_, PyAny>,
164    ) -> PyResult<Bound<'py, PyBytes>> {
165        pyobject_to_pem::<Self, _>(py, "CERTIFICATE REQUEST", &obj_or_list, |c| c.raw)
166    }
167
168    /// Subject distinguished name as a string.
169    #[getter]
170    fn subject<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
171        if let Some(cached) = self.subject_cache.get() {
172            return Ok(cached.clone_ref(py).into_bound(py));
173        }
174        let s = name_to_dn_string(&self.csr()?.certification_request_info.subject);
175        let py_str = PyString::new(py, &s).unbind();
176        let _ = self.subject_cache.set(py_str.clone_ref(py));
177        Ok(py_str.into_bound(py))
178    }
179
180    /// Raw DER bytes of the subject Name SEQUENCE.
181    #[getter]
182    fn subject_raw_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
183        if let Some(cached) = self.subject_raw_der_cache.get() {
184            return Ok(cached.clone_ref(py).into_bound(py));
185        }
186        let bytes = name_to_der_bytes(&self.csr()?.certification_request_info.subject);
187        let py_bytes = PyBytes::new(py, &bytes).unbind();
188        let _ = self.subject_raw_der_cache.set(py_bytes.clone_ref(py));
189        Ok(py_bytes.into_bound(py))
190    }
191
192    /// CSR version (0 = v1 per RFC 2986).
193    #[getter]
194    fn version(&self) -> PyResult<i64> {
195        Ok(self
196            .csr()?
197            .certification_request_info
198            .version
199            .as_i64()
200            .unwrap_or(0))
201    }
202
203    /// Signature algorithm name (e.g. "RSA", "ECDSA", "Ed25519").
204    #[getter]
205    fn signature_algorithm<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
206        if let Some(cached) = self.signature_algorithm_cache.get() {
207            return Ok(cached.clone_ref(py).into_bound(py));
208        }
209        let oid = &self.csr()?.signature_algorithm.algorithm;
210        let name = synta_certificate::identify_signature_algorithm(oid);
211        let s = if name != "Other" {
212            name.to_string()
213        } else {
214            oid.to_string()
215        };
216        let py_str = PyString::new(py, &s).unbind();
217        let _ = self.signature_algorithm_cache.set(py_str.clone_ref(py));
218        Ok(py_str.into_bound(py))
219    }
220
221    /// OID of the signature algorithm.
222    #[getter]
223    fn signature_algorithm_oid<'py>(
224        &self,
225        py: Python<'py>,
226    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
227        if let Some(cached) = self.signature_algorithm_oid_cache.get() {
228            return Ok(cached.clone_ref(py).into_bound(py));
229        }
230        let obj = Py::new(
231            py,
232            PyObjectIdentifier::from_oid(self.csr()?.signature_algorithm.algorithm.clone()),
233        )?;
234        let _ = self.signature_algorithm_oid_cache.set(obj.clone_ref(py));
235        Ok(obj.into_bound(py))
236    }
237
238    /// Raw signature bytes.
239    #[getter]
240    fn signature<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
241        if let Some(cached) = self.signature_cache.get() {
242            return Ok(cached.clone_ref(py).into_bound(py));
243        }
244        let py_bytes = PyBytes::new(py, self.csr()?.signature.as_bytes()).unbind();
245        let _ = self.signature_cache.set(py_bytes.clone_ref(py));
246        Ok(py_bytes.into_bound(py))
247    }
248
249    /// Public-key algorithm name (e.g. "RSA", "ECDSA", "Ed25519").
250    #[getter]
251    fn public_key_algorithm<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
252        if let Some(cached) = self.public_key_algorithm_cache.get() {
253            return Ok(cached.clone_ref(py).into_bound(py));
254        }
255        let oid = &self
256            .csr()?
257            .certification_request_info
258            .subject_pkinfo
259            .algorithm
260            .algorithm;
261        let s = synta_certificate::identify_public_key_algorithm(oid)
262            .map(|s| s.to_string())
263            .unwrap_or_else(|| oid.to_string());
264        let py_str = PyString::new(py, &s).unbind();
265        let _ = self.public_key_algorithm_cache.set(py_str.clone_ref(py));
266        Ok(py_str.into_bound(py))
267    }
268
269    /// OID of the subject public-key algorithm.
270    #[getter]
271    fn public_key_algorithm_oid<'py>(
272        &self,
273        py: Python<'py>,
274    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
275        if let Some(cached) = self.public_key_algorithm_oid_cache.get() {
276            return Ok(cached.clone_ref(py).into_bound(py));
277        }
278        let obj = Py::new(
279            py,
280            PyObjectIdentifier::from_oid(
281                self.csr()?
282                    .certification_request_info
283                    .subject_pkinfo
284                    .algorithm
285                    .algorithm
286                    .clone(),
287            ),
288        )?;
289        let _ = self.public_key_algorithm_oid_cache.set(obj.clone_ref(py));
290        Ok(obj.into_bound(py))
291    }
292
293    /// Raw subject public-key bytes.
294    #[getter]
295    fn public_key<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
296        if let Some(cached) = self.public_key_cache.get() {
297            return Ok(cached.clone_ref(py).into_bound(py));
298        }
299        let py_bytes = PyBytes::new(
300            py,
301            self.csr()?
302                .certification_request_info
303                .subject_pkinfo
304                .subject_public_key
305                .as_bytes(),
306        )
307        .unbind();
308        let _ = self.public_key_cache.set(py_bytes.clone_ref(py));
309        Ok(py_bytes.into_bound(py))
310    }
311
312    /// Return the DER value bytes of the named extension from this CSR's
313    /// ``extensionRequest`` attribute (OID ``1.2.840.113549.1.9.14``), or
314    /// ``None`` if the CSR carries no such extension.
315    ///
316    /// ``oid`` may be a dotted-decimal string or a
317    /// :class:`~synta.ObjectIdentifier`.  The value returned is the content of
318    /// the ``extnValue`` OCTET STRING — the DER encoding of the extension value
319    /// — without the outer OCTET STRING tag and length.  Returns ``None`` when
320    /// the CSR has no ``extensionRequest`` attribute, when the attribute
321    /// contains no extensions, or when the requested OID is absent.
322    ///
323    /// ```python,ignore
324    /// SAN_OID = "2.5.29.17"
325    /// san_der = csr.get_extension_value_der(SAN_OID)
326    /// if san_der:
327    ///     names = synta.parse_general_names(san_der)
328    /// ```
329    fn get_extension_value_der<'py>(
330        &self,
331        py: Python<'py>,
332        oid: &Bound<'_, PyAny>,
333    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
334        let target = super::oid_from_pyany(oid)?;
335        let csr = self.csr()?;
336        let attrs = match csr.certification_request_info.attributes.as_ref() {
337            Some(a) => a,
338            None => return Ok(None),
339        };
340        for attr in attrs.elements() {
341            if attr.attr_type.components() != synta_certificate::oids::PKCS9_EXTENSION_REQUEST {
342                continue;
343            }
344            // attr_values: SetOf<RawDer> — first element is the SEQUENCE OF Extension TLV
345            let Some(raw) = attr.attr_values.elements().first() else {
346                return Ok(None);
347            };
348            return Ok(synta_certificate::find_extension_value(
349                raw.as_bytes(),
350                target.components(),
351            )
352            .map(|v| PyBytes::new(py, v)));
353        }
354        Ok(None)
355    }
356
357    /// Return the Subject Alternative Names from this CSR's ``extensionRequest``
358    /// attribute as a list of ``(tag_number, content)`` tuples.  Returns an
359    /// empty list when the CSR carries no SAN extension.
360    ///
361    /// Tag numbers and content format follow the same convention as
362    /// :meth:`~synta.Certificate.subject_alt_names` and
363    /// :func:`~synta.parse_general_names`.
364    ///
365    /// ```python,ignore
366    /// import synta.general_name as gn
367    /// for tag_num, content in csr.subject_alt_names():
368    ///     if tag_num == gn.DNS_NAME:
369    ///         print("DNS:", content.decode("ascii"))
370    /// ```
371    fn subject_alt_names<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
372        use pyo3::types::{PyBytes, PyList, PyTuple};
373        let list = PyList::empty(py);
374        let csr = self.csr()?;
375        let attrs = match csr.certification_request_info.attributes.as_ref() {
376            Some(a) => a,
377            None => return Ok(list),
378        };
379        for attr in attrs.elements() {
380            if attr.attr_type.components() != synta_certificate::oids::PKCS9_EXTENSION_REQUEST {
381                continue;
382            }
383            let Some(raw) = attr.attr_values.elements().first() else {
384                return Ok(list);
385            };
386            if let Some(san_bytes) = synta_certificate::find_extension_value(
387                raw.as_bytes(),
388                synta_certificate::oids::SUBJECT_ALT_NAME,
389            ) {
390                for (tag_num, content) in synta_certificate::parse_general_names(san_bytes) {
391                    let tuple = PyTuple::new(
392                        py,
393                        [
394                            tag_num.into_pyobject(py)?.into_any(),
395                            PyBytes::new(py, &content).into_any(),
396                        ],
397                    )?;
398                    list.append(tuple)?;
399                }
400            }
401            break;
402        }
403        Ok(list)
404    }
405
406    /// Complete DER encoding of this CSR (the original bytes passed to ``from_der``).
407    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
408        self._data.clone_ref(py).into_bound(py)
409    }
410
411    fn __repr__(&self) -> PyResult<String> {
412        Ok(format!(
413            "CertificationRequest(subject={:?})",
414            name_to_dn_string(&self.csr()?.certification_request_info.subject),
415        ))
416    }
417}
418
419impl PyCsr {
420    /// Rust-internal constructor: parse DER bytes into a `PyCsr`.
421    ///
422    /// Mirrors `from_der` but is accessible from sibling Rust modules.
423    pub(crate) fn new_from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
424        Self::from_der(py, data)
425    }
426}
427
428// ── PyCrl ─────────────────────────────────────────────────────────────────────
429
430/// X.509 Certificate Revocation List accessible from Python.
431///
432/// ```python
433/// crl = CertificateList.from_der(open("crl.der", "rb").read())
434/// print(crl.issuer)
435/// print(crl.revoked_count)
436/// ```
437#[pyclass(frozen, name = "CertificateList")]
438pub struct PyCrl {
439    pub(super) _data: Py<PyBytes>,
440    pub(super) raw: &'static [u8],
441    inner: OnceLock<Box<synta_certificate::crl::CertificateList<'static>>>,
442    issuer_cache: OnceLock<Py<PyString>>,
443    issuer_raw_der_cache: OnceLock<Py<PyBytes>>,
444    this_update_cache: OnceLock<Py<PyString>>,
445    next_update_cache: OnceLock<Option<Py<PyString>>>,
446    signature_algorithm_cache: OnceLock<Py<PyString>>,
447    signature_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
448    signature_value_cache: OnceLock<Py<PyBytes>>,
449}
450
451impl PyCrl {
452    fn crl(&self) -> PyResult<&synta_certificate::crl::CertificateList<'static>> {
453        if let Some(v) = self.inner.get() {
454            return Ok(v.as_ref());
455        }
456        let mut decoder = Decoder::new(self.raw, Encoding::Der);
457        let decoded = decoder.decode().map_err(|e| {
458            pyo3::exceptions::PyValueError::new_err(format!(
459                "CertificateList DER decode failed: {e}"
460            ))
461        })?;
462        let _ = self.inner.set(Box::new(decoded));
463        Ok(self.inner.get().unwrap().as_ref())
464    }
465}
466
467#[pymethods]
468impl PyCrl {
469    /// Parse a DER-encoded X.509 Certificate Revocation List.
470    #[staticmethod]
471    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
472        let py_bytes = data.unbind();
473        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
474        // keeps the Python bytes object alive for the lifetime of this struct.
475        // CPython's bytes objects have a fixed-address, non-relocating payload
476        // buffer (CPython has no moving GC).  The slice lifetime is extended
477        // to 'static; the actual safety invariants are:
478        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
479        //       can outlive the struct, so `raw` is never read after drop begins.
480        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
481        //       allocation), so field drop order does not cause use-after-free.
482        //   (3) `inner` contains only borrow-typed fields; dropping
483        //       Box<CertificateList<'static>> does not read through the
484        //       contained &'static slices (borrows have no destructors).
485        // CPython-only: does not hold for PyPy or GraalPy.
486        let raw: &'static [u8] = unsafe {
487            let s = py_bytes.bind(py).as_bytes();
488            std::slice::from_raw_parts(s.as_ptr(), s.len())
489        };
490        {
491            let mut d = Decoder::new(raw, Encoding::Der);
492            d.read_tag()
493                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
494            d.read_length()
495                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
496        }
497        Ok(Self {
498            _data: py_bytes,
499            raw,
500            inner: OnceLock::new(),
501            issuer_cache: OnceLock::new(),
502            issuer_raw_der_cache: OnceLock::new(),
503            this_update_cache: OnceLock::new(),
504            next_update_cache: OnceLock::new(),
505            signature_algorithm_cache: OnceLock::new(),
506            signature_algorithm_oid_cache: OnceLock::new(),
507            signature_value_cache: OnceLock::new(),
508        })
509    }
510
511    /// Parse a PEM-encoded X.509 Certificate Revocation List.
512    ///
513    /// Returns a single :class:`CertificateList` for one PEM block, or a
514    /// :class:`list` of :class:`CertificateList` objects when multiple blocks
515    /// are present.  Raises :exc:`ValueError` if no PEM block is found.
516    ///
517    /// ```python
518    /// crl = CertificateList.from_pem(open("crl.pem", "rb").read())
519    /// print(crl.issuer)
520    /// ```
521    #[staticmethod]
522    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
523        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
524            let obj = Self::from_der(py, bytes)?;
525            Ok(Py::new(py, obj)?.into_bound(py).into_any())
526        })
527    }
528
529    /// Serialize one CRL or a list of CRLs to PEM format.
530    ///
531    /// ```python
532    /// pem = CertificateList.to_pem(crl)
533    /// pem = CertificateList.to_pem([crl1, crl2])
534    /// ```
535    #[staticmethod]
536    fn to_pem<'py>(
537        py: Python<'py>,
538        obj_or_list: Bound<'_, PyAny>,
539    ) -> PyResult<Bound<'py, PyBytes>> {
540        pyobject_to_pem::<Self, _>(py, "X509 CRL", &obj_or_list, |c| c.raw)
541    }
542
543    /// CRL version field (1 = v2), or ``None`` if absent (implies v1).
544    ///
545    /// RFC 5280 §5.1.2.1: the ``version`` field is present only when the CRL
546    /// is version 2.  For RFC 5280-compliant CRLs this value is always ``1``
547    /// (the integer 1 encodes version 2).  Returns ``None`` for v1 CRLs.
548    #[getter]
549    fn version(&self) -> PyResult<Option<i64>> {
550        Ok(self
551            .crl()?
552            .tbs_cert_list
553            .version
554            .as_ref()
555            .and_then(|v| v.as_i64().ok()))
556    }
557
558    /// Issuer distinguished name as a string.
559    #[getter]
560    fn issuer<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
561        if let Some(cached) = self.issuer_cache.get() {
562            return Ok(cached.clone_ref(py).into_bound(py));
563        }
564        let s = name_to_dn_string(&self.crl()?.tbs_cert_list.issuer);
565        let py_str = PyString::new(py, &s).unbind();
566        let _ = self.issuer_cache.set(py_str.clone_ref(py));
567        Ok(py_str.into_bound(py))
568    }
569
570    /// Raw DER bytes of the issuer Name SEQUENCE.
571    #[getter]
572    fn issuer_raw_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
573        if let Some(cached) = self.issuer_raw_der_cache.get() {
574            return Ok(cached.clone_ref(py).into_bound(py));
575        }
576        let bytes = name_to_der_bytes(&self.crl()?.tbs_cert_list.issuer);
577        let py_bytes = PyBytes::new(py, &bytes).unbind();
578        let _ = self.issuer_raw_der_cache.set(py_bytes.clone_ref(py));
579        Ok(py_bytes.into_bound(py))
580    }
581
582    /// thisUpdate time as a string.
583    #[getter]
584    fn this_update<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
585        if let Some(cached) = self.this_update_cache.get() {
586            return Ok(cached.clone_ref(py).into_bound(py));
587        }
588        let s = match &self.crl()?.tbs_cert_list.this_update {
589            Time::UtcTime(t) => t.to_string(),
590            Time::GeneralTime(t) => t.to_string(),
591        };
592        let py_str = PyString::new(py, &s).unbind();
593        let _ = self.this_update_cache.set(py_str.clone_ref(py));
594        Ok(py_str.into_bound(py))
595    }
596
597    /// nextUpdate time as a string, or ``None`` if absent.
598    #[getter]
599    fn next_update<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyString>>> {
600        if let Some(cached) = self.next_update_cache.get() {
601            return Ok(cached.as_ref().map(|s| s.clone_ref(py).into_bound(py)));
602        }
603        let computed: Option<Bound<'py, PyString>> =
604            self.crl()?.tbs_cert_list.next_update.as_ref().map(|t| {
605                let s = match t {
606                    Time::UtcTime(t) => t.to_string(),
607                    Time::GeneralTime(t) => t.to_string(),
608                };
609                PyString::new(py, &s)
610            });
611        let to_store = computed.as_ref().map(|s| s.as_unbound().clone_ref(py));
612        let _ = self.next_update_cache.set(to_store);
613        Ok(computed)
614    }
615
616    /// Signature algorithm name (e.g. "RSA", "ECDSA").
617    #[getter]
618    fn signature_algorithm<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
619        if let Some(cached) = self.signature_algorithm_cache.get() {
620            return Ok(cached.clone_ref(py).into_bound(py));
621        }
622        let oid = &self.crl()?.signature_algorithm.algorithm;
623        let name = synta_certificate::identify_signature_algorithm(oid);
624        let s = if name != "Other" {
625            name.to_string()
626        } else {
627            oid.to_string()
628        };
629        let py_str = PyString::new(py, &s).unbind();
630        let _ = self.signature_algorithm_cache.set(py_str.clone_ref(py));
631        Ok(py_str.into_bound(py))
632    }
633
634    /// OID of the signature algorithm.
635    #[getter]
636    fn signature_algorithm_oid<'py>(
637        &self,
638        py: Python<'py>,
639    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
640        if let Some(cached) = self.signature_algorithm_oid_cache.get() {
641            return Ok(cached.clone_ref(py).into_bound(py));
642        }
643        let obj = Py::new(
644            py,
645            PyObjectIdentifier::from_oid(self.crl()?.signature_algorithm.algorithm.clone()),
646        )?;
647        let _ = self.signature_algorithm_oid_cache.set(obj.clone_ref(py));
648        Ok(obj.into_bound(py))
649    }
650
651    /// Raw signature bytes.
652    #[getter]
653    fn signature_value<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
654        if let Some(cached) = self.signature_value_cache.get() {
655            return Ok(cached.clone_ref(py).into_bound(py));
656        }
657        let py_bytes = PyBytes::new(py, self.crl()?.signature_value.as_bytes()).unbind();
658        let _ = self.signature_value_cache.set(py_bytes.clone_ref(py));
659        Ok(py_bytes.into_bound(py))
660    }
661
662    /// CRL sequence number from the ``cRLNumber`` extension (OID ``2.5.29.20``),
663    /// or ``None`` if the CRL carries no such extension.
664    ///
665    /// Returns an arbitrary-precision Python :class:`int`.
666    ///
667    /// ```python,ignore
668    /// crl = synta.CertificateList.from_der(data)
669    /// print("CRL number:", crl.crl_number)
670    /// ```
671    #[getter]
672    fn crl_number<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyAny>>> {
673        let Some(n) = self.crl()?.crl_number() else {
674            return Ok(None);
675        };
676        let py_int = if let Ok(v) = n.as_i64() {
677            v.into_pyobject(py)?.into_any()
678        } else if let Ok(v) = n.as_i128() {
679            v.into_pyobject(py)?.into_any()
680        } else {
681            // Arbitrarily large CRL number — extremely uncommon but RFC-compliant.
682            let bytes_obj = PyBytes::new(py, n.as_bytes());
683            let kwargs = pyo3::types::PyDict::new(py);
684            kwargs.set_item(pyo3::intern!(py, "signed"), false)?;
685            py.get_type::<pyo3::types::PyInt>().call_method(
686                pyo3::intern!(py, "from_bytes"),
687                (bytes_obj, pyo3::intern!(py, "big")),
688                Some(&kwargs),
689            )?
690        };
691        Ok(Some(py_int))
692    }
693
694    /// Number of revoked certificates in this CRL (0 if no revoked entries).
695    #[getter]
696    fn revoked_count(&self) -> PyResult<usize> {
697        Ok(self
698            .crl()?
699            .tbs_cert_list
700            .revoked_certificates
701            .as_ref()
702            .map(|v| v.len())
703            .unwrap_or(0))
704    }
705
706    /// Return the DER value bytes of the named CRL extension, or ``None`` if
707    /// the CRL carries no such extension.
708    ///
709    /// ``oid`` may be a dotted-decimal string or a
710    /// :class:`~synta.ObjectIdentifier`.  The value returned is the content of
711    /// the ``extnValue`` OCTET STRING — the DER encoding of the extension value
712    /// — without the outer OCTET STRING tag and length.
713    ///
714    /// ```python,ignore
715    /// CRL_NUMBER_OID = "2.5.29.20"
716    /// crl_num_der = crl.get_extension_value_der(CRL_NUMBER_OID)
717    /// if crl_num_der:
718    ///     num = int(synta.Decoder(crl_num_der, synta.Encoding.DER).decode_integer())
719    ///     print("CRL Number:", num)
720    /// ```
721    fn get_extension_value_der<'py>(
722        &self,
723        py: Python<'py>,
724        oid: &Bound<'_, PyAny>,
725    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
726        let target = super::oid_from_pyany(oid)?;
727        let exts = match self.crl()?.tbs_cert_list.crl_extensions.as_ref() {
728            Some(e) => e,
729            None => return Ok(None),
730        };
731        for ext in exts {
732            if ext.extn_id == target {
733                return Ok(Some(PyBytes::new(py, ext.extn_value.as_bytes())));
734            }
735        }
736        Ok(None)
737    }
738
739    /// Complete DER encoding of this CRL (the original bytes passed to ``from_der``).
740    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
741        self._data.clone_ref(py).into_bound(py)
742    }
743
744    fn __repr__(&self) -> PyResult<String> {
745        let crl = self.crl()?;
746        Ok(format!(
747            "CertificateList(issuer={:?}, revoked_count={})",
748            name_to_dn_string(&crl.tbs_cert_list.issuer),
749            crl.tbs_cert_list
750                .revoked_certificates
751                .as_ref()
752                .map(|v| v.len())
753                .unwrap_or(0),
754        ))
755    }
756}
757
758// ── PyOcspResponse ────────────────────────────────────────────────────────────
759
760/// OCSP Response (RFC 6960) accessible from Python.
761///
762/// ```python
763/// resp = OCSPResponse.from_der(open("resp.der", "rb").read())
764/// print(resp.status)
765/// if resp.response_bytes:
766///     basic = resp.response_bytes  # DER of BasicOCSPResponse
767/// ```
768#[pyclass(frozen, name = "OCSPResponse")]
769pub struct PyOcspResponse {
770    pub(super) _data: Py<PyBytes>,
771    pub(super) raw: &'static [u8],
772    inner: OnceLock<Box<synta_certificate::ocsp::OCSPResponse<'static>>>,
773    status_cache: OnceLock<Py<PyString>>,
774    response_type_oid_cache: OnceLock<Option<Py<PyObjectIdentifier>>>,
775    response_bytes_cache: OnceLock<Option<Py<PyBytes>>>,
776}
777
778impl PyOcspResponse {
779    fn ocsp(&self) -> PyResult<&synta_certificate::ocsp::OCSPResponse<'static>> {
780        if let Some(v) = self.inner.get() {
781            return Ok(v.as_ref());
782        }
783        let mut decoder = Decoder::new(self.raw, Encoding::Der);
784        let decoded = decoder.decode().map_err(|e| {
785            pyo3::exceptions::PyValueError::new_err(format!("OCSPResponse DER decode failed: {e}"))
786        })?;
787        let _ = self.inner.set(Box::new(decoded));
788        Ok(self.inner.get().unwrap().as_ref())
789    }
790}
791
792#[pymethods]
793impl PyOcspResponse {
794    /// Parse a DER-encoded OCSP Response.
795    #[staticmethod]
796    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
797        let py_bytes = data.unbind();
798        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
799        // keeps the Python bytes object alive for the lifetime of this struct.
800        // CPython's bytes objects have a fixed-address, non-relocating payload
801        // buffer (CPython has no moving GC).  The slice lifetime is extended
802        // to 'static; the actual safety invariants are:
803        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
804        //       can outlive the struct, so `raw` is never read after drop begins.
805        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
806        //       allocation), so field drop order does not cause use-after-free.
807        //   (3) `inner` contains only borrow-typed fields; dropping
808        //       Box<OCSPResponse<'static>> does not read through the contained
809        //       &'static slices (borrows have no destructors).
810        // CPython-only: does not hold for PyPy or GraalPy.
811        let raw: &'static [u8] = unsafe {
812            let s = py_bytes.bind(py).as_bytes();
813            std::slice::from_raw_parts(s.as_ptr(), s.len())
814        };
815        {
816            let mut d = Decoder::new(raw, Encoding::Der);
817            d.read_tag()
818                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
819            d.read_length()
820                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
821        }
822        Ok(Self {
823            _data: py_bytes,
824            raw,
825            inner: OnceLock::new(),
826            status_cache: OnceLock::new(),
827            response_type_oid_cache: OnceLock::new(),
828            response_bytes_cache: OnceLock::new(),
829        })
830    }
831
832    /// Parse a PEM-encoded OCSP Response.
833    ///
834    /// Returns a single :class:`OCSPResponse` for one PEM block, or a
835    /// :class:`list` of :class:`OCSPResponse` objects when multiple blocks are
836    /// present.  Raises :exc:`ValueError` if no PEM block is found.
837    ///
838    /// ```python
839    /// resp = OCSPResponse.from_pem(open("ocsp.pem", "rb").read())
840    /// print(resp.status)
841    /// ```
842    #[staticmethod]
843    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
844        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
845            let obj = Self::from_der(py, bytes)?;
846            Ok(Py::new(py, obj)?.into_bound(py).into_any())
847        })
848    }
849
850    /// Serialize one OCSP response or a list of them to PEM format.
851    ///
852    /// ```python
853    /// pem = OCSPResponse.to_pem(resp)
854    /// pem = OCSPResponse.to_pem([resp1, resp2])
855    /// ```
856    #[staticmethod]
857    fn to_pem<'py>(
858        py: Python<'py>,
859        obj_or_list: Bound<'_, PyAny>,
860    ) -> PyResult<Bound<'py, PyBytes>> {
861        pyobject_to_pem::<Self, _>(py, "OCSP RESPONSE", &obj_or_list, |c| c.raw)
862    }
863
864    /// Response status string (e.g. ``"successful"``, ``"tryLater"``).
865    #[getter]
866    fn status<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
867        if let Some(cached) = self.status_cache.get() {
868            return Ok(cached.clone_ref(py).into_bound(py));
869        }
870        let s = ocsp_status_str(self.ocsp()?.response_status);
871        let py_str = PyString::new(py, s).unbind();
872        let _ = self.status_cache.set(py_str.clone_ref(py));
873        Ok(py_str.into_bound(py))
874    }
875
876    /// Dotted-decimal OID of the response type, or ``None`` for error responses
877    /// that carry no ``responseBytes`` (e.g. ``tryLater``).
878    ///
879    /// For successful responses this is typically the id-pkix-ocsp-basic OID
880    /// (``"1.3.6.1.5.5.7.48.1.1"``).
881    #[getter]
882    fn response_type_oid<'py>(
883        &self,
884        py: Python<'py>,
885    ) -> PyResult<Option<Bound<'py, PyObjectIdentifier>>> {
886        if let Some(cached) = self.response_type_oid_cache.get() {
887            return Ok(cached.as_ref().map(|o| o.clone_ref(py).into_bound(py)));
888        }
889        let computed: Option<Py<PyObjectIdentifier>> = self
890            .ocsp()?
891            .response_bytes
892            .as_ref()
893            .map(|rb| Py::new(py, PyObjectIdentifier::from_oid(rb.response_type.clone())))
894            .transpose()?;
895        let _ = self
896            .response_type_oid_cache
897            .set(computed.as_ref().map(|o| o.clone_ref(py)));
898        Ok(computed.map(|o| o.into_bound(py)))
899    }
900
901    /// Raw DER bytes of the embedded response (the OCTET STRING content of
902    /// ``ResponseBytes``), or ``None`` for error responses.
903    ///
904    /// For id-pkix-ocsp-basic responses, these bytes are a DER-encoded
905    /// ``BasicOCSPResponse``.
906    #[getter]
907    fn response_bytes<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
908        if let Some(cached) = self.response_bytes_cache.get() {
909            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
910        }
911        let computed: Option<Bound<'py, PyBytes>> = self
912            .ocsp()?
913            .response_bytes
914            .as_ref()
915            .map(|rb| PyBytes::new(py, rb.response.as_bytes()));
916        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
917        let _ = self.response_bytes_cache.set(to_store);
918        Ok(computed)
919    }
920
921    /// Complete DER encoding of this OCSP response (the original bytes passed to ``from_der``).
922    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
923        self._data.clone_ref(py).into_bound(py)
924    }
925
926    fn __repr__(&self) -> PyResult<String> {
927        Ok(format!(
928            "OCSPResponse(status={})",
929            ocsp_status_str(self.ocsp()?.response_status),
930        ))
931    }
932}
933
934// ── PKCS#7 / PKCS#12 free functions ─────────────────────────────────────────
935
936/// Build a `PyList[Certificate]` from a `Vec<Vec<u8>>` of DER-encoded certs.
937fn der_certs_to_pylist<'py>(
938    py: Python<'py>,
939    der_certs: Vec<Vec<u8>>,
940) -> PyResult<Bound<'py, PyList>> {
941    let list = PyList::empty(py);
942    for der in der_certs {
943        let py_bytes = PyBytes::new(py, &der);
944        let cert = PyCertificate::new_from_der(py, py_bytes)?;
945        list.append(Py::new(py, cert)?.into_bound(py))?;
946    }
947    Ok(list)
948}
949
950/// Extract X.509 certificates from a PKCS#7 SignedData blob (DER or BER).
951///
952/// Accepts raw DER or BER input; BER indefinite-length encodings (common in
953/// PKCS#7 files produced by older tools) are handled transparently.  Returns
954/// a list of :class:`Certificate` objects in document order.
955///
956/// Raises :exc:`ValueError` on any ASN.1 structural error or if the input
957/// is not id-signedData.
958///
959/// ```python,ignore
960/// data = open("bundle.p7b", "rb").read()
961/// certs = synta.load_der_pkcs7_certificates(data)
962/// for cert in certs:
963///     print(cert.subject)
964/// ```
965#[pyfunction]
966pub fn load_der_pkcs7_certificates<'py>(
967    py: Python<'py>,
968    data: &[u8],
969) -> PyResult<Bound<'py, PyList>> {
970    let der_certs = synta_certificate::certs_from_pkcs7(data)
971        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
972    der_certs_to_pylist(py, der_certs)
973}
974
975/// Extract X.509 certificates from a PEM-encoded PKCS#7 SignedData blob.
976///
977/// Decodes PEM block(s) in ``data`` (any label is accepted: ``PKCS7``,
978/// ``CMS``, ``CERTIFICATE``, etc.) then calls
979/// :func:`load_der_pkcs7_certificates` on each DER payload.  All certificates
980/// across all blocks are returned as a single flat list.
981///
982/// Raises :exc:`ValueError` if no PEM block is found or if any block fails
983/// to parse as PKCS#7 SignedData.
984///
985/// ```python,ignore
986/// data = open("bundle.pem", "rb").read()
987/// certs = synta.load_pem_pkcs7_certificates(data)
988/// for cert in certs:
989///     print(cert.subject)
990/// ```
991#[pyfunction]
992pub fn load_pem_pkcs7_certificates<'py>(
993    py: Python<'py>,
994    data: &[u8],
995) -> PyResult<Bound<'py, PyList>> {
996    let blocks = synta_certificate::pem_blocks(data);
997    if blocks.is_empty() {
998        return Err(pyo3::exceptions::PyValueError::new_err(
999            "no PEM block found in input",
1000        ));
1001    }
1002    let mut all_certs: Vec<Vec<u8>> = Vec::new();
1003    for (_, der) in &blocks {
1004        let certs = synta_certificate::certs_from_pkcs7(der)
1005            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1006        all_certs.extend(certs);
1007    }
1008    der_certs_to_pylist(py, all_certs)
1009}
1010
1011/// Extract X.509 certificates from a PKCS#12 archive.
1012///
1013/// Accepts raw DER or BER input; the encoding is detected automatically.
1014/// Returns a list of :class:`Certificate` objects in document order.
1015/// Non-certificate bag types (``keyBag``, ``pkcs8ShroudedKeyBag``,
1016/// ``crlBag``, ``secretBag``) are silently skipped.
1017///
1018/// ``password`` is the archive password as raw bytes (UTF-8 without a NUL
1019/// terminator).  Pass ``b""`` or omit the argument for password-less archives.
1020///
1021/// Encrypted bags are supported when the ``openssl`` Cargo feature is enabled.
1022/// Without that feature a :exc:`ValueError` is raised on any encrypted bag.
1023///
1024/// ```python,ignore
1025/// data = open("archive.p12", "rb").read()
1026/// certs = synta.load_pkcs12_certificates(data, b"s3cr3t")
1027/// for cert in certs:
1028///     print(cert.subject)
1029/// ```
1030#[pyfunction]
1031#[pyo3(signature = (data, password = None))]
1032pub fn load_pkcs12_certificates<'py>(
1033    py: Python<'py>,
1034    data: &[u8],
1035    password: Option<&[u8]>,
1036) -> PyResult<Bound<'py, PyList>> {
1037    let pw = password.unwrap_or(b"");
1038    #[cfg(feature = "openssl")]
1039    let der_certs =
1040        synta_certificate::certs_from_pkcs12(data, pw, &synta_certificate::OpensslDecryptor)
1041            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1042    #[cfg(not(feature = "openssl"))]
1043    let der_certs = synta_certificate::certs_from_pkcs12(data, pw, &synta_certificate::NoCrypto)
1044        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1045    der_certs_to_pylist(py, der_certs)
1046}
1047
1048/// Extract PKCS#8 private keys from a PKCS#12 archive.
1049///
1050/// Returns a :class:`list` of :class:`bytes` objects, one per private key
1051/// found in ``keyBag`` (unencrypted) and ``pkcs8ShroudedKeyBag`` (encrypted,
1052/// decrypted when the ``openssl`` Cargo feature is enabled and ``password``
1053/// is given) entries.  Each element is a DER-encoded ``OneAsymmetricKey``
1054/// (PKCS#8) structure, suitable for passing to a cryptography library.
1055///
1056/// ``certBag``, ``crlBag``, ``secretBag`` and any unknown bag types are
1057/// silently skipped.
1058///
1059/// ```python,ignore
1060/// data = open("archive.p12", "rb").read()
1061/// keys = synta.load_pkcs12_keys(data, b"s3cr3t")
1062/// for key_der in keys:
1063///     from cryptography.hazmat.primitives.serialization import load_der_private_key
1064///     key = load_der_private_key(key_der, None)
1065/// ```
1066#[pyfunction]
1067#[pyo3(signature = (data, password = None))]
1068pub fn load_pkcs12_keys<'py>(
1069    py: Python<'py>,
1070    data: &[u8],
1071    password: Option<&[u8]>,
1072) -> PyResult<Bound<'py, PyList>> {
1073    let pw = password.unwrap_or(b"");
1074    #[cfg(feature = "openssl")]
1075    let der_keys =
1076        synta_certificate::keys_from_pkcs12(data, pw, &synta_certificate::OpensslDecryptor)
1077            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1078    #[cfg(not(feature = "openssl"))]
1079    let der_keys = synta_certificate::keys_from_pkcs12(data, pw, &synta_certificate::NoCrypto)
1080        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1081    let list = PyList::empty(py);
1082    for der in der_keys {
1083        list.append(PyBytes::new(py, &der))?;
1084    }
1085    Ok(list)
1086}
1087
1088/// Extract both certificates and private keys from a PKCS#12 archive.
1089///
1090/// Returns a 2-tuple ``(certs, keys)`` where:
1091///
1092/// - ``certs`` is a :class:`list` of :class:`Certificate` objects.
1093/// - ``keys`` is a :class:`list` of :class:`bytes`, each a DER-encoded
1094///   PKCS#8 ``OneAsymmetricKey`` structure.
1095///
1096/// Encrypted bags are decrypted with ``password`` when the ``openssl``
1097/// Cargo feature is enabled; without it, encrypted bags are silently
1098/// skipped and a :exc:`ValueError` is raised only on structural errors.
1099/// Pass ``b""`` or omit ``password`` for password-less archives.
1100///
1101/// Use :func:`load_pkcs12_certificates` if you only need certificates, or
1102/// :func:`load_pkcs12_keys` if you only need keys.
1103///
1104/// ```python,ignore
1105/// data = open("archive.p12", "rb").read()
1106/// certs, keys = synta.load_pkcs12(data, b"s3cr3t")
1107/// for cert in certs:
1108///     print(cert.subject)
1109/// for key_der in keys:
1110///     from cryptography.hazmat.primitives.serialization import load_der_private_key
1111///     key = load_der_private_key(key_der, None)
1112/// ```
1113#[pyfunction]
1114#[pyo3(signature = (data, password = None))]
1115pub fn load_pkcs12<'py>(
1116    py: Python<'py>,
1117    data: &[u8],
1118    password: Option<&[u8]>,
1119) -> PyResult<Bound<'py, pyo3::types::PyTuple>> {
1120    let pw = password.unwrap_or(b"");
1121    #[cfg(feature = "openssl")]
1122    let pki = synta_certificate::pki_from_pkcs12(data, pw, &synta_certificate::OpensslDecryptor)
1123        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1124    #[cfg(not(feature = "openssl"))]
1125    let pki = synta_certificate::pki_from_pkcs12(data, pw, &synta_certificate::NoCrypto)
1126        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1127    let certs = der_certs_to_pylist(py, pki.certs)?;
1128    let keys = PyList::empty(py);
1129    for der in pki.keys {
1130        keys.append(PyBytes::new(py, &der))?;
1131    }
1132    pyo3::types::PyTuple::new(py, [certs.into_any(), keys.into_any()])
1133}
1134
1135/// Auto-detect the encoding of ``data`` and return every PKI object as a
1136/// ``(label, der_bytes)`` tuple, in document order.
1137///
1138/// Supported formats: PEM (any label), PKCS#7 / CMS SignedData (DER or BER),
1139/// PKCS#12 PFX (DER or BER), and raw DER.
1140///
1141/// **Labels:**
1142///
1143/// - PEM blocks whose payload is a PKCS#7 ``ContentInfo`` (detected by
1144///   structure, not label) are expanded: the embedded certificates are
1145///   extracted and each is returned as ``("CERTIFICATE", der)``.
1146/// - All other PEM block types pass through with their original label
1147///   (e.g. ``"CERTIFICATE"``, ``"PRIVATE KEY"``).  Malformed PEM blocks
1148///   (bad base64, truncated header) are silently skipped.
1149/// - Binary PKCS#7 and raw DER yield ``"CERTIFICATE"`` labels.
1150/// - Binary PKCS#12 yields ``"CERTIFICATE"`` for ``certBag`` entries and
1151///   ``"PRIVATE KEY"`` for ``keyBag`` / ``pkcs8ShroudedKeyBag`` entries.
1152///
1153/// **Decryption:**
1154///
1155/// ``password`` applies only to PKCS#12 archives.  When ``password`` is
1156/// given **and** the library was built with the ``openssl`` Cargo feature,
1157/// encrypted SafeBags are decrypted.  Otherwise encrypted bags are silently
1158/// skipped; unencrypted certificates in the same archive are still returned.
1159///
1160/// :raises ValueError: For any structural failure:
1161///
1162///   - A PKCS#7 block (binary or PEM-wrapped) has malformed DER, or its
1163///     ``contentType`` OID is not ``id-signedData``.
1164///   - The PKCS#12 input has a structural ASN.1 error.
1165///   - ``password`` was supplied with the ``openssl`` feature enabled and
1166///     decryption of an encrypted SafeBag failed (e.g. wrong password,
1167///     unsupported cipher).
1168///   - The PKCS#12 authSafe uses an unsupported ``ContentInfo`` type.
1169///
1170/// :raises TypeError: If ``data`` is not :class:`bytes`, or ``password``
1171///   is not :class:`bytes` or ``None``.
1172///
1173/// ```python,ignore
1174/// data = open("bundle.p7b", "rb").read()
1175/// blocks = synta.read_pki_blocks(data)
1176/// for label, der in blocks:
1177///     print(f"{label}: {len(der)} bytes")
1178///
1179/// # PKCS#12 with encryption
1180/// data = open("archive.p12", "rb").read()
1181/// blocks = synta.read_pki_blocks(data, b"s3cr3t")
1182/// ```
1183#[pyfunction]
1184#[pyo3(signature = (data, password = None))]
1185pub fn read_pki_blocks<'py>(
1186    py: Python<'py>,
1187    data: &[u8],
1188    password: Option<&[u8]>,
1189) -> PyResult<Bound<'py, PyList>> {
1190    let pw = password.unwrap_or(b"");
1191    #[cfg(feature = "openssl")]
1192    let blocks = if password.is_some() {
1193        synta_certificate::read_pki_blocks(
1194            data,
1195            pw,
1196            Some(&synta_certificate::OpensslDecryptor as &dyn synta_certificate::PkiDecryptor),
1197        )
1198    } else {
1199        synta_certificate::read_pki_blocks(data, pw, None::<&dyn synta_certificate::PkiDecryptor>)
1200    };
1201    #[cfg(not(feature = "openssl"))]
1202    let blocks =
1203        synta_certificate::read_pki_blocks(data, pw, None::<&dyn synta_certificate::PkiDecryptor>);
1204    let blocks = blocks.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1205    let list = PyList::empty(py);
1206    for (label, der) in blocks {
1207        let tuple = pyo3::types::PyTuple::new(
1208            py,
1209            [
1210                PyString::new(py, &label).into_any(),
1211                PyBytes::new(py, &der).into_any(),
1212            ],
1213        )?;
1214        list.append(tuple)?;
1215    }
1216    Ok(list)
1217}
1218
1219// ── X.509 encoding utilities ──────────────────────────────────────────────────
1220
1221/// Encode a list of OIDs as a DER ``SEQUENCE OF OID`` (ExtendedKeyUsage value).
1222///
1223/// ``oids`` must be a :class:`list` of :class:`~synta.ObjectIdentifier` or
1224/// dotted-decimal ``str`` values.  Returns the complete DER bytes of the
1225/// ``SEQUENCE``, suitable for use as an X.509v3 Extended Key Usage extension
1226/// value.
1227///
1228/// Delegates to synta's ``Vec<ObjectIdentifier>`` encoder which matches the
1229/// generated ``ExtendedKeyUsage = Vec<KeyPurposeId>`` type from the X.509 schema.
1230///
1231/// ```python,ignore
1232/// eku_der = synta.encode_extended_key_usage([
1233///     "1.3.6.1.5.5.7.3.1",   # id-kp-serverAuth
1234///     "1.3.6.1.5.5.7.3.2",   # id-kp-clientAuth
1235/// ])
1236/// ```
1237#[pyfunction]
1238pub fn encode_extended_key_usage<'py>(
1239    py: Python<'py>,
1240    oids: &Bound<'_, PyList>,
1241) -> PyResult<Bound<'py, PyBytes>> {
1242    use synta::traits::Encode;
1243    let mut eku: Vec<synta::ObjectIdentifier> = Vec::with_capacity(oids.len());
1244    for item in oids.iter() {
1245        eku.push(super::oid_from_pyany(&item)?);
1246    }
1247    let mut enc = synta::Encoder::new(synta::Encoding::Der);
1248    eku.encode(&mut enc)
1249        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1250    let der = enc
1251        .finish()
1252        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1253    Ok(PyBytes::new(py, &der))
1254}
1255
1256/// Encode a list of ``(tag_number, content_bytes)`` pairs as a DER
1257/// ``SEQUENCE OF GeneralName``.
1258///
1259/// This is the inverse of :func:`~synta.parse_general_names`: the input
1260/// format is identical to what that function returns.  Tag numbers follow
1261/// RFC 5280 §4.2.1.6; use the :mod:`synta.general_name` constants.
1262///
1263/// ``content_bytes`` must be the raw **value** bytes — without the outer
1264/// context-specific TLV — exactly as returned by
1265/// :func:`~synta.parse_general_names`.
1266///
1267/// Delegates to :func:`synta_certificate::encode_general_names` which uses the
1268/// generated :class:`GeneralName` enum's ``Encode`` implementation.
1269///
1270/// ```python,ignore
1271/// import synta.general_name as gn
1272///
1273/// san_der = synta.encode_subject_alt_names([
1274///     (gn.DNS_NAME,   b"example.com"),
1275///     (gn.IP_ADDRESS, b"\x7f\x00\x00\x01"),   # 127.0.0.1
1276/// ])
1277/// ```
1278#[pyfunction]
1279pub fn encode_subject_alt_names<'py>(
1280    py: Python<'py>,
1281    names: &Bound<'_, PyList>,
1282) -> PyResult<Bound<'py, PyBytes>> {
1283    // Collect owned (tag, bytes) first so we can create &[u8] references that
1284    // all share the same lifetime, satisfying Vec<GeneralName<'_>> constraints.
1285    let mut owned: Vec<(u32, Vec<u8>)> = Vec::with_capacity(names.len());
1286    for item in names.iter() {
1287        let tuple = item.cast::<pyo3::types::PyTuple>().map_err(|_| {
1288            pyo3::exceptions::PyTypeError::new_err(
1289                "each element must be a (tag_number, bytes) 2-tuple",
1290            )
1291        })?;
1292        if tuple.len() != 2 {
1293            return Err(pyo3::exceptions::PyTypeError::new_err(
1294                "each element must be a (tag_number, bytes) 2-tuple",
1295            ));
1296        }
1297        let tag_num: u32 = tuple.get_item(0)?.extract()?;
1298        let content: Vec<u8> = tuple.get_item(1)?.extract()?;
1299        owned.push((tag_num, content));
1300    }
1301    let entries: Vec<(u32, &[u8])> = owned.iter().map(|(t, v)| (*t, v.as_slice())).collect();
1302    let der = synta_certificate::encode_general_names(&entries).ok_or_else(|| {
1303        pyo3::exceptions::PyValueError::new_err(
1304            "failed to encode GeneralName entries (invalid content bytes or unsupported tag)",
1305        )
1306    })?;
1307    Ok(PyBytes::new(py, &der))
1308}
1309
1310/// Compare two DER-encoded X.500 Names for byte-for-byte equality.
1311///
1312/// Returns ``True`` if and only if the two byte sequences are identical.
1313/// This is a strict comparison; RFC 5280 name matching rules (case-insensitive
1314/// string comparison, etc.) are **not** applied.
1315///
1316/// Useful for quickly checking whether issuer and subject names match without
1317/// decoding the full structure:
1318///
1319/// ```python,ignore
1320/// same = synta.name_der_equal(cert.issuer_raw_der, ca_cert.subject_raw_der)
1321/// ```
1322#[pyfunction]
1323pub fn name_der_equal(a: &[u8], b: &[u8]) -> bool {
1324    a == b
1325}
1326
1327/// Build a DER-encoded PKCS#12 ``PFX`` archive.
1328///
1329/// Assembles a ``PFX`` from one or more certificates and an optional
1330/// DER-encoded PKCS#8 private key.  The archive is protected by ``password``
1331/// using PBES2 (PBKDF2-SHA256 + AES-256-CBC) for key encryption and
1332/// HMAC-SHA256 (PBKDF2-SHA256 derived key) for the integrity MAC.
1333///
1334/// :param certificates: sequence of :class:`~synta.Certificate` objects or
1335///     raw DER ``bytes``.  Both types may appear in the same list.
1336/// :param private_key: a :class:`~synta.PrivateKey` object or a DER-encoded
1337///     PKCS#8 ``OneAsymmetricKey`` ``bytes``, or ``None`` to omit.
1338/// :param password: archive password (``bytes``), or ``None`` for a
1339///     password-less archive.
1340/// :returns: DER-encoded ``PFX`` archive bytes.
1341/// :raises TypeError: if any element of ``certificates`` is neither a
1342///     :class:`~synta.Certificate` nor ``bytes``, or if ``private_key`` is
1343///     not a :class:`~synta.PrivateKey` nor ``bytes``.
1344/// :raises ValueError: if no certificates or key are supplied, or if
1345///     encryption fails (e.g. OpenSSL not available).
1346///
1347/// ```python,ignore
1348/// # Certificate and PrivateKey objects — no .to_der() call needed
1349/// pfx = synta.create_pkcs12(
1350///     [cert, ca_cert],
1351///     private_key=key,
1352///     password=b"s3cr3t",
1353/// )
1354///
1355/// # Raw DER bytes also accepted for both
1356/// pfx = synta.create_pkcs12([cert.to_der()], private_key=key.to_der())
1357///
1358/// with open("out.p12", "wb") as f:
1359///     f.write(pfx)
1360/// ```
1361#[pyfunction]
1362#[pyo3(signature = (certificates, private_key = None, password = None))]
1363pub fn create_pkcs12<'py>(
1364    py: Python<'py>,
1365    certificates: &Bound<'_, PyList>,
1366    private_key: Option<Bound<'_, PyAny>>,
1367    password: Option<&[u8]>,
1368) -> PyResult<Bound<'py, PyBytes>> {
1369    use synta_certificate::Pkcs12Builder;
1370
1371    let password = password.unwrap_or(b"");
1372    let mut builder = Pkcs12Builder::new();
1373    for item in certificates.iter() {
1374        if let Ok(py_cert) = item.cast::<PyCertificate>() {
1375            // Zero-copy: raw is &'static [u8] backed by the PyBytes in _data.
1376            // Pkcs12Builder::certificate() copies into Vec<u8> immediately, so
1377            // the borrow does not outlive this loop iteration.
1378            builder = builder.certificate(py_cert.get().raw);
1379        } else if let Ok(py_bytes) = item.cast::<PyBytes>() {
1380            builder = builder.certificate(py_bytes.as_bytes());
1381        } else {
1382            return Err(pyo3::exceptions::PyTypeError::new_err(
1383                "certificates must be synta.Certificate or bytes",
1384            ));
1385        }
1386    }
1387    if let Some(key_arg) = private_key {
1388        if let Ok(py_key) = key_arg.cast::<PyPrivateKey>() {
1389            // PyPrivateKey stores PKCS#8 DER internally; retrieve it directly.
1390            let key_der = py_key
1391                .get()
1392                .inner
1393                .to_der()
1394                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1395            builder = builder.private_key(&key_der);
1396        } else if let Ok(py_bytes) = key_arg.cast::<PyBytes>() {
1397            builder = builder.private_key(py_bytes.as_bytes());
1398        } else {
1399            return Err(pyo3::exceptions::PyTypeError::new_err(
1400                "private_key must be synta.PrivateKey or bytes",
1401            ));
1402        }
1403    }
1404
1405    #[cfg(feature = "openssl")]
1406    let pfx_der = builder
1407        .build(password, &synta_certificate::OpensslPkcs12Encryptor::new())
1408        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1409
1410    #[cfg(not(feature = "openssl"))]
1411    let pfx_der = {
1412        use synta_certificate::NoPkcs12Encryptor;
1413        builder
1414            .build(password, &NoPkcs12Encryptor)
1415            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?
1416    };
1417
1418    Ok(PyBytes::new(py, &pfx_der))
1419}