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