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    spki_der_cache: OnceLock<Py<PyBytes>>,
73}
74
75impl PyCsr {
76    fn csr(&self) -> PyResult<&synta_certificate::csr::CertificationRequest<'static>> {
77        if let Some(v) = self.inner.get() {
78            return Ok(v.as_ref());
79        }
80        let mut decoder = Decoder::new(self.raw, Encoding::Der);
81        let decoded = decoder.decode().map_err(|e| {
82            pyo3::exceptions::PyValueError::new_err(format!(
83                "CertificationRequest DER decode failed: {e}"
84            ))
85        })?;
86        let _ = self.inner.set(Box::new(decoded));
87        Ok(self.inner.get().unwrap().as_ref())
88    }
89}
90
91#[pymethods]
92impl PyCsr {
93    /// Parse a DER-encoded PKCS #10 Certificate Signing Request.
94    #[staticmethod]
95    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
96        let py_bytes = data.unbind();
97        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
98        // keeps the Python bytes object alive for the lifetime of this struct.
99        // CPython's bytes objects have a fixed-address, non-relocating payload
100        // buffer (CPython has no moving GC).  The slice lifetime is extended
101        // to 'static; the actual safety invariants are:
102        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
103        //       can outlive the struct, so `raw` is never read after drop begins.
104        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
105        //       allocation), so field drop order does not cause use-after-free.
106        //   (3) `inner` contains only borrow-typed fields; dropping
107        //       Box<CertificationRequest<'static>> does not read through the
108        //       contained &'static slices (borrows have no destructors).
109        // CPython-only: does not hold for PyPy or GraalPy.
110        let raw: &'static [u8] = unsafe {
111            let s = py_bytes.bind(py).as_bytes();
112            std::slice::from_raw_parts(s.as_ptr(), s.len())
113        };
114        // Minimal structural scan: outer SEQUENCE tag + length.
115        {
116            let mut d = Decoder::new(raw, Encoding::Der);
117            d.read_tag()
118                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
119            d.read_length()
120                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
121        }
122        Ok(Self {
123            _data: py_bytes,
124            raw,
125            inner: OnceLock::new(),
126            subject_cache: OnceLock::new(),
127            subject_raw_der_cache: OnceLock::new(),
128            signature_algorithm_cache: OnceLock::new(),
129            signature_algorithm_oid_cache: OnceLock::new(),
130            signature_cache: OnceLock::new(),
131            public_key_algorithm_cache: OnceLock::new(),
132            public_key_algorithm_oid_cache: OnceLock::new(),
133            public_key_cache: OnceLock::new(),
134            spki_der_cache: OnceLock::new(),
135        })
136    }
137
138    /// Parse a PEM-encoded PKCS #10 Certificate Signing Request.
139    ///
140    /// Returns a single :class:`CertificationRequest` for one PEM block, or a
141    /// :class:`list` of :class:`CertificationRequest` objects when multiple
142    /// blocks are present.  Raises :exc:`ValueError` if no PEM block is found.
143    ///
144    /// ```python
145    /// csr = CertificationRequest.from_pem(open("csr.pem", "rb").read())
146    /// print(csr.subject)
147    /// ```
148    #[staticmethod]
149    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
150        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
151            let obj = Self::from_der(py, bytes)?;
152            Ok(Py::new(py, obj)?.into_bound(py).into_any())
153        })
154    }
155
156    /// Serialize one CSR or a list of CSRs to PEM format.
157    ///
158    /// ```python
159    /// pem = CertificationRequest.to_pem(csr)
160    /// pem = CertificationRequest.to_pem([csr1, csr2])
161    /// ```
162    #[staticmethod]
163    fn to_pem<'py>(
164        py: Python<'py>,
165        obj_or_list: Bound<'_, PyAny>,
166    ) -> PyResult<Bound<'py, PyBytes>> {
167        pyobject_to_pem::<Self, _>(py, "CERTIFICATE REQUEST", &obj_or_list, |c| c.raw)
168    }
169
170    /// Subject distinguished name as a string.
171    #[getter]
172    fn subject<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
173        if let Some(cached) = self.subject_cache.get() {
174            return Ok(cached.clone_ref(py).into_bound(py));
175        }
176        let s = name_to_dn_string(&self.csr()?.certification_request_info.subject);
177        let py_str = PyString::new(py, &s).unbind();
178        let _ = self.subject_cache.set(py_str.clone_ref(py));
179        Ok(py_str.into_bound(py))
180    }
181
182    /// Raw DER bytes of the subject Name SEQUENCE.
183    #[getter]
184    fn subject_raw_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
185        if let Some(cached) = self.subject_raw_der_cache.get() {
186            return Ok(cached.clone_ref(py).into_bound(py));
187        }
188        let bytes = name_to_der_bytes(&self.csr()?.certification_request_info.subject);
189        let py_bytes = PyBytes::new(py, &bytes).unbind();
190        let _ = self.subject_raw_der_cache.set(py_bytes.clone_ref(py));
191        Ok(py_bytes.into_bound(py))
192    }
193
194    /// CSR version (0 = v1 per RFC 2986).
195    #[getter]
196    fn version(&self) -> PyResult<i64> {
197        Ok(self
198            .csr()?
199            .certification_request_info
200            .version
201            .as_i64()
202            .unwrap_or(0))
203    }
204
205    /// Signature algorithm name (e.g. "RSA", "ECDSA", "Ed25519").
206    #[getter]
207    fn signature_algorithm<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
208        if let Some(cached) = self.signature_algorithm_cache.get() {
209            return Ok(cached.clone_ref(py).into_bound(py));
210        }
211        let oid = &self.csr()?.signature_algorithm.algorithm;
212        let name = synta_certificate::identify_signature_algorithm(oid);
213        let s = if name != "Other" {
214            name.to_string()
215        } else {
216            oid.to_string()
217        };
218        let py_str = PyString::new(py, &s).unbind();
219        let _ = self.signature_algorithm_cache.set(py_str.clone_ref(py));
220        Ok(py_str.into_bound(py))
221    }
222
223    /// OID of the signature algorithm.
224    #[getter]
225    fn signature_algorithm_oid<'py>(
226        &self,
227        py: Python<'py>,
228    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
229        if let Some(cached) = self.signature_algorithm_oid_cache.get() {
230            return Ok(cached.clone_ref(py).into_bound(py));
231        }
232        let obj = Py::new(
233            py,
234            PyObjectIdentifier::from_oid(self.csr()?.signature_algorithm.algorithm.clone()),
235        )?;
236        let _ = self.signature_algorithm_oid_cache.set(obj.clone_ref(py));
237        Ok(obj.into_bound(py))
238    }
239
240    /// Raw signature bytes.
241    #[getter]
242    fn signature<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
243        if let Some(cached) = self.signature_cache.get() {
244            return Ok(cached.clone_ref(py).into_bound(py));
245        }
246        let py_bytes = PyBytes::new(py, self.csr()?.signature.as_bytes()).unbind();
247        let _ = self.signature_cache.set(py_bytes.clone_ref(py));
248        Ok(py_bytes.into_bound(py))
249    }
250
251    /// Public-key algorithm name (e.g. "RSA", "ECDSA", "Ed25519").
252    #[getter]
253    fn public_key_algorithm<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
254        if let Some(cached) = self.public_key_algorithm_cache.get() {
255            return Ok(cached.clone_ref(py).into_bound(py));
256        }
257        let oid = &self
258            .csr()?
259            .certification_request_info
260            .subject_pkinfo
261            .algorithm
262            .algorithm;
263        let s = synta_certificate::identify_public_key_algorithm(oid)
264            .map(|s| s.to_string())
265            .unwrap_or_else(|| oid.to_string());
266        let py_str = PyString::new(py, &s).unbind();
267        let _ = self.public_key_algorithm_cache.set(py_str.clone_ref(py));
268        Ok(py_str.into_bound(py))
269    }
270
271    /// OID of the subject public-key algorithm.
272    #[getter]
273    fn public_key_algorithm_oid<'py>(
274        &self,
275        py: Python<'py>,
276    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
277        if let Some(cached) = self.public_key_algorithm_oid_cache.get() {
278            return Ok(cached.clone_ref(py).into_bound(py));
279        }
280        let obj = Py::new(
281            py,
282            PyObjectIdentifier::from_oid(
283                self.csr()?
284                    .certification_request_info
285                    .subject_pkinfo
286                    .algorithm
287                    .algorithm
288                    .clone(),
289            ),
290        )?;
291        let _ = self.public_key_algorithm_oid_cache.set(obj.clone_ref(py));
292        Ok(obj.into_bound(py))
293    }
294
295    /// Raw subject public-key bytes.
296    #[getter]
297    fn public_key<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
298        if let Some(cached) = self.public_key_cache.get() {
299            return Ok(cached.clone_ref(py).into_bound(py));
300        }
301        let py_bytes = PyBytes::new(
302            py,
303            self.csr()?
304                .certification_request_info
305                .subject_pkinfo
306                .subject_public_key
307                .as_bytes(),
308        )
309        .unbind();
310        let _ = self.public_key_cache.set(py_bytes.clone_ref(py));
311        Ok(py_bytes.into_bound(py))
312    }
313
314    /// DER encoding of the ``SubjectPublicKeyInfo`` SEQUENCE from this CSR.
315    ///
316    /// This is the same encoding that
317    /// ``cryptography``'s ``public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)``
318    /// produces.  Pass it directly to ``CertificateBuilder.public_key_der`` to
319    /// copy the public key from a CSR into a new certificate without
320    /// re-encoding.
321    ///
322    /// ```python,ignore
323    /// csr = synta.CertificationRequest.from_pem(open("csr.pem", "rb").read())
324    /// spki_der = csr.subject_public_key_info_der
325    /// # Pass to a certificate builder:
326    /// builder.public_key_der(spki_der)
327    /// # Or load with cryptography:
328    /// from cryptography.hazmat.primitives.serialization import load_der_public_key
329    /// pub_key = load_der_public_key(spki_der)
330    /// ```
331    #[getter]
332    fn subject_public_key_info_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
333        if let Some(cached) = self.spki_der_cache.get() {
334            return Ok(cached.clone_ref(py).into_bound(py));
335        }
336        let spki = &self.csr()?.certification_request_info.subject_pkinfo;
337        let bytes = spki
338            .to_der()
339            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("SPKI encode: {e}")))?;
340        let py_bytes = PyBytes::new(py, &bytes).unbind();
341        let _ = self.spki_der_cache.set(py_bytes.clone_ref(py));
342        Ok(py_bytes.into_bound(py))
343    }
344
345    /// Return the DER value bytes of the named extension from this CSR's
346    /// ``extensionRequest`` attribute (OID ``1.2.840.113549.1.9.14``), or
347    /// ``None`` if the CSR carries no such extension.
348    ///
349    /// ``oid`` may be a dotted-decimal string or a
350    /// :class:`~synta.ObjectIdentifier`.  The value returned is the content of
351    /// the ``extnValue`` OCTET STRING — the DER encoding of the extension value
352    /// — without the outer OCTET STRING tag and length.  Returns ``None`` when
353    /// the CSR has no ``extensionRequest`` attribute, when the attribute
354    /// contains no extensions, or when the requested OID is absent.
355    ///
356    /// ```python,ignore
357    /// SAN_OID = "2.5.29.17"
358    /// san_der = csr.get_extension_value_der(SAN_OID)
359    /// if san_der:
360    ///     names = synta.parse_general_names(san_der)
361    /// ```
362    fn get_extension_value_der<'py>(
363        &self,
364        py: Python<'py>,
365        oid: &Bound<'_, PyAny>,
366    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
367        let target = super::oid_from_pyany(oid)?;
368        let csr = self.csr()?;
369        let attrs = match csr.certification_request_info.attributes.as_ref() {
370            Some(a) => a,
371            None => return Ok(None),
372        };
373        for attr in attrs.elements() {
374            if attr.attr_type.components() != synta_certificate::oids::PKCS9_EXTENSION_REQUEST {
375                continue;
376            }
377            // attr_values: SetOf<RawDer> — first element is the SEQUENCE OF Extension TLV
378            let Some(raw) = attr.attr_values.elements().first() else {
379                return Ok(None);
380            };
381            return Ok(synta_certificate::find_extension_value(
382                raw.as_bytes(),
383                target.components(),
384            )
385            .map(|v| PyBytes::new(py, v)));
386        }
387        Ok(None)
388    }
389
390    /// Return the Subject Alternative Names from this CSR's ``extensionRequest``
391    /// attribute as a list of ``(tag_number, content_bytes)`` tuples.  Returns an
392    /// empty list when the CSR carries no SAN extension.
393    ///
394    /// Returns raw tuples, not typed ``AnyGeneralName`` objects (unlike
395    /// :meth:`~synta.Certificate.subject_alt_names`).  Use the tag-number
396    /// constants from :mod:`synta.general_name` to interpret each entry.
397    ///
398    /// ```python,ignore
399    /// import synta.general_name as gn
400    /// for tag_num, content in csr.subject_alt_names():
401    ///     if tag_num == gn.DNS_NAME:
402    ///         print("DNS:", content.decode("ascii"))
403    /// ```
404    fn subject_alt_names<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
405        use pyo3::types::{PyBytes, PyList, PyTuple};
406        let list = PyList::empty(py);
407        let csr = self.csr()?;
408        let attrs = match csr.certification_request_info.attributes.as_ref() {
409            Some(a) => a,
410            None => return Ok(list),
411        };
412        for attr in attrs.elements() {
413            if attr.attr_type.components() != synta_certificate::oids::PKCS9_EXTENSION_REQUEST {
414                continue;
415            }
416            let Some(raw) = attr.attr_values.elements().first() else {
417                return Ok(list);
418            };
419            if let Some(san_bytes) = synta_certificate::find_extension_value(
420                raw.as_bytes(),
421                synta_certificate::oids::SUBJECT_ALT_NAME,
422            ) {
423                for (tag_num, content) in synta_certificate::parse_general_names(san_bytes) {
424                    let tuple = PyTuple::new(
425                        py,
426                        [
427                            tag_num.into_pyobject(py)?.into_any(),
428                            PyBytes::new(py, &content).into_any(),
429                        ],
430                    )?;
431                    list.append(tuple)?;
432                }
433            }
434            break;
435        }
436        Ok(list)
437    }
438
439    /// Complete DER encoding of this CSR (the original bytes passed to ``from_der``).
440    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
441        self._data.clone_ref(py).into_bound(py)
442    }
443
444    /// Verify the PKCS\#10 self-signature of this CSR.
445    ///
446    /// Checks that the ``signature`` field was produced over the DER encoding
447    /// of ``CertificationRequestInfo`` by the key in ``subjectPKInfo``.
448    ///
449    /// :raises ValueError: if the signature is invalid or the algorithm is
450    ///     unsupported.
451    ///
452    /// ```python,ignore
453    /// csr = synta.CertificationRequest.from_pem(open("csr.pem", "rb").read())
454    /// csr.verify_self_signature()   # raises ValueError if invalid
455    /// ```
456    fn verify_self_signature(&self) -> PyResult<()> {
457        use synta_certificate::{default_signature_verifier, SignatureVerifier};
458
459        let csr = self.csr()?;
460
461        // Re-encode CertificationRequestInfo (the TBS).
462        let tbs_der = csr.certification_request_info.to_der().map_err(|e| {
463            pyo3::exceptions::PyValueError::new_err(format!("CSR info encode: {e}"))
464        })?;
465
466        // Re-encode signatureAlgorithm.
467        let sig_alg_der = csr
468            .signature_algorithm
469            .to_der()
470            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("CSR alg encode: {e}")))?;
471
472        // Re-encode the subject's own SubjectPublicKeyInfo.
473        let spki_der = csr
474            .certification_request_info
475            .subject_pkinfo
476            .to_der()
477            .map_err(|e| {
478                pyo3::exceptions::PyValueError::new_err(format!("CSR SPKI encode: {e}"))
479            })?;
480
481        // Verify the self-signature using the backend-agnostic verifier.
482        default_signature_verifier()
483            .verify_certificate_signature(
484                &tbs_der,
485                &sig_alg_der,
486                csr.signature.as_bytes(),
487                &spki_der,
488            )
489            .map_err(|e| {
490                pyo3::exceptions::PyValueError::new_err(format!("CSR self-signature invalid: {e}"))
491            })
492    }
493
494    fn __repr__(&self) -> PyResult<String> {
495        Ok(format!(
496            "CertificationRequest(subject={:?})",
497            name_to_dn_string(&self.csr()?.certification_request_info.subject),
498        ))
499    }
500}
501
502impl PyCsr {
503    /// Rust-internal constructor: parse DER bytes into a `PyCsr`.
504    ///
505    /// Mirrors `from_der` but is accessible from sibling Rust modules.
506    pub(crate) fn new_from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
507        Self::from_der(py, data)
508    }
509}
510
511// ── PyCrl ─────────────────────────────────────────────────────────────────────
512
513/// X.509 Certificate Revocation List accessible from Python.
514///
515/// ```python
516/// crl = CertificateList.from_der(open("crl.der", "rb").read())
517/// print(crl.issuer)
518/// print(crl.revoked_count)
519/// ```
520#[pyclass(frozen, name = "CertificateList")]
521pub struct PyCrl {
522    pub(super) _data: Py<PyBytes>,
523    pub(super) raw: &'static [u8],
524    inner: OnceLock<Box<synta_certificate::crl::CertificateList<'static>>>,
525    issuer_cache: OnceLock<Py<PyString>>,
526    issuer_raw_der_cache: OnceLock<Py<PyBytes>>,
527    this_update_cache: OnceLock<Py<PyString>>,
528    next_update_cache: OnceLock<Option<Py<PyString>>>,
529    signature_algorithm_cache: OnceLock<Py<PyString>>,
530    signature_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
531    signature_value_cache: OnceLock<Py<PyBytes>>,
532}
533
534impl PyCrl {
535    fn crl(&self) -> PyResult<&synta_certificate::crl::CertificateList<'static>> {
536        if let Some(v) = self.inner.get() {
537            return Ok(v.as_ref());
538        }
539        let mut decoder = Decoder::new(self.raw, Encoding::Der);
540        let decoded = decoder.decode().map_err(|e| {
541            pyo3::exceptions::PyValueError::new_err(format!(
542                "CertificateList DER decode failed: {e}"
543            ))
544        })?;
545        let _ = self.inner.set(Box::new(decoded));
546        Ok(self.inner.get().unwrap().as_ref())
547    }
548}
549
550#[pymethods]
551impl PyCrl {
552    /// Parse a DER-encoded X.509 Certificate Revocation List.
553    #[staticmethod]
554    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
555        let py_bytes = data.unbind();
556        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
557        // keeps the Python bytes object alive for the lifetime of this struct.
558        // CPython's bytes objects have a fixed-address, non-relocating payload
559        // buffer (CPython has no moving GC).  The slice lifetime is extended
560        // to 'static; the actual safety invariants are:
561        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
562        //       can outlive the struct, so `raw` is never read after drop begins.
563        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
564        //       allocation), so field drop order does not cause use-after-free.
565        //   (3) `inner` contains only borrow-typed fields; dropping
566        //       Box<CertificateList<'static>> does not read through the
567        //       contained &'static slices (borrows have no destructors).
568        // CPython-only: does not hold for PyPy or GraalPy.
569        let raw: &'static [u8] = unsafe {
570            let s = py_bytes.bind(py).as_bytes();
571            std::slice::from_raw_parts(s.as_ptr(), s.len())
572        };
573        {
574            let mut d = Decoder::new(raw, Encoding::Der);
575            d.read_tag()
576                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
577            d.read_length()
578                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
579        }
580        Ok(Self {
581            _data: py_bytes,
582            raw,
583            inner: OnceLock::new(),
584            issuer_cache: OnceLock::new(),
585            issuer_raw_der_cache: OnceLock::new(),
586            this_update_cache: OnceLock::new(),
587            next_update_cache: OnceLock::new(),
588            signature_algorithm_cache: OnceLock::new(),
589            signature_algorithm_oid_cache: OnceLock::new(),
590            signature_value_cache: OnceLock::new(),
591        })
592    }
593
594    /// Parse a PEM-encoded X.509 Certificate Revocation List.
595    ///
596    /// Returns a single :class:`CertificateList` for one PEM block, or a
597    /// :class:`list` of :class:`CertificateList` objects when multiple blocks
598    /// are present.  Raises :exc:`ValueError` if no PEM block is found.
599    ///
600    /// ```python
601    /// crl = CertificateList.from_pem(open("crl.pem", "rb").read())
602    /// print(crl.issuer)
603    /// ```
604    #[staticmethod]
605    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
606        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
607            let obj = Self::from_der(py, bytes)?;
608            Ok(Py::new(py, obj)?.into_bound(py).into_any())
609        })
610    }
611
612    /// Serialize one CRL or a list of CRLs to PEM format.
613    ///
614    /// ```python
615    /// pem = CertificateList.to_pem(crl)
616    /// pem = CertificateList.to_pem([crl1, crl2])
617    /// ```
618    #[staticmethod]
619    fn to_pem<'py>(
620        py: Python<'py>,
621        obj_or_list: Bound<'_, PyAny>,
622    ) -> PyResult<Bound<'py, PyBytes>> {
623        pyobject_to_pem::<Self, _>(py, "X509 CRL", &obj_or_list, |c| c.raw)
624    }
625
626    /// CRL version field (1 = v2), or ``None`` if absent (implies v1).
627    ///
628    /// RFC 5280 §5.1.2.1: the ``version`` field is present only when the CRL
629    /// is version 2.  For RFC 5280-compliant CRLs this value is always ``1``
630    /// (the integer 1 encodes version 2).  Returns ``None`` for v1 CRLs.
631    #[getter]
632    fn version(&self) -> PyResult<Option<i64>> {
633        Ok(self
634            .crl()?
635            .tbs_cert_list
636            .version
637            .as_ref()
638            .and_then(|v| v.as_i64().ok()))
639    }
640
641    /// Issuer distinguished name as a string.
642    #[getter]
643    fn issuer<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
644        if let Some(cached) = self.issuer_cache.get() {
645            return Ok(cached.clone_ref(py).into_bound(py));
646        }
647        let s = name_to_dn_string(&self.crl()?.tbs_cert_list.issuer);
648        let py_str = PyString::new(py, &s).unbind();
649        let _ = self.issuer_cache.set(py_str.clone_ref(py));
650        Ok(py_str.into_bound(py))
651    }
652
653    /// Raw DER bytes of the issuer Name SEQUENCE.
654    #[getter]
655    fn issuer_raw_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
656        if let Some(cached) = self.issuer_raw_der_cache.get() {
657            return Ok(cached.clone_ref(py).into_bound(py));
658        }
659        let bytes = name_to_der_bytes(&self.crl()?.tbs_cert_list.issuer);
660        let py_bytes = PyBytes::new(py, &bytes).unbind();
661        let _ = self.issuer_raw_der_cache.set(py_bytes.clone_ref(py));
662        Ok(py_bytes.into_bound(py))
663    }
664
665    /// thisUpdate time as a string.
666    #[getter]
667    fn this_update<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
668        if let Some(cached) = self.this_update_cache.get() {
669            return Ok(cached.clone_ref(py).into_bound(py));
670        }
671        let s = match &self.crl()?.tbs_cert_list.this_update {
672            Time::UtcTime(t) => t.to_string(),
673            Time::GeneralTime(t) => t.to_string(),
674        };
675        let py_str = PyString::new(py, &s).unbind();
676        let _ = self.this_update_cache.set(py_str.clone_ref(py));
677        Ok(py_str.into_bound(py))
678    }
679
680    /// nextUpdate time as a string, or ``None`` if absent.
681    #[getter]
682    fn next_update<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyString>>> {
683        if let Some(cached) = self.next_update_cache.get() {
684            return Ok(cached.as_ref().map(|s| s.clone_ref(py).into_bound(py)));
685        }
686        let computed: Option<Bound<'py, PyString>> =
687            self.crl()?.tbs_cert_list.next_update.as_ref().map(|t| {
688                let s = match t {
689                    Time::UtcTime(t) => t.to_string(),
690                    Time::GeneralTime(t) => t.to_string(),
691                };
692                PyString::new(py, &s)
693            });
694        let to_store = computed.as_ref().map(|s| s.as_unbound().clone_ref(py));
695        let _ = self.next_update_cache.set(to_store);
696        Ok(computed)
697    }
698
699    /// Signature algorithm name (e.g. "RSA", "ECDSA").
700    #[getter]
701    fn signature_algorithm<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
702        if let Some(cached) = self.signature_algorithm_cache.get() {
703            return Ok(cached.clone_ref(py).into_bound(py));
704        }
705        let oid = &self.crl()?.signature_algorithm.algorithm;
706        let name = synta_certificate::identify_signature_algorithm(oid);
707        let s = if name != "Other" {
708            name.to_string()
709        } else {
710            oid.to_string()
711        };
712        let py_str = PyString::new(py, &s).unbind();
713        let _ = self.signature_algorithm_cache.set(py_str.clone_ref(py));
714        Ok(py_str.into_bound(py))
715    }
716
717    /// OID of the signature algorithm.
718    #[getter]
719    fn signature_algorithm_oid<'py>(
720        &self,
721        py: Python<'py>,
722    ) -> PyResult<Bound<'py, PyObjectIdentifier>> {
723        if let Some(cached) = self.signature_algorithm_oid_cache.get() {
724            return Ok(cached.clone_ref(py).into_bound(py));
725        }
726        let obj = Py::new(
727            py,
728            PyObjectIdentifier::from_oid(self.crl()?.signature_algorithm.algorithm.clone()),
729        )?;
730        let _ = self.signature_algorithm_oid_cache.set(obj.clone_ref(py));
731        Ok(obj.into_bound(py))
732    }
733
734    /// Raw signature bytes.
735    #[getter]
736    fn signature_value<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
737        if let Some(cached) = self.signature_value_cache.get() {
738            return Ok(cached.clone_ref(py).into_bound(py));
739        }
740        let py_bytes = PyBytes::new(py, self.crl()?.signature_value.as_bytes()).unbind();
741        let _ = self.signature_value_cache.set(py_bytes.clone_ref(py));
742        Ok(py_bytes.into_bound(py))
743    }
744
745    /// CRL sequence number from the ``cRLNumber`` extension (OID ``2.5.29.20``),
746    /// or ``None`` if the CRL carries no such extension.
747    ///
748    /// Returns an arbitrary-precision Python :class:`int`.
749    ///
750    /// ```python,ignore
751    /// crl = synta.CertificateList.from_der(data)
752    /// print("CRL number:", crl.crl_number)
753    /// ```
754    #[getter]
755    fn crl_number<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyAny>>> {
756        let Some(n) = self.crl()?.crl_number() else {
757            return Ok(None);
758        };
759        let py_int = if let Ok(v) = n.as_i64() {
760            v.into_pyobject(py)?.into_any()
761        } else if let Ok(v) = n.as_i128() {
762            v.into_pyobject(py)?.into_any()
763        } else {
764            // Arbitrarily large CRL number — extremely uncommon but RFC-compliant.
765            let bytes_obj = PyBytes::new(py, n.as_bytes());
766            let kwargs = pyo3::types::PyDict::new(py);
767            kwargs.set_item(pyo3::intern!(py, "signed"), false)?;
768            py.get_type::<pyo3::types::PyInt>().call_method(
769                pyo3::intern!(py, "from_bytes"),
770                (bytes_obj, pyo3::intern!(py, "big")),
771                Some(&kwargs),
772            )?
773        };
774        Ok(Some(py_int))
775    }
776
777    /// Number of revoked certificates in this CRL (0 if no revoked entries).
778    #[getter]
779    fn revoked_count(&self) -> PyResult<usize> {
780        Ok(self
781            .crl()?
782            .tbs_cert_list
783            .revoked_certificates
784            .as_ref()
785            .map(|v| v.len())
786            .unwrap_or(0))
787    }
788
789    /// Return the DER value bytes of the named CRL extension, or ``None`` if
790    /// the CRL carries no such extension.
791    ///
792    /// ``oid`` may be a dotted-decimal string or a
793    /// :class:`~synta.ObjectIdentifier`.  The value returned is the content of
794    /// the ``extnValue`` OCTET STRING — the DER encoding of the extension value
795    /// — without the outer OCTET STRING tag and length.
796    ///
797    /// ```python,ignore
798    /// CRL_NUMBER_OID = "2.5.29.20"
799    /// crl_num_der = crl.get_extension_value_der(CRL_NUMBER_OID)
800    /// if crl_num_der:
801    ///     num = int(synta.Decoder(crl_num_der, synta.Encoding.DER).decode_integer())
802    ///     print("CRL Number:", num)
803    /// ```
804    fn get_extension_value_der<'py>(
805        &self,
806        py: Python<'py>,
807        oid: &Bound<'_, PyAny>,
808    ) -> PyResult<Option<Bound<'py, PyBytes>>> {
809        let target = super::oid_from_pyany(oid)?;
810        let exts = match self.crl()?.tbs_cert_list.crl_extensions.as_ref() {
811            Some(e) => e,
812            None => return Ok(None),
813        };
814        for ext in exts {
815            if ext.extn_id == target {
816                return Ok(Some(PyBytes::new(py, ext.extn_value.as_bytes())));
817            }
818        }
819        Ok(None)
820    }
821
822    /// Complete DER encoding of this CRL (the original bytes passed to ``from_der``).
823    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
824        self._data.clone_ref(py).into_bound(py)
825    }
826
827    /// Verify that this CRL was signed by ``issuer``.
828    ///
829    /// Checks that the CRL's ``issuer`` Name matches the certificate's
830    /// ``subject`` Name, then verifies the outer signature against the
831    /// issuer's public key.
832    ///
833    /// :raises ValueError: if the issuer name does not match or the signature
834    ///     is invalid.
835    ///
836    /// ```python,ignore
837    /// ca_cert = synta.Certificate.from_pem(open("ca.pem", "rb").read())
838    /// crl = synta.CertificateList.from_der(open("crl.der", "rb").read())
839    /// crl.verify_issued_by(ca_cert)   # raises ValueError if not valid
840    /// ```
841    fn verify_issued_by(&self, issuer: &super::cert::PyCertificate) -> PyResult<()> {
842        use synta::traits::Encode;
843        use synta_certificate::{default_signature_verifier, SignatureVerifier};
844
845        let crl = self.crl()?;
846        let issuer_cert = issuer.cert()?;
847
848        // 1. Name check: CRL issuer (Name<'_>) must match issuer cert's subject (RawDer).
849        let crl_issuer_der = name_to_der_bytes(&crl.tbs_cert_list.issuer);
850        if crl_issuer_der.as_slice() != issuer_cert.tbs_certificate.subject.as_bytes() {
851            return Err(pyo3::exceptions::PyValueError::new_err(
852                "CRL issuer name does not match subject of provided certificate",
853            ));
854        }
855
856        // 2. Re-encode TBSCertList for the verifier.
857        let mut tbs_enc = synta::Encoder::new(synta::Encoding::Der);
858        crl.tbs_cert_list
859            .encode(&mut tbs_enc)
860            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("CRL TBS encode: {e}")))?;
861        let tbs_der = tbs_enc
862            .finish()
863            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("CRL TBS finish: {e}")))?;
864
865        // 3. Re-encode outer signatureAlgorithm.
866        let mut alg_enc = synta::Encoder::new(synta::Encoding::Der);
867        crl.signature_algorithm
868            .encode(&mut alg_enc)
869            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("CRL alg encode: {e}")))?;
870        let sig_alg_der = alg_enc
871            .finish()
872            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("CRL alg finish: {e}")))?;
873
874        // 4. Re-encode issuer SPKI.
875        let mut spki_enc = synta::Encoder::new(synta::Encoding::Der);
876        issuer_cert
877            .tbs_certificate
878            .subject_public_key_info
879            .encode(&mut spki_enc)
880            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("SPKI encode: {e}")))?;
881        let spki_der = spki_enc
882            .finish()
883            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("SPKI finish: {e}")))?;
884
885        // 5. Verify using the backend-agnostic verifier.
886        default_signature_verifier()
887            .verify_certificate_signature(
888                &tbs_der,
889                &sig_alg_der,
890                crl.signature_value.as_bytes(),
891                &spki_der,
892            )
893            .map_err(|e| {
894                pyo3::exceptions::PyValueError::new_err(format!("CRL signature invalid: {e}"))
895            })
896    }
897
898    fn __repr__(&self) -> PyResult<String> {
899        let crl = self.crl()?;
900        Ok(format!(
901            "CertificateList(issuer={:?}, revoked_count={})",
902            name_to_dn_string(&crl.tbs_cert_list.issuer),
903            crl.tbs_cert_list
904                .revoked_certificates
905                .as_ref()
906                .map(|v| v.len())
907                .unwrap_or(0),
908        ))
909    }
910}
911
912// ── PyOcspResponse ────────────────────────────────────────────────────────────
913
914/// OCSP Response (RFC 6960) accessible from Python.
915///
916/// ```python
917/// resp = OCSPResponse.from_der(open("resp.der", "rb").read())
918/// print(resp.status)
919/// if resp.response_bytes:
920///     basic = resp.response_bytes  # DER of BasicOCSPResponse
921/// ```
922#[pyclass(frozen, name = "OCSPResponse")]
923pub struct PyOcspResponse {
924    pub(super) _data: Py<PyBytes>,
925    pub(super) raw: &'static [u8],
926    inner: OnceLock<Box<synta_certificate::ocsp::OCSPResponse<'static>>>,
927    status_cache: OnceLock<Py<PyString>>,
928    response_type_oid_cache: OnceLock<Option<Py<PyObjectIdentifier>>>,
929    response_bytes_cache: OnceLock<Option<Py<PyBytes>>>,
930}
931
932impl PyOcspResponse {
933    fn ocsp(&self) -> PyResult<&synta_certificate::ocsp::OCSPResponse<'static>> {
934        if let Some(v) = self.inner.get() {
935            return Ok(v.as_ref());
936        }
937        let mut decoder = Decoder::new(self.raw, Encoding::Der);
938        let decoded = decoder.decode().map_err(|e| {
939            pyo3::exceptions::PyValueError::new_err(format!("OCSPResponse DER decode failed: {e}"))
940        })?;
941        let _ = self.inner.set(Box::new(decoded));
942        Ok(self.inner.get().unwrap().as_ref())
943    }
944}
945
946#[pymethods]
947impl PyOcspResponse {
948    /// Parse a DER-encoded OCSP Response.
949    #[staticmethod]
950    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
951        let py_bytes = data.unbind();
952        // SAFETY: `py_bytes` holds a strong reference (Py<PyBytes>) that
953        // keeps the Python bytes object alive for the lifetime of this struct.
954        // CPython's bytes objects have a fixed-address, non-relocating payload
955        // buffer (CPython has no moving GC).  The slice lifetime is extended
956        // to 'static; the actual safety invariants are:
957        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
958        //       can outlive the struct, so `raw` is never read after drop begins.
959        //   (2) `raw: &'static [u8]` has no destructor (fat pointer, no heap
960        //       allocation), so field drop order does not cause use-after-free.
961        //   (3) `inner` contains only borrow-typed fields; dropping
962        //       Box<OCSPResponse<'static>> does not read through the contained
963        //       &'static slices (borrows have no destructors).
964        // CPython-only: does not hold for PyPy or GraalPy.
965        let raw: &'static [u8] = unsafe {
966            let s = py_bytes.bind(py).as_bytes();
967            std::slice::from_raw_parts(s.as_ptr(), s.len())
968        };
969        {
970            let mut d = Decoder::new(raw, Encoding::Der);
971            d.read_tag()
972                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
973            d.read_length()
974                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
975        }
976        Ok(Self {
977            _data: py_bytes,
978            raw,
979            inner: OnceLock::new(),
980            status_cache: OnceLock::new(),
981            response_type_oid_cache: OnceLock::new(),
982            response_bytes_cache: OnceLock::new(),
983        })
984    }
985
986    /// Parse a PEM-encoded OCSP Response.
987    ///
988    /// Returns a single :class:`OCSPResponse` for one PEM block, or a
989    /// :class:`list` of :class:`OCSPResponse` objects when multiple blocks are
990    /// present.  Raises :exc:`ValueError` if no PEM block is found.
991    ///
992    /// ```python
993    /// resp = OCSPResponse.from_pem(open("ocsp.pem", "rb").read())
994    /// print(resp.status)
995    /// ```
996    #[staticmethod]
997    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
998        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
999            let obj = Self::from_der(py, bytes)?;
1000            Ok(Py::new(py, obj)?.into_bound(py).into_any())
1001        })
1002    }
1003
1004    /// Serialize one OCSP response or a list of them to PEM format.
1005    ///
1006    /// ```python
1007    /// pem = OCSPResponse.to_pem(resp)
1008    /// pem = OCSPResponse.to_pem([resp1, resp2])
1009    /// ```
1010    #[staticmethod]
1011    fn to_pem<'py>(
1012        py: Python<'py>,
1013        obj_or_list: Bound<'_, PyAny>,
1014    ) -> PyResult<Bound<'py, PyBytes>> {
1015        pyobject_to_pem::<Self, _>(py, "OCSP RESPONSE", &obj_or_list, |c| c.raw)
1016    }
1017
1018    /// Response status string (e.g. ``"successful"``, ``"tryLater"``).
1019    #[getter]
1020    fn status<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
1021        if let Some(cached) = self.status_cache.get() {
1022            return Ok(cached.clone_ref(py).into_bound(py));
1023        }
1024        let s = ocsp_status_str(self.ocsp()?.response_status);
1025        let py_str = PyString::new(py, s).unbind();
1026        let _ = self.status_cache.set(py_str.clone_ref(py));
1027        Ok(py_str.into_bound(py))
1028    }
1029
1030    /// Dotted-decimal OID of the response type, or ``None`` for error responses
1031    /// that carry no ``responseBytes`` (e.g. ``tryLater``).
1032    ///
1033    /// For successful responses this is typically the id-pkix-ocsp-basic OID
1034    /// (``"1.3.6.1.5.5.7.48.1.1"``).
1035    #[getter]
1036    fn response_type_oid<'py>(
1037        &self,
1038        py: Python<'py>,
1039    ) -> PyResult<Option<Bound<'py, PyObjectIdentifier>>> {
1040        if let Some(cached) = self.response_type_oid_cache.get() {
1041            return Ok(cached.as_ref().map(|o| o.clone_ref(py).into_bound(py)));
1042        }
1043        let computed: Option<Py<PyObjectIdentifier>> = self
1044            .ocsp()?
1045            .response_bytes
1046            .as_ref()
1047            .map(|rb| Py::new(py, PyObjectIdentifier::from_oid(rb.response_type.clone())))
1048            .transpose()?;
1049        let _ = self
1050            .response_type_oid_cache
1051            .set(computed.as_ref().map(|o| o.clone_ref(py)));
1052        Ok(computed.map(|o| o.into_bound(py)))
1053    }
1054
1055    /// Raw DER bytes of the embedded response (the OCTET STRING content of
1056    /// ``ResponseBytes``), or ``None`` for error responses.
1057    ///
1058    /// For id-pkix-ocsp-basic responses, these bytes are a DER-encoded
1059    /// ``BasicOCSPResponse``.
1060    #[getter]
1061    fn response_bytes<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
1062        if let Some(cached) = self.response_bytes_cache.get() {
1063            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
1064        }
1065        let computed: Option<Bound<'py, PyBytes>> = self
1066            .ocsp()?
1067            .response_bytes
1068            .as_ref()
1069            .map(|rb| PyBytes::new(py, rb.response.as_bytes()));
1070        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
1071        let _ = self.response_bytes_cache.set(to_store);
1072        Ok(computed)
1073    }
1074
1075    /// Complete DER encoding of this OCSP response (the original bytes passed to ``from_der``).
1076    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1077        self._data.clone_ref(py).into_bound(py)
1078    }
1079
1080    /// Verify the signature of a ``BasicOCSPResponse`` using the responder's certificate.
1081    ///
1082    /// Decodes the embedded ``BasicOCSPResponse``, re-encodes ``ResponseData``
1083    /// as the TBS bytes, and verifies the outer signature against ``responder``'s
1084    /// public key.
1085    ///
1086    /// :param responder: the OCSP responder's certificate.
1087    /// :raises ValueError: if the response carries no ``responseBytes``, the
1088    ///     response type is not ``id-pkix-ocsp-basic``, or the signature is
1089    ///     invalid.
1090    ///
1091    /// ```python,ignore
1092    /// ca_cert = synta.Certificate.from_pem(open("ca.pem", "rb").read())
1093    /// resp = synta.OCSPResponse.from_der(open("resp.der", "rb").read())
1094    /// resp.verify_signature(ca_cert)   # raises ValueError if invalid
1095    /// ```
1096    fn verify_signature(&self, responder: &super::cert::PyCertificate) -> PyResult<()> {
1097        use synta_certificate::{default_signature_verifier, SignatureVerifier};
1098
1099        let ocsp = self.ocsp()?;
1100
1101        // 1. Get ResponseBytes — absent for error-status responses.
1102        let rb = ocsp.response_bytes.as_ref().ok_or_else(|| {
1103            pyo3::exceptions::PyValueError::new_err(
1104                "OCSP response carries no responseBytes (error status response)",
1105            )
1106        })?;
1107
1108        // 2. Ensure the responseType is id-pkix-ocsp-basic.
1109        use synta_certificate::oids::ID_PKIX_OCSP_BASIC;
1110        if rb.response_type.components() != ID_PKIX_OCSP_BASIC {
1111            return Err(pyo3::exceptions::PyValueError::new_err(format!(
1112                "unsupported OCSP responseType: {}",
1113                rb.response_type,
1114            )));
1115        }
1116
1117        // 3. Decode the OCTET STRING contents as BasicOCSPResponse.
1118        let basic_der = rb.response.as_bytes();
1119        let basic: synta_certificate::ocsp::BasicOCSPResponse<'_> = {
1120            let mut dec = Decoder::new(basic_der, Encoding::Der);
1121            dec.decode().map_err(|e| {
1122                pyo3::exceptions::PyValueError::new_err(format!(
1123                    "BasicOCSPResponse decode failed: {e}"
1124                ))
1125            })?
1126        };
1127
1128        // 4. Re-encode ResponseData (TBS).
1129        let tbs_der = basic.tbs_response_data.to_der().map_err(|e| {
1130            pyo3::exceptions::PyValueError::new_err(format!("ResponseData encode: {e}"))
1131        })?;
1132
1133        // 5. Re-encode BasicOCSPResponse signatureAlgorithm.
1134        let sig_alg_der = basic.signature_algorithm.to_der().map_err(|e| {
1135            pyo3::exceptions::PyValueError::new_err(format!("OCSP alg encode: {e}"))
1136        })?;
1137
1138        // 6. Re-encode responder SPKI.
1139        let issuer_cert = responder.cert()?;
1140        let spki_der = issuer_cert
1141            .tbs_certificate
1142            .subject_public_key_info
1143            .to_der()
1144            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("SPKI encode: {e}")))?;
1145
1146        // 7. Verify using the backend-agnostic verifier.
1147        default_signature_verifier()
1148            .verify_certificate_signature(
1149                &tbs_der,
1150                &sig_alg_der,
1151                basic.signature.as_bytes(),
1152                &spki_der,
1153            )
1154            .map_err(|e| {
1155                pyo3::exceptions::PyValueError::new_err(format!("OCSP signature invalid: {e}"))
1156            })
1157    }
1158
1159    fn __repr__(&self) -> PyResult<String> {
1160        Ok(format!(
1161            "OCSPResponse(status={})",
1162            ocsp_status_str(self.ocsp()?.response_status),
1163        ))
1164    }
1165}
1166
1167// ── PKCS#7 / PKCS#12 free functions ─────────────────────────────────────────
1168
1169/// Build a `PyList[Certificate]` from a `Vec<Vec<u8>>` of DER-encoded certs.
1170fn der_certs_to_pylist<'py>(
1171    py: Python<'py>,
1172    der_certs: Vec<Vec<u8>>,
1173) -> PyResult<Bound<'py, PyList>> {
1174    let list = PyList::empty(py);
1175    for der in der_certs {
1176        let py_bytes = PyBytes::new(py, &der);
1177        let cert = PyCertificate::new_from_der(py, py_bytes)?;
1178        list.append(Py::new(py, cert)?.into_bound(py))?;
1179    }
1180    Ok(list)
1181}
1182
1183/// Extract X.509 certificates from a PKCS#7 SignedData blob (DER or BER).
1184///
1185/// Accepts raw DER or BER input; BER indefinite-length encodings (common in
1186/// PKCS#7 files produced by older tools) are handled transparently.  Returns
1187/// a list of :class:`Certificate` objects in document order.
1188///
1189/// Raises :exc:`ValueError` on any ASN.1 structural error or if the input
1190/// is not id-signedData.
1191///
1192/// ```python,ignore
1193/// data = open("bundle.p7b", "rb").read()
1194/// certs = synta.load_der_pkcs7_certificates(data)
1195/// for cert in certs:
1196///     print(cert.subject)
1197/// ```
1198#[pyfunction]
1199pub fn load_der_pkcs7_certificates<'py>(
1200    py: Python<'py>,
1201    data: &[u8],
1202) -> PyResult<Bound<'py, PyList>> {
1203    let der_certs = synta_certificate::certs_from_pkcs7(data)
1204        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1205    der_certs_to_pylist(py, der_certs)
1206}
1207
1208/// Extract X.509 certificates from a PEM-encoded PKCS#7 SignedData blob.
1209///
1210/// Decodes PEM block(s) in ``data`` (any label is accepted: ``PKCS7``,
1211/// ``CMS``, ``CERTIFICATE``, etc.) then calls
1212/// :func:`load_der_pkcs7_certificates` on each DER payload.  All certificates
1213/// across all blocks are returned as a single flat list.
1214///
1215/// Raises :exc:`ValueError` if no PEM block is found or if any block fails
1216/// to parse as PKCS#7 SignedData.
1217///
1218/// ```python,ignore
1219/// data = open("bundle.pem", "rb").read()
1220/// certs = synta.load_pem_pkcs7_certificates(data)
1221/// for cert in certs:
1222///     print(cert.subject)
1223/// ```
1224#[pyfunction]
1225pub fn load_pem_pkcs7_certificates<'py>(
1226    py: Python<'py>,
1227    data: &[u8],
1228) -> PyResult<Bound<'py, PyList>> {
1229    let blocks = synta_certificate::pem_blocks(data);
1230    if blocks.is_empty() {
1231        return Err(pyo3::exceptions::PyValueError::new_err(
1232            "no PEM block found in input",
1233        ));
1234    }
1235    let mut all_certs: Vec<Vec<u8>> = Vec::new();
1236    for (_, der) in &blocks {
1237        let certs = synta_certificate::certs_from_pkcs7(der)
1238            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1239        all_certs.extend(certs);
1240    }
1241    der_certs_to_pylist(py, all_certs)
1242}
1243
1244/// Extract X.509 certificates from a PKCS#12 archive.
1245///
1246/// Accepts raw DER or BER input; the encoding is detected automatically.
1247/// Returns a list of :class:`Certificate` objects in document order.
1248/// Non-certificate bag types (``keyBag``, ``pkcs8ShroudedKeyBag``,
1249/// ``crlBag``, ``secretBag``) are silently skipped.
1250///
1251/// ``password`` is the archive password as raw bytes (UTF-8 without a NUL
1252/// terminator).  Pass ``b""`` or omit the argument for password-less archives.
1253///
1254/// Encrypted bags are supported when the library is built with a crypto backend
1255/// (``openssl`` or ``nss`` Cargo feature).
1256///
1257/// ```python,ignore
1258/// data = open("archive.p12", "rb").read()
1259/// certs = synta.load_pkcs12_certificates(data, b"s3cr3t")
1260/// for cert in certs:
1261///     print(cert.subject)
1262/// ```
1263#[pyfunction]
1264#[pyo3(signature = (data, password = None))]
1265pub fn load_pkcs12_certificates<'py>(
1266    py: Python<'py>,
1267    data: &[u8],
1268    password: Option<&[u8]>,
1269) -> PyResult<Bound<'py, PyList>> {
1270    let pw = password.unwrap_or(b"");
1271    let der_certs =
1272        synta_certificate::certs_from_pkcs12(data, pw, &synta_certificate::DefaultCrypto)
1273            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1274    der_certs_to_pylist(py, der_certs)
1275}
1276
1277/// Extract PKCS#8 private keys from a PKCS#12 archive.
1278///
1279/// Returns a :class:`list` of :class:`bytes` objects, one per private key
1280/// found in ``keyBag`` (unencrypted) and ``pkcs8ShroudedKeyBag`` (encrypted,
1281/// decrypted when a crypto backend is enabled and ``password`` is given)
1282/// entries.  Each element is a DER-encoded ``OneAsymmetricKey``
1283/// (PKCS#8) structure, suitable for passing to a cryptography library.
1284///
1285/// ``certBag``, ``crlBag``, ``secretBag`` and any unknown bag types are
1286/// silently skipped.
1287///
1288/// ```python,ignore
1289/// data = open("archive.p12", "rb").read()
1290/// keys = synta.load_pkcs12_keys(data, b"s3cr3t")
1291/// for key_der in keys:
1292///     from cryptography.hazmat.primitives.serialization import load_der_private_key
1293///     key = load_der_private_key(key_der, None)
1294/// ```
1295#[pyfunction]
1296#[pyo3(signature = (data, password = None))]
1297pub fn load_pkcs12_keys<'py>(
1298    py: Python<'py>,
1299    data: &[u8],
1300    password: Option<&[u8]>,
1301) -> PyResult<Bound<'py, PyList>> {
1302    let pw = password.unwrap_or(b"");
1303    let der_keys = synta_certificate::keys_from_pkcs12(data, pw, &synta_certificate::DefaultCrypto)
1304        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1305    let list = PyList::empty(py);
1306    for der in der_keys {
1307        list.append(PyBytes::new(py, &der))?;
1308    }
1309    Ok(list)
1310}
1311
1312/// Extract both certificates and private keys from a PKCS#12 archive.
1313///
1314/// Returns a 2-tuple ``(certs, keys)`` where:
1315///
1316/// - ``certs`` is a :class:`list` of :class:`Certificate` objects.
1317/// - ``keys`` is a :class:`list` of :class:`bytes`, each a DER-encoded
1318///   PKCS#8 ``OneAsymmetricKey`` structure.
1319///
1320/// Encrypted bags are decrypted with ``password`` when a crypto backend is
1321/// available.  Pass ``b""`` or omit ``password`` for password-less archives.
1322///
1323/// Use :func:`load_pkcs12_certificates` if you only need certificates, or
1324/// :func:`load_pkcs12_keys` if you only need keys.
1325///
1326/// ```python,ignore
1327/// data = open("archive.p12", "rb").read()
1328/// certs, keys = synta.load_pkcs12(data, b"s3cr3t")
1329/// for cert in certs:
1330///     print(cert.subject)
1331/// for key_der in keys:
1332///     from cryptography.hazmat.primitives.serialization import load_der_private_key
1333///     key = load_der_private_key(key_der, None)
1334/// ```
1335#[pyfunction]
1336#[pyo3(signature = (data, password = None))]
1337pub fn load_pkcs12<'py>(
1338    py: Python<'py>,
1339    data: &[u8],
1340    password: Option<&[u8]>,
1341) -> PyResult<Bound<'py, pyo3::types::PyTuple>> {
1342    let pw = password.unwrap_or(b"");
1343    let pki = synta_certificate::pki_from_pkcs12(data, pw, &synta_certificate::DefaultCrypto)
1344        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1345    let certs = der_certs_to_pylist(py, pki.certs)?;
1346    let keys = PyList::empty(py);
1347    for der in pki.keys {
1348        keys.append(PyBytes::new(py, &der))?;
1349    }
1350    pyo3::types::PyTuple::new(py, [certs.into_any(), keys.into_any()])
1351}
1352
1353/// Auto-detect the encoding of ``data`` and return every PKI object as a
1354/// ``(label, der_bytes)`` tuple, in document order.
1355///
1356/// Supported formats: PEM (any label), PKCS#7 / CMS SignedData (DER or BER),
1357/// PKCS#12 PFX (DER or BER), and raw DER.
1358///
1359/// **Labels:**
1360///
1361/// - PEM blocks whose payload is a PKCS#7 ``ContentInfo`` (detected by
1362///   structure, not label) are expanded: the embedded certificates are
1363///   extracted and each is returned as ``("CERTIFICATE", der)``.
1364/// - All other PEM block types pass through with their original label
1365///   (e.g. ``"CERTIFICATE"``, ``"PRIVATE KEY"``).  Malformed PEM blocks
1366///   (bad base64, truncated header) are silently skipped.
1367/// - Binary PKCS#7 and raw DER yield ``"CERTIFICATE"`` labels.
1368/// - Binary PKCS#12 yields ``"CERTIFICATE"`` for ``certBag`` entries and
1369///   ``"PRIVATE KEY"`` for ``keyBag`` / ``pkcs8ShroudedKeyBag`` entries.
1370///
1371/// **Decryption:**
1372///
1373/// ``password`` applies only to PKCS#12 archives.  When ``password`` is
1374/// given and a crypto backend is available, encrypted SafeBags are decrypted.
1375/// Otherwise encrypted bags are silently skipped; unencrypted certificates in
1376/// the same archive are still returned.
1377///
1378/// :raises ValueError: For any structural failure:
1379///
1380///   - A PKCS#7 block (binary or PEM-wrapped) has malformed DER, or its
1381///     ``contentType`` OID is not ``id-signedData``.
1382///   - The PKCS#12 input has a structural ASN.1 error.
1383///   - ``password`` was supplied and decryption of an encrypted SafeBag
1384///     failed (e.g. wrong password, unsupported cipher).
1385///   - The PKCS#12 authSafe uses an unsupported ``ContentInfo`` type.
1386///
1387/// :raises TypeError: If ``data`` is not :class:`bytes`, or ``password``
1388///   is not :class:`bytes` or ``None``.
1389///
1390/// ```python,ignore
1391/// data = open("bundle.p7b", "rb").read()
1392/// blocks = synta.read_pki_blocks(data)
1393/// for label, der in blocks:
1394///     print(f"{label}: {len(der)} bytes")
1395///
1396/// # PKCS#12 with encryption
1397/// data = open("archive.p12", "rb").read()
1398/// blocks = synta.read_pki_blocks(data, b"s3cr3t")
1399/// ```
1400#[pyfunction]
1401#[pyo3(signature = (data, password = None))]
1402pub fn read_pki_blocks<'py>(
1403    py: Python<'py>,
1404    data: &[u8],
1405    password: Option<&[u8]>,
1406) -> PyResult<Bound<'py, PyList>> {
1407    let pw = password.unwrap_or(b"");
1408    let blocks = if password.is_some() {
1409        synta_certificate::read_pki_blocks(
1410            data,
1411            pw,
1412            Some(&synta_certificate::DefaultCrypto as &dyn synta_certificate::PkiDecryptor),
1413        )
1414    } else {
1415        synta_certificate::read_pki_blocks(data, pw, None::<&dyn synta_certificate::PkiDecryptor>)
1416    };
1417    let blocks = blocks.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1418    let list = PyList::empty(py);
1419    for (label, der) in blocks {
1420        let tuple = pyo3::types::PyTuple::new(
1421            py,
1422            [
1423                PyString::new(py, &label).into_any(),
1424                PyBytes::new(py, &der).into_any(),
1425            ],
1426        )?;
1427        list.append(tuple)?;
1428    }
1429    Ok(list)
1430}
1431
1432// ── X.509 encoding utilities ──────────────────────────────────────────────────
1433
1434/// Encode a list of OIDs as a DER ``SEQUENCE OF OID`` (ExtendedKeyUsage value).
1435///
1436/// ``oids`` must be a :class:`list` of :class:`~synta.ObjectIdentifier` or
1437/// dotted-decimal ``str`` values.  Returns the complete DER bytes of the
1438/// ``SEQUENCE``, suitable for use as an X.509v3 Extended Key Usage extension
1439/// value.
1440///
1441/// Delegates to synta's ``Vec<ObjectIdentifier>`` encoder which matches the
1442/// generated ``ExtendedKeyUsage = Vec<KeyPurposeId>`` type from the X.509 schema.
1443///
1444/// ```python,ignore
1445/// eku_der = synta.encode_extended_key_usage([
1446///     "1.3.6.1.5.5.7.3.1",   # id-kp-serverAuth
1447///     "1.3.6.1.5.5.7.3.2",   # id-kp-clientAuth
1448/// ])
1449/// ```
1450#[pyfunction]
1451pub fn encode_extended_key_usage<'py>(
1452    py: Python<'py>,
1453    oids: &Bound<'_, PyList>,
1454) -> PyResult<Bound<'py, PyBytes>> {
1455    use synta::traits::Encode;
1456    let mut eku: Vec<synta::ObjectIdentifier> = Vec::with_capacity(oids.len());
1457    for item in oids.iter() {
1458        eku.push(super::oid_from_pyany(&item)?);
1459    }
1460    let mut enc = synta::Encoder::new(synta::Encoding::Der);
1461    eku.encode(&mut enc)
1462        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1463    let der = enc
1464        .finish()
1465        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1466    Ok(PyBytes::new(py, &der))
1467}
1468
1469/// Encode a list of ``(tag_number, content_bytes)`` pairs as a DER
1470/// ``SEQUENCE OF GeneralName``.
1471///
1472/// This is the inverse of :func:`~synta.parse_general_names`: the input
1473/// format is identical to what that function returns.  Tag numbers follow
1474/// RFC 5280 §4.2.1.6; use the :mod:`synta.general_name` constants.
1475///
1476/// ``content_bytes`` must be the raw **value** bytes — without the outer
1477/// context-specific TLV — exactly as returned by
1478/// :func:`~synta.parse_general_names`.
1479///
1480/// Delegates to :func:`synta_certificate::encode_general_names` which uses the
1481/// generated :class:`GeneralName` enum's ``Encode`` implementation.
1482///
1483/// ```python,ignore
1484/// import synta.general_name as gn
1485///
1486/// san_der = synta.encode_subject_alt_names([
1487///     (gn.DNS_NAME,   b"example.com"),
1488///     (gn.IP_ADDRESS, b"\x7f\x00\x00\x01"),   # 127.0.0.1
1489/// ])
1490/// ```
1491#[pyfunction]
1492pub fn encode_subject_alt_names<'py>(
1493    py: Python<'py>,
1494    names: &Bound<'_, PyList>,
1495) -> PyResult<Bound<'py, PyBytes>> {
1496    // Collect owned (tag, bytes) first so we can create &[u8] references that
1497    // all share the same lifetime, satisfying Vec<GeneralName<'_>> constraints.
1498    let mut owned: Vec<(u32, Vec<u8>)> = Vec::with_capacity(names.len());
1499    for item in names.iter() {
1500        let tuple = item.cast::<pyo3::types::PyTuple>().map_err(|_| {
1501            pyo3::exceptions::PyTypeError::new_err(
1502                "each element must be a (tag_number, bytes) 2-tuple",
1503            )
1504        })?;
1505        if tuple.len() != 2 {
1506            return Err(pyo3::exceptions::PyTypeError::new_err(
1507                "each element must be a (tag_number, bytes) 2-tuple",
1508            ));
1509        }
1510        let tag_num: u32 = tuple.get_item(0)?.extract()?;
1511        let content: Vec<u8> = tuple.get_item(1)?.extract()?;
1512        owned.push((tag_num, content));
1513    }
1514    let entries: Vec<(u32, &[u8])> = owned.iter().map(|(t, v)| (*t, v.as_slice())).collect();
1515    let der = synta_certificate::encode_general_names(&entries).ok_or_else(|| {
1516        pyo3::exceptions::PyValueError::new_err(
1517            "failed to encode GeneralName entries (invalid content bytes or unsupported tag)",
1518        )
1519    })?;
1520    Ok(PyBytes::new(py, &der))
1521}
1522
1523/// Compare two DER-encoded X.500 Names for byte-for-byte equality.
1524///
1525/// Returns ``True`` if and only if the two byte sequences are identical.
1526/// This is a strict comparison; RFC 5280 name matching rules (case-insensitive
1527/// string comparison, etc.) are **not** applied.
1528///
1529/// Useful for quickly checking whether issuer and subject names match without
1530/// decoding the full structure:
1531///
1532/// ```python,ignore
1533/// same = synta.name_der_equal(cert.issuer_raw_der, ca_cert.subject_raw_der)
1534/// ```
1535#[pyfunction]
1536pub fn name_der_equal(a: &[u8], b: &[u8]) -> bool {
1537    a == b
1538}
1539
1540/// Build a DER-encoded PKCS#12 ``PFX`` archive.
1541///
1542/// Assembles a ``PFX`` from one or more certificates and an optional
1543/// DER-encoded PKCS#8 private key.  The archive is protected by ``password``
1544/// using PBES2 (PBKDF2 + the selected cipher) for key encryption and HMAC
1545/// (PBKDF2-derived key) for the integrity MAC.
1546///
1547/// :param certificates: sequence of :class:`~synta.Certificate` objects or
1548///     raw DER ``bytes``.  Both types may appear in the same list.
1549/// :param private_key: a :class:`~synta.PrivateKey` object or a DER-encoded
1550///     PKCS#8 ``OneAsymmetricKey`` ``bytes``, or ``None`` to omit.
1551/// :param password: archive password (``bytes``), or ``None`` for a
1552///     password-less archive.
1553/// :param cipher: symmetric cipher for private-key encryption.  One of
1554///     ``"aes128"`` or ``"aes256"`` (default).
1555/// :param mac_algorithm: HMAC algorithm for the integrity MAC and PBKDF2 PRF.
1556///     One of ``"sha256"`` (default), ``"sha384"``, or ``"sha512"``.
1557/// :returns: DER-encoded ``PFX`` archive bytes.
1558/// :raises TypeError: if any element of ``certificates`` is neither a
1559///     :class:`~synta.Certificate` nor ``bytes``, or if ``private_key`` is
1560///     not a :class:`~synta.PrivateKey` nor ``bytes``.
1561/// :raises ValueError: if no certificates or key are supplied, if an unknown
1562///     cipher or MAC algorithm name is given, or if encryption fails.
1563///
1564/// ```python,ignore
1565/// # Certificate and PrivateKey objects — no .to_der() call needed
1566/// pfx = synta.create_pkcs12(
1567///     [cert, ca_cert],
1568///     private_key=key,
1569///     password=b"s3cr3t",
1570/// )
1571///
1572/// # Custom cipher and MAC algorithm
1573/// pfx = synta.create_pkcs12(
1574///     [cert],
1575///     private_key=key,
1576///     password=b"pass",
1577///     cipher="aes128",
1578///     mac_algorithm="sha384",
1579/// )
1580///
1581/// # Raw DER bytes also accepted for both
1582/// pfx = synta.create_pkcs12([cert.to_der()], private_key=key.to_der())
1583///
1584/// with open("out.p12", "wb") as f:
1585///     f.write(pfx)
1586/// ```
1587#[pyfunction]
1588#[pyo3(signature = (certificates, private_key = None, password = None, cipher = None, mac_algorithm = None))]
1589pub fn create_pkcs12<'py>(
1590    py: Python<'py>,
1591    certificates: &Bound<'_, PyList>,
1592    private_key: Option<Bound<'_, PyAny>>,
1593    password: Option<&[u8]>,
1594    cipher: Option<&str>,
1595    mac_algorithm: Option<&str>,
1596) -> PyResult<Bound<'py, PyBytes>> {
1597    use pyo3::exceptions::PyValueError;
1598    use synta_certificate::{
1599        OpensslPkcs12Encryptor, Pkcs12Builder, Pkcs12Cipher, Pkcs12Config, Pkcs12HmacAlgorithm,
1600    };
1601
1602    let pkcs12_cipher = match cipher.unwrap_or("aes256") {
1603        "aes128" => Pkcs12Cipher::Aes128Cbc,
1604        "aes256" => Pkcs12Cipher::Aes256Cbc,
1605        "3des" => {
1606            return Err(PyValueError::new_err(
1607                "cipher '3des' is not supported; use 'aes128' or 'aes256'",
1608            ))
1609        }
1610        other => {
1611            return Err(PyValueError::new_err(format!(
1612                "unknown cipher: {other}; use 'aes128' or 'aes256'"
1613            )))
1614        }
1615    };
1616    let pkcs12_mac = match mac_algorithm.unwrap_or("sha256") {
1617        "sha256" => Pkcs12HmacAlgorithm::Sha256,
1618        "sha384" => Pkcs12HmacAlgorithm::Sha384,
1619        "sha512" => Pkcs12HmacAlgorithm::Sha512,
1620        "sha1" => {
1621            return Err(PyValueError::new_err(
1622                "mac_algorithm 'sha1' is not supported; use 'sha256', 'sha384', or 'sha512'",
1623            ))
1624        }
1625        other => {
1626            return Err(PyValueError::new_err(format!(
1627                "unknown mac_algorithm: {other}; use 'sha256', 'sha384', or 'sha512'"
1628            )))
1629        }
1630    };
1631    let config = Pkcs12Config {
1632        cipher: pkcs12_cipher,
1633        encryption_prf: pkcs12_mac,
1634        mac_algorithm: pkcs12_mac,
1635        ..Pkcs12Config::default()
1636    };
1637    let encryptor = OpensslPkcs12Encryptor::with_config(config);
1638
1639    let password = password.unwrap_or(b"");
1640    let mut builder = Pkcs12Builder::new();
1641    for item in certificates.iter() {
1642        if let Ok(py_cert) = item.cast::<PyCertificate>() {
1643            // Zero-copy: raw is &'static [u8] backed by the PyBytes in _data.
1644            // Pkcs12Builder::certificate() copies into Vec<u8> immediately, so
1645            // the borrow does not outlive this loop iteration.
1646            builder = builder.certificate(py_cert.get().raw);
1647        } else if let Ok(py_bytes) = item.cast::<PyBytes>() {
1648            builder = builder.certificate(py_bytes.as_bytes());
1649        } else {
1650            return Err(pyo3::exceptions::PyTypeError::new_err(
1651                "certificates must be synta.Certificate or bytes",
1652            ));
1653        }
1654    }
1655    if let Some(key_arg) = private_key {
1656        if let Ok(py_key) = key_arg.cast::<PyPrivateKey>() {
1657            // PyPrivateKey stores PKCS#8 DER internally; retrieve it directly.
1658            let key_der = py_key
1659                .get()
1660                .inner
1661                .to_der()
1662                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1663            builder = builder.private_key(&key_der);
1664        } else if let Ok(py_bytes) = key_arg.cast::<PyBytes>() {
1665            builder = builder.private_key(py_bytes.as_bytes());
1666        } else {
1667            return Err(pyo3::exceptions::PyTypeError::new_err(
1668                "private_key must be synta.PrivateKey or bytes",
1669            ));
1670        }
1671    }
1672
1673    let pfx_der = builder
1674        .build(password, &encryptor)
1675        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1676
1677    Ok(PyBytes::new(py, &pfx_der))
1678}
1679
1680// ── PyCertificateListBuilder helpers ─────────────────────────────────────────
1681
1682/// Convert a Python `datetime.datetime` (must be timezone-aware) to a
1683/// GeneralizedTime string in `"YYYYMMDDHHmmssZ"` format accepted by the
1684/// string-based `CertificateListBuilder` setters.
1685///
1686/// Uses `datetime.timestamp()` to obtain a Unix timestamp and delegates to
1687/// [`synta::GeneralizedTime::from_unix`] for the calendar conversion.
1688fn py_dt_to_gen_time_str(dt: &Bound<'_, PyAny>) -> PyResult<String> {
1689    if dt.getattr("tzinfo")?.is_none() {
1690        return Err(pyo3::exceptions::PyValueError::new_err(
1691            "datetime must be timezone-aware (e.g. datetime.timezone.utc)",
1692        ));
1693    }
1694    let ts: f64 = dt.call_method0("timestamp")?.extract()?;
1695    let secs = ts.floor() as i64;
1696    let gt = synta::GeneralizedTime::from_unix(secs).ok_or_else(|| {
1697        pyo3::exceptions::PyValueError::new_err("datetime out of valid ASN.1 year range 1–9999")
1698    })?;
1699    Ok(gt.to_string())
1700}
1701
1702// ── PyCertificateListBuilder ──────────────────────────────────────────────────
1703
1704/// Fluent builder for a DER-encoded X.509 CRL (RFC 5280 §5).
1705///
1706/// Chain setters to configure the CRL, then call :meth:`build` to obtain the
1707/// DER-encoded ``TBSCertList`` SEQUENCE.  Sign the TBS externally and call the
1708/// static :meth:`assemble` to produce the final ``CertificateList``.
1709///
1710/// ```python,ignore
1711/// import synta
1712///
1713/// name_der = synta.NameBuilder().common_name("Test CA").build()
1714/// alg_der  = bytes.fromhex("300d06092a864886f70d01010b0500")  # sha256WithRSAEncryption
1715/// tbs = (
1716///     synta.CertificateListBuilder()
1717///     .issuer(name_der)
1718///     .signature_algorithm(alg_der)
1719///     .this_update("20240101120000Z")
1720///     .next_update("20240201120000Z")
1721///     .revoke(bytes([1]), "20231201000000Z", 1)  # keyCompromise
1722///     .build()
1723/// )
1724/// # sign tbs externally, then:
1725/// crl_der = synta.CertificateListBuilder.assemble(tbs, alg_der, sig_bytes)
1726/// ```
1727#[pyclass(name = "CertificateListBuilder")]
1728pub struct PyCertificateListBuilder {
1729    inner: synta_certificate::CertificateListBuilder,
1730}
1731
1732#[pymethods]
1733impl PyCertificateListBuilder {
1734    /// Create a new, empty ``CertificateListBuilder``.
1735    #[new]
1736    fn new() -> Self {
1737        Self {
1738            inner: synta_certificate::CertificateListBuilder::new(),
1739        }
1740    }
1741
1742    /// Set the issuer ``Name`` from pre-encoded DER bytes.
1743    fn issuer<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
1744        let old = std::mem::replace(
1745            &mut slf.borrow_mut().inner,
1746            synta_certificate::CertificateListBuilder::new(),
1747        );
1748        slf.borrow_mut().inner = old.issuer(name_der);
1749        slf
1750    }
1751
1752    /// Set the ``thisUpdate`` time (``"YYYYMMDDHHmmssZ"`` or ``"YYMMDDHHmmssZ"``).
1753    fn this_update<'py>(slf: Bound<'py, Self>, time: &str) -> Bound<'py, Self> {
1754        let old = std::mem::replace(
1755            &mut slf.borrow_mut().inner,
1756            synta_certificate::CertificateListBuilder::new(),
1757        );
1758        slf.borrow_mut().inner = old.this_update(time);
1759        slf
1760    }
1761
1762    /// Set the optional ``nextUpdate`` time (same format as :meth:`this_update`).
1763    fn next_update<'py>(slf: Bound<'py, Self>, time: &str) -> Bound<'py, Self> {
1764        let old = std::mem::replace(
1765            &mut slf.borrow_mut().inner,
1766            synta_certificate::CertificateListBuilder::new(),
1767        );
1768        slf.borrow_mut().inner = old.next_update(time);
1769        slf
1770    }
1771
1772    /// Add a revoked certificate entry.
1773    ///
1774    /// ``serial`` is the big-endian DER INTEGER value bytes of the certificate
1775    /// serial number.  ``revocation_date`` uses the same time format as
1776    /// :meth:`this_update`.  ``reason`` is an optional CRL reason code integer
1777    /// (0=unspecified, 1=keyCompromise, 2=cACompromise, …, 10=aACompromise).
1778    fn revoke<'py>(
1779        slf: Bound<'py, Self>,
1780        serial: &[u8],
1781        revocation_date: &str,
1782        reason: Option<u8>,
1783    ) -> Bound<'py, Self> {
1784        let old = std::mem::replace(
1785            &mut slf.borrow_mut().inner,
1786            synta_certificate::CertificateListBuilder::new(),
1787        );
1788        slf.borrow_mut().inner = old.revoke(serial, revocation_date, reason);
1789        slf
1790    }
1791
1792    /// Set the ``thisUpdate`` time from a timezone-aware
1793    /// :class:`datetime.datetime`.
1794    ///
1795    /// Converts the datetime to a Unix timestamp and formats it as
1796    /// ``"YYYYMMDDHHmmssZ"`` (GeneralizedTime).
1797    ///
1798    /// :raises ValueError: if ``dt`` is naive (no tzinfo) or out of range.
1799    fn this_update_utc<'py>(
1800        slf: Bound<'py, Self>,
1801        dt: &Bound<'_, PyAny>,
1802    ) -> PyResult<Bound<'py, Self>> {
1803        let time_str = py_dt_to_gen_time_str(dt)?;
1804        let old = std::mem::replace(
1805            &mut slf.borrow_mut().inner,
1806            synta_certificate::CertificateListBuilder::new(),
1807        );
1808        slf.borrow_mut().inner = old.this_update(&time_str);
1809        Ok(slf)
1810    }
1811
1812    /// Set the optional ``nextUpdate`` time from a timezone-aware
1813    /// :class:`datetime.datetime`.
1814    ///
1815    /// :raises ValueError: if ``dt`` is naive (no tzinfo) or out of range.
1816    fn next_update_utc<'py>(
1817        slf: Bound<'py, Self>,
1818        dt: &Bound<'_, PyAny>,
1819    ) -> PyResult<Bound<'py, Self>> {
1820        let time_str = py_dt_to_gen_time_str(dt)?;
1821        let old = std::mem::replace(
1822            &mut slf.borrow_mut().inner,
1823            synta_certificate::CertificateListBuilder::new(),
1824        );
1825        slf.borrow_mut().inner = old.next_update(&time_str);
1826        Ok(slf)
1827    }
1828
1829    /// Add a revoked certificate entry using a timezone-aware
1830    /// :class:`datetime.datetime` for the revocation time.
1831    ///
1832    /// ``serial`` is the big-endian DER INTEGER value bytes of the certificate
1833    /// serial number.  ``reason`` is an optional CRL reason code integer
1834    /// (0=unspecified, 1=keyCompromise, 2=cACompromise, …, 10=aACompromise).
1835    ///
1836    /// :raises ValueError: if ``revocation_dt`` is naive (no tzinfo) or out of range.
1837    fn revoke_utc<'py>(
1838        slf: Bound<'py, Self>,
1839        serial: &[u8],
1840        revocation_dt: &Bound<'_, PyAny>,
1841        reason: Option<u8>,
1842    ) -> PyResult<Bound<'py, Self>> {
1843        let time_str = py_dt_to_gen_time_str(revocation_dt)?;
1844        let old = std::mem::replace(
1845            &mut slf.borrow_mut().inner,
1846            synta_certificate::CertificateListBuilder::new(),
1847        );
1848        slf.borrow_mut().inner = old.revoke(serial, &time_str, reason);
1849        Ok(slf)
1850    }
1851
1852    /// Set the signature ``AlgorithmIdentifier`` DER for ``TBSCertList.signature``.
1853    ///
1854    /// Must be a complete ``AlgorithmIdentifier`` SEQUENCE TLV.
1855    fn signature_algorithm<'py>(slf: Bound<'py, Self>, alg_der: &[u8]) -> Bound<'py, Self> {
1856        let old = std::mem::replace(
1857            &mut slf.borrow_mut().inner,
1858            synta_certificate::CertificateListBuilder::new(),
1859        );
1860        slf.borrow_mut().inner = old.signature_algorithm(alg_der);
1861        slf
1862    }
1863
1864    /// Build the DER-encoded ``TBSCertList`` SEQUENCE.
1865    ///
1866    /// Required fields: ``issuer``, ``this_update``, ``signature_algorithm``.
1867    ///
1868    /// :raises ValueError: if any required field is absent or encoding fails.
1869    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
1870        let inner = std::mem::replace(
1871            &mut self.inner,
1872            synta_certificate::CertificateListBuilder::new(),
1873        );
1874        let der = inner
1875            .build()
1876            .map_err(pyo3::exceptions::PyValueError::new_err)?;
1877        Ok(PyBytes::new(py, &der))
1878    }
1879
1880    /// Assemble a DER-encoded ``CertificateList`` from its three components.
1881    ///
1882    /// ``tbs_der`` is the ``TBSCertList`` from :meth:`build`.
1883    /// ``sig_alg_der`` is the outer ``AlgorithmIdentifier`` SEQUENCE TLV.
1884    /// ``signature`` is the raw signature bytes (BIT STRING value; unused-bits byte
1885    /// is added automatically).
1886    ///
1887    /// :raises ValueError: if DER encoding fails.
1888    #[staticmethod]
1889    fn assemble<'py>(
1890        py: Python<'py>,
1891        tbs_der: &[u8],
1892        sig_alg_der: &[u8],
1893        signature: &[u8],
1894    ) -> PyResult<Bound<'py, PyBytes>> {
1895        let der =
1896            synta_certificate::CertificateListBuilder::assemble(tbs_der, sig_alg_der, signature)
1897                .map_err(pyo3::exceptions::PyValueError::new_err)?;
1898        Ok(PyBytes::new(py, &der))
1899    }
1900
1901    fn __repr__(&self) -> String {
1902        "CertificateListBuilder()".to_string()
1903    }
1904}
1905
1906// ── OCSPResponseBuilder ───────────────────────────────────────────────────────
1907
1908/// Parameters for a single OCSP response entry.
1909///
1910/// Pass an instance to :meth:`OCSPResponseBuilder.add_response`.
1911///
1912/// ```python,ignore
1913/// resp = synta.OCSPSingleResponse(
1914///     hash_algorithm_der=sha1_alg_der,
1915///     issuer_name_hash=name_hash,
1916///     issuer_key_hash=key_hash,
1917///     serial=serial_bytes,
1918///     status=0,
1919///     this_update="20240101120000Z",
1920///     next_update="20240201120000Z",  # optional
1921/// )
1922/// ```
1923#[pyclass(name = "OCSPSingleResponse")]
1924pub struct PyOCSPSingleResponse {
1925    pub(crate) hash_algorithm_der: Vec<u8>,
1926    pub(crate) issuer_name_hash: Vec<u8>,
1927    pub(crate) issuer_key_hash: Vec<u8>,
1928    pub(crate) serial: Vec<u8>,
1929    pub(crate) status: u8,
1930    pub(crate) this_update: String,
1931    pub(crate) next_update: Option<String>,
1932}
1933
1934#[pymethods]
1935impl PyOCSPSingleResponse {
1936    /// Create an ``OCSPSingleResponse`` entry.
1937    ///
1938    /// :param hash_algorithm_der: DER-encoded ``AlgorithmIdentifier`` SEQUENCE TLV used to
1939    ///     hash the issuer name and key.
1940    /// :param issuer_name_hash: hash of the issuer ``Name`` DER encoding.
1941    /// :param issuer_key_hash: hash of the issuer public key BIT STRING value.
1942    /// :param serial: big-endian serial number bytes of the certificate being checked.
1943    /// :param status: revocation status integer — ``0`` = good, ``1`` = revoked, ``2`` = unknown.
1944    /// :param this_update: ``GeneralizedTime`` string (e.g. ``"20240101120000Z"``).
1945    /// :param next_update: optional ``GeneralizedTime`` string for the next update time.
1946    #[new]
1947    #[pyo3(signature = (hash_algorithm_der, issuer_name_hash, issuer_key_hash, serial, status, this_update, next_update=None))]
1948    fn new(
1949        hash_algorithm_der: &[u8],
1950        issuer_name_hash: &[u8],
1951        issuer_key_hash: &[u8],
1952        serial: &[u8],
1953        status: u8,
1954        this_update: &str,
1955        next_update: Option<&str>,
1956    ) -> Self {
1957        Self {
1958            hash_algorithm_der: hash_algorithm_der.to_vec(),
1959            issuer_name_hash: issuer_name_hash.to_vec(),
1960            issuer_key_hash: issuer_key_hash.to_vec(),
1961            serial: serial.to_vec(),
1962            status,
1963            this_update: this_update.to_owned(),
1964            next_update: next_update.map(str::to_owned),
1965        }
1966    }
1967}
1968
1969/// Python-facing wrapper for [`synta_certificate::OCSPResponseBuilder`].
1970///
1971/// Builds a DER-encoded ``ResponseData`` (RFC 6960) for external signing.
1972/// After signing, call :meth:`assemble` to produce the complete
1973/// ``OCSPResponse`` DER blob.
1974///
1975/// ```python,ignore
1976/// import synta
1977///
1978/// resp = synta.OCSPSingleResponse(
1979///     hash_algorithm_der=sha1_alg_der,
1980///     issuer_name_hash=name_hash,
1981///     issuer_key_hash=key_hash,
1982///     serial=serial_bytes,
1983///     status=0,  # good
1984///     this_update="20240101120000Z",
1985///     next_update="20240201120000Z",
1986/// )
1987/// tbs = (
1988///     synta.OCSPResponseBuilder()
1989///     .responder_key_hash(key_hash_bytes)
1990///     .produced_at("20240101120000Z")
1991///     .add_response(resp)
1992///     .build_tbs()
1993/// )
1994/// ocsp_der = synta.OCSPResponseBuilder.assemble(tbs, sig_alg_der, sig_bytes)
1995/// ```
1996#[pyclass(name = "OCSPResponseBuilder")]
1997pub struct PyOCSPResponseBuilder {
1998    inner: synta_certificate::OCSPResponseBuilder,
1999}
2000
2001#[pymethods]
2002impl PyOCSPResponseBuilder {
2003    /// Create a new, empty ``OCSPResponseBuilder``.
2004    #[new]
2005    fn new() -> Self {
2006        Self {
2007            inner: synta_certificate::OCSPResponseBuilder::new(),
2008        }
2009    }
2010
2011    /// Set ``responderID byName`` from a pre-encoded DER Name SEQUENCE TLV.
2012    ///
2013    /// :param name_der: DER-encoded ``Name`` SEQUENCE bytes.
2014    /// :raises ValueError: if ``name_der`` is not a valid Name.
2015    fn responder_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
2016        let old = std::mem::replace(
2017            &mut slf.borrow_mut().inner,
2018            synta_certificate::OCSPResponseBuilder::new(),
2019        );
2020        slf.borrow_mut().inner = old.responder_name(name_der);
2021        slf
2022    }
2023
2024    /// Set ``responderID byKey`` from the raw key-hash bytes.
2025    ///
2026    /// ``key_hash`` is the raw hash bytes without any TLV wrapper — typically
2027    /// the SHA-1 hash of the issuer's ``subjectPublicKey`` BIT STRING value.
2028    ///
2029    /// :param key_hash: raw SHA-1 key-hash bytes.
2030    fn responder_key_hash<'py>(slf: Bound<'py, Self>, key_hash: &[u8]) -> Bound<'py, Self> {
2031        let old = std::mem::replace(
2032            &mut slf.borrow_mut().inner,
2033            synta_certificate::OCSPResponseBuilder::new(),
2034        );
2035        slf.borrow_mut().inner = old.responder_key_hash(key_hash);
2036        slf
2037    }
2038
2039    /// Set ``producedAt`` GeneralizedTime.
2040    ///
2041    /// :param time: time string in ``YYYYMMDDHHmmssZ`` format.
2042    /// :raises ValueError: if the time string is malformed (deferred to :meth:`build_tbs`).
2043    fn produced_at<'py>(slf: Bound<'py, Self>, time: &str) -> Bound<'py, Self> {
2044        let old = std::mem::replace(
2045            &mut slf.borrow_mut().inner,
2046            synta_certificate::OCSPResponseBuilder::new(),
2047        );
2048        slf.borrow_mut().inner = old.produced_at(time);
2049        slf
2050    }
2051
2052    /// Add a ``SingleResponse`` entry.
2053    ///
2054    /// :param response: an :class:`OCSPSingleResponse` instance.
2055    /// :raises ValueError: if any parameter is invalid (deferred to :meth:`build_tbs`).
2056    fn add_response<'py>(
2057        slf: Bound<'py, Self>,
2058        response: &PyOCSPSingleResponse,
2059    ) -> Bound<'py, Self> {
2060        let old = std::mem::replace(
2061            &mut slf.borrow_mut().inner,
2062            synta_certificate::OCSPResponseBuilder::new(),
2063        );
2064        slf.borrow_mut().inner = old.add_response(synta_certificate::SingleResponseSpec {
2065            hash_algorithm_der: &response.hash_algorithm_der,
2066            issuer_name_hash: &response.issuer_name_hash,
2067            issuer_key_hash: &response.issuer_key_hash,
2068            serial: &response.serial,
2069            status: response.status,
2070            this_update: &response.this_update,
2071            next_update: response.next_update.as_deref(),
2072        });
2073        slf
2074    }
2075
2076    /// Build the DER-encoded ``ResponseData`` SEQUENCE.
2077    ///
2078    /// :returns: DER bytes of the ``ResponseData``.
2079    /// :raises ValueError: if any required field is absent or encoding fails.
2080    fn build_tbs<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
2081        let inner = std::mem::replace(
2082            &mut self.inner,
2083            synta_certificate::OCSPResponseBuilder::new(),
2084        );
2085        let der = inner
2086            .build_tbs()
2087            .map_err(pyo3::exceptions::PyValueError::new_err)?;
2088        Ok(PyBytes::new(py, &der))
2089    }
2090
2091    /// Assemble a complete DER-encoded ``OCSPResponse``.
2092    ///
2093    /// :param tbs_der: ``ResponseData`` bytes from :meth:`build_tbs`.
2094    /// :param sig_alg_der: outer ``AlgorithmIdentifier`` SEQUENCE TLV.
2095    /// :param signature: raw signature bytes (BIT STRING value).
2096    /// :raises ValueError: if DER encoding fails.
2097    #[staticmethod]
2098    fn assemble<'py>(
2099        py: Python<'py>,
2100        tbs_der: &[u8],
2101        sig_alg_der: &[u8],
2102        signature: &[u8],
2103    ) -> PyResult<Bound<'py, PyBytes>> {
2104        let der = synta_certificate::OCSPResponseBuilder::assemble(tbs_der, sig_alg_der, signature)
2105            .map_err(pyo3::exceptions::PyValueError::new_err)?;
2106        Ok(PyBytes::new(py, &der))
2107    }
2108
2109    fn __repr__(&self) -> String {
2110        "OCSPResponseBuilder()".to_string()
2111    }
2112}