Skip to main content

_mtc/
mtc.rs

1//! Python bindings for synta.mtc — Merkle Tree Certificate (MTC) ASN.1 types.
2//!
3//! Exposes ``synta.mtc`` — a submodule containing all ASN.1 types from the MTC
4//! specification (draft-ietf-plants-merkle-tree-certs), decoded from DER.
5//!
6//! # Classes
7//!
8//! | Python name                | Schema type                | Description                         |
9//! |----------------------------|----------------------------|-------------------------------------|
10//! | `ProofNode`                | `ProofNode`                | Left/right hash in inclusion path   |
11//! | `Subtree`                  | `Subtree`                  | Hash subtree range                  |
12//! | `SubtreeProof`             | `SubtreeProof`             | Left + right subtrees               |
13//! | `InclusionProof`           | `InclusionProof`           | Log inclusion proof                 |
14//! | `LogID`                    | `LogID`                    | Log identifier (hash alg + pubkey)  |
15//! | `CosignerID`               | `CosignerID`               | Cosigner identity                   |
16//! | `Checkpoint`               | `Checkpoint`               | Signed tree checkpoint              |
17//! | `SubtreeSignature`         | `SubtreeSignature`         | Cosigner subtree signature          |
18//! | `TbsCertificateLogEntry`   | `TBSCertificateLogEntry`   | Log entry for a certificate         |
19//! | `MerkleTreeCertEntry`      | `MerkleTreeCertEntry`      | CHOICE: null or TBS entry           |
20//! | `LandmarkID`               | `LandmarkID`               | Landmark log identifier             |
21//! | `StandaloneCertificate`    | `StandaloneCertificate`    | Full standalone MTC certificate     |
22//! | `LandmarkCertificate`      | `LandmarkCertificate`      | Landmark certificate                |
23
24use pyo3::prelude::*;
25use pyo3::types::{PyBytes, PyList};
26use synta::traits::{Decode, Encode};
27use synta_mtc::types::{
28    CosignerID, InclusionProof, LandmarkCertificate, LandmarkID, LogID, MerkleTreeCertEntry,
29    ProofNode, StandaloneCertificate, Subtree, SubtreeProof, SubtreeSignature,
30    TBSCertificateLogEntry,
31};
32// The local MTC Name type (CHOICE wrapping RDNSequence), aliased to avoid clash.
33use synta_mtc::types::Name as MtcName;
34
35use synta_python_common::{opt_py_list, SyntaErr};
36
37// ── Encoding helpers ──────────────────────────────────────────────────────────
38
39/// Encode any ASN.1 value implementing Encode to DER bytes.
40fn encode_to_der<T: Encode>(val: &T) -> PyResult<Vec<u8>> {
41    let mut enc = synta::Encoder::new(synta::Encoding::Der);
42    val.encode(&mut enc).map_err(SyntaErr)?;
43    Ok(enc.finish().map_err(SyntaErr)?)
44}
45
46/// Extract the dotted-decimal OID string from an AlgorithmIdentifier.
47fn alg_oid_str(alg: &synta_certificate::AlgorithmIdentifier<'_>) -> String {
48    alg.algorithm.to_string()
49}
50
51/// Encode a SubjectPublicKeyInfo to raw DER bytes.
52fn spki_to_der(spki: &synta_certificate::SubjectPublicKeyInfo<'_>) -> PyResult<Vec<u8>> {
53    encode_to_der(spki)
54}
55
56/// Encode a TBSCertificate to raw DER bytes.
57fn tbs_cert_to_der(tbs: &synta_certificate::TBSCertificate<'_>) -> PyResult<Vec<u8>> {
58    encode_to_der(tbs)
59}
60
61/// Format a Validity Time value as a GeneralizedTime string.
62fn time_to_str(t: &synta_certificate::Time) -> String {
63    match t {
64        synta_certificate::Time::UtcTime(t) => t.to_string(),
65        synta_certificate::Time::GeneralTime(t) => t.to_string(),
66    }
67}
68
69/// Convert synta Integer to i64, propagating errors.
70fn int_to_i64(i: &synta::Integer) -> PyResult<i64> {
71    Ok(i.as_i64().map_err(SyntaErr)?)
72}
73
74/// Encode a local MTC Name (CHOICE RDNSequence) to DER bytes.
75fn mtc_name_to_der(name: &MtcName) -> PyResult<Vec<u8>> {
76    encode_to_der(name)
77}
78
79// ── ProofNode ─────────────────────────────────────────────────────────────────
80
81/// A node in a Merkle inclusion proof path.
82///
83/// ``is_left`` indicates whether this hash is on the left (True) or right
84/// (False) side of the tree path. ``hash`` is the raw hash bytes.
85///
86/// Example:
87///
88/// ```python,ignore
89/// import synta.mtc as mtc
90///
91/// node = mtc.ProofNode.from_der(der_bytes)
92/// print(node.is_left, node.hash.hex())
93/// ```
94#[pyclass(frozen, name = "ProofNode")]
95pub struct PyProofNode {
96    is_left: bool,
97    hash: Vec<u8>,
98}
99
100#[pymethods]
101impl PyProofNode {
102    /// Parse a DER-encoded ``ProofNode`` SEQUENCE.
103    #[staticmethod]
104    pub fn from_der(data: &[u8]) -> PyResult<Self> {
105        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
106        let node = ProofNode::decode(&mut dec).map_err(SyntaErr)?;
107        Ok(PyProofNode {
108            is_left: node.is_left.value(),
109            hash: node.hash.as_bytes().to_vec(),
110        })
111    }
112
113    /// ``True`` if this hash is on the left side of the path.
114    #[getter]
115    fn is_left(&self) -> bool {
116        self.is_left
117    }
118
119    /// Raw hash bytes at this proof node.
120    #[getter]
121    fn hash<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
122        PyBytes::new(py, &self.hash)
123    }
124
125    fn __repr__(&self) -> String {
126        format!("ProofNode(is_left={})", self.is_left)
127    }
128
129    fn __eq__(&self, other: &Self) -> bool {
130        self.is_left == other.is_left && self.hash == other.hash
131    }
132}
133
134// ── Subtree ───────────────────────────────────────────────────────────────────
135
136/// A hash subtree range in the Merkle tree.
137///
138/// ``start`` and ``end`` are integer leaf indices; ``value`` is the aggregated
139/// hash covering entries ``[start, end)``.
140#[pyclass(frozen, name = "Subtree")]
141pub struct PySubtree {
142    start: i64,
143    end: i64,
144    value: Vec<u8>,
145}
146
147pub(crate) fn make_subtree(py: Python<'_>, s: Subtree) -> PyResult<Py<PySubtree>> {
148    Py::new(
149        py,
150        PySubtree {
151            start: int_to_i64(&s.start)?,
152            end: int_to_i64(&s.end)?,
153            value: s.value.as_bytes().to_vec(),
154        },
155    )
156}
157
158#[pymethods]
159impl PySubtree {
160    /// Parse a DER-encoded ``Subtree`` SEQUENCE.
161    #[staticmethod]
162    pub fn from_der(data: &[u8]) -> PyResult<Self> {
163        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
164        let s = Subtree::decode(&mut dec).map_err(SyntaErr)?;
165        Ok(PySubtree {
166            start: int_to_i64(&s.start)?,
167            end: int_to_i64(&s.end)?,
168            value: s.value.as_bytes().to_vec(),
169        })
170    }
171
172    /// Start leaf index (inclusive).
173    #[getter]
174    fn start(&self) -> i64 {
175        self.start
176    }
177
178    /// End leaf index (exclusive).
179    #[getter]
180    fn end(&self) -> i64 {
181        self.end
182    }
183
184    /// Aggregated hash bytes covering entries ``[start, end)``.
185    #[getter]
186    fn value<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
187        PyBytes::new(py, &self.value)
188    }
189
190    fn __repr__(&self) -> String {
191        format!("Subtree(start={}, end={})", self.start, self.end)
192    }
193
194    fn __eq__(&self, other: &Self) -> bool {
195        self.start == other.start && self.end == other.end && self.value == other.value
196    }
197}
198
199// ── SubtreeProof ──────────────────────────────────────────────────────────────
200
201/// Proof consisting of optional left and right subtree lists.
202///
203/// Both ``left_subtrees`` and ``right_subtrees`` may be ``None`` if absent
204/// in the encoded structure.
205#[pyclass(frozen, name = "SubtreeProof")]
206pub struct PySubtreeProof {
207    left_subtrees: Option<Vec<Py<PySubtree>>>,
208    right_subtrees: Option<Vec<Py<PySubtree>>>,
209}
210
211pub(crate) fn make_subtree_proof(py: Python<'_>, sp: SubtreeProof) -> PyResult<Py<PySubtreeProof>> {
212    let left_subtrees = sp
213        .left_subtrees
214        .map(|v| {
215            v.into_iter()
216                .map(|s| make_subtree(py, s))
217                .collect::<PyResult<Vec<_>>>()
218        })
219        .transpose()?;
220    let right_subtrees = sp
221        .right_subtrees
222        .map(|v| {
223            v.into_iter()
224                .map(|s| make_subtree(py, s))
225                .collect::<PyResult<Vec<_>>>()
226        })
227        .transpose()?;
228    Py::new(
229        py,
230        PySubtreeProof {
231            left_subtrees,
232            right_subtrees,
233        },
234    )
235}
236
237#[pymethods]
238impl PySubtreeProof {
239    /// Parse a DER-encoded ``SubtreeProof`` SEQUENCE.
240    #[staticmethod]
241    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
242        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
243        let sp = SubtreeProof::decode(&mut dec).map_err(SyntaErr)?;
244        let left_subtrees = sp
245            .left_subtrees
246            .map(|v| {
247                v.into_iter()
248                    .map(|s| make_subtree(py, s))
249                    .collect::<PyResult<Vec<_>>>()
250            })
251            .transpose()?;
252        let right_subtrees = sp
253            .right_subtrees
254            .map(|v| {
255                v.into_iter()
256                    .map(|s| make_subtree(py, s))
257                    .collect::<PyResult<Vec<_>>>()
258            })
259            .transpose()?;
260        Ok(PySubtreeProof {
261            left_subtrees,
262            right_subtrees,
263        })
264    }
265
266    /// Left subtrees, or ``None`` if absent.
267    #[getter]
268    fn left_subtrees<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyList>>> {
269        opt_py_list(py, &self.left_subtrees)
270    }
271
272    /// Right subtrees, or ``None`` if absent.
273    #[getter]
274    fn right_subtrees<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyList>>> {
275        opt_py_list(py, &self.right_subtrees)
276    }
277
278    fn __repr__(&self) -> String {
279        let l = self.left_subtrees.as_ref().map(|v| v.len()).unwrap_or(0);
280        let r = self.right_subtrees.as_ref().map(|v| v.len()).unwrap_or(0);
281        format!("SubtreeProof(left={l}, right={r})")
282    }
283}
284
285// ── InclusionProof ────────────────────────────────────────────────────────────
286
287/// Merkle tree inclusion proof for a log entry.
288///
289/// ``log_entry_index`` is the leaf position; ``tree_size`` is the total tree
290/// size at the time of the proof; ``inclusion_path`` is the ordered list of
291/// sibling hashes.
292#[pyclass(frozen, name = "InclusionProof")]
293pub struct PyInclusionProof {
294    log_entry_index: i64,
295    tree_size: i64,
296    inclusion_path: Vec<Py<PyProofNode>>,
297}
298
299pub(crate) fn make_inclusion_proof(
300    py: Python<'_>,
301    ip: InclusionProof,
302) -> PyResult<Py<PyInclusionProof>> {
303    let log_entry_index = int_to_i64(&ip.log_entry_index)?;
304    let tree_size = int_to_i64(&ip.tree_size)?;
305    let inclusion_path = ip
306        .inclusion_path
307        .into_iter()
308        .map(|n| {
309            Py::new(
310                py,
311                PyProofNode {
312                    is_left: n.is_left.value(),
313                    hash: n.hash.as_bytes().to_vec(),
314                },
315            )
316        })
317        .collect::<PyResult<Vec<_>>>()?;
318    Py::new(
319        py,
320        PyInclusionProof {
321            log_entry_index,
322            tree_size,
323            inclusion_path,
324        },
325    )
326}
327
328#[pymethods]
329impl PyInclusionProof {
330    /// Parse a DER-encoded ``InclusionProof`` SEQUENCE.
331    #[staticmethod]
332    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
333        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
334        let ip = InclusionProof::decode(&mut dec).map_err(SyntaErr)?;
335        let log_entry_index = int_to_i64(&ip.log_entry_index)?;
336        let tree_size = int_to_i64(&ip.tree_size)?;
337        let inclusion_path = ip
338            .inclusion_path
339            .into_iter()
340            .map(|n| {
341                Py::new(
342                    py,
343                    PyProofNode {
344                        is_left: n.is_left.value(),
345                        hash: n.hash.as_bytes().to_vec(),
346                    },
347                )
348            })
349            .collect::<PyResult<Vec<_>>>()?;
350        Ok(PyInclusionProof {
351            log_entry_index,
352            tree_size,
353            inclusion_path,
354        })
355    }
356
357    /// Leaf index of the certified entry.
358    #[getter]
359    fn log_entry_index(&self) -> i64 {
360        self.log_entry_index
361    }
362
363    /// Total number of leaves in the tree at proof time.
364    #[getter]
365    fn tree_size(&self) -> i64 {
366        self.tree_size
367    }
368
369    /// Ordered list of sibling hashes forming the inclusion path.
370    #[getter]
371    fn inclusion_path<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
372        let elems: Vec<Bound<'_, PyProofNode>> = self
373            .inclusion_path
374            .iter()
375            .map(|x| x.clone_ref(py).into_bound(py))
376            .collect();
377        PyList::new(py, elems)
378    }
379
380    fn __repr__(&self) -> String {
381        format!(
382            "InclusionProof(log_entry_index={}, tree_size={})",
383            self.log_entry_index, self.tree_size
384        )
385    }
386}
387
388// ── LogID ─────────────────────────────────────────────────────────────────────
389
390/// Log identifier: the hash algorithm OID and the log's public key DER.
391///
392/// ``hash_algorithm_oid`` is the dotted-decimal OID (e.g. ``"2.16.840.1.101.3.4.2.1"``
393/// for SHA-256).  ``public_key_der`` is the DER-encoded ``SubjectPublicKeyInfo``.
394#[pyclass(frozen, name = "LogID")]
395pub struct PyLogID {
396    hash_algorithm_oid: String,
397    public_key_der: Vec<u8>,
398}
399
400pub(crate) fn make_log_id(py: Python<'_>, lid: LogID<'_>) -> PyResult<Py<PyLogID>> {
401    let hash_algorithm_oid = alg_oid_str(&lid.hash_algorithm);
402    let public_key_der = spki_to_der(&lid.public_key)?;
403    Py::new(
404        py,
405        PyLogID {
406            hash_algorithm_oid,
407            public_key_der,
408        },
409    )
410}
411
412#[pymethods]
413impl PyLogID {
414    /// Parse a DER-encoded ``LogID`` SEQUENCE.
415    #[staticmethod]
416    pub fn from_der(data: &[u8]) -> PyResult<Self> {
417        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
418        let lid = LogID::decode(&mut dec).map_err(SyntaErr)?;
419        let hash_algorithm_oid = alg_oid_str(&lid.hash_algorithm);
420        let public_key_der = spki_to_der(&lid.public_key)?;
421        Ok(PyLogID {
422            hash_algorithm_oid,
423            public_key_der,
424        })
425    }
426
427    /// Dotted-decimal OID of the hash algorithm used by this log.
428    #[getter]
429    fn hash_algorithm_oid(&self) -> &str {
430        &self.hash_algorithm_oid
431    }
432
433    /// DER-encoded ``SubjectPublicKeyInfo`` of the log's signing key.
434    #[getter]
435    fn public_key_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
436        PyBytes::new(py, &self.public_key_der)
437    }
438
439    fn __repr__(&self) -> String {
440        format!("LogID(hash_algorithm_oid='{}')", self.hash_algorithm_oid)
441    }
442
443    fn __eq__(&self, other: &Self) -> bool {
444        self.hash_algorithm_oid == other.hash_algorithm_oid
445            && self.public_key_der == other.public_key_der
446    }
447}
448
449// ── CosignerID ────────────────────────────────────────────────────────────────
450
451/// Cosigner identity: issuer Name and serial number.
452///
453/// ``issuer_der`` is the DER-encoded issuer Name (pass to
454/// ``synta.parse_name_attrs()`` to decode). ``serial_number`` is the integer
455/// serial.
456#[pyclass(frozen, name = "CosignerID")]
457pub struct PyCosignerID {
458    issuer_der: Vec<u8>,
459    serial_number: i64,
460}
461
462pub(crate) fn make_cosigner_id(py: Python<'_>, cid: CosignerID) -> PyResult<Py<PyCosignerID>> {
463    let issuer_der = mtc_name_to_der(&cid.issuer)?;
464    let serial_number = int_to_i64(&cid.serial_number)?;
465    Py::new(
466        py,
467        PyCosignerID {
468            issuer_der,
469            serial_number,
470        },
471    )
472}
473
474#[pymethods]
475impl PyCosignerID {
476    /// Parse a DER-encoded ``CosignerID`` SEQUENCE.
477    #[staticmethod]
478    pub fn from_der(data: &[u8]) -> PyResult<Self> {
479        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
480        let cid = CosignerID::decode(&mut dec).map_err(SyntaErr)?;
481        let issuer_der = mtc_name_to_der(&cid.issuer)?;
482        let serial_number = int_to_i64(&cid.serial_number)?;
483        Ok(PyCosignerID {
484            issuer_der,
485            serial_number,
486        })
487    }
488
489    /// DER-encoded issuer Name.  Pass to ``synta.parse_name_attrs()`` to decode.
490    #[getter]
491    fn issuer_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
492        PyBytes::new(py, &self.issuer_der)
493    }
494
495    /// Integer serial number of the cosigner certificate.
496    #[getter]
497    fn serial_number(&self) -> i64 {
498        self.serial_number
499    }
500
501    fn __repr__(&self) -> String {
502        format!("CosignerID(serial_number={})", self.serial_number)
503    }
504
505    fn __eq__(&self, other: &Self) -> bool {
506        self.issuer_der == other.issuer_der && self.serial_number == other.serial_number
507    }
508}
509
510// ── Checkpoint ────────────────────────────────────────────────────────────────
511
512/// A signed Merkle tree checkpoint.
513///
514/// Attributes:
515/// - ``log_id`` — :class:`LogID` identifying the log
516/// - ``tree_size`` — total leaf count
517/// - ``tree_minimum_index`` — optional lower bound on included entries
518/// - ``root_value`` — Merkle root hash bytes
519/// - ``timestamp`` — GeneralizedTime string
520#[pyclass(frozen, name = "Checkpoint")]
521pub struct PyCheckpoint {
522    log_id: Py<PyLogID>,
523    tree_size: i64,
524    tree_minimum_index: Option<i64>,
525    root_value: Vec<u8>,
526    timestamp: String,
527}
528
529pub(crate) fn make_checkpoint(
530    py: Python<'_>,
531    cp: synta_mtc::types::Checkpoint<'_>,
532) -> PyResult<Py<PyCheckpoint>> {
533    let log_id = make_log_id(py, cp.log_id)?;
534    let tree_size = int_to_i64(&cp.tree_size)?;
535    let tree_minimum_index = cp.tree_minimum_index.as_ref().map(int_to_i64).transpose()?;
536    let root_value = cp.root_value.as_bytes().to_vec();
537    let timestamp = cp.timestamp.to_string();
538    Py::new(
539        py,
540        PyCheckpoint {
541            log_id,
542            tree_size,
543            tree_minimum_index,
544            root_value,
545            timestamp,
546        },
547    )
548}
549
550#[pymethods]
551impl PyCheckpoint {
552    /// Parse a DER-encoded ``Checkpoint`` SEQUENCE.
553    #[staticmethod]
554    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
555        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
556        let cp = synta_mtc::types::Checkpoint::decode(&mut dec).map_err(SyntaErr)?;
557        let log_id = make_log_id(py, cp.log_id)?;
558        let tree_size = int_to_i64(&cp.tree_size)?;
559        let tree_minimum_index = cp.tree_minimum_index.as_ref().map(int_to_i64).transpose()?;
560        let root_value = cp.root_value.as_bytes().to_vec();
561        let timestamp = cp.timestamp.to_string();
562        Ok(PyCheckpoint {
563            log_id,
564            tree_size,
565            tree_minimum_index,
566            root_value,
567            timestamp,
568        })
569    }
570
571    /// :class:`LogID` identifying the log that issued this checkpoint.
572    #[getter]
573    fn log_id<'py>(&self, py: Python<'py>) -> Bound<'py, PyLogID> {
574        self.log_id.clone_ref(py).into_bound(py)
575    }
576
577    /// Total number of leaves in the tree.
578    #[getter]
579    fn tree_size(&self) -> i64 {
580        self.tree_size
581    }
582
583    /// Minimum leaf index covered by this checkpoint, or ``None``.
584    #[getter]
585    fn tree_minimum_index(&self) -> Option<i64> {
586        self.tree_minimum_index
587    }
588
589    /// Merkle root hash bytes.
590    #[getter]
591    fn root_value<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
592        PyBytes::new(py, &self.root_value)
593    }
594
595    /// Timestamp of this checkpoint as a GeneralizedTime string.
596    #[getter]
597    fn timestamp(&self) -> &str {
598        &self.timestamp
599    }
600
601    fn __repr__(&self) -> String {
602        format!(
603            "Checkpoint(tree_size={}, timestamp='{}')",
604            self.tree_size, self.timestamp
605        )
606    }
607}
608
609// ── SubtreeSignature ──────────────────────────────────────────────────────────
610
611/// A cosigner's signature over a subtree and checkpoint.
612///
613/// Contains the cosigner identity, the subtree, the checkpoint being signed,
614/// the signature algorithm OID, and the raw signature bytes.
615#[pyclass(frozen, name = "SubtreeSignature")]
616pub struct PySubtreeSignature {
617    cosigner: Py<PyCosignerID>,
618    subtree: Py<PySubtree>,
619    checkpoint: Py<PyCheckpoint>,
620    signature_algorithm_oid: String,
621    signature: Vec<u8>,
622}
623
624pub(crate) fn make_subtree_signature(
625    py: Python<'_>,
626    ss: SubtreeSignature<'_>,
627) -> PyResult<Py<PySubtreeSignature>> {
628    let cosigner = make_cosigner_id(py, ss.cosigner)?;
629    let subtree = make_subtree(py, ss.subtree)?;
630    let checkpoint = make_checkpoint(py, ss.checkpoint)?;
631    let signature_algorithm_oid = alg_oid_str(&ss.signature_algorithm);
632    let signature = ss.signature.as_bytes().to_vec();
633    Py::new(
634        py,
635        PySubtreeSignature {
636            cosigner,
637            subtree,
638            checkpoint,
639            signature_algorithm_oid,
640            signature,
641        },
642    )
643}
644
645#[pymethods]
646impl PySubtreeSignature {
647    /// Parse a DER-encoded ``SubtreeSignature`` SEQUENCE.
648    #[staticmethod]
649    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
650        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
651        let ss = SubtreeSignature::decode(&mut dec).map_err(SyntaErr)?;
652        let cosigner = make_cosigner_id(py, ss.cosigner)?;
653        let subtree = make_subtree(py, ss.subtree)?;
654        let checkpoint = make_checkpoint(py, ss.checkpoint)?;
655        let signature_algorithm_oid = alg_oid_str(&ss.signature_algorithm);
656        let signature = ss.signature.as_bytes().to_vec();
657        Ok(PySubtreeSignature {
658            cosigner,
659            subtree,
660            checkpoint,
661            signature_algorithm_oid,
662            signature,
663        })
664    }
665
666    /// :class:`CosignerID` identifying the cosigner.
667    #[getter]
668    fn cosigner<'py>(&self, py: Python<'py>) -> Bound<'py, PyCosignerID> {
669        self.cosigner.clone_ref(py).into_bound(py)
670    }
671
672    /// :class:`Subtree` being signed.
673    #[getter]
674    fn subtree<'py>(&self, py: Python<'py>) -> Bound<'py, PySubtree> {
675        self.subtree.clone_ref(py).into_bound(py)
676    }
677
678    /// :class:`Checkpoint` signed along with the subtree.
679    #[getter]
680    fn checkpoint<'py>(&self, py: Python<'py>) -> Bound<'py, PyCheckpoint> {
681        self.checkpoint.clone_ref(py).into_bound(py)
682    }
683
684    /// Dotted-decimal OID of the signature algorithm.
685    #[getter]
686    fn signature_algorithm_oid(&self) -> &str {
687        &self.signature_algorithm_oid
688    }
689
690    /// Raw signature bytes.
691    #[getter]
692    fn signature<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
693        PyBytes::new(py, &self.signature)
694    }
695
696    fn __repr__(&self) -> String {
697        format!(
698            "SubtreeSignature(algorithm='{}')",
699            self.signature_algorithm_oid
700        )
701    }
702}
703
704// ── TbsCertificateLogEntry ────────────────────────────────────────────────────
705
706/// To-be-signed log entry for a certificate.
707///
708/// Attributes:
709/// - ``issuer_der`` — DER-encoded issuer Name (pass to ``synta.parse_name_attrs()``)
710/// - ``validity_not_before`` / ``validity_not_after`` — GeneralizedTime strings
711/// - ``subject_der`` — DER-encoded subject Name
712/// - ``subject_public_key_algorithm_oid`` — algorithm OID string
713/// - ``subject_public_key_hash`` — SHA-256 hash of the public key
714/// - ``issuer_unique_id`` — optional raw bit-string bytes
715/// - ``subject_unique_id`` — optional raw bit-string bytes
716/// - ``extensions_der`` — optional DER-encoded SEQUENCE OF Extension
717#[pyclass(frozen, name = "TbsCertificateLogEntry")]
718pub struct PyTbsCertificateLogEntry {
719    issuer_der: Vec<u8>,
720    validity_not_before: String,
721    validity_not_after: String,
722    subject_der: Vec<u8>,
723    subject_public_key_algorithm_oid: String,
724    subject_public_key_hash: Vec<u8>,
725    issuer_unique_id: Option<Vec<u8>>,
726    subject_unique_id: Option<Vec<u8>>,
727    extensions_der: Option<Vec<u8>>,
728}
729
730pub(crate) fn make_tbs_log_entry(
731    py: Python<'_>,
732    entry: TBSCertificateLogEntry<'_>,
733) -> PyResult<Py<PyTbsCertificateLogEntry>> {
734    let issuer_der = mtc_name_to_der(&entry.issuer)?;
735    let validity_not_before = time_to_str(&entry.validity.not_before);
736    let validity_not_after = time_to_str(&entry.validity.not_after);
737    let subject_der = mtc_name_to_der(&entry.subject)?;
738    let subject_public_key_algorithm_oid = alg_oid_str(&entry.subject_public_key_algorithm);
739    let subject_public_key_hash = entry.subject_public_key_hash.as_bytes().to_vec();
740    let issuer_unique_id = entry
741        .issuer_unique_id
742        .as_ref()
743        .map(|b| b.as_bytes().to_vec());
744    let subject_unique_id = entry
745        .subject_unique_id
746        .as_ref()
747        .map(|b| b.as_bytes().to_vec());
748    let extensions_der = entry.extensions.as_ref().map(encode_to_der).transpose()?;
749    Py::new(
750        py,
751        PyTbsCertificateLogEntry {
752            issuer_der,
753            validity_not_before,
754            validity_not_after,
755            subject_der,
756            subject_public_key_algorithm_oid,
757            subject_public_key_hash,
758            issuer_unique_id,
759            subject_unique_id,
760            extensions_der,
761        },
762    )
763}
764
765#[pymethods]
766impl PyTbsCertificateLogEntry {
767    /// Parse a DER-encoded ``TBSCertificateLogEntry`` SEQUENCE.
768    #[staticmethod]
769    pub fn from_der(data: &[u8]) -> PyResult<Self> {
770        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
771        let entry = TBSCertificateLogEntry::decode(&mut dec).map_err(SyntaErr)?;
772        let issuer_der = mtc_name_to_der(&entry.issuer)?;
773        let validity_not_before = time_to_str(&entry.validity.not_before);
774        let validity_not_after = time_to_str(&entry.validity.not_after);
775        let subject_der = mtc_name_to_der(&entry.subject)?;
776        let subject_public_key_algorithm_oid = alg_oid_str(&entry.subject_public_key_algorithm);
777        let subject_public_key_hash = entry.subject_public_key_hash.as_bytes().to_vec();
778        let issuer_unique_id = entry
779            .issuer_unique_id
780            .as_ref()
781            .map(|b| b.as_bytes().to_vec());
782        let subject_unique_id = entry
783            .subject_unique_id
784            .as_ref()
785            .map(|b| b.as_bytes().to_vec());
786        let extensions_der = entry.extensions.as_ref().map(encode_to_der).transpose()?;
787        Ok(PyTbsCertificateLogEntry {
788            issuer_der,
789            validity_not_before,
790            validity_not_after,
791            subject_der,
792            subject_public_key_algorithm_oid,
793            subject_public_key_hash,
794            issuer_unique_id,
795            subject_unique_id,
796            extensions_der,
797        })
798    }
799
800    /// DER-encoded issuer Name.  Pass to ``synta.parse_name_attrs()`` to decode.
801    #[getter]
802    fn issuer_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
803        PyBytes::new(py, &self.issuer_der)
804    }
805
806    /// Validity start time as a GeneralizedTime string.
807    #[getter]
808    fn validity_not_before(&self) -> &str {
809        &self.validity_not_before
810    }
811
812    /// Validity end time as a GeneralizedTime string.
813    #[getter]
814    fn validity_not_after(&self) -> &str {
815        &self.validity_not_after
816    }
817
818    /// DER-encoded subject Name.  Pass to ``synta.parse_name_attrs()`` to decode.
819    #[getter]
820    fn subject_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
821        PyBytes::new(py, &self.subject_der)
822    }
823
824    /// Dotted-decimal OID of the subject public key algorithm.
825    #[getter]
826    fn subject_public_key_algorithm_oid(&self) -> &str {
827        &self.subject_public_key_algorithm_oid
828    }
829
830    /// SHA-256 hash of the subject public key.
831    #[getter]
832    fn subject_public_key_hash<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
833        PyBytes::new(py, &self.subject_public_key_hash)
834    }
835
836    /// Issuer unique ID bit-string bytes, or ``None``.
837    #[getter]
838    fn issuer_unique_id<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
839        self.issuer_unique_id.as_ref().map(|b| PyBytes::new(py, b))
840    }
841
842    /// Subject unique ID bit-string bytes, or ``None``.
843    #[getter]
844    fn subject_unique_id<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
845        self.subject_unique_id.as_ref().map(|b| PyBytes::new(py, b))
846    }
847
848    /// DER-encoded ``SEQUENCE OF Extension``, or ``None`` if absent.
849    #[getter]
850    fn extensions_der<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
851        self.extensions_der.as_ref().map(|b| PyBytes::new(py, b))
852    }
853
854    fn __repr__(&self) -> String {
855        format!(
856            "TbsCertificateLogEntry(algorithm='{}')",
857            self.subject_public_key_algorithm_oid
858        )
859    }
860}
861
862// ── MerkleTreeCertEntry ───────────────────────────────────────────────────────
863
864/// CHOICE: a null log entry or a ``TBSCertificateLogEntry``.
865///
866/// ``variant`` is either ``"NullEntry"`` or ``"TbsCertEntry"``.
867/// ``tbs_cert_entry`` is set when ``variant == "TbsCertEntry"``, else ``None``.
868#[pyclass(frozen, name = "MerkleTreeCertEntry")]
869pub struct PyMerkleTreeCertEntry {
870    variant: &'static str,
871    tbs_cert_entry: Option<Py<PyTbsCertificateLogEntry>>,
872}
873
874#[pymethods]
875impl PyMerkleTreeCertEntry {
876    /// Parse a DER-encoded ``MerkleTreeCertEntry`` CHOICE.
877    #[staticmethod]
878    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
879        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
880        let entry = MerkleTreeCertEntry::decode(&mut dec).map_err(SyntaErr)?;
881        match entry {
882            MerkleTreeCertEntry::NullEntry(_) => Ok(PyMerkleTreeCertEntry {
883                variant: "NullEntry",
884                tbs_cert_entry: None,
885            }),
886            MerkleTreeCertEntry::TbsCertEntry(e) => {
887                let tbs = make_tbs_log_entry(py, e)?;
888                Ok(PyMerkleTreeCertEntry {
889                    variant: "TbsCertEntry",
890                    tbs_cert_entry: Some(tbs),
891                })
892            }
893        }
894    }
895
896    /// Active CHOICE variant: ``"NullEntry"`` or ``"TbsCertEntry"``.
897    #[getter]
898    fn variant(&self) -> &str {
899        self.variant
900    }
901
902    /// :class:`TbsCertificateLogEntry`, or ``None`` when ``variant == "NullEntry"``.
903    #[getter]
904    fn tbs_cert_entry<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyTbsCertificateLogEntry>> {
905        self.tbs_cert_entry
906            .as_ref()
907            .map(|x| x.clone_ref(py).into_bound(py))
908    }
909
910    fn __repr__(&self) -> String {
911        format!("MerkleTreeCertEntry(variant='{}')", self.variant)
912    }
913}
914
915// ── LandmarkID ────────────────────────────────────────────────────────────────
916
917/// Landmark log identifier: a ``LogID`` plus the tree size at issuance.
918#[pyclass(frozen, name = "LandmarkID")]
919pub struct PyLandmarkID {
920    log_id: Py<PyLogID>,
921    tree_size: i64,
922}
923
924pub(crate) fn make_landmark_id(py: Python<'_>, lid: LandmarkID<'_>) -> PyResult<Py<PyLandmarkID>> {
925    let log_id = make_log_id(py, lid.log_id)?;
926    let tree_size = int_to_i64(&lid.tree_size)?;
927    Py::new(py, PyLandmarkID { log_id, tree_size })
928}
929
930#[pymethods]
931impl PyLandmarkID {
932    /// Parse a DER-encoded ``LandmarkID`` SEQUENCE.
933    #[staticmethod]
934    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
935        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
936        let lid = LandmarkID::decode(&mut dec).map_err(SyntaErr)?;
937        let log_id = make_log_id(py, lid.log_id)?;
938        let tree_size = int_to_i64(&lid.tree_size)?;
939        Ok(PyLandmarkID { log_id, tree_size })
940    }
941
942    /// :class:`LogID` identifying the landmark log.
943    #[getter]
944    fn log_id<'py>(&self, py: Python<'py>) -> Bound<'py, PyLogID> {
945        self.log_id.clone_ref(py).into_bound(py)
946    }
947
948    /// Tree size at the time the landmark certificate was issued.
949    #[getter]
950    fn tree_size(&self) -> i64 {
951        self.tree_size
952    }
953
954    fn __repr__(&self) -> String {
955        format!("LandmarkID(tree_size={})", self.tree_size)
956    }
957}
958
959// ── StandaloneCertificate ─────────────────────────────────────────────────────
960
961/// A full standalone Merkle Tree Certificate.
962///
963/// Contains the TBS certificate DER bytes (decodable with
964/// ``synta.Certificate``), the inclusion proof, subtree proof, cosigner
965/// subtree signatures, the signature algorithm OID, and the raw signature.
966#[pyclass(frozen, name = "StandaloneCertificate")]
967pub struct PyStandaloneCertificate {
968    tbs_certificate_der: Vec<u8>,
969    inclusion_proof: Py<PyInclusionProof>,
970    subtree_proof: Py<PySubtreeProof>,
971    subtree_signatures: Vec<Py<PySubtreeSignature>>,
972    signature_algorithm_oid: String,
973    signature: Vec<u8>,
974}
975
976#[pymethods]
977impl PyStandaloneCertificate {
978    /// Parse a DER-encoded ``StandaloneCertificate`` SEQUENCE.
979    #[staticmethod]
980    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
981        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
982        let sc = StandaloneCertificate::decode(&mut dec).map_err(SyntaErr)?;
983        let tbs_certificate_der = tbs_cert_to_der(&sc.tbs_certificate)?;
984        let inclusion_proof = make_inclusion_proof(py, sc.inclusion_proof)?;
985        let subtree_proof = make_subtree_proof(py, sc.subtree_proof)?;
986        let subtree_signatures = sc
987            .subtree_signatures
988            .into_iter()
989            .map(|ss| make_subtree_signature(py, ss))
990            .collect::<PyResult<Vec<_>>>()?;
991        let signature_algorithm_oid = alg_oid_str(&sc.signature_algorithm);
992        let signature = sc.signature.as_bytes().to_vec();
993        Ok(PyStandaloneCertificate {
994            tbs_certificate_der,
995            inclusion_proof,
996            subtree_proof,
997            subtree_signatures,
998            signature_algorithm_oid,
999            signature,
1000        })
1001    }
1002
1003    /// DER-encoded ``TBSCertificate``.  Pass to ``synta.Certificate.from_der()``
1004    /// (after wrapping in a full certificate) or inspect fields via ``Decoder``.
1005    #[getter]
1006    fn tbs_certificate_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1007        PyBytes::new(py, &self.tbs_certificate_der)
1008    }
1009
1010    /// :class:`InclusionProof` certifying the log entry.
1011    #[getter]
1012    fn inclusion_proof<'py>(&self, py: Python<'py>) -> Bound<'py, PyInclusionProof> {
1013        self.inclusion_proof.clone_ref(py).into_bound(py)
1014    }
1015
1016    /// :class:`SubtreeProof` for log compaction.
1017    #[getter]
1018    fn subtree_proof<'py>(&self, py: Python<'py>) -> Bound<'py, PySubtreeProof> {
1019        self.subtree_proof.clone_ref(py).into_bound(py)
1020    }
1021
1022    /// List of :class:`SubtreeSignature` cosignatures.
1023    #[getter]
1024    fn subtree_signatures<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
1025        let elems: Vec<Bound<'_, PySubtreeSignature>> = self
1026            .subtree_signatures
1027            .iter()
1028            .map(|x| x.clone_ref(py).into_bound(py))
1029            .collect();
1030        PyList::new(py, elems)
1031    }
1032
1033    /// Dotted-decimal OID of the signature algorithm.
1034    #[getter]
1035    fn signature_algorithm_oid(&self) -> &str {
1036        &self.signature_algorithm_oid
1037    }
1038
1039    /// Raw signature bytes.
1040    #[getter]
1041    fn signature<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1042        PyBytes::new(py, &self.signature)
1043    }
1044
1045    fn __repr__(&self) -> String {
1046        format!(
1047            "StandaloneCertificate(algorithm='{}', sigs={})",
1048            self.signature_algorithm_oid,
1049            self.subtree_signatures.len()
1050        )
1051    }
1052}
1053
1054// ── LandmarkCertificate ───────────────────────────────────────────────────────
1055
1056/// A Merkle Tree Landmark Certificate.
1057///
1058/// Similar to :class:`StandaloneCertificate` but references a landmark log
1059/// via a :class:`LandmarkID` instead of a subtree proof and cosignatures.
1060#[pyclass(frozen, name = "LandmarkCertificate")]
1061pub struct PyLandmarkCertificate {
1062    tbs_certificate_der: Vec<u8>,
1063    inclusion_proof: Py<PyInclusionProof>,
1064    landmark_id: Py<PyLandmarkID>,
1065    signature_algorithm_oid: String,
1066    signature: Vec<u8>,
1067}
1068
1069#[pymethods]
1070impl PyLandmarkCertificate {
1071    /// Parse a DER-encoded ``LandmarkCertificate`` SEQUENCE.
1072    #[staticmethod]
1073    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
1074        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
1075        let lc = LandmarkCertificate::decode(&mut dec).map_err(SyntaErr)?;
1076        let tbs_certificate_der = tbs_cert_to_der(&lc.tbs_certificate)?;
1077        let inclusion_proof = make_inclusion_proof(py, lc.inclusion_proof)?;
1078        let landmark_id = make_landmark_id(py, lc.landmark_id)?;
1079        let signature_algorithm_oid = alg_oid_str(&lc.signature_algorithm);
1080        let signature = lc.signature.as_bytes().to_vec();
1081        Ok(PyLandmarkCertificate {
1082            tbs_certificate_der,
1083            inclusion_proof,
1084            landmark_id,
1085            signature_algorithm_oid,
1086            signature,
1087        })
1088    }
1089
1090    /// DER-encoded ``TBSCertificate``.
1091    #[getter]
1092    fn tbs_certificate_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1093        PyBytes::new(py, &self.tbs_certificate_der)
1094    }
1095
1096    /// :class:`InclusionProof` certifying the log entry.
1097    #[getter]
1098    fn inclusion_proof<'py>(&self, py: Python<'py>) -> Bound<'py, PyInclusionProof> {
1099        self.inclusion_proof.clone_ref(py).into_bound(py)
1100    }
1101
1102    /// :class:`LandmarkID` referencing the landmark log.
1103    #[getter]
1104    fn landmark_id<'py>(&self, py: Python<'py>) -> Bound<'py, PyLandmarkID> {
1105        self.landmark_id.clone_ref(py).into_bound(py)
1106    }
1107
1108    /// Dotted-decimal OID of the signature algorithm.
1109    #[getter]
1110    fn signature_algorithm_oid(&self) -> &str {
1111        &self.signature_algorithm_oid
1112    }
1113
1114    /// Raw signature bytes.
1115    #[getter]
1116    fn signature<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1117        PyBytes::new(py, &self.signature)
1118    }
1119
1120    fn __repr__(&self) -> String {
1121        format!(
1122            "LandmarkCertificate(algorithm='{}')",
1123            self.signature_algorithm_oid
1124        )
1125    }
1126}
1127
1128// ── Module registration ───────────────────────────────────────────────────────
1129
1130/// Register all MTC classes into the ``synta.mtc`` submodule.
1131/// Populate the `synta.mtc` module with all classes.
1132///
1133/// `m` is the pre-created `synta.mtc` submodule passed in from `lib.rs`.
1134pub fn register_mtc_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
1135    m.add_class::<PyProofNode>()?;
1136    m.add_class::<PySubtree>()?;
1137    m.add_class::<PySubtreeProof>()?;
1138    m.add_class::<PyInclusionProof>()?;
1139    m.add_class::<PyLogID>()?;
1140    m.add_class::<PyCosignerID>()?;
1141    m.add_class::<PyCheckpoint>()?;
1142    m.add_class::<PySubtreeSignature>()?;
1143    m.add_class::<PyTbsCertificateLogEntry>()?;
1144    m.add_class::<PyMerkleTreeCertEntry>()?;
1145    m.add_class::<PyLandmarkID>()?;
1146    m.add_class::<PyStandaloneCertificate>()?;
1147    m.add_class::<PyLandmarkCertificate>()?;
1148    Ok(())
1149}