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 ``openssl`` Cargo feature is enabled.
1255/// Without that feature a :exc:`ValueError` is raised on any encrypted bag.
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    #[cfg(feature = "openssl")]
1272    let der_certs =
1273        synta_certificate::certs_from_pkcs12(data, pw, &synta_certificate::OpensslDecryptor)
1274            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1275    #[cfg(not(feature = "openssl"))]
1276    let der_certs = synta_certificate::certs_from_pkcs12(data, pw, &synta_certificate::NoCrypto)
1277        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1278    der_certs_to_pylist(py, der_certs)
1279}
1280
1281/// Extract PKCS#8 private keys from a PKCS#12 archive.
1282///
1283/// Returns a :class:`list` of :class:`bytes` objects, one per private key
1284/// found in ``keyBag`` (unencrypted) and ``pkcs8ShroudedKeyBag`` (encrypted,
1285/// decrypted when the ``openssl`` Cargo feature is enabled and ``password``
1286/// is given) entries.  Each element is a DER-encoded ``OneAsymmetricKey``
1287/// (PKCS#8) structure, suitable for passing to a cryptography library.
1288///
1289/// ``certBag``, ``crlBag``, ``secretBag`` and any unknown bag types are
1290/// silently skipped.
1291///
1292/// ```python,ignore
1293/// data = open("archive.p12", "rb").read()
1294/// keys = synta.load_pkcs12_keys(data, b"s3cr3t")
1295/// for key_der in keys:
1296///     from cryptography.hazmat.primitives.serialization import load_der_private_key
1297///     key = load_der_private_key(key_der, None)
1298/// ```
1299#[pyfunction]
1300#[pyo3(signature = (data, password = None))]
1301pub fn load_pkcs12_keys<'py>(
1302    py: Python<'py>,
1303    data: &[u8],
1304    password: Option<&[u8]>,
1305) -> PyResult<Bound<'py, PyList>> {
1306    let pw = password.unwrap_or(b"");
1307    #[cfg(feature = "openssl")]
1308    let der_keys =
1309        synta_certificate::keys_from_pkcs12(data, pw, &synta_certificate::OpensslDecryptor)
1310            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1311    #[cfg(not(feature = "openssl"))]
1312    let der_keys = synta_certificate::keys_from_pkcs12(data, pw, &synta_certificate::NoCrypto)
1313        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1314    let list = PyList::empty(py);
1315    for der in der_keys {
1316        list.append(PyBytes::new(py, &der))?;
1317    }
1318    Ok(list)
1319}
1320
1321/// Extract both certificates and private keys from a PKCS#12 archive.
1322///
1323/// Returns a 2-tuple ``(certs, keys)`` where:
1324///
1325/// - ``certs`` is a :class:`list` of :class:`Certificate` objects.
1326/// - ``keys`` is a :class:`list` of :class:`bytes`, each a DER-encoded
1327///   PKCS#8 ``OneAsymmetricKey`` structure.
1328///
1329/// Encrypted bags are decrypted with ``password`` when the ``openssl``
1330/// Cargo feature is enabled; without it, encrypted bags are silently
1331/// skipped and a :exc:`ValueError` is raised only on structural errors.
1332/// Pass ``b""`` or omit ``password`` for password-less archives.
1333///
1334/// Use :func:`load_pkcs12_certificates` if you only need certificates, or
1335/// :func:`load_pkcs12_keys` if you only need keys.
1336///
1337/// ```python,ignore
1338/// data = open("archive.p12", "rb").read()
1339/// certs, keys = synta.load_pkcs12(data, b"s3cr3t")
1340/// for cert in certs:
1341///     print(cert.subject)
1342/// for key_der in keys:
1343///     from cryptography.hazmat.primitives.serialization import load_der_private_key
1344///     key = load_der_private_key(key_der, None)
1345/// ```
1346#[pyfunction]
1347#[pyo3(signature = (data, password = None))]
1348pub fn load_pkcs12<'py>(
1349    py: Python<'py>,
1350    data: &[u8],
1351    password: Option<&[u8]>,
1352) -> PyResult<Bound<'py, pyo3::types::PyTuple>> {
1353    let pw = password.unwrap_or(b"");
1354    #[cfg(feature = "openssl")]
1355    let pki = synta_certificate::pki_from_pkcs12(data, pw, &synta_certificate::OpensslDecryptor)
1356        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1357    #[cfg(not(feature = "openssl"))]
1358    let pki = synta_certificate::pki_from_pkcs12(data, pw, &synta_certificate::NoCrypto)
1359        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1360    let certs = der_certs_to_pylist(py, pki.certs)?;
1361    let keys = PyList::empty(py);
1362    for der in pki.keys {
1363        keys.append(PyBytes::new(py, &der))?;
1364    }
1365    pyo3::types::PyTuple::new(py, [certs.into_any(), keys.into_any()])
1366}
1367
1368/// Auto-detect the encoding of ``data`` and return every PKI object as a
1369/// ``(label, der_bytes)`` tuple, in document order.
1370///
1371/// Supported formats: PEM (any label), PKCS#7 / CMS SignedData (DER or BER),
1372/// PKCS#12 PFX (DER or BER), and raw DER.
1373///
1374/// **Labels:**
1375///
1376/// - PEM blocks whose payload is a PKCS#7 ``ContentInfo`` (detected by
1377///   structure, not label) are expanded: the embedded certificates are
1378///   extracted and each is returned as ``("CERTIFICATE", der)``.
1379/// - All other PEM block types pass through with their original label
1380///   (e.g. ``"CERTIFICATE"``, ``"PRIVATE KEY"``).  Malformed PEM blocks
1381///   (bad base64, truncated header) are silently skipped.
1382/// - Binary PKCS#7 and raw DER yield ``"CERTIFICATE"`` labels.
1383/// - Binary PKCS#12 yields ``"CERTIFICATE"`` for ``certBag`` entries and
1384///   ``"PRIVATE KEY"`` for ``keyBag`` / ``pkcs8ShroudedKeyBag`` entries.
1385///
1386/// **Decryption:**
1387///
1388/// ``password`` applies only to PKCS#12 archives.  When ``password`` is
1389/// given **and** the library was built with the ``openssl`` Cargo feature,
1390/// encrypted SafeBags are decrypted.  Otherwise encrypted bags are silently
1391/// skipped; unencrypted certificates in the same archive are still returned.
1392///
1393/// :raises ValueError: For any structural failure:
1394///
1395///   - A PKCS#7 block (binary or PEM-wrapped) has malformed DER, or its
1396///     ``contentType`` OID is not ``id-signedData``.
1397///   - The PKCS#12 input has a structural ASN.1 error.
1398///   - ``password`` was supplied with the ``openssl`` feature enabled and
1399///     decryption of an encrypted SafeBag failed (e.g. wrong password,
1400///     unsupported cipher).
1401///   - The PKCS#12 authSafe uses an unsupported ``ContentInfo`` type.
1402///
1403/// :raises TypeError: If ``data`` is not :class:`bytes`, or ``password``
1404///   is not :class:`bytes` or ``None``.
1405///
1406/// ```python,ignore
1407/// data = open("bundle.p7b", "rb").read()
1408/// blocks = synta.read_pki_blocks(data)
1409/// for label, der in blocks:
1410///     print(f"{label}: {len(der)} bytes")
1411///
1412/// # PKCS#12 with encryption
1413/// data = open("archive.p12", "rb").read()
1414/// blocks = synta.read_pki_blocks(data, b"s3cr3t")
1415/// ```
1416#[pyfunction]
1417#[pyo3(signature = (data, password = None))]
1418pub fn read_pki_blocks<'py>(
1419    py: Python<'py>,
1420    data: &[u8],
1421    password: Option<&[u8]>,
1422) -> PyResult<Bound<'py, PyList>> {
1423    let pw = password.unwrap_or(b"");
1424    #[cfg(feature = "openssl")]
1425    let blocks = if password.is_some() {
1426        synta_certificate::read_pki_blocks(
1427            data,
1428            pw,
1429            Some(&synta_certificate::OpensslDecryptor as &dyn synta_certificate::PkiDecryptor),
1430        )
1431    } else {
1432        synta_certificate::read_pki_blocks(data, pw, None::<&dyn synta_certificate::PkiDecryptor>)
1433    };
1434    #[cfg(not(feature = "openssl"))]
1435    let blocks =
1436        synta_certificate::read_pki_blocks(data, pw, None::<&dyn synta_certificate::PkiDecryptor>);
1437    let blocks = blocks.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1438    let list = PyList::empty(py);
1439    for (label, der) in blocks {
1440        let tuple = pyo3::types::PyTuple::new(
1441            py,
1442            [
1443                PyString::new(py, &label).into_any(),
1444                PyBytes::new(py, &der).into_any(),
1445            ],
1446        )?;
1447        list.append(tuple)?;
1448    }
1449    Ok(list)
1450}
1451
1452// ── X.509 encoding utilities ──────────────────────────────────────────────────
1453
1454/// Encode a list of OIDs as a DER ``SEQUENCE OF OID`` (ExtendedKeyUsage value).
1455///
1456/// ``oids`` must be a :class:`list` of :class:`~synta.ObjectIdentifier` or
1457/// dotted-decimal ``str`` values.  Returns the complete DER bytes of the
1458/// ``SEQUENCE``, suitable for use as an X.509v3 Extended Key Usage extension
1459/// value.
1460///
1461/// Delegates to synta's ``Vec<ObjectIdentifier>`` encoder which matches the
1462/// generated ``ExtendedKeyUsage = Vec<KeyPurposeId>`` type from the X.509 schema.
1463///
1464/// ```python,ignore
1465/// eku_der = synta.encode_extended_key_usage([
1466///     "1.3.6.1.5.5.7.3.1",   # id-kp-serverAuth
1467///     "1.3.6.1.5.5.7.3.2",   # id-kp-clientAuth
1468/// ])
1469/// ```
1470#[pyfunction]
1471pub fn encode_extended_key_usage<'py>(
1472    py: Python<'py>,
1473    oids: &Bound<'_, PyList>,
1474) -> PyResult<Bound<'py, PyBytes>> {
1475    use synta::traits::Encode;
1476    let mut eku: Vec<synta::ObjectIdentifier> = Vec::with_capacity(oids.len());
1477    for item in oids.iter() {
1478        eku.push(super::oid_from_pyany(&item)?);
1479    }
1480    let mut enc = synta::Encoder::new(synta::Encoding::Der);
1481    eku.encode(&mut enc)
1482        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1483    let der = enc
1484        .finish()
1485        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1486    Ok(PyBytes::new(py, &der))
1487}
1488
1489/// Encode a list of ``(tag_number, content_bytes)`` pairs as a DER
1490/// ``SEQUENCE OF GeneralName``.
1491///
1492/// This is the inverse of :func:`~synta.parse_general_names`: the input
1493/// format is identical to what that function returns.  Tag numbers follow
1494/// RFC 5280 §4.2.1.6; use the :mod:`synta.general_name` constants.
1495///
1496/// ``content_bytes`` must be the raw **value** bytes — without the outer
1497/// context-specific TLV — exactly as returned by
1498/// :func:`~synta.parse_general_names`.
1499///
1500/// Delegates to :func:`synta_certificate::encode_general_names` which uses the
1501/// generated :class:`GeneralName` enum's ``Encode`` implementation.
1502///
1503/// ```python,ignore
1504/// import synta.general_name as gn
1505///
1506/// san_der = synta.encode_subject_alt_names([
1507///     (gn.DNS_NAME,   b"example.com"),
1508///     (gn.IP_ADDRESS, b"\x7f\x00\x00\x01"),   # 127.0.0.1
1509/// ])
1510/// ```
1511#[pyfunction]
1512pub fn encode_subject_alt_names<'py>(
1513    py: Python<'py>,
1514    names: &Bound<'_, PyList>,
1515) -> PyResult<Bound<'py, PyBytes>> {
1516    // Collect owned (tag, bytes) first so we can create &[u8] references that
1517    // all share the same lifetime, satisfying Vec<GeneralName<'_>> constraints.
1518    let mut owned: Vec<(u32, Vec<u8>)> = Vec::with_capacity(names.len());
1519    for item in names.iter() {
1520        let tuple = item.cast::<pyo3::types::PyTuple>().map_err(|_| {
1521            pyo3::exceptions::PyTypeError::new_err(
1522                "each element must be a (tag_number, bytes) 2-tuple",
1523            )
1524        })?;
1525        if tuple.len() != 2 {
1526            return Err(pyo3::exceptions::PyTypeError::new_err(
1527                "each element must be a (tag_number, bytes) 2-tuple",
1528            ));
1529        }
1530        let tag_num: u32 = tuple.get_item(0)?.extract()?;
1531        let content: Vec<u8> = tuple.get_item(1)?.extract()?;
1532        owned.push((tag_num, content));
1533    }
1534    let entries: Vec<(u32, &[u8])> = owned.iter().map(|(t, v)| (*t, v.as_slice())).collect();
1535    let der = synta_certificate::encode_general_names(&entries).ok_or_else(|| {
1536        pyo3::exceptions::PyValueError::new_err(
1537            "failed to encode GeneralName entries (invalid content bytes or unsupported tag)",
1538        )
1539    })?;
1540    Ok(PyBytes::new(py, &der))
1541}
1542
1543/// Compare two DER-encoded X.500 Names for byte-for-byte equality.
1544///
1545/// Returns ``True`` if and only if the two byte sequences are identical.
1546/// This is a strict comparison; RFC 5280 name matching rules (case-insensitive
1547/// string comparison, etc.) are **not** applied.
1548///
1549/// Useful for quickly checking whether issuer and subject names match without
1550/// decoding the full structure:
1551///
1552/// ```python,ignore
1553/// same = synta.name_der_equal(cert.issuer_raw_der, ca_cert.subject_raw_der)
1554/// ```
1555#[pyfunction]
1556pub fn name_der_equal(a: &[u8], b: &[u8]) -> bool {
1557    a == b
1558}
1559
1560/// Build a DER-encoded PKCS#12 ``PFX`` archive.
1561///
1562/// Assembles a ``PFX`` from one or more certificates and an optional
1563/// DER-encoded PKCS#8 private key.  The archive is protected by ``password``
1564/// using PBES2 (PBKDF2-SHA256 + AES-256-CBC) for key encryption and
1565/// HMAC-SHA256 (PBKDF2-SHA256 derived key) for the integrity MAC.
1566///
1567/// :param certificates: sequence of :class:`~synta.Certificate` objects or
1568///     raw DER ``bytes``.  Both types may appear in the same list.
1569/// :param private_key: a :class:`~synta.PrivateKey` object or a DER-encoded
1570///     PKCS#8 ``OneAsymmetricKey`` ``bytes``, or ``None`` to omit.
1571/// :param password: archive password (``bytes``), or ``None`` for a
1572///     password-less archive.
1573/// :returns: DER-encoded ``PFX`` archive bytes.
1574/// :raises TypeError: if any element of ``certificates`` is neither a
1575///     :class:`~synta.Certificate` nor ``bytes``, or if ``private_key`` is
1576///     not a :class:`~synta.PrivateKey` nor ``bytes``.
1577/// :raises ValueError: if no certificates or key are supplied, or if
1578///     encryption fails (e.g. OpenSSL not available).
1579///
1580/// ```python,ignore
1581/// # Certificate and PrivateKey objects — no .to_der() call needed
1582/// pfx = synta.create_pkcs12(
1583///     [cert, ca_cert],
1584///     private_key=key,
1585///     password=b"s3cr3t",
1586/// )
1587///
1588/// # Raw DER bytes also accepted for both
1589/// pfx = synta.create_pkcs12([cert.to_der()], private_key=key.to_der())
1590///
1591/// with open("out.p12", "wb") as f:
1592///     f.write(pfx)
1593/// ```
1594#[pyfunction]
1595#[pyo3(signature = (certificates, private_key = None, password = None))]
1596pub fn create_pkcs12<'py>(
1597    py: Python<'py>,
1598    certificates: &Bound<'_, PyList>,
1599    private_key: Option<Bound<'_, PyAny>>,
1600    password: Option<&[u8]>,
1601) -> PyResult<Bound<'py, PyBytes>> {
1602    use synta_certificate::Pkcs12Builder;
1603
1604    let password = password.unwrap_or(b"");
1605    let mut builder = Pkcs12Builder::new();
1606    for item in certificates.iter() {
1607        if let Ok(py_cert) = item.cast::<PyCertificate>() {
1608            // Zero-copy: raw is &'static [u8] backed by the PyBytes in _data.
1609            // Pkcs12Builder::certificate() copies into Vec<u8> immediately, so
1610            // the borrow does not outlive this loop iteration.
1611            builder = builder.certificate(py_cert.get().raw);
1612        } else if let Ok(py_bytes) = item.cast::<PyBytes>() {
1613            builder = builder.certificate(py_bytes.as_bytes());
1614        } else {
1615            return Err(pyo3::exceptions::PyTypeError::new_err(
1616                "certificates must be synta.Certificate or bytes",
1617            ));
1618        }
1619    }
1620    if let Some(key_arg) = private_key {
1621        if let Ok(py_key) = key_arg.cast::<PyPrivateKey>() {
1622            // PyPrivateKey stores PKCS#8 DER internally; retrieve it directly.
1623            let key_der = py_key
1624                .get()
1625                .inner
1626                .to_der()
1627                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1628            builder = builder.private_key(&key_der);
1629        } else if let Ok(py_bytes) = key_arg.cast::<PyBytes>() {
1630            builder = builder.private_key(py_bytes.as_bytes());
1631        } else {
1632            return Err(pyo3::exceptions::PyTypeError::new_err(
1633                "private_key must be synta.PrivateKey or bytes",
1634            ));
1635        }
1636    }
1637
1638    #[cfg(feature = "openssl")]
1639    let pfx_der = builder
1640        .build(password, &synta_certificate::OpensslPkcs12Encryptor::new())
1641        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1642
1643    #[cfg(not(feature = "openssl"))]
1644    let pfx_der = {
1645        use synta_certificate::NoPkcs12Encryptor;
1646        builder
1647            .build(password, &NoPkcs12Encryptor)
1648            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?
1649    };
1650
1651    Ok(PyBytes::new(py, &pfx_der))
1652}
1653
1654// ── PyCertificateListBuilder helpers ─────────────────────────────────────────
1655
1656/// Convert a Python `datetime.datetime` (must be timezone-aware) to a
1657/// GeneralizedTime string in `"YYYYMMDDHHmmssZ"` format accepted by the
1658/// string-based `CertificateListBuilder` setters.
1659///
1660/// Uses `datetime.timestamp()` to obtain a Unix timestamp and delegates to
1661/// [`synta::GeneralizedTime::from_unix`] for the calendar conversion.
1662fn py_dt_to_gen_time_str(dt: &Bound<'_, PyAny>) -> PyResult<String> {
1663    if dt.getattr("tzinfo")?.is_none() {
1664        return Err(pyo3::exceptions::PyValueError::new_err(
1665            "datetime must be timezone-aware (e.g. datetime.timezone.utc)",
1666        ));
1667    }
1668    let ts: f64 = dt.call_method0("timestamp")?.extract()?;
1669    let secs = ts.floor() as i64;
1670    let gt = synta::GeneralizedTime::from_unix(secs).ok_or_else(|| {
1671        pyo3::exceptions::PyValueError::new_err("datetime out of valid ASN.1 year range 1–9999")
1672    })?;
1673    Ok(gt.to_string())
1674}
1675
1676// ── PyCertificateListBuilder ──────────────────────────────────────────────────
1677
1678/// Fluent builder for a DER-encoded X.509 CRL (RFC 5280 §5).
1679///
1680/// Chain setters to configure the CRL, then call :meth:`build` to obtain the
1681/// DER-encoded ``TBSCertList`` SEQUENCE.  Sign the TBS externally and call the
1682/// static :meth:`assemble` to produce the final ``CertificateList``.
1683///
1684/// ```python,ignore
1685/// import synta
1686///
1687/// name_der = synta.NameBuilder().common_name("Test CA").build()
1688/// alg_der  = bytes.fromhex("300d06092a864886f70d01010b0500")  # sha256WithRSAEncryption
1689/// tbs = (
1690///     synta.CertificateListBuilder()
1691///     .issuer(name_der)
1692///     .signature_algorithm(alg_der)
1693///     .this_update("20240101120000Z")
1694///     .next_update("20240201120000Z")
1695///     .revoke(bytes([1]), "20231201000000Z", 1)  # keyCompromise
1696///     .build()
1697/// )
1698/// # sign tbs externally, then:
1699/// crl_der = synta.CertificateListBuilder.assemble(tbs, alg_der, sig_bytes)
1700/// ```
1701#[pyclass(name = "CertificateListBuilder")]
1702pub struct PyCertificateListBuilder {
1703    inner: synta_certificate::CertificateListBuilder,
1704}
1705
1706#[pymethods]
1707impl PyCertificateListBuilder {
1708    /// Create a new, empty ``CertificateListBuilder``.
1709    #[new]
1710    fn new() -> Self {
1711        Self {
1712            inner: synta_certificate::CertificateListBuilder::new(),
1713        }
1714    }
1715
1716    /// Set the issuer ``Name`` from pre-encoded DER bytes.
1717    fn issuer<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
1718        let old = std::mem::replace(
1719            &mut slf.borrow_mut().inner,
1720            synta_certificate::CertificateListBuilder::new(),
1721        );
1722        slf.borrow_mut().inner = old.issuer(name_der);
1723        slf
1724    }
1725
1726    /// Set the ``thisUpdate`` time (``"YYYYMMDDHHmmssZ"`` or ``"YYMMDDHHmmssZ"``).
1727    fn this_update<'py>(slf: Bound<'py, Self>, time: &str) -> Bound<'py, Self> {
1728        let old = std::mem::replace(
1729            &mut slf.borrow_mut().inner,
1730            synta_certificate::CertificateListBuilder::new(),
1731        );
1732        slf.borrow_mut().inner = old.this_update(time);
1733        slf
1734    }
1735
1736    /// Set the optional ``nextUpdate`` time (same format as :meth:`this_update`).
1737    fn next_update<'py>(slf: Bound<'py, Self>, time: &str) -> Bound<'py, Self> {
1738        let old = std::mem::replace(
1739            &mut slf.borrow_mut().inner,
1740            synta_certificate::CertificateListBuilder::new(),
1741        );
1742        slf.borrow_mut().inner = old.next_update(time);
1743        slf
1744    }
1745
1746    /// Add a revoked certificate entry.
1747    ///
1748    /// ``serial`` is the big-endian DER INTEGER value bytes of the certificate
1749    /// serial number.  ``revocation_date`` uses the same time format as
1750    /// :meth:`this_update`.  ``reason`` is an optional CRL reason code integer
1751    /// (0=unspecified, 1=keyCompromise, 2=cACompromise, …, 10=aACompromise).
1752    fn revoke<'py>(
1753        slf: Bound<'py, Self>,
1754        serial: &[u8],
1755        revocation_date: &str,
1756        reason: Option<u8>,
1757    ) -> Bound<'py, Self> {
1758        let old = std::mem::replace(
1759            &mut slf.borrow_mut().inner,
1760            synta_certificate::CertificateListBuilder::new(),
1761        );
1762        slf.borrow_mut().inner = old.revoke(serial, revocation_date, reason);
1763        slf
1764    }
1765
1766    /// Set the ``thisUpdate`` time from a timezone-aware
1767    /// :class:`datetime.datetime`.
1768    ///
1769    /// Converts the datetime to a Unix timestamp and formats it as
1770    /// ``"YYYYMMDDHHmmssZ"`` (GeneralizedTime).
1771    ///
1772    /// :raises ValueError: if ``dt`` is naive (no tzinfo) or out of range.
1773    fn this_update_utc<'py>(
1774        slf: Bound<'py, Self>,
1775        dt: &Bound<'_, PyAny>,
1776    ) -> PyResult<Bound<'py, Self>> {
1777        let time_str = py_dt_to_gen_time_str(dt)?;
1778        let old = std::mem::replace(
1779            &mut slf.borrow_mut().inner,
1780            synta_certificate::CertificateListBuilder::new(),
1781        );
1782        slf.borrow_mut().inner = old.this_update(&time_str);
1783        Ok(slf)
1784    }
1785
1786    /// Set the optional ``nextUpdate`` time from a timezone-aware
1787    /// :class:`datetime.datetime`.
1788    ///
1789    /// :raises ValueError: if ``dt`` is naive (no tzinfo) or out of range.
1790    fn next_update_utc<'py>(
1791        slf: Bound<'py, Self>,
1792        dt: &Bound<'_, PyAny>,
1793    ) -> PyResult<Bound<'py, Self>> {
1794        let time_str = py_dt_to_gen_time_str(dt)?;
1795        let old = std::mem::replace(
1796            &mut slf.borrow_mut().inner,
1797            synta_certificate::CertificateListBuilder::new(),
1798        );
1799        slf.borrow_mut().inner = old.next_update(&time_str);
1800        Ok(slf)
1801    }
1802
1803    /// Add a revoked certificate entry using a timezone-aware
1804    /// :class:`datetime.datetime` for the revocation time.
1805    ///
1806    /// ``serial`` is the big-endian DER INTEGER value bytes of the certificate
1807    /// serial number.  ``reason`` is an optional CRL reason code integer
1808    /// (0=unspecified, 1=keyCompromise, 2=cACompromise, …, 10=aACompromise).
1809    ///
1810    /// :raises ValueError: if ``revocation_dt`` is naive (no tzinfo) or out of range.
1811    fn revoke_utc<'py>(
1812        slf: Bound<'py, Self>,
1813        serial: &[u8],
1814        revocation_dt: &Bound<'_, PyAny>,
1815        reason: Option<u8>,
1816    ) -> PyResult<Bound<'py, Self>> {
1817        let time_str = py_dt_to_gen_time_str(revocation_dt)?;
1818        let old = std::mem::replace(
1819            &mut slf.borrow_mut().inner,
1820            synta_certificate::CertificateListBuilder::new(),
1821        );
1822        slf.borrow_mut().inner = old.revoke(serial, &time_str, reason);
1823        Ok(slf)
1824    }
1825
1826    /// Set the signature ``AlgorithmIdentifier`` DER for ``TBSCertList.signature``.
1827    ///
1828    /// Must be a complete ``AlgorithmIdentifier`` SEQUENCE TLV.
1829    fn signature_algorithm<'py>(slf: Bound<'py, Self>, alg_der: &[u8]) -> Bound<'py, Self> {
1830        let old = std::mem::replace(
1831            &mut slf.borrow_mut().inner,
1832            synta_certificate::CertificateListBuilder::new(),
1833        );
1834        slf.borrow_mut().inner = old.signature_algorithm(alg_der);
1835        slf
1836    }
1837
1838    /// Build the DER-encoded ``TBSCertList`` SEQUENCE.
1839    ///
1840    /// Required fields: ``issuer``, ``this_update``, ``signature_algorithm``.
1841    ///
1842    /// :raises ValueError: if any required field is absent or encoding fails.
1843    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
1844        let inner = std::mem::replace(
1845            &mut self.inner,
1846            synta_certificate::CertificateListBuilder::new(),
1847        );
1848        let der = inner
1849            .build()
1850            .map_err(pyo3::exceptions::PyValueError::new_err)?;
1851        Ok(PyBytes::new(py, &der))
1852    }
1853
1854    /// Assemble a DER-encoded ``CertificateList`` from its three components.
1855    ///
1856    /// ``tbs_der`` is the ``TBSCertList`` from :meth:`build`.
1857    /// ``sig_alg_der`` is the outer ``AlgorithmIdentifier`` SEQUENCE TLV.
1858    /// ``signature`` is the raw signature bytes (BIT STRING value; unused-bits byte
1859    /// is added automatically).
1860    ///
1861    /// :raises ValueError: if DER encoding fails.
1862    #[staticmethod]
1863    fn assemble<'py>(
1864        py: Python<'py>,
1865        tbs_der: &[u8],
1866        sig_alg_der: &[u8],
1867        signature: &[u8],
1868    ) -> PyResult<Bound<'py, PyBytes>> {
1869        let der =
1870            synta_certificate::CertificateListBuilder::assemble(tbs_der, sig_alg_der, signature)
1871                .map_err(pyo3::exceptions::PyValueError::new_err)?;
1872        Ok(PyBytes::new(py, &der))
1873    }
1874
1875    fn __repr__(&self) -> String {
1876        "CertificateListBuilder()".to_string()
1877    }
1878}
1879
1880// ── OCSPResponseBuilder ───────────────────────────────────────────────────────
1881
1882/// Python-facing wrapper for [`synta_certificate::OCSPResponseBuilder`].
1883///
1884/// Builds a DER-encoded ``ResponseData`` (RFC 6960) for external signing.
1885/// After signing, call :meth:`assemble` to produce the complete
1886/// ``OCSPResponse`` DER blob.
1887///
1888/// # Example
1889///
1890/// ```python,ignore
1891/// import synta
1892///
1893/// resp = synta.OCSPSingleResponse(
1894///     hash_algorithm_der=sha1_alg_der,
1895///     issuer_name_hash=name_hash,
1896///     issuer_key_hash=key_hash,
1897///     serial=serial_bytes,
1898///     status=0,  # good
1899///     this_update="20240101120000Z",
1900///     next_update="20240201120000Z",
1901/// )
1902/// tbs = (
1903///     synta.OCSPResponseBuilder()
1904///     .responder_key_hash(key_hash_bytes)
1905///     .produced_at("20240101120000Z")
1906///     .add_response(resp)
1907///     .build_tbs()
1908/// )
1909/// ocsp_der = synta.OCSPResponseBuilder.assemble(tbs, sig_alg_der, sig_bytes)
1910/// ```
1911/// Parameters for a single OCSP response entry.
1912///
1913/// Pass an instance to :meth:`OCSPResponseBuilder.add_response`.
1914///
1915/// ```python,ignore
1916/// resp = synta.OCSPSingleResponse(
1917///     hash_algorithm_der=sha1_alg_der,
1918///     issuer_name_hash=name_hash,
1919///     issuer_key_hash=key_hash,
1920///     serial=serial_bytes,
1921///     status=0,
1922///     this_update="20240101120000Z",
1923///     next_update="20240201120000Z",  # optional
1924/// )
1925/// ```
1926#[pyclass(name = "OCSPSingleResponse")]
1927pub struct PyOCSPSingleResponse {
1928    pub(crate) hash_algorithm_der: Vec<u8>,
1929    pub(crate) issuer_name_hash: Vec<u8>,
1930    pub(crate) issuer_key_hash: Vec<u8>,
1931    pub(crate) serial: Vec<u8>,
1932    pub(crate) status: u8,
1933    pub(crate) this_update: String,
1934    pub(crate) next_update: Option<String>,
1935}
1936
1937#[pymethods]
1938impl PyOCSPSingleResponse {
1939    #[new]
1940    #[pyo3(signature = (hash_algorithm_der, issuer_name_hash, issuer_key_hash, serial, status, this_update, next_update=None))]
1941    fn new(
1942        hash_algorithm_der: &[u8],
1943        issuer_name_hash: &[u8],
1944        issuer_key_hash: &[u8],
1945        serial: &[u8],
1946        status: u8,
1947        this_update: &str,
1948        next_update: Option<&str>,
1949    ) -> Self {
1950        Self {
1951            hash_algorithm_der: hash_algorithm_der.to_vec(),
1952            issuer_name_hash: issuer_name_hash.to_vec(),
1953            issuer_key_hash: issuer_key_hash.to_vec(),
1954            serial: serial.to_vec(),
1955            status,
1956            this_update: this_update.to_owned(),
1957            next_update: next_update.map(str::to_owned),
1958        }
1959    }
1960}
1961
1962#[pyclass(name = "OCSPResponseBuilder")]
1963pub struct PyOCSPResponseBuilder {
1964    inner: synta_certificate::OCSPResponseBuilder,
1965}
1966
1967#[pymethods]
1968impl PyOCSPResponseBuilder {
1969    #[new]
1970    fn new() -> Self {
1971        Self {
1972            inner: synta_certificate::OCSPResponseBuilder::new(),
1973        }
1974    }
1975
1976    /// Set ``responderID byName`` from a pre-encoded DER Name SEQUENCE TLV.
1977    ///
1978    /// :param name_der: DER-encoded ``Name`` SEQUENCE bytes.
1979    /// :raises ValueError: if ``name_der`` is not a valid Name.
1980    fn responder_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
1981        let old = std::mem::replace(
1982            &mut slf.borrow_mut().inner,
1983            synta_certificate::OCSPResponseBuilder::new(),
1984        );
1985        slf.borrow_mut().inner = old.responder_name(name_der);
1986        slf
1987    }
1988
1989    /// Set ``responderID byKey`` from the raw key-hash bytes.
1990    ///
1991    /// ``key_hash`` is the raw hash bytes without any TLV wrapper — typically
1992    /// the SHA-1 hash of the issuer's ``subjectPublicKey`` BIT STRING value.
1993    ///
1994    /// :param key_hash: raw SHA-1 key-hash bytes.
1995    fn responder_key_hash<'py>(slf: Bound<'py, Self>, key_hash: &[u8]) -> Bound<'py, Self> {
1996        let old = std::mem::replace(
1997            &mut slf.borrow_mut().inner,
1998            synta_certificate::OCSPResponseBuilder::new(),
1999        );
2000        slf.borrow_mut().inner = old.responder_key_hash(key_hash);
2001        slf
2002    }
2003
2004    /// Set ``producedAt`` GeneralizedTime.
2005    ///
2006    /// :param time: time string in ``YYYYMMDDHHmmssZ`` format.
2007    /// :raises ValueError: if the time string is malformed (deferred to :meth:`build_tbs`).
2008    fn produced_at<'py>(slf: Bound<'py, Self>, time: &str) -> Bound<'py, Self> {
2009        let old = std::mem::replace(
2010            &mut slf.borrow_mut().inner,
2011            synta_certificate::OCSPResponseBuilder::new(),
2012        );
2013        slf.borrow_mut().inner = old.produced_at(time);
2014        slf
2015    }
2016
2017    /// Add a ``SingleResponse`` entry.
2018    ///
2019    /// :param response: an :class:`OCSPSingleResponse` instance.
2020    /// :raises ValueError: if any parameter is invalid (deferred to :meth:`build_tbs`).
2021    fn add_response<'py>(
2022        slf: Bound<'py, Self>,
2023        response: &PyOCSPSingleResponse,
2024    ) -> Bound<'py, Self> {
2025        let old = std::mem::replace(
2026            &mut slf.borrow_mut().inner,
2027            synta_certificate::OCSPResponseBuilder::new(),
2028        );
2029        slf.borrow_mut().inner = old.add_response(synta_certificate::SingleResponseSpec {
2030            hash_algorithm_der: &response.hash_algorithm_der,
2031            issuer_name_hash: &response.issuer_name_hash,
2032            issuer_key_hash: &response.issuer_key_hash,
2033            serial: &response.serial,
2034            status: response.status,
2035            this_update: &response.this_update,
2036            next_update: response.next_update.as_deref(),
2037        });
2038        slf
2039    }
2040
2041    /// Build the DER-encoded ``ResponseData`` SEQUENCE.
2042    ///
2043    /// :returns: DER bytes of the ``ResponseData``.
2044    /// :raises ValueError: if any required field is absent or encoding fails.
2045    fn build_tbs<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
2046        let inner = std::mem::replace(
2047            &mut self.inner,
2048            synta_certificate::OCSPResponseBuilder::new(),
2049        );
2050        let der = inner
2051            .build_tbs()
2052            .map_err(pyo3::exceptions::PyValueError::new_err)?;
2053        Ok(PyBytes::new(py, &der))
2054    }
2055
2056    /// Assemble a complete DER-encoded ``OCSPResponse``.
2057    ///
2058    /// :param tbs_der: ``ResponseData`` bytes from :meth:`build_tbs`.
2059    /// :param sig_alg_der: outer ``AlgorithmIdentifier`` SEQUENCE TLV.
2060    /// :param signature: raw signature bytes (BIT STRING value).
2061    /// :raises ValueError: if DER encoding fails.
2062    #[staticmethod]
2063    fn assemble<'py>(
2064        py: Python<'py>,
2065        tbs_der: &[u8],
2066        sig_alg_der: &[u8],
2067        signature: &[u8],
2068    ) -> PyResult<Bound<'py, PyBytes>> {
2069        let der = synta_certificate::OCSPResponseBuilder::assemble(tbs_der, sig_alg_der, signature)
2070            .map_err(pyo3::exceptions::PyValueError::new_err)?;
2071        Ok(PyBytes::new(py, &der))
2072    }
2073
2074    fn __repr__(&self) -> String {
2075        "OCSPResponseBuilder()".to_string()
2076    }
2077}