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    /// Complete DER encoding of this CSR (the original bytes passed to ``from_der``).
313    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
314        self._data.clone_ref(py).into_bound(py)
315    }
316
317    fn __repr__(&self) -> PyResult<String> {
318        Ok(format!(
319            "CertificationRequest(subject={:?})",
320            name_to_dn_string(&self.csr()?.certification_request_info.subject),
321        ))
322    }
323}
324
325impl PyCsr {
326    /// Rust-internal constructor: parse DER bytes into a `PyCsr`.
327    ///
328    /// Mirrors `from_der` but is accessible from sibling Rust modules.
329    pub(crate) fn new_from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
330        Self::from_der(py, data)
331    }
332}
333
334// ── PyCrl ─────────────────────────────────────────────────────────────────────
335
336/// X.509 Certificate Revocation List accessible from Python.
337///
338/// ```python
339/// crl = CertificateList.from_der(open("crl.der", "rb").read())
340/// print(crl.issuer)
341/// print(crl.revoked_count)
342/// ```
343#[pyclass(frozen, name = "CertificateList")]
344pub struct PyCrl {
345    pub(super) _data: Py<PyBytes>,
346    pub(super) raw: &'static [u8],
347    inner: OnceLock<Box<synta_certificate::crl::CertificateList<'static>>>,
348    issuer_cache: OnceLock<Py<PyString>>,
349    issuer_raw_der_cache: OnceLock<Py<PyBytes>>,
350    this_update_cache: OnceLock<Py<PyString>>,
351    next_update_cache: OnceLock<Option<Py<PyString>>>,
352    signature_algorithm_cache: OnceLock<Py<PyString>>,
353    signature_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
354    signature_value_cache: OnceLock<Py<PyBytes>>,
355}
356
357impl PyCrl {
358    fn crl(&self) -> PyResult<&synta_certificate::crl::CertificateList<'static>> {
359        if let Some(v) = self.inner.get() {
360            return Ok(v.as_ref());
361        }
362        let mut decoder = Decoder::new(self.raw, Encoding::Der);
363        let decoded = decoder.decode().map_err(|e| {
364            pyo3::exceptions::PyValueError::new_err(format!(
365                "CertificateList DER decode failed: {e}"
366            ))
367        })?;
368        let _ = self.inner.set(Box::new(decoded));
369        Ok(self.inner.get().unwrap().as_ref())
370    }
371}
372
373#[pymethods]
374impl PyCrl {
375    /// Parse a DER-encoded X.509 Certificate Revocation List.
376    #[staticmethod]
377    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
378        let py_bytes = data.unbind();
379        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
380        // keeps the Python bytes object alive for the lifetime of this struct.
381        // CPython's bytes objects have a fixed-address, non-relocating payload
382        // buffer (CPython has no moving GC).  The slice lifetime is extended
383        // to 'static; the actual safety invariants are:
384        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
385        //       can outlive the struct, so `raw` is never read after drop begins.
386        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
387        //       allocation), so field drop order does not cause use-after-free.
388        //   (3) `inner` contains only borrow-typed fields; dropping
389        //       Box<CertificateList<'static>> does not read through the
390        //       contained &'static slices (borrows have no destructors).
391        // CPython-only: does not hold for PyPy or GraalPy.
392        let raw: &'static [u8] = unsafe {
393            let s = py_bytes.bind(py).as_bytes();
394            std::slice::from_raw_parts(s.as_ptr(), s.len())
395        };
396        {
397            let mut d = Decoder::new(raw, Encoding::Der);
398            d.read_tag()
399                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
400            d.read_length()
401                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
402        }
403        Ok(Self {
404            _data: py_bytes,
405            raw,
406            inner: OnceLock::new(),
407            issuer_cache: OnceLock::new(),
408            issuer_raw_der_cache: OnceLock::new(),
409            this_update_cache: OnceLock::new(),
410            next_update_cache: OnceLock::new(),
411            signature_algorithm_cache: OnceLock::new(),
412            signature_algorithm_oid_cache: OnceLock::new(),
413            signature_value_cache: OnceLock::new(),
414        })
415    }
416
417    /// Parse a PEM-encoded X.509 Certificate Revocation List.
418    ///
419    /// Returns a single :class:`CertificateList` for one PEM block, or a
420    /// :class:`list` of :class:`CertificateList` objects when multiple blocks
421    /// are present.  Raises :exc:`ValueError` if no PEM block is found.
422    ///
423    /// ```python
424    /// crl = CertificateList.from_pem(open("crl.pem", "rb").read())
425    /// print(crl.issuer)
426    /// ```
427    #[staticmethod]
428    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
429        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
430            let obj = Self::from_der(py, bytes)?;
431            Ok(Py::new(py, obj)?.into_bound(py).into_any())
432        })
433    }
434
435    /// Serialize one CRL or a list of CRLs to PEM format.
436    ///
437    /// ```python
438    /// pem = CertificateList.to_pem(crl)
439    /// pem = CertificateList.to_pem([crl1, crl2])
440    /// ```
441    #[staticmethod]
442    fn to_pem<'py>(
443        py: Python<'py>,
444        obj_or_list: Bound<'_, PyAny>,
445    ) -> PyResult<Bound<'py, PyBytes>> {
446        pyobject_to_pem::<Self, _>(py, "X509 CRL", &obj_or_list, |c| c.raw)
447    }
448
449    /// Issuer distinguished name as a string.
450    #[getter]
451    fn issuer<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
452        if let Some(cached) = self.issuer_cache.get() {
453            return Ok(cached.clone_ref(py).into_bound(py));
454        }
455        let s = name_to_dn_string(&self.crl()?.tbs_cert_list.issuer);
456        let py_str = PyString::new(py, &s).unbind();
457        let _ = self.issuer_cache.set(py_str.clone_ref(py));
458        Ok(py_str.into_bound(py))
459    }
460
461    /// Raw DER bytes of the issuer Name SEQUENCE.
462    #[getter]
463    fn issuer_raw_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
464        if let Some(cached) = self.issuer_raw_der_cache.get() {
465            return Ok(cached.clone_ref(py).into_bound(py));
466        }
467        let bytes = name_to_der_bytes(&self.crl()?.tbs_cert_list.issuer);
468        let py_bytes = PyBytes::new(py, &bytes).unbind();
469        let _ = self.issuer_raw_der_cache.set(py_bytes.clone_ref(py));
470        Ok(py_bytes.into_bound(py))
471    }
472
473    /// thisUpdate time as a string.
474    #[getter]
475    fn this_update<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
476        if let Some(cached) = self.this_update_cache.get() {
477            return Ok(cached.clone_ref(py).into_bound(py));
478        }
479        let s = match &self.crl()?.tbs_cert_list.this_update {
480            Time::UtcTime(t) => t.to_string(),
481            Time::GeneralTime(t) => t.to_string(),
482        };
483        let py_str = PyString::new(py, &s).unbind();
484        let _ = self.this_update_cache.set(py_str.clone_ref(py));
485        Ok(py_str.into_bound(py))
486    }
487
488    /// nextUpdate time as a string, or ``None`` if absent.
489    #[getter]
490    fn next_update<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyString>>> {
491        if let Some(cached) = self.next_update_cache.get() {
492            return Ok(cached.as_ref().map(|s| s.clone_ref(py).into_bound(py)));
493        }
494        let computed: Option<Bound<'py, PyString>> =
495            self.crl()?.tbs_cert_list.next_update.as_ref().map(|t| {
496                let s = match t {
497                    Time::UtcTime(t) => t.to_string(),
498                    Time::GeneralTime(t) => t.to_string(),
499                };
500                PyString::new(py, &s)
501            });
502        let to_store = computed.as_ref().map(|s| s.as_unbound().clone_ref(py));
503        let _ = self.next_update_cache.set(to_store);
504        Ok(computed)
505    }
506
507    /// Signature algorithm name (e.g. "RSA", "ECDSA").
508    #[getter]
509    fn signature_algorithm<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
510        if let Some(cached) = self.signature_algorithm_cache.get() {
511            return Ok(cached.clone_ref(py).into_bound(py));
512        }
513        let oid = &self.crl()?.signature_algorithm.algorithm;
514        let name = synta_certificate::identify_signature_algorithm(oid);
515        let s = if name != "Other" {
516            name.to_string()
517        } else {
518            oid.to_string()
519        };
520        let py_str = PyString::new(py, &s).unbind();
521        let _ = self.signature_algorithm_cache.set(py_str.clone_ref(py));
522        Ok(py_str.into_bound(py))
523    }
524
525    /// OID of the signature algorithm.
526    #[getter]
527    fn signature_algorithm_oid<'py>(
528        &self,
529        py: Python<'py>,
530    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
531        if let Some(cached) = self.signature_algorithm_oid_cache.get() {
532            return Ok(cached.clone_ref(py).into_bound(py));
533        }
534        let obj = Py::new(
535            py,
536            PyObjectIdentifier::from_oid(self.crl()?.signature_algorithm.algorithm.clone()),
537        )?;
538        let _ = self.signature_algorithm_oid_cache.set(obj.clone_ref(py));
539        Ok(obj.into_bound(py))
540    }
541
542    /// Raw signature bytes.
543    #[getter]
544    fn signature_value<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
545        if let Some(cached) = self.signature_value_cache.get() {
546            return Ok(cached.clone_ref(py).into_bound(py));
547        }
548        let py_bytes = PyBytes::new(py, self.crl()?.signature_value.as_bytes()).unbind();
549        let _ = self.signature_value_cache.set(py_bytes.clone_ref(py));
550        Ok(py_bytes.into_bound(py))
551    }
552
553    /// Number of revoked certificates in this CRL (0 if no revoked entries).
554    #[getter]
555    fn revoked_count(&self) -> PyResult<usize> {
556        Ok(self
557            .crl()?
558            .tbs_cert_list
559            .revoked_certificates
560            .as_ref()
561            .map(|v| v.len())
562            .unwrap_or(0))
563    }
564
565    /// Complete DER encoding of this CRL (the original bytes passed to ``from_der``).
566    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
567        self._data.clone_ref(py).into_bound(py)
568    }
569
570    fn __repr__(&self) -> PyResult<String> {
571        let crl = self.crl()?;
572        Ok(format!(
573            "CertificateList(issuer={:?}, revoked_count={})",
574            name_to_dn_string(&crl.tbs_cert_list.issuer),
575            crl.tbs_cert_list
576                .revoked_certificates
577                .as_ref()
578                .map(|v| v.len())
579                .unwrap_or(0),
580        ))
581    }
582}
583
584// ── PyOcspResponse ────────────────────────────────────────────────────────────
585
586/// OCSP Response (RFC 6960) accessible from Python.
587///
588/// ```python
589/// resp = OCSPResponse.from_der(open("resp.der", "rb").read())
590/// print(resp.status)
591/// if resp.response_bytes:
592///     basic = resp.response_bytes  # DER of BasicOCSPResponse
593/// ```
594#[pyclass(frozen, name = "OCSPResponse")]
595pub struct PyOcspResponse {
596    pub(super) _data: Py<PyBytes>,
597    pub(super) raw: &'static [u8],
598    inner: OnceLock<Box<synta_certificate::ocsp::OCSPResponse<'static>>>,
599    status_cache: OnceLock<Py<PyString>>,
600    response_type_oid_cache: OnceLock<Option<Py<PyObjectIdentifier>>>,
601    response_bytes_cache: OnceLock<Option<Py<PyBytes>>>,
602}
603
604impl PyOcspResponse {
605    fn ocsp(&self) -> PyResult<&synta_certificate::ocsp::OCSPResponse<'static>> {
606        if let Some(v) = self.inner.get() {
607            return Ok(v.as_ref());
608        }
609        let mut decoder = Decoder::new(self.raw, Encoding::Der);
610        let decoded = decoder.decode().map_err(|e| {
611            pyo3::exceptions::PyValueError::new_err(format!("OCSPResponse DER decode failed: {e}"))
612        })?;
613        let _ = self.inner.set(Box::new(decoded));
614        Ok(self.inner.get().unwrap().as_ref())
615    }
616}
617
618#[pymethods]
619impl PyOcspResponse {
620    /// Parse a DER-encoded OCSP Response.
621    #[staticmethod]
622    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
623        let py_bytes = data.unbind();
624        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
625        // keeps the Python bytes object alive for the lifetime of this struct.
626        // CPython's bytes objects have a fixed-address, non-relocating payload
627        // buffer (CPython has no moving GC).  The slice lifetime is extended
628        // to 'static; the actual safety invariants are:
629        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
630        //       can outlive the struct, so `raw` is never read after drop begins.
631        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
632        //       allocation), so field drop order does not cause use-after-free.
633        //   (3) `inner` contains only borrow-typed fields; dropping
634        //       Box<OCSPResponse<'static>> does not read through the contained
635        //       &'static slices (borrows have no destructors).
636        // CPython-only: does not hold for PyPy or GraalPy.
637        let raw: &'static [u8] = unsafe {
638            let s = py_bytes.bind(py).as_bytes();
639            std::slice::from_raw_parts(s.as_ptr(), s.len())
640        };
641        {
642            let mut d = Decoder::new(raw, Encoding::Der);
643            d.read_tag()
644                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
645            d.read_length()
646                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
647        }
648        Ok(Self {
649            _data: py_bytes,
650            raw,
651            inner: OnceLock::new(),
652            status_cache: OnceLock::new(),
653            response_type_oid_cache: OnceLock::new(),
654            response_bytes_cache: OnceLock::new(),
655        })
656    }
657
658    /// Parse a PEM-encoded OCSP Response.
659    ///
660    /// Returns a single :class:`OCSPResponse` for one PEM block, or a
661    /// :class:`list` of :class:`OCSPResponse` objects when multiple blocks are
662    /// present.  Raises :exc:`ValueError` if no PEM block is found.
663    ///
664    /// ```python
665    /// resp = OCSPResponse.from_pem(open("ocsp.pem", "rb").read())
666    /// print(resp.status)
667    /// ```
668    #[staticmethod]
669    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
670        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
671            let obj = Self::from_der(py, bytes)?;
672            Ok(Py::new(py, obj)?.into_bound(py).into_any())
673        })
674    }
675
676    /// Serialize one OCSP response or a list of them to PEM format.
677    ///
678    /// ```python
679    /// pem = OCSPResponse.to_pem(resp)
680    /// pem = OCSPResponse.to_pem([resp1, resp2])
681    /// ```
682    #[staticmethod]
683    fn to_pem<'py>(
684        py: Python<'py>,
685        obj_or_list: Bound<'_, PyAny>,
686    ) -> PyResult<Bound<'py, PyBytes>> {
687        pyobject_to_pem::<Self, _>(py, "OCSP RESPONSE", &obj_or_list, |c| c.raw)
688    }
689
690    /// Response status string (e.g. ``"successful"``, ``"tryLater"``).
691    #[getter]
692    fn status<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
693        if let Some(cached) = self.status_cache.get() {
694            return Ok(cached.clone_ref(py).into_bound(py));
695        }
696        let s = ocsp_status_str(self.ocsp()?.response_status);
697        let py_str = PyString::new(py, s).unbind();
698        let _ = self.status_cache.set(py_str.clone_ref(py));
699        Ok(py_str.into_bound(py))
700    }
701
702    /// Dotted-decimal OID of the response type, or ``None`` for error responses
703    /// that carry no ``responseBytes`` (e.g. ``tryLater``).
704    ///
705    /// For successful responses this is typically the id-pkix-ocsp-basic OID
706    /// (``"1.3.6.1.5.5.7.48.1.1"``).
707    #[getter]
708    fn response_type_oid<'py>(
709        &self,
710        py: Python<'py>,
711    ) -> PyResult<Option<Bound<'py, PyObjectIdentifier>>> {
712        if let Some(cached) = self.response_type_oid_cache.get() {
713            return Ok(cached.as_ref().map(|o| o.clone_ref(py).into_bound(py)));
714        }
715        let computed: Option<Py<PyObjectIdentifier>> = self
716            .ocsp()?
717            .response_bytes
718            .as_ref()
719            .map(|rb| Py::new(py, PyObjectIdentifier::from_oid(rb.response_type.clone())))
720            .transpose()?;
721        let _ = self
722            .response_type_oid_cache
723            .set(computed.as_ref().map(|o| o.clone_ref(py)));
724        Ok(computed.map(|o| o.into_bound(py)))
725    }
726
727    /// Raw DER bytes of the embedded response (the OCTET STRING content of
728    /// ``ResponseBytes``), or ``None`` for error responses.
729    ///
730    /// For id-pkix-ocsp-basic responses, these bytes are a DER-encoded
731    /// ``BasicOCSPResponse``.
732    #[getter]
733    fn response_bytes<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
734        if let Some(cached) = self.response_bytes_cache.get() {
735            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
736        }
737        let computed: Option<Bound<'py, PyBytes>> = self
738            .ocsp()?
739            .response_bytes
740            .as_ref()
741            .map(|rb| PyBytes::new(py, rb.response.as_bytes()));
742        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
743        let _ = self.response_bytes_cache.set(to_store);
744        Ok(computed)
745    }
746
747    /// Complete DER encoding of this OCSP response (the original bytes passed to ``from_der``).
748    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
749        self._data.clone_ref(py).into_bound(py)
750    }
751
752    fn __repr__(&self) -> PyResult<String> {
753        Ok(format!(
754            "OCSPResponse(status={})",
755            ocsp_status_str(self.ocsp()?.response_status),
756        ))
757    }
758}
759
760// ── PKCS#7 / PKCS#12 free functions ─────────────────────────────────────────
761
762/// Build a `PyList[Certificate]` from a `Vec<Vec<u8>>` of DER-encoded certs.
763fn der_certs_to_pylist<'py>(
764    py: Python<'py>,
765    der_certs: Vec<Vec<u8>>,
766) -> PyResult<Bound<'py, PyList>> {
767    let list = PyList::empty(py);
768    for der in der_certs {
769        let py_bytes = PyBytes::new(py, &der);
770        let cert = PyCertificate::new_from_der(py, py_bytes)?;
771        list.append(Py::new(py, cert)?.into_bound(py))?;
772    }
773    Ok(list)
774}
775
776/// Extract X.509 certificates from a PKCS#7 SignedData blob (DER or BER).
777///
778/// Accepts raw DER or BER input; BER indefinite-length encodings (common in
779/// PKCS#7 files produced by older tools) are handled transparently.  Returns
780/// a list of :class:`Certificate` objects in document order.
781///
782/// Raises :exc:`ValueError` on any ASN.1 structural error or if the input
783/// is not id-signedData.
784///
785/// ```python,ignore
786/// data = open("bundle.p7b", "rb").read()
787/// certs = synta.load_der_pkcs7_certificates(data)
788/// for cert in certs:
789///     print(cert.subject)
790/// ```
791#[pyfunction]
792pub fn load_der_pkcs7_certificates<'py>(
793    py: Python<'py>,
794    data: &[u8],
795) -> PyResult<Bound<'py, PyList>> {
796    let der_certs = synta_certificate::certs_from_pkcs7(data)
797        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
798    der_certs_to_pylist(py, der_certs)
799}
800
801/// Extract X.509 certificates from a PEM-encoded PKCS#7 SignedData blob.
802///
803/// Decodes PEM block(s) in ``data`` (any label is accepted: ``PKCS7``,
804/// ``CMS``, ``CERTIFICATE``, etc.) then calls
805/// :func:`load_der_pkcs7_certificates` on each DER payload.  All certificates
806/// across all blocks are returned as a single flat list.
807///
808/// Raises :exc:`ValueError` if no PEM block is found or if any block fails
809/// to parse as PKCS#7 SignedData.
810///
811/// ```python,ignore
812/// data = open("bundle.pem", "rb").read()
813/// certs = synta.load_pem_pkcs7_certificates(data)
814/// for cert in certs:
815///     print(cert.subject)
816/// ```
817#[pyfunction]
818pub fn load_pem_pkcs7_certificates<'py>(
819    py: Python<'py>,
820    data: &[u8],
821) -> PyResult<Bound<'py, PyList>> {
822    let blocks = synta_certificate::pem_blocks(data);
823    if blocks.is_empty() {
824        return Err(pyo3::exceptions::PyValueError::new_err(
825            "no PEM block found in input",
826        ));
827    }
828    let mut all_certs: Vec<Vec<u8>> = Vec::new();
829    for (_, der) in &blocks {
830        let certs = synta_certificate::certs_from_pkcs7(der)
831            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
832        all_certs.extend(certs);
833    }
834    der_certs_to_pylist(py, all_certs)
835}
836
837/// Extract X.509 certificates from a PKCS#12 archive.
838///
839/// Accepts raw DER or BER input; the encoding is detected automatically.
840/// Returns a list of :class:`Certificate` objects in document order.
841/// Non-certificate bag types (``keyBag``, ``pkcs8ShroudedKeyBag``,
842/// ``crlBag``, ``secretBag``) are silently skipped.
843///
844/// ``password`` is the archive password as raw bytes (UTF-8 without a NUL
845/// terminator).  Pass ``b""`` or omit the argument for password-less archives.
846///
847/// Encrypted bags are supported when the ``openssl`` Cargo feature is enabled.
848/// Without that feature a :exc:`ValueError` is raised on any encrypted bag.
849///
850/// ```python,ignore
851/// data = open("archive.p12", "rb").read()
852/// certs = synta.load_pkcs12_certificates(data, b"s3cr3t")
853/// for cert in certs:
854///     print(cert.subject)
855/// ```
856#[pyfunction]
857#[pyo3(signature = (data, password = None))]
858pub fn load_pkcs12_certificates<'py>(
859    py: Python<'py>,
860    data: &[u8],
861    password: Option<&[u8]>,
862) -> PyResult<Bound<'py, PyList>> {
863    let pw = password.unwrap_or(b"");
864    #[cfg(feature = "openssl")]
865    let der_certs =
866        synta_certificate::certs_from_pkcs12(data, pw, &synta_certificate::OpensslDecryptor)
867            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
868    #[cfg(not(feature = "openssl"))]
869    let der_certs = synta_certificate::certs_from_pkcs12(data, pw, &synta_certificate::NoCrypto)
870        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
871    der_certs_to_pylist(py, der_certs)
872}
873
874/// Extract PKCS#8 private keys from a PKCS#12 archive.
875///
876/// Returns a :class:`list` of :class:`bytes` objects, one per private key
877/// found in ``keyBag`` (unencrypted) and ``pkcs8ShroudedKeyBag`` (encrypted,
878/// decrypted when the ``openssl`` Cargo feature is enabled and ``password``
879/// is given) entries.  Each element is a DER-encoded ``OneAsymmetricKey``
880/// (PKCS#8) structure, suitable for passing to a cryptography library.
881///
882/// ``certBag``, ``crlBag``, ``secretBag`` and any unknown bag types are
883/// silently skipped.
884///
885/// ```python,ignore
886/// data = open("archive.p12", "rb").read()
887/// keys = synta.load_pkcs12_keys(data, b"s3cr3t")
888/// for key_der in keys:
889///     from cryptography.hazmat.primitives.serialization import load_der_private_key
890///     key = load_der_private_key(key_der, None)
891/// ```
892#[pyfunction]
893#[pyo3(signature = (data, password = None))]
894pub fn load_pkcs12_keys<'py>(
895    py: Python<'py>,
896    data: &[u8],
897    password: Option<&[u8]>,
898) -> PyResult<Bound<'py, PyList>> {
899    let pw = password.unwrap_or(b"");
900    #[cfg(feature = "openssl")]
901    let der_keys =
902        synta_certificate::keys_from_pkcs12(data, pw, &synta_certificate::OpensslDecryptor)
903            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
904    #[cfg(not(feature = "openssl"))]
905    let der_keys = synta_certificate::keys_from_pkcs12(data, pw, &synta_certificate::NoCrypto)
906        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
907    let list = PyList::empty(py);
908    for der in der_keys {
909        list.append(PyBytes::new(py, &der))?;
910    }
911    Ok(list)
912}
913
914/// Extract both certificates and private keys from a PKCS#12 archive.
915///
916/// Returns a 2-tuple ``(certs, keys)`` where:
917///
918/// - ``certs`` is a :class:`list` of :class:`Certificate` objects.
919/// - ``keys`` is a :class:`list` of :class:`bytes`, each a DER-encoded
920///   PKCS#8 ``OneAsymmetricKey`` structure.
921///
922/// Encrypted bags are decrypted with ``password`` when the ``openssl``
923/// Cargo feature is enabled; without it, encrypted bags are silently
924/// skipped and a :exc:`ValueError` is raised only on structural errors.
925/// Pass ``b""`` or omit ``password`` for password-less archives.
926///
927/// Use :func:`load_pkcs12_certificates` if you only need certificates, or
928/// :func:`load_pkcs12_keys` if you only need keys.
929///
930/// ```python,ignore
931/// data = open("archive.p12", "rb").read()
932/// certs, keys = synta.load_pkcs12(data, b"s3cr3t")
933/// for cert in certs:
934///     print(cert.subject)
935/// for key_der in keys:
936///     from cryptography.hazmat.primitives.serialization import load_der_private_key
937///     key = load_der_private_key(key_der, None)
938/// ```
939#[pyfunction]
940#[pyo3(signature = (data, password = None))]
941pub fn load_pkcs12<'py>(
942    py: Python<'py>,
943    data: &[u8],
944    password: Option<&[u8]>,
945) -> PyResult<Bound<'py, pyo3::types::PyTuple>> {
946    let pw = password.unwrap_or(b"");
947    #[cfg(feature = "openssl")]
948    let pki = synta_certificate::pki_from_pkcs12(data, pw, &synta_certificate::OpensslDecryptor)
949        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
950    #[cfg(not(feature = "openssl"))]
951    let pki = synta_certificate::pki_from_pkcs12(data, pw, &synta_certificate::NoCrypto)
952        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
953    let certs = der_certs_to_pylist(py, pki.certs)?;
954    let keys = PyList::empty(py);
955    for der in pki.keys {
956        keys.append(PyBytes::new(py, &der))?;
957    }
958    pyo3::types::PyTuple::new(py, [certs.into_any(), keys.into_any()])
959}
960
961/// Auto-detect the encoding of ``data`` and return every PKI object as a
962/// ``(label, der_bytes)`` tuple, in document order.
963///
964/// Supported formats: PEM (any label), PKCS#7 / CMS SignedData (DER or BER),
965/// PKCS#12 PFX (DER or BER), and raw DER.
966///
967/// **Labels:**
968///
969/// - PEM blocks whose payload is a PKCS#7 ``ContentInfo`` (detected by
970///   structure, not label) are expanded: the embedded certificates are
971///   extracted and each is returned as ``("CERTIFICATE", der)``.
972/// - All other PEM block types pass through with their original label
973///   (e.g. ``"CERTIFICATE"``, ``"PRIVATE KEY"``).  Malformed PEM blocks
974///   (bad base64, truncated header) are silently skipped.
975/// - Binary PKCS#7 and raw DER yield ``"CERTIFICATE"`` labels.
976/// - Binary PKCS#12 yields ``"CERTIFICATE"`` for ``certBag`` entries and
977///   ``"PRIVATE KEY"`` for ``keyBag`` / ``pkcs8ShroudedKeyBag`` entries.
978///
979/// **Decryption:**
980///
981/// ``password`` applies only to PKCS#12 archives.  When ``password`` is
982/// given **and** the library was built with the ``openssl`` Cargo feature,
983/// encrypted SafeBags are decrypted.  Otherwise encrypted bags are silently
984/// skipped; unencrypted certificates in the same archive are still returned.
985///
986/// :raises ValueError: For any structural failure:
987///
988///   - A PKCS#7 block (binary or PEM-wrapped) has malformed DER, or its
989///     ``contentType`` OID is not ``id-signedData``.
990///   - The PKCS#12 input has a structural ASN.1 error.
991///   - ``password`` was supplied with the ``openssl`` feature enabled and
992///     decryption of an encrypted SafeBag failed (e.g. wrong password,
993///     unsupported cipher).
994///   - The PKCS#12 authSafe uses an unsupported ``ContentInfo`` type.
995///
996/// :raises TypeError: If ``data`` is not :class:`bytes`, or ``password``
997///   is not :class:`bytes` or ``None``.
998///
999/// ```python,ignore
1000/// data = open("bundle.p7b", "rb").read()
1001/// blocks = synta.read_pki_blocks(data)
1002/// for label, der in blocks:
1003///     print(f"{label}: {len(der)} bytes")
1004///
1005/// # PKCS#12 with encryption
1006/// data = open("archive.p12", "rb").read()
1007/// blocks = synta.read_pki_blocks(data, b"s3cr3t")
1008/// ```
1009#[pyfunction]
1010#[pyo3(signature = (data, password = None))]
1011pub fn read_pki_blocks<'py>(
1012    py: Python<'py>,
1013    data: &[u8],
1014    password: Option<&[u8]>,
1015) -> PyResult<Bound<'py, PyList>> {
1016    let pw = password.unwrap_or(b"");
1017    #[cfg(feature = "openssl")]
1018    let blocks = if password.is_some() {
1019        synta_certificate::read_pki_blocks(
1020            data,
1021            pw,
1022            Some(&synta_certificate::OpensslDecryptor as &dyn synta_certificate::PkiDecryptor),
1023        )
1024    } else {
1025        synta_certificate::read_pki_blocks(data, pw, None::<&dyn synta_certificate::PkiDecryptor>)
1026    };
1027    #[cfg(not(feature = "openssl"))]
1028    let blocks =
1029        synta_certificate::read_pki_blocks(data, pw, None::<&dyn synta_certificate::PkiDecryptor>);
1030    let blocks = blocks.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1031    let list = PyList::empty(py);
1032    for (label, der) in blocks {
1033        let tuple = pyo3::types::PyTuple::new(
1034            py,
1035            [
1036                PyString::new(py, &label).into_any(),
1037                PyBytes::new(py, &der).into_any(),
1038            ],
1039        )?;
1040        list.append(tuple)?;
1041    }
1042    Ok(list)
1043}
1044
1045// ── X.509 encoding utilities ──────────────────────────────────────────────────
1046
1047/// Encode a list of OIDs as a DER ``SEQUENCE OF OID`` (ExtendedKeyUsage value).
1048///
1049/// ``oids`` must be a :class:`list` of :class:`~synta.ObjectIdentifier` or
1050/// dotted-decimal ``str`` values.  Returns the complete DER bytes of the
1051/// ``SEQUENCE``, suitable for use as an X.509v3 Extended Key Usage extension
1052/// value.
1053///
1054/// Delegates to synta's ``Vec<ObjectIdentifier>`` encoder which matches the
1055/// generated ``ExtendedKeyUsage = Vec<KeyPurposeId>`` type from the X.509 schema.
1056///
1057/// ```python,ignore
1058/// eku_der = synta.encode_extended_key_usage([
1059///     "1.3.6.1.5.5.7.3.1",   # id-kp-serverAuth
1060///     "1.3.6.1.5.5.7.3.2",   # id-kp-clientAuth
1061/// ])
1062/// ```
1063#[pyfunction]
1064pub fn encode_extended_key_usage<'py>(
1065    py: Python<'py>,
1066    oids: &Bound<'_, PyList>,
1067) -> PyResult<Bound<'py, PyBytes>> {
1068    use synta::traits::Encode;
1069    let mut eku: Vec<synta::ObjectIdentifier> = Vec::with_capacity(oids.len());
1070    for item in oids.iter() {
1071        eku.push(super::oid_from_pyany(&item)?);
1072    }
1073    let mut enc = synta::Encoder::new(synta::Encoding::Der);
1074    eku.encode(&mut enc)
1075        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1076    let der = enc
1077        .finish()
1078        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1079    Ok(PyBytes::new(py, &der))
1080}
1081
1082/// Encode a list of ``(tag_number, content_bytes)`` pairs as a DER
1083/// ``SEQUENCE OF GeneralName``.
1084///
1085/// This is the inverse of :func:`~synta.parse_general_names`: the input
1086/// format is identical to what that function returns.  Tag numbers follow
1087/// RFC 5280 §4.2.1.6; use the :mod:`synta.general_name` constants.
1088///
1089/// ``content_bytes`` must be the raw **value** bytes — without the outer
1090/// context-specific TLV — exactly as returned by
1091/// :func:`~synta.parse_general_names`.
1092///
1093/// Delegates to :func:`synta_certificate::encode_general_names` which uses the
1094/// generated :class:`GeneralName` enum's ``Encode`` implementation.
1095///
1096/// ```python,ignore
1097/// import synta.general_name as gn
1098///
1099/// san_der = synta.encode_subject_alt_names([
1100///     (gn.DNS_NAME,   b"example.com"),
1101///     (gn.IP_ADDRESS, b"\x7f\x00\x00\x01"),   # 127.0.0.1
1102/// ])
1103/// ```
1104#[pyfunction]
1105pub fn encode_subject_alt_names<'py>(
1106    py: Python<'py>,
1107    names: &Bound<'_, PyList>,
1108) -> PyResult<Bound<'py, PyBytes>> {
1109    // Collect owned (tag, bytes) first so we can create &[u8] references that
1110    // all share the same lifetime, satisfying Vec<GeneralName<'_>> constraints.
1111    let mut owned: Vec<(u32, Vec<u8>)> = Vec::with_capacity(names.len());
1112    for item in names.iter() {
1113        let tuple = item.cast::<pyo3::types::PyTuple>().map_err(|_| {
1114            pyo3::exceptions::PyTypeError::new_err(
1115                "each element must be a (tag_number, bytes) 2-tuple",
1116            )
1117        })?;
1118        if tuple.len() != 2 {
1119            return Err(pyo3::exceptions::PyTypeError::new_err(
1120                "each element must be a (tag_number, bytes) 2-tuple",
1121            ));
1122        }
1123        let tag_num: u32 = tuple.get_item(0)?.extract()?;
1124        let content: Vec<u8> = tuple.get_item(1)?.extract()?;
1125        owned.push((tag_num, content));
1126    }
1127    let entries: Vec<(u32, &[u8])> = owned.iter().map(|(t, v)| (*t, v.as_slice())).collect();
1128    let der = synta_certificate::encode_general_names(&entries).ok_or_else(|| {
1129        pyo3::exceptions::PyValueError::new_err(
1130            "failed to encode GeneralName entries (invalid content bytes or unsupported tag)",
1131        )
1132    })?;
1133    Ok(PyBytes::new(py, &der))
1134}
1135
1136/// Compare two DER-encoded X.500 Names for byte-for-byte equality.
1137///
1138/// Returns ``True`` if and only if the two byte sequences are identical.
1139/// This is a strict comparison; RFC 5280 name matching rules (case-insensitive
1140/// string comparison, etc.) are **not** applied.
1141///
1142/// Useful for quickly checking whether issuer and subject names match without
1143/// decoding the full structure:
1144///
1145/// ```python,ignore
1146/// same = synta.name_der_equal(cert.issuer_raw_der, ca_cert.subject_raw_der)
1147/// ```
1148#[pyfunction]
1149pub fn name_der_equal(a: &[u8], b: &[u8]) -> bool {
1150    a == b
1151}
1152
1153/// Build a DER-encoded PKCS#12 ``PFX`` archive.
1154///
1155/// Assembles a ``PFX`` from one or more certificates and an optional
1156/// DER-encoded PKCS#8 private key.  The archive is protected by ``password``
1157/// using PBES2 (PBKDF2-SHA256 + AES-256-CBC) for key encryption and
1158/// HMAC-SHA256 (PBKDF2-SHA256 derived key) for the integrity MAC.
1159///
1160/// :param certificates: sequence of :class:`~synta.Certificate` objects or
1161///     raw DER ``bytes``.  Both types may appear in the same list.
1162/// :param private_key: a :class:`~synta.PrivateKey` object or a DER-encoded
1163///     PKCS#8 ``OneAsymmetricKey`` ``bytes``, or ``None`` to omit.
1164/// :param password: archive password (``bytes``), or ``None`` for a
1165///     password-less archive.
1166/// :returns: DER-encoded ``PFX`` archive bytes.
1167/// :raises TypeError: if any element of ``certificates`` is neither a
1168///     :class:`~synta.Certificate` nor ``bytes``, or if ``private_key`` is
1169///     not a :class:`~synta.PrivateKey` nor ``bytes``.
1170/// :raises ValueError: if no certificates or key are supplied, or if
1171///     encryption fails (e.g. OpenSSL not available).
1172///
1173/// ```python,ignore
1174/// # Certificate and PrivateKey objects — no .to_der() call needed
1175/// pfx = synta.create_pkcs12(
1176///     [cert, ca_cert],
1177///     private_key=key,
1178///     password=b"s3cr3t",
1179/// )
1180///
1181/// # Raw DER bytes also accepted for both
1182/// pfx = synta.create_pkcs12([cert.to_der()], private_key=key.to_der())
1183///
1184/// with open("out.p12", "wb") as f:
1185///     f.write(pfx)
1186/// ```
1187#[pyfunction]
1188#[pyo3(signature = (certificates, private_key = None, password = None))]
1189pub fn create_pkcs12<'py>(
1190    py: Python<'py>,
1191    certificates: &Bound<'_, PyList>,
1192    private_key: Option<Bound<'_, PyAny>>,
1193    password: Option<&[u8]>,
1194) -> PyResult<Bound<'py, PyBytes>> {
1195    use synta_certificate::Pkcs12Builder;
1196
1197    let password = password.unwrap_or(b"");
1198    let mut builder = Pkcs12Builder::new();
1199    for item in certificates.iter() {
1200        if let Ok(py_cert) = item.cast::<PyCertificate>() {
1201            // Zero-copy: raw is &'static [u8] backed by the PyBytes in _data.
1202            // Pkcs12Builder::certificate() copies into Vec<u8> immediately, so
1203            // the borrow does not outlive this loop iteration.
1204            builder = builder.certificate(py_cert.get().raw);
1205        } else if let Ok(py_bytes) = item.cast::<PyBytes>() {
1206            builder = builder.certificate(py_bytes.as_bytes());
1207        } else {
1208            return Err(pyo3::exceptions::PyTypeError::new_err(
1209                "certificates must be synta.Certificate or bytes",
1210            ));
1211        }
1212    }
1213    if let Some(key_arg) = private_key {
1214        if let Ok(py_key) = key_arg.cast::<PyPrivateKey>() {
1215            // PyPrivateKey stores PKCS#8 DER internally; retrieve it directly.
1216            let key_der = py_key
1217                .get()
1218                .inner
1219                .to_der()
1220                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1221            builder = builder.private_key(&key_der);
1222        } else if let Ok(py_bytes) = key_arg.cast::<PyBytes>() {
1223            builder = builder.private_key(py_bytes.as_bytes());
1224        } else {
1225            return Err(pyo3::exceptions::PyTypeError::new_err(
1226                "private_key must be synta.PrivateKey or bytes",
1227            ));
1228        }
1229    }
1230
1231    #[cfg(feature = "openssl")]
1232    let pfx_der = builder
1233        .build(password, &synta_certificate::OpensslPkcs12Encryptor::new())
1234        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1235
1236    #[cfg(not(feature = "openssl"))]
1237    let pfx_der = {
1238        use synta_certificate::NoPkcs12Encryptor;
1239        builder
1240            .build(password, &NoPkcs12Encryptor)
1241            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?
1242    };
1243
1244    Ok(PyBytes::new(py, &pfx_der))
1245}