Skip to main content

_synta/certificate/
cmp.rs

1//! Python bindings for RFC 9810 Certificate Management Protocol (CMP) types.
2//!
3//! Exposes ``CMPMessage`` (wrapping ``PKIMessage``) as a Python class and
4//! installs MAC algorithm and key-purpose OID constants into the ``synta.cmp``
5//! submodule.
6
7use std::sync::OnceLock;
8
9use pyo3::prelude::*;
10use pyo3::types::{PyBytes, PyString};
11
12use synta::{Decoder, Encoding};
13
14use crate::error::SyntaErr;
15use crate::types::PyObjectIdentifier;
16
17// ── helpers ───────────────────────────────────────────────────────────────────
18
19/// Map a `PKIBody` variant to its CMP message-type name (lowercase, per RFC 9810).
20fn body_type_name(body: &synta_certificate::cmp_types::PKIBody<'_>) -> &'static str {
21    use synta_certificate::cmp_types::PKIBody::*;
22    match body {
23        Ir(_) => "ir",
24        Ip(_) => "ip",
25        Cr(_) => "cr",
26        Cp(_) => "cp",
27        P10cr(_) => "p10cr",
28        Popdecc(_) => "popdecc",
29        Popdecr(_) => "popdecr",
30        Kur(_) => "kur",
31        Kup(_) => "kup",
32        Krr(_) => "krr",
33        Krp(_) => "krp",
34        Rr(_) => "rr",
35        Rp(_) => "rp",
36        Ccr(_) => "ccr",
37        Ccp(_) => "ccp",
38        Ckuann(_) => "ckuann",
39        Cann(_) => "cann",
40        Rann(_) => "rann",
41        Crlann(_) => "crlann",
42        Pkiconf(_) => "pkiconf",
43        Nested(_) => "nested",
44        Genm(_) => "genm",
45        Genp(_) => "genp",
46        Error(_) => "error",
47        CertConf(_) => "certConf",
48        PollReq(_) => "pollReq",
49        PollRep(_) => "pollRep",
50    }
51}
52
53/// Extract raw body DER from a `PKIBody` variant.  Returns `None` for
54/// the `pkiconf` (NULL) arm; returns the `RawDer` bytes for all others.
55fn body_raw_der(body: &synta_certificate::cmp_types::PKIBody<'_>) -> Option<Vec<u8>> {
56    use synta_certificate::cmp_types::PKIBody::*;
57    match body {
58        Pkiconf(_) => None,
59        Ir(r) | Ip(r) | Cr(r) | Cp(r) | P10cr(r) | Popdecc(r) | Popdecr(r) | Kur(r) | Kup(r)
60        | Krr(r) | Krp(r) | Rr(r) | Rp(r) | Ccr(r) | Ccp(r) | Ckuann(r) | Cann(r) | Rann(r)
61        | Crlann(r) | Nested(r) | Genm(r) | Genp(r) | Error(r) | CertConf(r) | PollReq(r)
62        | PollRep(r) => Some(r.as_bytes().to_vec()),
63    }
64}
65
66// ── PyCMPMessage ──────────────────────────────────────────────────────────────
67
68/// A CMP ``PKIMessage`` (RFC 9810 §5.1).
69///
70/// ``PKIMessage`` is the top-level CMP envelope, carrying a ``PKIHeader``
71/// (metadata: pvno, sender, recipient, timestamps, nonces, …) and a
72/// ``PKIBody`` (the message payload: IR, IP, CR, CP, RR, RP, …).
73///
74/// The ``body_type`` property names the active ``PKIBody`` arm in lowercase
75/// (``"ir"``, ``"ip"``, ``"cr"``, ``"cp"``, ``"pkiconf"``, …).  For all arms
76/// except ``pkiconf``, ``body_der`` returns the raw DER bytes of the body
77/// content for further decoding with a ``synta.Decoder``.
78///
79/// ```python,ignore
80/// import synta.cmp as cmp
81///
82/// msg = cmp.CMPMessage.from_der(open("cmp.der", "rb").read())
83/// print(msg.pvno, msg.body_type)
84/// if msg.body_type == "ir":
85///     # body_der contains the DER of CertReqMessages
86///     import synta.crmf as crmf
87///     reqs = crmf.CertReqMessages.from_der(msg.body_der)
88///     for req in reqs:
89///         print(req.cert_req_id, req.popo_type)
90/// ```
91#[pyclass(frozen, name = "CMPMessage")]
92pub struct PyCMPMessage {
93    _data: Py<PyBytes>,
94    raw: &'static [u8],
95    inner: OnceLock<Box<synta_certificate::cmp_types::PKIMessage<'static>>>,
96    // Field caches
97    pvno_cache: OnceLock<i64>,
98    body_type_cache: OnceLock<&'static str>,
99    body_der_cache: OnceLock<Option<Py<PyBytes>>>,
100    sender_der_cache: OnceLock<Py<PyBytes>>,
101    recipient_der_cache: OnceLock<Py<PyBytes>>,
102    transaction_id_cache: OnceLock<Option<Py<PyBytes>>>,
103    sender_nonce_cache: OnceLock<Option<Py<PyBytes>>>,
104    recip_nonce_cache: OnceLock<Option<Py<PyBytes>>>,
105    protection_alg_oid_cache: OnceLock<Option<Py<PyObjectIdentifier>>>,
106    message_time_cache: OnceLock<Option<Py<PyString>>>,
107}
108
109impl PyCMPMessage {
110    fn msg(&self) -> PyResult<&synta_certificate::cmp_types::PKIMessage<'static>> {
111        if let Some(v) = self.inner.get() {
112            return Ok(v.as_ref());
113        }
114        let mut dec = Decoder::new(self.raw, Encoding::Der);
115        let decoded = dec
116            .decode::<synta_certificate::cmp_types::PKIMessage<'static>>()
117            .map_err(SyntaErr)?;
118        let _ = self.inner.set(Box::new(decoded));
119        Ok(self.inner.get().unwrap().as_ref())
120    }
121}
122
123#[pymethods]
124impl PyCMPMessage {
125    /// Parse a DER-encoded ``PKIMessage`` SEQUENCE.
126    ///
127    /// :param data: DER bytes of the ``PKIMessage``.
128    /// :raises ValueError: if the bytes cannot be decoded.
129    #[staticmethod]
130    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
131        let py_bytes = data.unbind();
132        {
133            let raw = py_bytes.as_bytes(py);
134            Decoder::new(raw, Encoding::Der)
135                .decode::<synta_certificate::cmp_types::PKIMessage<'_>>()
136                .map_err(SyntaErr)?;
137        }
138        let raw: &'static [u8] = unsafe {
139            let s = py_bytes.bind(py).as_bytes();
140            std::slice::from_raw_parts(s.as_ptr(), s.len())
141        };
142        Ok(Self {
143            _data: py_bytes,
144            raw,
145            inner: OnceLock::new(),
146            pvno_cache: OnceLock::new(),
147            body_type_cache: OnceLock::new(),
148            body_der_cache: OnceLock::new(),
149            sender_der_cache: OnceLock::new(),
150            recipient_der_cache: OnceLock::new(),
151            transaction_id_cache: OnceLock::new(),
152            sender_nonce_cache: OnceLock::new(),
153            recip_nonce_cache: OnceLock::new(),
154            protection_alg_oid_cache: OnceLock::new(),
155            message_time_cache: OnceLock::new(),
156        })
157    }
158
159    /// Return the DER encoding of this ``PKIMessage``.
160    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
161        Ok(PyBytes::new(py, &self.msg()?.to_der().map_err(SyntaErr)?))
162    }
163
164    /// CMP protocol version number (``pvno`` field; ``2`` for CMP v2/v3).
165    #[getter]
166    fn pvno(&self) -> PyResult<i64> {
167        if let Some(v) = self.pvno_cache.get() {
168            return Ok(*v);
169        }
170        let v = self.msg()?.header.pvno.as_i64().unwrap_or(2);
171        let _ = self.pvno_cache.set(v);
172        Ok(v)
173    }
174
175    /// Name of the active ``PKIBody`` arm in lowercase (e.g. ``"ir"``, ``"ip"``,
176    /// ``"cr"``, ``"cp"``, ``"rr"``, ``"rp"``, ``"pkiconf"``).
177    #[getter]
178    fn body_type(&self) -> PyResult<&'static str> {
179        if let Some(v) = self.body_type_cache.get() {
180            return Ok(v);
181        }
182        let name = body_type_name(&self.msg()?.body);
183        let _ = self.body_type_cache.set(name);
184        Ok(name)
185    }
186
187    /// Raw DER bytes of the ``PKIBody`` content, or ``None`` for ``pkiconf``
188    /// (which carries a NULL value and has no further content to decode).
189    ///
190    /// For ``"ir"``, ``"cr"`` etc., these bytes are a ``CertReqMessages``
191    /// SEQUENCE OF that can be decoded with ``synta.crmf.CertReqMessages.from_der``.
192    #[getter]
193    fn body_der<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
194        if let Some(cached) = self.body_der_cache.get() {
195            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
196        }
197        let der = body_raw_der(&self.msg()?.body);
198        let py_opt = der
199            .as_deref()
200            .map(|b| PyBytes::new(py, b).as_unbound().clone_ref(py));
201        let _ = self.body_der_cache.set(py_opt);
202        Ok(self
203            .body_der_cache
204            .get()
205            .unwrap()
206            .as_ref()
207            .map(|b| b.clone_ref(py).into_bound(py)))
208    }
209
210    /// Raw DER bytes of the ``sender`` ``GeneralName`` field.
211    #[getter]
212    fn sender_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
213        if let Some(c) = self.sender_der_cache.get() {
214            return Ok(c.clone_ref(py).into_bound(py));
215        }
216        let der = self.msg()?.header.sender.to_der().map_err(SyntaErr)?;
217        let b = PyBytes::new(py, &der);
218        let _ = self.sender_der_cache.set(b.as_unbound().clone_ref(py));
219        Ok(b)
220    }
221
222    /// Raw DER bytes of the ``recipient`` ``GeneralName`` field.
223    #[getter]
224    fn recipient_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
225        if let Some(c) = self.recipient_der_cache.get() {
226            return Ok(c.clone_ref(py).into_bound(py));
227        }
228        let der = self.msg()?.header.recipient.to_der().map_err(SyntaErr)?;
229        let b = PyBytes::new(py, &der);
230        let _ = self.recipient_der_cache.set(b.as_unbound().clone_ref(py));
231        Ok(b)
232    }
233
234    /// ``transactionID`` bytes, or ``None`` if not present.
235    #[getter]
236    fn transaction_id<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
237        if let Some(cached) = self.transaction_id_cache.get() {
238            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
239        }
240        let opt = self
241            .msg()?
242            .header
243            .transaction_id
244            .as_ref()
245            .map(|o| PyBytes::new(py, o.as_bytes()).as_unbound().clone_ref(py));
246        let _ = self.transaction_id_cache.set(opt);
247        Ok(self
248            .transaction_id_cache
249            .get()
250            .unwrap()
251            .as_ref()
252            .map(|b| b.clone_ref(py).into_bound(py)))
253    }
254
255    /// ``senderNonce`` bytes, or ``None`` if not present.
256    #[getter]
257    fn sender_nonce<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
258        if let Some(cached) = self.sender_nonce_cache.get() {
259            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
260        }
261        let opt = self
262            .msg()?
263            .header
264            .sender_nonce
265            .as_ref()
266            .map(|o| PyBytes::new(py, o.as_bytes()).as_unbound().clone_ref(py));
267        let _ = self.sender_nonce_cache.set(opt);
268        Ok(self
269            .sender_nonce_cache
270            .get()
271            .unwrap()
272            .as_ref()
273            .map(|b| b.clone_ref(py).into_bound(py)))
274    }
275
276    /// ``recipNonce`` bytes, or ``None`` if not present.
277    #[getter]
278    fn recip_nonce<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
279        if let Some(cached) = self.recip_nonce_cache.get() {
280            return Ok(cached.as_ref().map(|b| b.clone_ref(py).into_bound(py)));
281        }
282        let opt = self
283            .msg()?
284            .header
285            .recip_nonce
286            .as_ref()
287            .map(|o| PyBytes::new(py, o.as_bytes()).as_unbound().clone_ref(py));
288        let _ = self.recip_nonce_cache.set(opt);
289        Ok(self
290            .recip_nonce_cache
291            .get()
292            .unwrap()
293            .as_ref()
294            .map(|b| b.clone_ref(py).into_bound(py)))
295    }
296
297    /// OID of the ``protectionAlg`` ``AlgorithmIdentifier``, or ``None``.
298    #[getter]
299    fn protection_alg_oid(&self, py: Python<'_>) -> PyResult<Option<Py<PyObjectIdentifier>>> {
300        if let Some(cached) = self.protection_alg_oid_cache.get() {
301            return Ok(cached.as_ref().map(|o| o.clone_ref(py)));
302        }
303        let opt = self
304            .msg()?
305            .header
306            .protection_alg
307            .as_ref()
308            .map(|alg| Py::new(py, PyObjectIdentifier::from_oid(alg.algorithm.clone())))
309            .transpose()?;
310        let _ = self
311            .protection_alg_oid_cache
312            .set(opt.as_ref().map(|o| o.clone_ref(py)));
313        Ok(opt)
314    }
315
316    /// ``messageTime`` as a GeneralizedTime string, or ``None`` if not present.
317    #[getter]
318    fn message_time<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyString>>> {
319        if let Some(cached) = self.message_time_cache.get() {
320            return Ok(cached.as_ref().map(|s| s.clone_ref(py).into_bound(py)));
321        }
322        let opt = self
323            .msg()?
324            .header
325            .message_time
326            .as_ref()
327            .map(|t| PyString::new(py, &t.to_string()).as_unbound().clone_ref(py));
328        let _ = self.message_time_cache.set(opt);
329        Ok(self
330            .message_time_cache
331            .get()
332            .unwrap()
333            .as_ref()
334            .map(|s| s.clone_ref(py).into_bound(py)))
335    }
336
337    fn __repr__(&self) -> PyResult<String> {
338        Ok(format!(
339            "CMPMessage(pvno={}, body_type={})",
340            self.pvno()?,
341            self.body_type()?,
342        ))
343    }
344}
345
346// ── PyCMPMessageBuilder ───────────────────────────────────────────────────────
347
348/// Fluent builder for a CMP ``PKIMessage`` (RFC 9810 / RFC 4210).
349///
350/// Set sender, recipient, optional nonces, and a body payload, then call
351/// :meth:`build` to produce a :class:`CMPMessage`.
352///
353/// ```python,ignore
354/// import synta.cmp as cmp
355///
356/// msg = (
357///     cmp.CMPMessageBuilder()
358///     .sender_rfc822("ca@example.com")
359///     .recipient_directory_name(subject_der)
360///     .transaction_id(b"\x01\x02\x03\x04")
361///     .body_pkiconf()
362///     .build()
363/// )
364/// print(msg.pvno, msg.body_type)
365/// ```
366#[pyclass(name = "CMPMessageBuilder")]
367pub struct PyCMPMessageBuilder {
368    pvno: i64,
369    sender: Option<synta_certificate::GeneralNameSpec>,
370    recipient: Option<synta_certificate::GeneralNameSpec>,
371    transaction_id: Option<Vec<u8>>,
372    sender_nonce: Option<Vec<u8>>,
373    recip_nonce: Option<Vec<u8>>,
374    /// Body type tag: 0=pkiconf, 1=ir, 2=cr, 3=kur, 4=p10cr, 5=genm
375    body_tag: u8,
376    body_der: Option<Vec<u8>>,
377}
378
379#[pymethods]
380impl PyCMPMessageBuilder {
381    /// Create a new ``CMPMessageBuilder`` with default ``pvno=2`` (cmp2000) and no body.
382    #[new]
383    fn new() -> Self {
384        Self {
385            pvno: 2,
386            sender: None,
387            recipient: None,
388            transaction_id: None,
389            sender_nonce: None,
390            recip_nonce: None,
391            body_tag: 0,
392            body_der: None,
393        }
394    }
395
396    /// Set the ``pvno`` version integer (``2`` = cmp2000, ``3`` = cmp2021).
397    fn pvno<'py>(slf: Bound<'py, Self>, pvno: i64) -> Bound<'py, Self> {
398        slf.borrow_mut().pvno = pvno;
399        slf
400    }
401
402    /// Set the sender as an ``rfc822Name`` (e-mail address).
403    fn sender_rfc822<'py>(slf: Bound<'py, Self>, email: &str) -> Bound<'py, Self> {
404        slf.borrow_mut().sender = Some(synta_certificate::GeneralNameSpec::rfc822(email));
405        slf
406    }
407
408    /// Set the sender as a ``dNSName`` (DNS host name).
409    fn sender_dns<'py>(slf: Bound<'py, Self>, host: &str) -> Bound<'py, Self> {
410        slf.borrow_mut().sender = Some(synta_certificate::GeneralNameSpec::dns(host));
411        slf
412    }
413
414    /// Set the sender as a ``directoryName`` from a DER-encoded ``Name`` SEQUENCE TLV.
415    ///
416    /// Pass the output of :class:`synta.NameBuilder` or a certificate's
417    /// ``subject_raw_der`` bytes.
418    fn sender_directory_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
419        slf.borrow_mut().sender =
420            Some(synta_certificate::GeneralNameSpec::directory_name(name_der));
421        slf
422    }
423
424    /// Set the sender as a ``uniformResourceIdentifier`` (URI).
425    fn sender_uri<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
426        slf.borrow_mut().sender = Some(synta_certificate::GeneralNameSpec::uri(uri));
427        slf
428    }
429
430    /// Set the recipient as an ``rfc822Name`` (e-mail address).
431    fn recipient_rfc822<'py>(slf: Bound<'py, Self>, email: &str) -> Bound<'py, Self> {
432        slf.borrow_mut().recipient = Some(synta_certificate::GeneralNameSpec::rfc822(email));
433        slf
434    }
435
436    /// Set the recipient as a ``dNSName`` (DNS host name).
437    fn recipient_dns<'py>(slf: Bound<'py, Self>, host: &str) -> Bound<'py, Self> {
438        slf.borrow_mut().recipient = Some(synta_certificate::GeneralNameSpec::dns(host));
439        slf
440    }
441
442    /// Set the recipient as a ``uniformResourceIdentifier`` (URI).
443    fn recipient_uri<'py>(slf: Bound<'py, Self>, uri: &str) -> Bound<'py, Self> {
444        slf.borrow_mut().recipient = Some(synta_certificate::GeneralNameSpec::uri(uri));
445        slf
446    }
447
448    /// Set the recipient as a ``directoryName`` from a DER-encoded ``Name`` SEQUENCE TLV.
449    fn recipient_directory_name<'py>(slf: Bound<'py, Self>, name_der: &[u8]) -> Bound<'py, Self> {
450        slf.borrow_mut().recipient =
451            Some(synta_certificate::GeneralNameSpec::directory_name(name_der));
452        slf
453    }
454
455    /// Set the ``transactionID`` from raw OCTET STRING value bytes.
456    fn transaction_id<'py>(slf: Bound<'py, Self>, bytes: &[u8]) -> Bound<'py, Self> {
457        slf.borrow_mut().transaction_id = Some(bytes.to_vec());
458        slf
459    }
460
461    /// Set the ``senderNonce`` from raw OCTET STRING value bytes.
462    fn sender_nonce<'py>(slf: Bound<'py, Self>, bytes: &[u8]) -> Bound<'py, Self> {
463        slf.borrow_mut().sender_nonce = Some(bytes.to_vec());
464        slf
465    }
466
467    /// Set the ``recipNonce`` from raw OCTET STRING value bytes.
468    fn recip_nonce<'py>(slf: Bound<'py, Self>, bytes: &[u8]) -> Bound<'py, Self> {
469        slf.borrow_mut().recip_nonce = Some(bytes.to_vec());
470        slf
471    }
472
473    /// Set the body to ``pkiConf [19]`` (confirmation, no content).
474    fn body_pkiconf<'py>(slf: Bound<'py, Self>) -> Bound<'py, Self> {
475        let mut b = slf.borrow_mut();
476        b.body_tag = 0;
477        b.body_der = None;
478        drop(b);
479        slf
480    }
481
482    /// Set the body to ``ir [0]`` (Initialization Request) from DER-encoded
483    /// ``CertReqMessages`` bytes.
484    fn body_ir<'py>(slf: Bound<'py, Self>, cert_req_messages_der: &[u8]) -> Bound<'py, Self> {
485        let mut b = slf.borrow_mut();
486        b.body_tag = 1;
487        b.body_der = Some(cert_req_messages_der.to_vec());
488        drop(b);
489        slf
490    }
491
492    /// Set the body to ``cr [2]`` (Certification Request) from DER-encoded
493    /// ``CertReqMessages`` bytes.
494    fn body_cr<'py>(slf: Bound<'py, Self>, cert_req_messages_der: &[u8]) -> Bound<'py, Self> {
495        let mut b = slf.borrow_mut();
496        b.body_tag = 2;
497        b.body_der = Some(cert_req_messages_der.to_vec());
498        drop(b);
499        slf
500    }
501
502    /// Set the body to ``kur [7]`` (Key Update Request) from DER-encoded
503    /// ``CertReqMessages`` bytes.
504    fn body_kur<'py>(slf: Bound<'py, Self>, cert_req_messages_der: &[u8]) -> Bound<'py, Self> {
505        let mut b = slf.borrow_mut();
506        b.body_tag = 3;
507        b.body_der = Some(cert_req_messages_der.to_vec());
508        drop(b);
509        slf
510    }
511
512    /// Set the body to ``p10cr [4]`` (PKCS #10 Certification Request) from
513    /// DER-encoded CSR bytes.
514    fn body_p10cr<'py>(slf: Bound<'py, Self>, csr_der: &[u8]) -> Bound<'py, Self> {
515        let mut b = slf.borrow_mut();
516        b.body_tag = 4;
517        b.body_der = Some(csr_der.to_vec());
518        drop(b);
519        slf
520    }
521
522    /// Set the body to ``genm [21]`` (General Message) from DER-encoded
523    /// ``GenMsgContent`` bytes.
524    fn body_genm<'py>(slf: Bound<'py, Self>, gen_msg_der: &[u8]) -> Bound<'py, Self> {
525        let mut b = slf.borrow_mut();
526        b.body_tag = 5;
527        b.body_der = Some(gen_msg_der.to_vec());
528        drop(b);
529        slf
530    }
531
532    /// Build the :class:`CMPMessage`.
533    ///
534    /// :raises ValueError: if sender or recipient are not set, or if any
535    ///     ``GeneralName`` data is invalid.
536    fn build(&self, py: Python<'_>) -> PyResult<PyCMPMessage> {
537        use pyo3::exceptions::PyValueError;
538        use synta_certificate::CMPMessageBuilder;
539
540        let sender_spec = self
541            .sender
542            .clone()
543            .ok_or_else(|| PyValueError::new_err("sender is required"))?;
544        let recipient_spec = self
545            .recipient
546            .clone()
547            .ok_or_else(|| PyValueError::new_err("recipient is required"))?;
548
549        let mut rust_builder = CMPMessageBuilder::new()
550            .pvno(self.pvno)
551            .sender(sender_spec)
552            .recipient(recipient_spec);
553
554        if let Some(ref b) = self.transaction_id {
555            rust_builder = rust_builder.transaction_id(b);
556        }
557        if let Some(ref b) = self.sender_nonce {
558            rust_builder = rust_builder.sender_nonce(b);
559        }
560        if let Some(ref b) = self.recip_nonce {
561            rust_builder = rust_builder.recip_nonce(b);
562        }
563
564        match self.body_tag {
565            0 => {
566                rust_builder = rust_builder.body_pkiconf();
567            }
568            1 => {
569                rust_builder = rust_builder.body_ir(self.body_der.as_deref().unwrap_or(&[]));
570            }
571            2 => {
572                rust_builder = rust_builder.body_cr(self.body_der.as_deref().unwrap_or(&[]));
573            }
574            3 => {
575                rust_builder = rust_builder.body_kur(self.body_der.as_deref().unwrap_or(&[]));
576            }
577            4 => {
578                rust_builder = rust_builder.body_p10cr(self.body_der.as_deref().unwrap_or(&[]));
579            }
580            5 => {
581                rust_builder = rust_builder.body_genm(self.body_der.as_deref().unwrap_or(&[]));
582            }
583            _ => {}
584        }
585
586        let der = rust_builder
587            .build()
588            .map_err(|e| PyValueError::new_err(e.to_string()))?;
589
590        let py_bytes = PyBytes::new(py, &der).unbind();
591        let raw: &'static [u8] = unsafe {
592            let s = py_bytes.bind(py).as_bytes();
593            std::slice::from_raw_parts(s.as_ptr(), s.len())
594        };
595        Ok(PyCMPMessage {
596            _data: py_bytes,
597            raw,
598            inner: OnceLock::new(),
599            pvno_cache: OnceLock::new(),
600            body_type_cache: OnceLock::new(),
601            body_der_cache: OnceLock::new(),
602            sender_der_cache: OnceLock::new(),
603            recipient_der_cache: OnceLock::new(),
604            transaction_id_cache: OnceLock::new(),
605            sender_nonce_cache: OnceLock::new(),
606            recip_nonce_cache: OnceLock::new(),
607            protection_alg_oid_cache: OnceLock::new(),
608            message_time_cache: OnceLock::new(),
609        })
610    }
611
612    fn __repr__(&self) -> String {
613        use synta_certificate::GeneralNameSpec;
614        let sender = match &self.sender {
615            None => "None".to_string(),
616            Some(GeneralNameSpec::Rfc822(s)) => format!("rfc822:{s}"),
617            Some(GeneralNameSpec::Dns(s)) => format!("dns:{s}"),
618            Some(GeneralNameSpec::Uri(s)) => format!("uri:{s}"),
619            Some(GeneralNameSpec::DirectoryName(_)) => "directoryName".to_string(),
620            Some(GeneralNameSpec::IpAddress(_)) => "iPAddress".to_string(),
621            Some(GeneralNameSpec::RegisteredId(oid)) => format!("registeredID:{oid}"),
622        };
623        format!("CMPMessageBuilder(pvno={}, sender={})", self.pvno, sender)
624    }
625}
626
627// ── register_cmp_submodule ────────────────────────────────────────────────────
628
629/// Build and register the ``synta.cmp`` submodule.
630pub(super) fn register_cmp_submodule(parent: &Bound<'_, PyModule>) -> PyResult<()> {
631    let py = parent.py();
632    let m = PyModule::new(py, "cmp")?;
633
634    m.add_class::<PyCMPMessage>()?;
635    m.add_class::<PyCMPMessageBuilder>()?;
636
637    // ── MAC algorithm OIDs (RFC 9810 Appendix D) ─────────────────────────────
638    m.add(
639        "ID_PASSWORD_BASED_MAC",
640        super::oid_const(py, synta_certificate::cmp_types::ID_PASSWORD_BASED_MAC),
641    )?;
642    m.add(
643        "ID_DHBASED_MAC",
644        super::oid_const(py, synta_certificate::cmp_types::ID_DHBASED_MAC),
645    )?;
646    m.add(
647        "ID_KEM_BASED_MAC",
648        super::oid_const(py, synta_certificate::cmp_types::ID_KEM_BASED_MAC),
649    )?;
650
651    // ── CMP key-purpose OID (RFC 9810 §4) ────────────────────────────────────
652    m.add(
653        "ID_KP_CM_KGA",
654        super::oid_const(py, synta_certificate::cmp_types::ID_KP_CM_KGA),
655    )?;
656
657    // ── CMP registration-control OIDs shared with CRMF (RFC 9810 Appendix) ──
658    m.add(
659        "ID_REG_CTRL_ALT_CERT_TEMPLATE",
660        super::oid_const(
661            py,
662            synta_certificate::cmp_types::ID_REG_CTRL_ALT_CERT_TEMPLATE,
663        ),
664    )?;
665    m.add(
666        "ID_REG_CTRL_ALG_ID",
667        super::oid_const(py, synta_certificate::cmp_types::ID_REG_CTRL_ALG_ID),
668    )?;
669    m.add(
670        "ID_REG_CTRL_RSA_KEY_LEN",
671        super::oid_const(py, synta_certificate::cmp_types::ID_REG_CTRL_RSA_KEY_LEN),
672    )?;
673
674    crate::install_submodule(
675        parent,
676        &m,
677        "synta.cmp",
678        Some(concat!(
679            "synta.cmp — RFC 9810 Certificate Management Protocol v3 types.\n\n",
680            "Provides CMPMessage (wrapping PKIMessage) for decoding CMP\n",
681            "envelopes.  body_type names the active PKIBody arm; body_der\n",
682            "returns the raw bytes for further decoding with synta.crmf or\n",
683            "a synta.Decoder.  Also exposes MAC algorithm and key-purpose\n",
684            "OID constants.",
685        )),
686    )
687}