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// ── OCSPRequest / CertID ─────────────────────────────────────────────────────
1168
1169type OcspRequest2024 = synta_certificate::ocsp_2024_88_types::OCSPRequest<'static>;
1170
1171/// OCSP CertID — identifies a certificate by its issuer hashes and serial number.
1172#[pyclass(frozen, name = "CertID")]
1173pub struct PyCertID {
1174    hash_alg_oid: synta::ObjectIdentifier,
1175    issuer_name_hash: Vec<u8>,
1176    issuer_key_hash: Vec<u8>,
1177    serial: synta::Integer,
1178    hash_algorithm_oid_cache: OnceLock<Py<PyObjectIdentifier>>,
1179    serial_number_cache: OnceLock<Py<PyAny>>,
1180}
1181
1182impl PyCertID {
1183    fn from_cert_id(c: &synta_certificate::ocsp_2024_88_types::CertID<'_>) -> Self {
1184        Self {
1185            hash_alg_oid: c.hash_algorithm.algorithm.clone(),
1186            issuer_name_hash: c.issuer_name_hash.as_bytes().to_vec(),
1187            issuer_key_hash: c.issuer_key_hash.as_bytes().to_vec(),
1188            serial: c.serial_number.clone(),
1189            hash_algorithm_oid_cache: OnceLock::new(),
1190            serial_number_cache: OnceLock::new(),
1191        }
1192    }
1193}
1194
1195#[pymethods]
1196impl PyCertID {
1197    /// Dotted-decimal OID of the hash algorithm used to compute the issuer hashes.
1198    #[getter]
1199    fn hash_algorithm_oid<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
1200        if let Some(cached) = self.hash_algorithm_oid_cache.get() {
1201            return Ok(cached.clone_ref(py).into_bound(py));
1202        }
1203        let oid = Py::new(py, PyObjectIdentifier::from_oid(self.hash_alg_oid.clone()))?;
1204        let _ = self.hash_algorithm_oid_cache.set(oid.clone_ref(py));
1205        Ok(oid.into_bound(py))
1206    }
1207
1208    /// Raw bytes of the issuer DN hash.
1209    #[getter]
1210    fn issuer_name_hash<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1211        PyBytes::new(py, &self.issuer_name_hash)
1212    }
1213
1214    /// Raw bytes of the issuer public key hash.
1215    #[getter]
1216    fn issuer_key_hash<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1217        PyBytes::new(py, &self.issuer_key_hash)
1218    }
1219
1220    /// Certificate serial number as a Python ``int``.
1221    #[getter]
1222    fn serial_number<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1223        if let Some(cached) = self.serial_number_cache.get() {
1224            return Ok(cached.clone_ref(py).into_bound(py));
1225        }
1226        let py_int: Py<PyAny> = if let Ok(v) = self.serial.as_i64() {
1227            v.into_pyobject(py)?.into_any().unbind()
1228        } else if let Ok(v) = self.serial.as_i128() {
1229            v.into_pyobject(py)?.into_any().unbind()
1230        } else {
1231            let bytes_obj = PyBytes::new(py, self.serial.as_bytes());
1232            let kwargs = pyo3::types::PyDict::new(py);
1233            kwargs.set_item(pyo3::intern!(py, "signed"), true)?;
1234            py.get_type::<pyo3::types::PyInt>()
1235                .call_method(
1236                    pyo3::intern!(py, "from_bytes"),
1237                    (bytes_obj, pyo3::intern!(py, "big")),
1238                    Some(&kwargs),
1239                )
1240                .map(|r| r.unbind())?
1241        };
1242        let _ = self.serial_number_cache.set(py_int.clone_ref(py));
1243        Ok(py_int.into_bound(py))
1244    }
1245
1246    fn __repr__(&self) -> String {
1247        format!(
1248            "CertID(hash_alg={}, serial=<{} bytes>)",
1249            self.hash_alg_oid,
1250            self.serial.as_bytes().len(),
1251        )
1252    }
1253}
1254
1255/// Parsed OCSP Request (RFC 9654 / RFC 6960).
1256///
1257/// ```python,ignore
1258/// req = OCSPRequest.from_der(open("request.der", "rb").read())
1259/// for cert_id in req.request_list:
1260///     print(cert_id.serial_number)
1261/// ```
1262#[pyclass(frozen, name = "OCSPRequest")]
1263pub struct PyOcspRequest {
1264    pub(super) _data: Py<PyBytes>,
1265    pub(super) raw: &'static [u8],
1266    inner: OnceLock<Box<OcspRequest2024>>,
1267    request_list_cache: OnceLock<Py<PyList>>,
1268    requestor_name_cache: OnceLock<Option<Py<PyBytes>>>,
1269    request_extensions_cache: OnceLock<Option<Py<PyBytes>>>,
1270}
1271
1272impl PyOcspRequest {
1273    fn req(&self) -> PyResult<&OcspRequest2024> {
1274        if let Some(v) = self.inner.get() {
1275            return Ok(v.as_ref());
1276        }
1277        let mut decoder = Decoder::new(self.raw, Encoding::Der);
1278        let decoded = decoder.decode::<OcspRequest2024>().map_err(|e| {
1279            pyo3::exceptions::PyValueError::new_err(format!("OCSPRequest DER decode failed: {e}"))
1280        })?;
1281        let _ = self.inner.set(Box::new(decoded));
1282        Ok(self.inner.get().unwrap().as_ref())
1283    }
1284}
1285
1286#[pymethods]
1287impl PyOcspRequest {
1288    /// Parse a DER-encoded OCSP Request.
1289    #[staticmethod]
1290    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
1291        let py_bytes = data.unbind();
1292        // SAFETY: `py_bytes` holds a strong Py<PyBytes> reference keeping the
1293        // Python bytes object alive for the lifetime of this struct.  CPython's
1294        // bytes objects have a fixed-address, non-relocating payload buffer
1295        // (CPython has no moving GC).  The slice lifetime is extended to
1296        // 'static; the safety invariants are:
1297        //   (1) All reads of `raw` go through `&self`; no borrow of the struct
1298        //       can outlive the struct, so `raw` is never read after drop begins.
1299        //   (2) `raw: &'static [u8]` has no destructor, so field drop order
1300        //       does not cause use-after-free.
1301        //   (3) `inner` contains only borrow-typed fields; dropping
1302        //       Box<OCSPRequest<'static>> does not read through the &'static
1303        //       slices (borrows have no destructors).
1304        // CPython-only: does not hold for PyPy or GraalPy.
1305        let raw: &'static [u8] = unsafe {
1306            let s = py_bytes.bind(py).as_bytes();
1307            std::slice::from_raw_parts(s.as_ptr(), s.len())
1308        };
1309        {
1310            let mut d = Decoder::new(raw, Encoding::Der);
1311            d.read_tag()
1312                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1313            d.read_length()
1314                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1315        }
1316        Ok(Self {
1317            _data: py_bytes,
1318            raw,
1319            inner: OnceLock::new(),
1320            request_list_cache: OnceLock::new(),
1321            requestor_name_cache: OnceLock::new(),
1322            request_extensions_cache: OnceLock::new(),
1323        })
1324    }
1325
1326    /// Parse a PEM-encoded OCSP Request (``OCSP REQUEST`` block).
1327    #[staticmethod]
1328    fn from_pem<'py>(py: Python<'py>, data: Bound<'_, PyBytes>) -> PyResult<Bound<'py, PyAny>> {
1329        pem_blocks_to_pyobject(py, data.as_bytes(), |py, bytes| {
1330            let obj = Self::from_der(py, bytes)?;
1331            Ok(Py::new(py, obj)?.into_bound(py).into_any())
1332        })
1333    }
1334
1335    /// Serialize one OCSP request or a list of them to PEM format.
1336    #[staticmethod]
1337    fn to_pem<'py>(
1338        py: Python<'py>,
1339        obj_or_list: Bound<'_, PyAny>,
1340    ) -> PyResult<Bound<'py, PyBytes>> {
1341        pyobject_to_pem::<Self, _>(py, "OCSP REQUEST", &obj_or_list, |c| c.raw)
1342    }
1343
1344    /// Return the original DER bytes.
1345    fn to_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1346        PyBytes::new(py, self.raw)
1347    }
1348
1349    /// List of :class:`CertID` entries — the certificates whose status is being queried.
1350    #[getter]
1351    fn request_list<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
1352        if let Some(cached) = self.request_list_cache.get() {
1353            return Ok(cached.clone_ref(py).into_bound(py));
1354        }
1355        let req = self.req()?;
1356        let list = PyList::empty(py);
1357        for request in &req.tbs_request.request_list {
1358            let cert_id = PyCertID::from_cert_id(&request.req_cert);
1359            list.append(Py::new(py, cert_id)?)?;
1360        }
1361        let py_list = list.unbind();
1362        let _ = self.request_list_cache.set(py_list.clone_ref(py));
1363        Ok(py_list.into_bound(py))
1364    }
1365
1366    /// DER bytes of the requestor ``GeneralName``, or ``None`` if absent.
1367    #[getter]
1368    fn requestor_name<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
1369        if let Some(cached) = self.requestor_name_cache.get() {
1370            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
1371        }
1372        let computed: Option<Bound<'py, PyBytes>> = self
1373            .req()?
1374            .tbs_request
1375            .requestor_name
1376            .as_ref()
1377            .map(|gn| -> PyResult<Bound<'py, PyBytes>> {
1378                let mut enc = synta::Encoder::new(synta::Encoding::Der);
1379                gn.encode(&mut enc)
1380                    .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1381                let der = enc
1382                    .finish()
1383                    .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1384                Ok(PyBytes::new(py, &der))
1385            })
1386            .transpose()?;
1387        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
1388        let _ = self.requestor_name_cache.set(to_store);
1389        Ok(computed)
1390    }
1391
1392    /// DER bytes of the request extensions ``SEQUENCE OF Extension``, or ``None``.
1393    #[getter]
1394    fn request_extensions<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
1395        if let Some(cached) = self.request_extensions_cache.get() {
1396            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
1397        }
1398        let computed: Option<Bound<'py, PyBytes>> = self
1399            .req()?
1400            .tbs_request
1401            .request_extensions
1402            .as_ref()
1403            .map(|exts| -> PyResult<Bound<'py, PyBytes>> {
1404                let mut enc = synta::Encoder::new(synta::Encoding::Der);
1405                exts.encode(&mut enc)
1406                    .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1407                let der = enc
1408                    .finish()
1409                    .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1410                Ok(PyBytes::new(py, &der))
1411            })
1412            .transpose()?;
1413        let to_store = computed.as_ref().map(|b| b.as_unbound().clone_ref(py));
1414        let _ = self.request_extensions_cache.set(to_store);
1415        Ok(computed)
1416    }
1417
1418    fn __repr__(&self) -> PyResult<String> {
1419        Ok(format!(
1420            "OCSPRequest(requests={})",
1421            self.req()?.tbs_request.request_list.len(),
1422        ))
1423    }
1424}
1425
1426// ── PKCS#7 / PKCS#12 free functions ─────────────────────────────────────────
1427
1428/// Build a `PyList[Certificate]` from a `Vec<Vec<u8>>` of DER-encoded certs.
1429fn der_certs_to_pylist<'py>(
1430    py: Python<'py>,
1431    der_certs: Vec<Vec<u8>>,
1432) -> PyResult<Bound<'py, PyList>> {
1433    let list = PyList::empty(py);
1434    for der in der_certs {
1435        let py_bytes = PyBytes::new(py, &der);
1436        let cert = PyCertificate::new_from_der(py, py_bytes)?;
1437        list.append(Py::new(py, cert)?.into_bound(py))?;
1438    }
1439    Ok(list)
1440}
1441
1442/// Extract X.509 certificates from a PKCS#7 SignedData blob (DER or BER).
1443///
1444/// Accepts raw DER or BER input; BER indefinite-length encodings (common in
1445/// PKCS#7 files produced by older tools) are handled transparently.  Returns
1446/// a list of :class:`Certificate` objects in document order.
1447///
1448/// Raises :exc:`ValueError` on any ASN.1 structural error or if the input
1449/// is not id-signedData.
1450///
1451/// ```python,ignore
1452/// data = open("bundle.p7b", "rb").read()
1453/// certs = synta.load_der_pkcs7_certificates(data)
1454/// for cert in certs:
1455///     print(cert.subject)
1456/// ```
1457#[pyfunction]
1458pub fn load_der_pkcs7_certificates<'py>(
1459    py: Python<'py>,
1460    data: &[u8],
1461) -> PyResult<Bound<'py, PyList>> {
1462    let der_certs = synta_certificate::certs_from_pkcs7(data)
1463        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1464    der_certs_to_pylist(py, der_certs)
1465}
1466
1467/// Extract X.509 certificates from a PEM-encoded PKCS#7 SignedData blob.
1468///
1469/// Decodes PEM block(s) in ``data`` (any label is accepted: ``PKCS7``,
1470/// ``CMS``, ``CERTIFICATE``, etc.) then calls
1471/// :func:`load_der_pkcs7_certificates` on each DER payload.  All certificates
1472/// across all blocks are returned as a single flat list.
1473///
1474/// Raises :exc:`ValueError` if no PEM block is found or if any block fails
1475/// to parse as PKCS#7 SignedData.
1476///
1477/// ```python,ignore
1478/// data = open("bundle.pem", "rb").read()
1479/// certs = synta.load_pem_pkcs7_certificates(data)
1480/// for cert in certs:
1481///     print(cert.subject)
1482/// ```
1483#[pyfunction]
1484pub fn load_pem_pkcs7_certificates<'py>(
1485    py: Python<'py>,
1486    data: &[u8],
1487) -> PyResult<Bound<'py, PyList>> {
1488    let blocks = synta_certificate::pem_blocks(data);
1489    if blocks.is_empty() {
1490        return Err(pyo3::exceptions::PyValueError::new_err(
1491            "no PEM block found in input",
1492        ));
1493    }
1494    let mut all_certs: Vec<Vec<u8>> = Vec::new();
1495    for (_, der) in &blocks {
1496        let certs = synta_certificate::certs_from_pkcs7(der)
1497            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1498        all_certs.extend(certs);
1499    }
1500    der_certs_to_pylist(py, all_certs)
1501}
1502
1503/// Extract X.509 certificates from a PKCS#12 archive.
1504///
1505/// Accepts raw DER or BER input; the encoding is detected automatically.
1506/// Returns a list of :class:`Certificate` objects in document order.
1507/// Non-certificate bag types (``keyBag``, ``pkcs8ShroudedKeyBag``,
1508/// ``crlBag``, ``secretBag``) are silently skipped.
1509///
1510/// ``password`` is the archive password as raw bytes (UTF-8 without a NUL
1511/// terminator).  Pass ``b""`` or omit the argument for password-less archives.
1512///
1513/// Encrypted bags are supported when the library is built with a crypto backend
1514/// (``openssl`` or ``nss`` Cargo feature).
1515///
1516/// ```python,ignore
1517/// data = open("archive.p12", "rb").read()
1518/// certs = synta.load_pkcs12_certificates(data, b"s3cr3t")
1519/// for cert in certs:
1520///     print(cert.subject)
1521/// ```
1522#[pyfunction]
1523#[pyo3(signature = (data, password = None))]
1524pub fn load_pkcs12_certificates<'py>(
1525    py: Python<'py>,
1526    data: &[u8],
1527    password: Option<&[u8]>,
1528) -> PyResult<Bound<'py, PyList>> {
1529    let pw = password.unwrap_or(b"");
1530    let der_certs =
1531        synta_certificate::certs_from_pkcs12(data, pw, &synta_certificate::DefaultCrypto)
1532            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1533    der_certs_to_pylist(py, der_certs)
1534}
1535
1536/// Extract PKCS#8 private keys from a PKCS#12 archive.
1537///
1538/// Returns a :class:`list` of :class:`bytes` objects, one per private key
1539/// found in ``keyBag`` (unencrypted) and ``pkcs8ShroudedKeyBag`` (encrypted,
1540/// decrypted when a crypto backend is enabled and ``password`` is given)
1541/// entries.  Each element is a DER-encoded ``OneAsymmetricKey``
1542/// (PKCS#8) structure, suitable for passing to a cryptography library.
1543///
1544/// ``certBag``, ``crlBag``, ``secretBag`` and any unknown bag types are
1545/// silently skipped.
1546///
1547/// ```python,ignore
1548/// data = open("archive.p12", "rb").read()
1549/// keys = synta.load_pkcs12_keys(data, b"s3cr3t")
1550/// for key_der in keys:
1551///     from cryptography.hazmat.primitives.serialization import load_der_private_key
1552///     key = load_der_private_key(key_der, None)
1553/// ```
1554#[pyfunction]
1555#[pyo3(signature = (data, password = None))]
1556pub fn load_pkcs12_keys<'py>(
1557    py: Python<'py>,
1558    data: &[u8],
1559    password: Option<&[u8]>,
1560) -> PyResult<Bound<'py, PyList>> {
1561    let pw = password.unwrap_or(b"");
1562    let der_keys = synta_certificate::keys_from_pkcs12(data, pw, &synta_certificate::DefaultCrypto)
1563        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1564    let list = PyList::empty(py);
1565    for der in der_keys {
1566        list.append(PyBytes::new(py, &der))?;
1567    }
1568    Ok(list)
1569}
1570
1571/// Extract both certificates and private keys from a PKCS#12 archive.
1572///
1573/// Returns a 2-tuple ``(certs, keys)`` where:
1574///
1575/// - ``certs`` is a :class:`list` of :class:`Certificate` objects.
1576/// - ``keys`` is a :class:`list` of :class:`bytes`, each a DER-encoded
1577///   PKCS#8 ``OneAsymmetricKey`` structure.
1578///
1579/// Encrypted bags are decrypted with ``password`` when a crypto backend is
1580/// available.  Pass ``b""`` or omit ``password`` for password-less archives.
1581///
1582/// Use :func:`load_pkcs12_certificates` if you only need certificates, or
1583/// :func:`load_pkcs12_keys` if you only need keys.
1584///
1585/// ```python,ignore
1586/// data = open("archive.p12", "rb").read()
1587/// certs, keys = synta.load_pkcs12(data, b"s3cr3t")
1588/// for cert in certs:
1589///     print(cert.subject)
1590/// for key_der in keys:
1591///     from cryptography.hazmat.primitives.serialization import load_der_private_key
1592///     key = load_der_private_key(key_der, None)
1593/// ```
1594#[pyfunction]
1595#[pyo3(signature = (data, password = None))]
1596pub fn load_pkcs12<'py>(
1597    py: Python<'py>,
1598    data: &[u8],
1599    password: Option<&[u8]>,
1600) -> PyResult<Bound<'py, pyo3::types::PyTuple>> {
1601    let pw = password.unwrap_or(b"");
1602    let pki = synta_certificate::pki_from_pkcs12(data, pw, &synta_certificate::DefaultCrypto)
1603        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1604    let certs = der_certs_to_pylist(py, pki.certs)?;
1605    let keys = PyList::empty(py);
1606    for der in pki.keys {
1607        keys.append(PyBytes::new(py, &der))?;
1608    }
1609    pyo3::types::PyTuple::new(py, [certs.into_any(), keys.into_any()])
1610}
1611
1612/// Auto-detect the encoding of ``data`` and return every PKI object as a
1613/// ``(label, der_bytes)`` tuple, in document order.
1614///
1615/// Supported formats: PEM (any label), PKCS#7 / CMS SignedData (DER or BER),
1616/// PKCS#12 PFX (DER or BER), and raw DER.
1617///
1618/// **Labels:**
1619///
1620/// - PEM blocks whose payload is a PKCS#7 ``ContentInfo`` (detected by
1621///   structure, not label) are expanded: the embedded certificates are
1622///   extracted and each is returned as ``("CERTIFICATE", der)``.
1623/// - All other PEM block types pass through with their original label
1624///   (e.g. ``"CERTIFICATE"``, ``"PRIVATE KEY"``).  Malformed PEM blocks
1625///   (bad base64, truncated header) are silently skipped.
1626/// - Binary PKCS#7 and raw DER yield ``"CERTIFICATE"`` labels.
1627/// - Binary PKCS#12 yields ``"CERTIFICATE"`` for ``certBag`` entries and
1628///   ``"PRIVATE KEY"`` for ``keyBag`` / ``pkcs8ShroudedKeyBag`` entries.
1629///
1630/// **Decryption:**
1631///
1632/// ``password`` applies only to PKCS#12 archives.  When ``password`` is
1633/// given and a crypto backend is available, encrypted SafeBags are decrypted.
1634/// Otherwise encrypted bags are silently skipped; unencrypted certificates in
1635/// the same archive are still returned.
1636///
1637/// :raises ValueError: For any structural failure:
1638///
1639///   - A PKCS#7 block (binary or PEM-wrapped) has malformed DER, or its
1640///     ``contentType`` OID is not ``id-signedData``.
1641///   - The PKCS#12 input has a structural ASN.1 error.
1642///   - ``password`` was supplied and decryption of an encrypted SafeBag
1643///     failed (e.g. wrong password, unsupported cipher).
1644///   - The PKCS#12 authSafe uses an unsupported ``ContentInfo`` type.
1645///
1646/// :raises TypeError: If ``data`` is not :class:`bytes`, or ``password``
1647///   is not :class:`bytes` or ``None``.
1648///
1649/// ```python,ignore
1650/// data = open("bundle.p7b", "rb").read()
1651/// blocks = synta.read_pki_blocks(data)
1652/// for label, der in blocks:
1653///     print(f"{label}: {len(der)} bytes")
1654///
1655/// # PKCS#12 with encryption
1656/// data = open("archive.p12", "rb").read()
1657/// blocks = synta.read_pki_blocks(data, b"s3cr3t")
1658/// ```
1659#[pyfunction]
1660#[pyo3(signature = (data, password = None))]
1661pub fn read_pki_blocks<'py>(
1662    py: Python<'py>,
1663    data: &[u8],
1664    password: Option<&[u8]>,
1665) -> PyResult<Bound<'py, PyList>> {
1666    let pw = password.unwrap_or(b"");
1667    let blocks = if password.is_some() {
1668        synta_certificate::read_pki_blocks(
1669            data,
1670            pw,
1671            Some(&synta_certificate::DefaultCrypto as &dyn synta_certificate::PkiDecryptor),
1672        )
1673    } else {
1674        synta_certificate::read_pki_blocks(data, pw, None::<&dyn synta_certificate::PkiDecryptor>)
1675    };
1676    let blocks = blocks.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1677    let list = PyList::empty(py);
1678    for (label, der) in blocks {
1679        let tuple = pyo3::types::PyTuple::new(
1680            py,
1681            [
1682                PyString::new(py, &label).into_any(),
1683                PyBytes::new(py, &der).into_any(),
1684            ],
1685        )?;
1686        list.append(tuple)?;
1687    }
1688    Ok(list)
1689}
1690
1691// ── X.509 encoding utilities ──────────────────────────────────────────────────
1692
1693/// Encode a list of OIDs as a DER ``SEQUENCE OF OID`` (ExtendedKeyUsage value).
1694///
1695/// ``oids`` must be a :class:`list` of :class:`~synta.ObjectIdentifier` or
1696/// dotted-decimal ``str`` values.  Returns the complete DER bytes of the
1697/// ``SEQUENCE``, suitable for use as an X.509v3 Extended Key Usage extension
1698/// value.
1699///
1700/// Delegates to synta's ``Vec<ObjectIdentifier>`` encoder which matches the
1701/// generated ``ExtendedKeyUsage = Vec<KeyPurposeId>`` type from the X.509 schema.
1702///
1703/// ```python,ignore
1704/// eku_der = synta.encode_extended_key_usage([
1705///     "1.3.6.1.5.5.7.3.1",   # id-kp-serverAuth
1706///     "1.3.6.1.5.5.7.3.2",   # id-kp-clientAuth
1707/// ])
1708/// ```
1709#[pyfunction]
1710pub fn encode_extended_key_usage<'py>(
1711    py: Python<'py>,
1712    oids: &Bound<'_, PyList>,
1713) -> PyResult<Bound<'py, PyBytes>> {
1714    use synta::traits::Encode;
1715    let mut eku: Vec<synta::ObjectIdentifier> = Vec::with_capacity(oids.len());
1716    for item in oids.iter() {
1717        eku.push(super::oid_from_pyany(&item)?);
1718    }
1719    let mut enc = synta::Encoder::new(synta::Encoding::Der);
1720    eku.encode(&mut enc)
1721        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1722    let der = enc
1723        .finish()
1724        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1725    Ok(PyBytes::new(py, &der))
1726}
1727
1728/// Encode a list of ``(tag_number, content_bytes)`` pairs as a DER
1729/// ``SEQUENCE OF GeneralName``.
1730///
1731/// This is the inverse of :func:`~synta.parse_general_names`: the input
1732/// format is identical to what that function returns.  Tag numbers follow
1733/// RFC 5280 §4.2.1.6; use the :mod:`synta.general_name` constants.
1734///
1735/// ``content_bytes`` must be the raw **value** bytes — without the outer
1736/// context-specific TLV — exactly as returned by
1737/// :func:`~synta.parse_general_names`.
1738///
1739/// Delegates to :func:`synta_certificate::encode_general_names` which uses the
1740/// generated :class:`GeneralName` enum's ``Encode`` implementation.
1741///
1742/// ```python,ignore
1743/// import synta.general_name as gn
1744///
1745/// san_der = synta.encode_subject_alt_names([
1746///     (gn.DNS_NAME,   b"example.com"),
1747///     (gn.IP_ADDRESS, b"\x7f\x00\x00\x01"),   # 127.0.0.1
1748/// ])
1749/// ```
1750#[pyfunction]
1751pub fn encode_subject_alt_names<'py>(
1752    py: Python<'py>,
1753    names: &Bound<'_, PyList>,
1754) -> PyResult<Bound<'py, PyBytes>> {
1755    // Collect owned (tag, bytes) first so we can create &[u8] references that
1756    // all share the same lifetime, satisfying Vec<GeneralName<'_>> constraints.
1757    let mut owned: Vec<(u32, Vec<u8>)> = Vec::with_capacity(names.len());
1758    for item in names.iter() {
1759        let tuple = item.cast::<pyo3::types::PyTuple>().map_err(|_| {
1760            pyo3::exceptions::PyTypeError::new_err(
1761                "each element must be a (tag_number, bytes) 2-tuple",
1762            )
1763        })?;
1764        if tuple.len() != 2 {
1765            return Err(pyo3::exceptions::PyTypeError::new_err(
1766                "each element must be a (tag_number, bytes) 2-tuple",
1767            ));
1768        }
1769        let tag_num: u32 = tuple.get_item(0)?.extract()?;
1770        let content: Vec<u8> = tuple.get_item(1)?.extract()?;
1771        owned.push((tag_num, content));
1772    }
1773    let entries: Vec<(u32, &[u8])> = owned.iter().map(|(t, v)| (*t, v.as_slice())).collect();
1774    let der = synta_certificate::encode_general_names(&entries).ok_or_else(|| {
1775        pyo3::exceptions::PyValueError::new_err(
1776            "failed to encode GeneralName entries (invalid content bytes or unsupported tag)",
1777        )
1778    })?;
1779    Ok(PyBytes::new(py, &der))
1780}
1781
1782/// Compare two DER-encoded X.500 Names for byte-for-byte equality.
1783///
1784/// Returns ``True`` if and only if the two byte sequences are identical.
1785/// This is a strict comparison; RFC 5280 name matching rules (case-insensitive
1786/// string comparison, etc.) are **not** applied.
1787///
1788/// Useful for quickly checking whether issuer and subject names match without
1789/// decoding the full structure:
1790///
1791/// ```python,ignore
1792/// same = synta.name_der_equal(cert.issuer_raw_der, ca_cert.subject_raw_der)
1793/// ```
1794#[pyfunction]
1795pub fn name_der_equal(a: &[u8], b: &[u8]) -> bool {
1796    a == b
1797}
1798
1799/// Build a DER-encoded PKCS#12 ``PFX`` archive.
1800///
1801/// Assembles a ``PFX`` from one or more certificates and an optional
1802/// DER-encoded PKCS#8 private key.  The archive is protected by ``password``
1803/// using PBES2 (PBKDF2 + the selected cipher) for key encryption and HMAC
1804/// (PBKDF2-derived key) for the integrity MAC.
1805///
1806/// :param certificates: sequence of :class:`~synta.Certificate` objects or
1807///     raw DER ``bytes``.  Both types may appear in the same list.
1808/// :param private_key: a :class:`~synta.PrivateKey` object or a DER-encoded
1809///     PKCS#8 ``OneAsymmetricKey`` ``bytes``, or ``None`` to omit.
1810/// :param password: archive password (``bytes``), or ``None`` for a
1811///     password-less archive.
1812/// :param cipher: symmetric cipher for private-key encryption.  One of
1813///     ``"aes128"`` or ``"aes256"`` (default).
1814/// :param mac_algorithm: HMAC algorithm for the integrity MAC and PBKDF2 PRF.
1815///     One of ``"sha256"`` (default), ``"sha384"``, or ``"sha512"``.
1816/// :returns: DER-encoded ``PFX`` archive bytes.
1817/// :raises TypeError: if any element of ``certificates`` is neither a
1818///     :class:`~synta.Certificate` nor ``bytes``, or if ``private_key`` is
1819///     not a :class:`~synta.PrivateKey` nor ``bytes``.
1820/// :raises ValueError: if no certificates or key are supplied, if an unknown
1821///     cipher or MAC algorithm name is given, or if encryption fails.
1822///
1823/// ```python,ignore
1824/// # Certificate and PrivateKey objects — no .to_der() call needed
1825/// pfx = synta.create_pkcs12(
1826///     [cert, ca_cert],
1827///     private_key=key,
1828///     password=b"s3cr3t",
1829/// )
1830///
1831/// # Custom cipher and MAC algorithm
1832/// pfx = synta.create_pkcs12(
1833///     [cert],
1834///     private_key=key,
1835///     password=b"pass",
1836///     cipher="aes128",
1837///     mac_algorithm="sha384",
1838/// )
1839///
1840/// # Raw DER bytes also accepted for both
1841/// pfx = synta.create_pkcs12([cert.to_der()], private_key=key.to_der())
1842///
1843/// with open("out.p12", "wb") as f:
1844///     f.write(pfx)
1845/// ```
1846#[pyfunction]
1847#[pyo3(signature = (certificates, private_key = None, password = None, cipher = None, mac_algorithm = None))]
1848pub fn create_pkcs12<'py>(
1849    py: Python<'py>,
1850    certificates: &Bound<'_, PyList>,
1851    private_key: Option<Bound<'_, PyAny>>,
1852    password: Option<&[u8]>,
1853    cipher: Option<&str>,
1854    mac_algorithm: Option<&str>,
1855) -> PyResult<Bound<'py, PyBytes>> {
1856    use pyo3::exceptions::PyValueError;
1857    use synta_certificate::{
1858        OpensslPkcs12Encryptor, Pkcs12Builder, Pkcs12Cipher, Pkcs12Config, Pkcs12HmacAlgorithm,
1859    };
1860
1861    let pkcs12_cipher = match cipher.unwrap_or("aes256") {
1862        "aes128" => Pkcs12Cipher::Aes128Cbc,
1863        "aes256" => Pkcs12Cipher::Aes256Cbc,
1864        "3des" => {
1865            return Err(PyValueError::new_err(
1866                "cipher '3des' is not supported; use 'aes128' or 'aes256'",
1867            ))
1868        }
1869        other => {
1870            return Err(PyValueError::new_err(format!(
1871                "unknown cipher: {other}; use 'aes128' or 'aes256'"
1872            )))
1873        }
1874    };
1875    let pkcs12_mac = match mac_algorithm.unwrap_or("sha256") {
1876        "sha256" => Pkcs12HmacAlgorithm::Sha256,
1877        "sha384" => Pkcs12HmacAlgorithm::Sha384,
1878        "sha512" => Pkcs12HmacAlgorithm::Sha512,
1879        "sha1" => {
1880            return Err(PyValueError::new_err(
1881                "mac_algorithm 'sha1' is not supported; use 'sha256', 'sha384', or 'sha512'",
1882            ))
1883        }
1884        other => {
1885            return Err(PyValueError::new_err(format!(
1886                "unknown mac_algorithm: {other}; use 'sha256', 'sha384', or 'sha512'"
1887            )))
1888        }
1889    };
1890    let config = Pkcs12Config {
1891        cipher: pkcs12_cipher,
1892        encryption_prf: pkcs12_mac,
1893        mac_algorithm: pkcs12_mac,
1894        ..Pkcs12Config::default()
1895    };
1896    let encryptor = OpensslPkcs12Encryptor::with_config(config);
1897
1898    let password = password.unwrap_or(b"");
1899    let mut builder = Pkcs12Builder::new();
1900    for item in certificates.iter() {
1901        if let Ok(py_cert) = item.cast::<PyCertificate>() {
1902            // Zero-copy: raw is &'static [u8] backed by the PyBytes in _data.
1903            // Pkcs12Builder::certificate() copies into Vec<u8> immediately, so
1904            // the borrow does not outlive this loop iteration.
1905            builder = builder.certificate(py_cert.get().raw);
1906        } else if let Ok(py_bytes) = item.cast::<PyBytes>() {
1907            builder = builder.certificate(py_bytes.as_bytes());
1908        } else {
1909            return Err(pyo3::exceptions::PyTypeError::new_err(
1910                "certificates must be synta.Certificate or bytes",
1911            ));
1912        }
1913    }
1914    if let Some(key_arg) = private_key {
1915        if let Ok(py_key) = key_arg.cast::<PyPrivateKey>() {
1916            // PyPrivateKey stores PKCS#8 DER internally; retrieve it directly.
1917            let key_der = py_key
1918                .get()
1919                .inner
1920                .to_der()
1921                .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1922            builder = builder.private_key(&key_der);
1923        } else if let Ok(py_bytes) = key_arg.cast::<PyBytes>() {
1924            builder = builder.private_key(py_bytes.as_bytes());
1925        } else {
1926            return Err(pyo3::exceptions::PyTypeError::new_err(
1927                "private_key must be synta.PrivateKey or bytes",
1928            ));
1929        }
1930    }
1931
1932    let pfx_der = builder
1933        .build(password, &encryptor)
1934        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
1935
1936    Ok(PyBytes::new(py, &pfx_der))
1937}
1938
1939// ── PyCertificateListBuilder helpers ─────────────────────────────────────────
1940
1941/// Convert a Python `datetime.datetime` (must be timezone-aware) to a
1942/// GeneralizedTime string in `"YYYYMMDDHHmmssZ"` format accepted by the
1943/// string-based `CertificateListBuilder` setters.
1944///
1945/// Uses `datetime.timestamp()` to obtain a Unix timestamp and delegates to
1946/// [`synta::GeneralizedTime::from_unix`] for the calendar conversion.
1947fn py_dt_to_gen_time_str(dt: &Bound<'_, PyAny>) -> PyResult<String> {
1948    if dt.getattr("tzinfo")?.is_none() {
1949        return Err(pyo3::exceptions::PyValueError::new_err(
1950            "datetime must be timezone-aware (e.g. datetime.timezone.utc)",
1951        ));
1952    }
1953    let ts: f64 = dt.call_method0("timestamp")?.extract()?;
1954    let secs = ts.floor() as i64;
1955    let gt = synta::GeneralizedTime::from_unix(secs).ok_or_else(|| {
1956        pyo3::exceptions::PyValueError::new_err("datetime out of valid ASN.1 year range 1–9999")
1957    })?;
1958    Ok(gt.to_string())
1959}
1960
1961// ── PyCertificateListBuilder ──────────────────────────────────────────────────
1962
1963/// Fluent builder for a DER-encoded X.509 CRL (RFC 5280 §5).
1964///
1965/// Chain setters to configure the CRL, then call :meth:`build` to obtain the
1966/// DER-encoded ``TBSCertList`` SEQUENCE.  Sign the TBS externally and call the
1967/// static :meth:`assemble` to produce the final ``CertificateList``.
1968///
1969/// ```python,ignore
1970/// import synta
1971///
1972/// name_der = synta.NameBuilder().common_name("Test CA").build()
1973/// alg_der  = bytes.fromhex("300d06092a864886f70d01010b0500")  # sha256WithRSAEncryption
1974/// tbs = (
1975///     synta.CertificateListBuilder()
1976///     .issuer(name_der)
1977///     .signature_algorithm(alg_der)
1978///     .this_update("20240101120000Z")
1979///     .next_update("20240201120000Z")
1980///     .revoke(bytes([1]), "20231201000000Z", 1)  # keyCompromise
1981///     .build()
1982/// )
1983/// # sign tbs externally, then:
1984/// crl_der = synta.CertificateListBuilder.assemble(tbs, alg_der, sig_bytes)
1985/// ```
1986#[pyclass(name = "CertificateListBuilder")]
1987pub struct PyCertificateListBuilder {
1988    inner: synta_certificate::CertificateListBuilder,
1989}
1990
1991#[pymethods]
1992impl PyCertificateListBuilder {
1993    /// Create a new, empty ``CertificateListBuilder``.
1994    #[new]
1995    fn new() -> Self {
1996        Self {
1997            inner: synta_certificate::CertificateListBuilder::new(),
1998        }
1999    }
2000
2001    /// Set the issuer ``Name`` from pre-encoded DER bytes.
2002    fn issuer<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
2003        let mut guard = slf.borrow_mut();
2004        let old = std::mem::replace(
2005            &mut guard.inner,
2006            synta_certificate::CertificateListBuilder::new(),
2007        );
2008        guard.inner = old.issuer(name_der);
2009        drop(guard);
2010        slf
2011    }
2012
2013    /// Set the ``thisUpdate`` time (``"YYYYMMDDHHmmssZ"`` or ``"YYMMDDHHmmssZ"``).
2014    fn this_update<'py>(slf: Bound<'py, Self>, time: &str) -> Bound<'py, Self> {
2015        let mut guard = slf.borrow_mut();
2016        let old = std::mem::replace(
2017            &mut guard.inner,
2018            synta_certificate::CertificateListBuilder::new(),
2019        );
2020        guard.inner = old.this_update(time);
2021        drop(guard);
2022        slf
2023    }
2024
2025    /// Set the optional ``nextUpdate`` time (same format as :meth:`this_update`).
2026    fn next_update<'py>(slf: Bound<'py, Self>, time: &str) -> Bound<'py, Self> {
2027        let mut guard = slf.borrow_mut();
2028        let old = std::mem::replace(
2029            &mut guard.inner,
2030            synta_certificate::CertificateListBuilder::new(),
2031        );
2032        guard.inner = old.next_update(time);
2033        drop(guard);
2034        slf
2035    }
2036
2037    /// Add a revoked certificate entry.
2038    ///
2039    /// ``serial`` is the big-endian DER INTEGER value bytes of the certificate
2040    /// serial number.  ``revocation_date`` uses the same time format as
2041    /// :meth:`this_update`.  ``reason`` is an optional CRL reason code integer
2042    /// (0=unspecified, 1=keyCompromise, 2=cACompromise, …, 10=aACompromise).
2043    fn revoke<'py>(
2044        slf: Bound<'py, Self>,
2045        serial: &[u8],
2046        revocation_date: &str,
2047        reason: Option<u8>,
2048    ) -> Bound<'py, Self> {
2049        let mut guard = slf.borrow_mut();
2050        let old = std::mem::replace(
2051            &mut guard.inner,
2052            synta_certificate::CertificateListBuilder::new(),
2053        );
2054        guard.inner = old.revoke(serial, revocation_date, reason);
2055        drop(guard);
2056        slf
2057    }
2058
2059    /// Set the ``thisUpdate`` time from a timezone-aware
2060    /// :class:`datetime.datetime`.
2061    ///
2062    /// Converts the datetime to a Unix timestamp and formats it as
2063    /// ``"YYYYMMDDHHmmssZ"`` (GeneralizedTime).
2064    ///
2065    /// :raises ValueError: if ``dt`` is naive (no tzinfo) or out of range.
2066    fn this_update_utc<'py>(
2067        slf: Bound<'py, Self>,
2068        dt: &Bound<'_, PyAny>,
2069    ) -> PyResult<Bound<'py, Self>> {
2070        let time_str = py_dt_to_gen_time_str(dt)?;
2071        let mut guard = slf.borrow_mut();
2072        let old = std::mem::replace(
2073            &mut guard.inner,
2074            synta_certificate::CertificateListBuilder::new(),
2075        );
2076        guard.inner = old.this_update(&time_str);
2077        drop(guard);
2078        Ok(slf)
2079    }
2080
2081    /// Set the optional ``nextUpdate`` time from a timezone-aware
2082    /// :class:`datetime.datetime`.
2083    ///
2084    /// :raises ValueError: if ``dt`` is naive (no tzinfo) or out of range.
2085    fn next_update_utc<'py>(
2086        slf: Bound<'py, Self>,
2087        dt: &Bound<'_, PyAny>,
2088    ) -> PyResult<Bound<'py, Self>> {
2089        let time_str = py_dt_to_gen_time_str(dt)?;
2090        let mut guard = slf.borrow_mut();
2091        let old = std::mem::replace(
2092            &mut guard.inner,
2093            synta_certificate::CertificateListBuilder::new(),
2094        );
2095        guard.inner = old.next_update(&time_str);
2096        drop(guard);
2097        Ok(slf)
2098    }
2099
2100    /// Add a revoked certificate entry using a timezone-aware
2101    /// :class:`datetime.datetime` for the revocation time.
2102    ///
2103    /// ``serial`` is the big-endian DER INTEGER value bytes of the certificate
2104    /// serial number.  ``reason`` is an optional CRL reason code integer
2105    /// (0=unspecified, 1=keyCompromise, 2=cACompromise, …, 10=aACompromise).
2106    ///
2107    /// :raises ValueError: if ``revocation_dt`` is naive (no tzinfo) or out of range.
2108    fn revoke_utc<'py>(
2109        slf: Bound<'py, Self>,
2110        serial: &[u8],
2111        revocation_dt: &Bound<'_, PyAny>,
2112        reason: Option<u8>,
2113    ) -> PyResult<Bound<'py, Self>> {
2114        let time_str = py_dt_to_gen_time_str(revocation_dt)?;
2115        let mut guard = slf.borrow_mut();
2116        let old = std::mem::replace(
2117            &mut guard.inner,
2118            synta_certificate::CertificateListBuilder::new(),
2119        );
2120        guard.inner = old.revoke(serial, &time_str, reason);
2121        drop(guard);
2122        Ok(slf)
2123    }
2124
2125    /// Set the signature ``AlgorithmIdentifier`` DER for ``TBSCertList.signature``.
2126    ///
2127    /// Must be a complete ``AlgorithmIdentifier`` SEQUENCE TLV.
2128    fn signature_algorithm<'py>(slf: Bound<'py, Self>, alg_der: &[u8]) -> Bound<'py, Self> {
2129        let mut guard = slf.borrow_mut();
2130        let old = std::mem::replace(
2131            &mut guard.inner,
2132            synta_certificate::CertificateListBuilder::new(),
2133        );
2134        guard.inner = old.signature_algorithm(alg_der);
2135        drop(guard);
2136        slf
2137    }
2138
2139    /// Add a CRL-level extension.
2140    ///
2141    /// ``oid`` is the dotted-decimal OID string (e.g. ``"2.5.29.20"`` for
2142    /// CRL Number).  ``critical`` marks the extension as critical.
2143    /// ``value_der`` is the raw DER of the extension value (the OCTET STRING
2144    /// wrapper is added automatically).
2145    ///
2146    /// :raises ValueError: if ``oid`` is not a valid dotted-decimal OID string.
2147    fn add_extension<'py>(
2148        slf: Bound<'py, Self>,
2149        oid: Bound<'_, pyo3::PyAny>,
2150        critical: bool,
2151        value_der: &[u8],
2152    ) -> PyResult<Bound<'py, Self>> {
2153        use std::str::FromStr;
2154        let parsed_oid: synta::ObjectIdentifier = if let Ok(s) = oid.extract::<String>() {
2155            synta::ObjectIdentifier::from_str(&s)
2156                .map_err(|_| pyo3::exceptions::PyValueError::new_err(format!("invalid OID: {s}")))?
2157        } else if let Ok(o) = oid.extract::<pyo3::PyRef<'_, PyObjectIdentifier>>() {
2158            o.inner.clone()
2159        } else {
2160            return Err(pyo3::exceptions::PyTypeError::new_err(
2161                "oid must be a str or ObjectIdentifier",
2162            ));
2163        };
2164        let mut guard = slf.borrow_mut();
2165        let old = std::mem::replace(
2166            &mut guard.inner,
2167            synta_certificate::CertificateListBuilder::new(),
2168        );
2169        guard.inner = old.add_crl_extension(parsed_oid.components(), critical, value_der);
2170        drop(guard);
2171        Ok(slf)
2172    }
2173
2174    /// Build the DER-encoded ``TBSCertList`` SEQUENCE.
2175    ///
2176    /// Required fields: ``issuer``, ``this_update``, ``signature_algorithm``.
2177    ///
2178    /// :raises ValueError: if any required field is absent or encoding fails.
2179    fn build<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
2180        let inner = std::mem::replace(
2181            &mut self.inner,
2182            synta_certificate::CertificateListBuilder::new(),
2183        );
2184        let der = inner
2185            .build()
2186            .map_err(pyo3::exceptions::PyValueError::new_err)?;
2187        Ok(PyBytes::new(py, &der))
2188    }
2189
2190    /// Assemble a DER-encoded ``CertificateList`` from its three components.
2191    ///
2192    /// ``tbs_der`` is the ``TBSCertList`` from :meth:`build`.
2193    /// ``sig_alg_der`` is the outer ``AlgorithmIdentifier`` SEQUENCE TLV.
2194    /// ``signature`` is the raw signature bytes (BIT STRING value; unused-bits byte
2195    /// is added automatically).
2196    ///
2197    /// :raises ValueError: if DER encoding fails.
2198    #[staticmethod]
2199    fn assemble<'py>(
2200        py: Python<'py>,
2201        tbs_der: &[u8],
2202        sig_alg_der: &[u8],
2203        signature: &[u8],
2204    ) -> PyResult<Bound<'py, PyBytes>> {
2205        let der =
2206            synta_certificate::CertificateListBuilder::assemble(tbs_der, sig_alg_der, signature)
2207                .map_err(pyo3::exceptions::PyValueError::new_err)?;
2208        Ok(PyBytes::new(py, &der))
2209    }
2210
2211    fn __repr__(&self) -> String {
2212        "CertificateListBuilder()".to_string()
2213    }
2214}
2215
2216// ── OCSPResponseBuilder ───────────────────────────────────────────────────────
2217
2218/// Parameters for a single OCSP response entry.
2219///
2220/// Pass an instance to :meth:`OCSPResponseBuilder.add_response`.
2221///
2222/// ```python,ignore
2223/// resp = synta.OCSPSingleResponse(
2224///     hash_algorithm_der=sha1_alg_der,
2225///     issuer_name_hash=name_hash,
2226///     issuer_key_hash=key_hash,
2227///     serial=serial_bytes,
2228///     status=0,
2229///     this_update="20240101120000Z",
2230///     next_update="20240201120000Z",  # optional
2231/// )
2232/// ```
2233#[pyclass(frozen, name = "OCSPSingleResponse")]
2234pub struct PyOCSPSingleResponse {
2235    pub(crate) hash_algorithm_der: Vec<u8>,
2236    pub(crate) issuer_name_hash: Vec<u8>,
2237    pub(crate) issuer_key_hash: Vec<u8>,
2238    pub(crate) serial: Vec<u8>,
2239    pub(crate) status: u8,
2240    pub(crate) this_update: String,
2241    pub(crate) next_update: Option<String>,
2242}
2243
2244#[pymethods]
2245impl PyOCSPSingleResponse {
2246    /// Create an ``OCSPSingleResponse`` entry.
2247    ///
2248    /// :param hash_algorithm_der: DER-encoded ``AlgorithmIdentifier`` SEQUENCE TLV used to
2249    ///     hash the issuer name and key.
2250    /// :param issuer_name_hash: hash of the issuer ``Name`` DER encoding.
2251    /// :param issuer_key_hash: hash of the issuer public key BIT STRING value.
2252    /// :param serial: big-endian serial number bytes of the certificate being checked.
2253    /// :param status: revocation status integer — ``0`` = good, ``1`` = revoked, ``2`` = unknown.
2254    /// :param this_update: ``GeneralizedTime`` string (e.g. ``"20240101120000Z"``).
2255    /// :param next_update: optional ``GeneralizedTime`` string for the next update time.
2256    #[new]
2257    #[pyo3(signature = (hash_algorithm_der, issuer_name_hash, issuer_key_hash, serial, status, this_update, next_update=None))]
2258    fn new(
2259        hash_algorithm_der: &[u8],
2260        issuer_name_hash: &[u8],
2261        issuer_key_hash: &[u8],
2262        serial: &[u8],
2263        status: u8,
2264        this_update: &str,
2265        next_update: Option<&str>,
2266    ) -> Self {
2267        Self {
2268            hash_algorithm_der: hash_algorithm_der.to_vec(),
2269            issuer_name_hash: issuer_name_hash.to_vec(),
2270            issuer_key_hash: issuer_key_hash.to_vec(),
2271            serial: serial.to_vec(),
2272            status,
2273            this_update: this_update.to_owned(),
2274            next_update: next_update.map(str::to_owned),
2275        }
2276    }
2277}
2278
2279/// Python-facing wrapper for [`synta_certificate::OCSPResponseBuilder`].
2280///
2281/// Builds a DER-encoded ``ResponseData`` (RFC 6960) for external signing.
2282/// After signing, call :meth:`assemble` to produce the complete
2283/// ``OCSPResponse`` DER blob.
2284///
2285/// ```python,ignore
2286/// import synta
2287///
2288/// resp = synta.OCSPSingleResponse(
2289///     hash_algorithm_der=sha1_alg_der,
2290///     issuer_name_hash=name_hash,
2291///     issuer_key_hash=key_hash,
2292///     serial=serial_bytes,
2293///     status=0,  # good
2294///     this_update="20240101120000Z",
2295///     next_update="20240201120000Z",
2296/// )
2297/// tbs = (
2298///     synta.OCSPResponseBuilder()
2299///     .responder_key_hash(key_hash_bytes)
2300///     .produced_at("20240101120000Z")
2301///     .add_response(resp)
2302///     .build_tbs()
2303/// )
2304/// ocsp_der = synta.OCSPResponseBuilder.assemble(tbs, sig_alg_der, sig_bytes)
2305/// ```
2306#[pyclass(name = "OCSPResponseBuilder")]
2307pub struct PyOCSPResponseBuilder {
2308    inner: synta_certificate::OCSPResponseBuilder,
2309}
2310
2311#[pymethods]
2312impl PyOCSPResponseBuilder {
2313    /// Create a new, empty ``OCSPResponseBuilder``.
2314    #[new]
2315    fn new() -> Self {
2316        Self {
2317            inner: synta_certificate::OCSPResponseBuilder::new(),
2318        }
2319    }
2320
2321    /// Set ``responderID byName`` from a pre-encoded DER Name SEQUENCE TLV.
2322    ///
2323    /// :param name_der: DER-encoded ``Name`` SEQUENCE bytes.
2324    /// :raises ValueError: if ``name_der`` is not a valid Name.
2325    fn responder_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
2326        let mut guard = slf.borrow_mut();
2327        let old = std::mem::replace(
2328            &mut guard.inner,
2329            synta_certificate::OCSPResponseBuilder::new(),
2330        );
2331        guard.inner = old.responder_name(name_der);
2332        drop(guard);
2333        slf
2334    }
2335
2336    /// Set ``responderID byKey`` from the raw key-hash bytes.
2337    ///
2338    /// ``key_hash`` is the raw hash bytes without any TLV wrapper — typically
2339    /// the SHA-1 hash of the issuer's ``subjectPublicKey`` BIT STRING value.
2340    ///
2341    /// :param key_hash: raw SHA-1 key-hash bytes.
2342    fn responder_key_hash<'py>(slf: Bound<'py, Self>, key_hash: &[u8]) -> Bound<'py, Self> {
2343        let mut guard = slf.borrow_mut();
2344        let old = std::mem::replace(
2345            &mut guard.inner,
2346            synta_certificate::OCSPResponseBuilder::new(),
2347        );
2348        guard.inner = old.responder_key_hash(key_hash);
2349        drop(guard);
2350        slf
2351    }
2352
2353    /// Set ``producedAt`` GeneralizedTime.
2354    ///
2355    /// :param time: time string in ``YYYYMMDDHHmmssZ`` format.
2356    /// :raises ValueError: if the time string is malformed (deferred to :meth:`build_tbs`).
2357    fn produced_at<'py>(slf: Bound<'py, Self>, time: &str) -> Bound<'py, Self> {
2358        let mut guard = slf.borrow_mut();
2359        let old = std::mem::replace(
2360            &mut guard.inner,
2361            synta_certificate::OCSPResponseBuilder::new(),
2362        );
2363        guard.inner = old.produced_at(time);
2364        drop(guard);
2365        slf
2366    }
2367
2368    /// Add a ``SingleResponse`` entry.
2369    ///
2370    /// :param response: an :class:`OCSPSingleResponse` instance.
2371    /// :raises ValueError: if any parameter is invalid (deferred to :meth:`build_tbs`).
2372    fn add_response<'py>(
2373        slf: Bound<'py, Self>,
2374        response: &PyOCSPSingleResponse,
2375    ) -> Bound<'py, Self> {
2376        let mut guard = slf.borrow_mut();
2377        let old = std::mem::replace(
2378            &mut guard.inner,
2379            synta_certificate::OCSPResponseBuilder::new(),
2380        );
2381        guard.inner = old.add_response(synta_certificate::SingleResponseSpec {
2382            hash_algorithm_der: &response.hash_algorithm_der,
2383            issuer_name_hash: &response.issuer_name_hash,
2384            issuer_key_hash: &response.issuer_key_hash,
2385            serial: &response.serial,
2386            status: response.status,
2387            this_update: &response.this_update,
2388            next_update: response.next_update.as_deref(),
2389        });
2390        drop(guard);
2391        slf
2392    }
2393
2394    /// Build the DER-encoded ``ResponseData`` SEQUENCE.
2395    ///
2396    /// :returns: DER bytes of the ``ResponseData``.
2397    /// :raises ValueError: if any required field is absent or encoding fails.
2398    fn build_tbs<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
2399        let inner = std::mem::replace(
2400            &mut self.inner,
2401            synta_certificate::OCSPResponseBuilder::new(),
2402        );
2403        let der = inner
2404            .build_tbs()
2405            .map_err(pyo3::exceptions::PyValueError::new_err)?;
2406        Ok(PyBytes::new(py, &der))
2407    }
2408
2409    /// Assemble a complete DER-encoded ``OCSPResponse``.
2410    ///
2411    /// :param tbs_der: ``ResponseData`` bytes from :meth:`build_tbs`.
2412    /// :param sig_alg_der: outer ``AlgorithmIdentifier`` SEQUENCE TLV.
2413    /// :param signature: raw signature bytes (BIT STRING value).
2414    /// :raises ValueError: if DER encoding fails.
2415    #[staticmethod]
2416    fn assemble<'py>(
2417        py: Python<'py>,
2418        tbs_der: &[u8],
2419        sig_alg_der: &[u8],
2420        signature: &[u8],
2421    ) -> PyResult<Bound<'py, PyBytes>> {
2422        let der = synta_certificate::OCSPResponseBuilder::assemble(tbs_der, sig_alg_der, signature)
2423            .map_err(pyo3::exceptions::PyValueError::new_err)?;
2424        Ok(PyBytes::new(py, &der))
2425    }
2426
2427    fn __repr__(&self) -> String {
2428        "OCSPResponseBuilder()".to_string()
2429    }
2430}
2431
2432// ── OCSPRequestBuilder ────────────────────────────────────────────────────────
2433
2434/// Parameters for a single OCSP request entry (one certificate to check).
2435///
2436/// Pass an instance to :meth:`OCSPRequestBuilder.add_request`.
2437///
2438/// ```python,ignore
2439/// spec = synta.OCSPCertIDSpec(
2440///     hash_algorithm_der=sha1_alg_der,
2441///     issuer_name_hash=name_hash,
2442///     issuer_key_hash=key_hash,
2443///     serial=serial_bytes,
2444/// )
2445/// ```
2446#[pyclass(frozen, name = "OCSPCertIDSpec")]
2447pub struct PyOCSPCertIDSpec {
2448    pub(crate) hash_algorithm_der: Vec<u8>,
2449    pub(crate) issuer_name_hash: Vec<u8>,
2450    pub(crate) issuer_key_hash: Vec<u8>,
2451    pub(crate) serial: Vec<u8>,
2452}
2453
2454#[pymethods]
2455impl PyOCSPCertIDSpec {
2456    /// Create an ``OCSPCertIDSpec`` entry.
2457    ///
2458    /// :param hash_algorithm_der: DER-encoded ``AlgorithmIdentifier`` SEQUENCE TLV used to
2459    ///     hash the issuer name and key.
2460    /// :param issuer_name_hash: hash of the issuer ``Name`` DER encoding.
2461    /// :param issuer_key_hash: hash of the issuer public key BIT STRING value.
2462    /// :param serial: big-endian serial number bytes of the certificate being checked.
2463    #[new]
2464    fn new(
2465        hash_algorithm_der: &[u8],
2466        issuer_name_hash: &[u8],
2467        issuer_key_hash: &[u8],
2468        serial: &[u8],
2469    ) -> Self {
2470        Self {
2471            hash_algorithm_der: hash_algorithm_der.to_vec(),
2472            issuer_name_hash: issuer_name_hash.to_vec(),
2473            issuer_key_hash: issuer_key_hash.to_vec(),
2474            serial: serial.to_vec(),
2475        }
2476    }
2477}
2478
2479/// Python-facing wrapper for [`synta_certificate::OCSPRequestBuilder`].
2480///
2481/// Builds a DER-encoded ``OCSPRequest`` (RFC 6960) for direct submission to an
2482/// OCSP responder (unsigned) or for external signing.  The unsigned form is
2483/// produced by :meth:`build_tbs`.  For signed requests, use
2484/// :meth:`build_tbs_inner` to obtain the inner ``TBSRequest`` bytes for
2485/// signing, then call :meth:`assemble` to produce the complete signed
2486/// ``OCSPRequest``.
2487///
2488/// ```python,ignore
2489/// import synta
2490///
2491/// spec = synta.OCSPCertIDSpec(
2492///     hash_algorithm_der=sha1_alg_der,
2493///     issuer_name_hash=name_hash,
2494///     issuer_key_hash=key_hash,
2495///     serial=serial_bytes,
2496/// )
2497/// # Unsigned request (most common):
2498/// ocsp_der = (
2499///     synta.OCSPRequestBuilder()
2500///     .add_request(spec)
2501///     .build_tbs()
2502/// )
2503/// # Signed request:
2504/// tbs_inner = synta.OCSPRequestBuilder().add_request(spec).build_tbs_inner()
2505/// ocsp_der = synta.OCSPRequestBuilder.assemble(tbs_inner, sig_alg_der, sig_bytes)
2506/// ```
2507#[pyclass(name = "OCSPRequestBuilder")]
2508pub struct PyOCSPRequestBuilder {
2509    inner: synta_certificate::OCSPRequestBuilder,
2510    built: bool,
2511}
2512
2513#[pymethods]
2514impl PyOCSPRequestBuilder {
2515    /// Create a new, empty ``OCSPRequestBuilder``.
2516    #[new]
2517    fn new() -> Self {
2518        Self {
2519            inner: synta_certificate::OCSPRequestBuilder::new(),
2520            built: false,
2521        }
2522    }
2523
2524    /// Set the optional ``requestorName`` from a pre-encoded DER ``GeneralName`` TLV.
2525    ///
2526    /// :param name_der: DER-encoded ``GeneralName`` TLV bytes.
2527    /// :raises ValueError: if ``name_der`` is not a valid ``GeneralName`` (deferred to
2528    ///     :meth:`build_tbs`).
2529    fn requestor_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
2530        let mut guard = slf.borrow_mut();
2531        let old = std::mem::replace(
2532            &mut guard.inner,
2533            synta_certificate::OCSPRequestBuilder::new(),
2534        );
2535        guard.inner = old.requestor_name(name_der);
2536        drop(guard);
2537        slf
2538    }
2539
2540    /// Add a ``Request`` entry (one certificate whose status is being queried).
2541    ///
2542    /// :param spec: an :class:`OCSPCertIDSpec` instance.
2543    /// :raises ValueError: if any parameter is invalid (deferred to :meth:`build_tbs`).
2544    fn add_request<'py>(slf: Bound<'py, Self>, spec: &PyOCSPCertIDSpec) -> Bound<'py, Self> {
2545        let mut guard = slf.borrow_mut();
2546        let old = std::mem::replace(
2547            &mut guard.inner,
2548            synta_certificate::OCSPRequestBuilder::new(),
2549        );
2550        guard.inner = old.add_request(synta_certificate::CertIDSpec {
2551            hash_algorithm_der: &spec.hash_algorithm_der,
2552            issuer_name_hash: &spec.issuer_name_hash,
2553            issuer_key_hash: &spec.issuer_key_hash,
2554            serial: &spec.serial,
2555        });
2556        drop(guard);
2557        slf
2558    }
2559
2560    /// Build the complete DER-encoded unsigned ``OCSPRequest`` SEQUENCE.
2561    ///
2562    /// The returned bytes are a valid ``OCSPRequest`` (with the inner
2563    /// ``TBSRequest`` and no optional signature) and can be submitted directly
2564    /// to an OCSP responder.
2565    ///
2566    /// :returns: DER bytes of the complete unsigned ``OCSPRequest``.
2567    /// :raises ValueError: if no request entry was added, or if encoding fails.
2568    fn build_tbs<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
2569        if self.built {
2570            return Err(pyo3::exceptions::PyValueError::new_err(
2571                "build_tbs already called on this OCSPRequestBuilder",
2572            ));
2573        }
2574        self.built = true;
2575        let inner = std::mem::replace(
2576            &mut self.inner,
2577            synta_certificate::OCSPRequestBuilder::new(),
2578        );
2579        let der = inner
2580            .build_tbs()
2581            .map_err(pyo3::exceptions::PyValueError::new_err)?;
2582        Ok(PyBytes::new(py, &der))
2583    }
2584
2585    /// Build the DER-encoded inner ``TBSRequest`` SEQUENCE for signing.
2586    ///
2587    /// These bytes are intended to be signed externally.  After signing, pass
2588    /// them to :meth:`assemble` along with the signature to produce the
2589    /// complete signed ``OCSPRequest``.
2590    ///
2591    /// :returns: DER bytes of the inner ``TBSRequest``.
2592    /// :raises ValueError: if no request entry was added, or if encoding fails.
2593    fn build_tbs_inner<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
2594        if self.built {
2595            return Err(pyo3::exceptions::PyValueError::new_err(
2596                "build_tbs_inner already called on this OCSPRequestBuilder",
2597            ));
2598        }
2599        self.built = true;
2600        let inner = std::mem::replace(
2601            &mut self.inner,
2602            synta_certificate::OCSPRequestBuilder::new(),
2603        );
2604        let der = inner
2605            .build_tbs_inner()
2606            .map_err(pyo3::exceptions::PyValueError::new_err)?;
2607        Ok(PyBytes::new(py, &der))
2608    }
2609
2610    /// Assemble a complete DER-encoded signed ``OCSPRequest``.
2611    ///
2612    /// :param tbs_der: inner ``TBSRequest`` bytes from :meth:`build_tbs_inner`.
2613    /// :param sig_alg_der: outer ``AlgorithmIdentifier`` SEQUENCE TLV.
2614    /// :param signature: raw signature bytes (BIT STRING value).
2615    /// :raises ValueError: if DER encoding fails.
2616    #[staticmethod]
2617    fn assemble<'py>(
2618        py: Python<'py>,
2619        tbs_der: &[u8],
2620        sig_alg_der: &[u8],
2621        signature: &[u8],
2622    ) -> PyResult<Bound<'py, PyBytes>> {
2623        let der = synta_certificate::OCSPRequestBuilder::assemble(tbs_der, sig_alg_der, signature)
2624            .map_err(pyo3::exceptions::PyValueError::new_err)?;
2625        Ok(PyBytes::new(py, &der))
2626    }
2627
2628    fn __repr__(&self) -> String {
2629        "OCSPRequestBuilder()".to_string()
2630    }
2631}