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