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 (TrustAnchorID): an OBJECT IDENTIFIER within the CA's OID arc.
442///
443/// ``oid`` is the dotted-decimal string representation of the trust anchor OID,
444/// e.g. ``"1.3.6.1.4.1.44363.47.1"``.
445#[pyclass(frozen, name = "CosignerID")]
446pub struct PyCosignerID {
447    oid: String,
448}
449
450pub(crate) fn make_cosigner_id(py: Python<'_>, cid: CosignerID) -> PyResult<Py<PyCosignerID>> {
451    Py::new(
452        py,
453        PyCosignerID {
454            oid: cid.to_string(),
455        },
456    )
457}
458
459#[pymethods]
460impl PyCosignerID {
461    /// Parse a DER-encoded ``CosignerID`` (OBJECT IDENTIFIER).
462    #[staticmethod]
463    pub fn from_der(data: &[u8]) -> PyResult<Self> {
464        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
465        let cid = CosignerID::decode(&mut dec).map_err(SyntaErr)?;
466        Ok(PyCosignerID {
467            oid: cid.to_string(),
468        })
469    }
470
471    /// Dotted-decimal OID string for this trust anchor.
472    #[getter]
473    fn oid(&self) -> &str {
474        &self.oid
475    }
476
477    fn __repr__(&self) -> String {
478        format!("CosignerID(oid='{}')", self.oid)
479    }
480
481    fn __eq__(&self, other: &Self) -> bool {
482        self.oid == other.oid
483    }
484}
485
486// ── Checkpoint ────────────────────────────────────────────────────────────────
487
488/// A signed Merkle tree checkpoint.
489///
490/// Attributes:
491/// - ``log_id`` — :class:`LogID` identifying the log
492/// - ``tree_size`` — total leaf count
493/// - ``tree_minimum_index`` — optional lower bound on included entries
494/// - ``root_value`` — Merkle root hash bytes
495/// - ``timestamp`` — GeneralizedTime string
496#[pyclass(frozen, name = "Checkpoint")]
497pub struct PyCheckpoint {
498    log_id: Py<PyLogID>,
499    tree_size: i64,
500    tree_minimum_index: Option<i64>,
501    root_value: Vec<u8>,
502    timestamp: String,
503}
504
505pub(crate) fn make_checkpoint(
506    py: Python<'_>,
507    cp: synta_mtc::types::Checkpoint<'_>,
508) -> PyResult<Py<PyCheckpoint>> {
509    let log_id = make_log_id(py, cp.log_id)?;
510    let tree_size = int_to_i64(&cp.tree_size)?;
511    let tree_minimum_index = cp.tree_minimum_index.as_ref().map(int_to_i64).transpose()?;
512    let root_value = cp.root_value.as_bytes().to_vec();
513    let timestamp = cp.timestamp.to_string();
514    Py::new(
515        py,
516        PyCheckpoint {
517            log_id,
518            tree_size,
519            tree_minimum_index,
520            root_value,
521            timestamp,
522        },
523    )
524}
525
526#[pymethods]
527impl PyCheckpoint {
528    /// Parse a DER-encoded ``Checkpoint`` SEQUENCE.
529    #[staticmethod]
530    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
531        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
532        let cp = synta_mtc::types::Checkpoint::decode(&mut dec).map_err(SyntaErr)?;
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        Ok(PyCheckpoint {
539            log_id,
540            tree_size,
541            tree_minimum_index,
542            root_value,
543            timestamp,
544        })
545    }
546
547    /// :class:`LogID` identifying the log that issued this checkpoint.
548    #[getter]
549    fn log_id<'py>(&self, py: Python<'py>) -> Bound<'py, PyLogID> {
550        self.log_id.clone_ref(py).into_bound(py)
551    }
552
553    /// Total number of leaves in the tree.
554    #[getter]
555    fn tree_size(&self) -> i64 {
556        self.tree_size
557    }
558
559    /// Minimum leaf index covered by this checkpoint, or ``None``.
560    #[getter]
561    fn tree_minimum_index(&self) -> Option<i64> {
562        self.tree_minimum_index
563    }
564
565    /// Merkle root hash bytes.
566    #[getter]
567    fn root_value<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
568        PyBytes::new(py, &self.root_value)
569    }
570
571    /// Timestamp of this checkpoint as a GeneralizedTime string.
572    #[getter]
573    fn timestamp(&self) -> &str {
574        &self.timestamp
575    }
576
577    fn __repr__(&self) -> String {
578        format!(
579            "Checkpoint(tree_size={}, timestamp='{}')",
580            self.tree_size, self.timestamp
581        )
582    }
583}
584
585// ── SubtreeSignature ──────────────────────────────────────────────────────────
586
587/// A cosigner's signature over a subtree and checkpoint.
588///
589/// Contains the cosigner identity, the subtree, the checkpoint being signed,
590/// the signature algorithm OID, and the raw signature bytes.
591#[pyclass(frozen, name = "SubtreeSignature")]
592pub struct PySubtreeSignature {
593    cosigner: Py<PyCosignerID>,
594    subtree: Py<PySubtree>,
595    checkpoint: Py<PyCheckpoint>,
596    signature_algorithm_oid: String,
597    signature: Vec<u8>,
598}
599
600pub(crate) fn make_subtree_signature(
601    py: Python<'_>,
602    ss: SubtreeSignature<'_>,
603) -> PyResult<Py<PySubtreeSignature>> {
604    let cosigner = make_cosigner_id(py, ss.cosigner)?;
605    let subtree = make_subtree(py, ss.subtree)?;
606    let checkpoint = make_checkpoint(py, ss.checkpoint)?;
607    let signature_algorithm_oid = alg_oid_str(&ss.signature_algorithm);
608    let signature = ss.signature.as_bytes().to_vec();
609    Py::new(
610        py,
611        PySubtreeSignature {
612            cosigner,
613            subtree,
614            checkpoint,
615            signature_algorithm_oid,
616            signature,
617        },
618    )
619}
620
621#[pymethods]
622impl PySubtreeSignature {
623    /// Parse a DER-encoded ``SubtreeSignature`` SEQUENCE.
624    #[staticmethod]
625    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
626        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
627        let ss = SubtreeSignature::decode(&mut dec).map_err(SyntaErr)?;
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        Ok(PySubtreeSignature {
634            cosigner,
635            subtree,
636            checkpoint,
637            signature_algorithm_oid,
638            signature,
639        })
640    }
641
642    /// :class:`CosignerID` identifying the cosigner.
643    #[getter]
644    fn cosigner<'py>(&self, py: Python<'py>) -> Bound<'py, PyCosignerID> {
645        self.cosigner.clone_ref(py).into_bound(py)
646    }
647
648    /// :class:`Subtree` being signed.
649    #[getter]
650    fn subtree<'py>(&self, py: Python<'py>) -> Bound<'py, PySubtree> {
651        self.subtree.clone_ref(py).into_bound(py)
652    }
653
654    /// :class:`Checkpoint` signed along with the subtree.
655    #[getter]
656    fn checkpoint<'py>(&self, py: Python<'py>) -> Bound<'py, PyCheckpoint> {
657        self.checkpoint.clone_ref(py).into_bound(py)
658    }
659
660    /// Dotted-decimal OID of the signature algorithm.
661    #[getter]
662    fn signature_algorithm_oid(&self) -> &str {
663        &self.signature_algorithm_oid
664    }
665
666    /// Raw signature bytes.
667    #[getter]
668    fn signature<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
669        PyBytes::new(py, &self.signature)
670    }
671
672    fn __repr__(&self) -> String {
673        format!(
674            "SubtreeSignature(algorithm='{}')",
675            self.signature_algorithm_oid
676        )
677    }
678}
679
680// ── TbsCertificateLogEntry ────────────────────────────────────────────────────
681
682/// To-be-signed log entry for a certificate.
683///
684/// Attributes:
685/// - ``issuer_der`` — DER-encoded issuer Name (pass to ``synta.parse_name_attrs()``)
686/// - ``validity_not_before`` / ``validity_not_after`` — GeneralizedTime strings
687/// - ``subject_der`` — DER-encoded subject Name
688/// - ``subject_public_key_algorithm_oid`` — algorithm OID string
689/// - ``subject_public_key_info_hash`` — SHA-256 hash of the public key
690/// - ``issuer_unique_id`` — optional raw bit-string bytes
691/// - ``subject_unique_id`` — optional raw bit-string bytes
692/// - ``extensions_der`` — optional DER-encoded SEQUENCE OF Extension
693#[pyclass(frozen, name = "TbsCertificateLogEntry")]
694pub struct PyTbsCertificateLogEntry {
695    issuer_der: Vec<u8>,
696    validity_not_before: String,
697    validity_not_after: String,
698    subject_der: Vec<u8>,
699    subject_public_key_algorithm_oid: String,
700    subject_public_key_info_hash: Vec<u8>,
701    issuer_unique_id: Option<Vec<u8>>,
702    subject_unique_id: Option<Vec<u8>>,
703    extensions_der: Option<Vec<u8>>,
704}
705
706pub(crate) fn make_tbs_log_entry(
707    py: Python<'_>,
708    entry: TBSCertificateLogEntry<'_>,
709) -> PyResult<Py<PyTbsCertificateLogEntry>> {
710    let issuer_der = mtc_name_to_der(&entry.issuer)?;
711    let validity_not_before = time_to_str(&entry.validity.not_before);
712    let validity_not_after = time_to_str(&entry.validity.not_after);
713    let subject_der = mtc_name_to_der(&entry.subject)?;
714    let subject_public_key_algorithm_oid = alg_oid_str(&entry.subject_public_key_algorithm);
715    let subject_public_key_info_hash = entry.subject_public_key_info_hash.as_bytes().to_vec();
716    let issuer_unique_id = entry
717        .issuer_unique_id
718        .as_ref()
719        .map(|b| b.as_bytes().to_vec());
720    let subject_unique_id = entry
721        .subject_unique_id
722        .as_ref()
723        .map(|b| b.as_bytes().to_vec());
724    let extensions_der = entry.extensions.as_ref().map(encode_to_der).transpose()?;
725    Py::new(
726        py,
727        PyTbsCertificateLogEntry {
728            issuer_der,
729            validity_not_before,
730            validity_not_after,
731            subject_der,
732            subject_public_key_algorithm_oid,
733            subject_public_key_info_hash,
734            issuer_unique_id,
735            subject_unique_id,
736            extensions_der,
737        },
738    )
739}
740
741#[pymethods]
742impl PyTbsCertificateLogEntry {
743    /// Parse a DER-encoded ``TBSCertificateLogEntry`` SEQUENCE.
744    #[staticmethod]
745    pub fn from_der(data: &[u8]) -> PyResult<Self> {
746        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
747        let entry = TBSCertificateLogEntry::decode(&mut dec).map_err(SyntaErr)?;
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        Ok(PyTbsCertificateLogEntry {
764            issuer_der,
765            validity_not_before,
766            validity_not_after,
767            subject_der,
768            subject_public_key_algorithm_oid,
769            subject_public_key_info_hash,
770            issuer_unique_id,
771            subject_unique_id,
772            extensions_der,
773        })
774    }
775
776    /// DER-encoded issuer Name.  Pass to ``synta.parse_name_attrs()`` to decode.
777    #[getter]
778    fn issuer_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
779        PyBytes::new(py, &self.issuer_der)
780    }
781
782    /// Validity start time as a GeneralizedTime string.
783    #[getter]
784    fn validity_not_before(&self) -> &str {
785        &self.validity_not_before
786    }
787
788    /// Validity end time as a GeneralizedTime string.
789    #[getter]
790    fn validity_not_after(&self) -> &str {
791        &self.validity_not_after
792    }
793
794    /// DER-encoded subject Name.  Pass to ``synta.parse_name_attrs()`` to decode.
795    #[getter]
796    fn subject_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
797        PyBytes::new(py, &self.subject_der)
798    }
799
800    /// Dotted-decimal OID of the subject public key algorithm.
801    #[getter]
802    fn subject_public_key_algorithm_oid(&self) -> &str {
803        &self.subject_public_key_algorithm_oid
804    }
805
806    /// SHA-256 hash of the subject public key.
807    #[getter]
808    fn subject_public_key_info_hash<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
809        PyBytes::new(py, &self.subject_public_key_info_hash)
810    }
811
812    /// Issuer unique ID bit-string bytes, or ``None``.
813    #[getter]
814    fn issuer_unique_id<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
815        self.issuer_unique_id.as_ref().map(|b| PyBytes::new(py, b))
816    }
817
818    /// Subject unique ID bit-string bytes, or ``None``.
819    #[getter]
820    fn subject_unique_id<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
821        self.subject_unique_id.as_ref().map(|b| PyBytes::new(py, b))
822    }
823
824    /// DER-encoded ``SEQUENCE OF Extension``, or ``None`` if absent.
825    #[getter]
826    fn extensions_der<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
827        self.extensions_der.as_ref().map(|b| PyBytes::new(py, b))
828    }
829
830    fn __repr__(&self) -> String {
831        format!(
832            "TbsCertificateLogEntry(algorithm='{}')",
833            self.subject_public_key_algorithm_oid
834        )
835    }
836}
837
838// ── MerkleTreeCertEntry ───────────────────────────────────────────────────────
839
840/// CHOICE: a null log entry or a ``TBSCertificateLogEntry``.
841///
842/// ``variant`` is either ``"NullEntry"`` or ``"TbsCertEntry"``.
843/// ``tbs_cert_entry`` is set when ``variant == "TbsCertEntry"``, else ``None``.
844#[pyclass(frozen, name = "MerkleTreeCertEntry")]
845pub struct PyMerkleTreeCertEntry {
846    variant: &'static str,
847    tbs_cert_entry: Option<Py<PyTbsCertificateLogEntry>>,
848}
849
850#[pymethods]
851impl PyMerkleTreeCertEntry {
852    /// Parse a DER-encoded ``MerkleTreeCertEntry`` CHOICE.
853    #[staticmethod]
854    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
855        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
856        let entry = MerkleTreeCertEntry::decode(&mut dec).map_err(SyntaErr)?;
857        match entry {
858            MerkleTreeCertEntry::NullEntry(_) => Ok(PyMerkleTreeCertEntry {
859                variant: "NullEntry",
860                tbs_cert_entry: None,
861            }),
862            MerkleTreeCertEntry::TbsCertEntry(e) => {
863                let tbs = make_tbs_log_entry(py, e)?;
864                Ok(PyMerkleTreeCertEntry {
865                    variant: "TbsCertEntry",
866                    tbs_cert_entry: Some(tbs),
867                })
868            }
869        }
870    }
871
872    /// Active CHOICE variant: ``"NullEntry"`` or ``"TbsCertEntry"``.
873    #[getter]
874    fn variant(&self) -> &str {
875        self.variant
876    }
877
878    /// :class:`TbsCertificateLogEntry`, or ``None`` when ``variant == "NullEntry"``.
879    #[getter]
880    fn tbs_cert_entry<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyTbsCertificateLogEntry>> {
881        self.tbs_cert_entry
882            .as_ref()
883            .map(|x| x.clone_ref(py).into_bound(py))
884    }
885
886    fn __repr__(&self) -> String {
887        format!("MerkleTreeCertEntry(variant='{}')", self.variant)
888    }
889}
890
891// ── LandmarkID ────────────────────────────────────────────────────────────────
892
893/// Landmark log identifier: a ``LogID`` plus the tree size at issuance.
894#[pyclass(frozen, name = "LandmarkID")]
895pub struct PyLandmarkID {
896    log_id: Py<PyLogID>,
897    tree_size: i64,
898}
899
900pub(crate) fn make_landmark_id(py: Python<'_>, lid: LandmarkID<'_>) -> PyResult<Py<PyLandmarkID>> {
901    let log_id = make_log_id(py, lid.log_id)?;
902    let tree_size = int_to_i64(&lid.tree_size)?;
903    Py::new(py, PyLandmarkID { log_id, tree_size })
904}
905
906#[pymethods]
907impl PyLandmarkID {
908    /// Parse a DER-encoded ``LandmarkID`` SEQUENCE.
909    #[staticmethod]
910    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
911        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
912        let lid = LandmarkID::decode(&mut dec).map_err(SyntaErr)?;
913        let log_id = make_log_id(py, lid.log_id)?;
914        let tree_size = int_to_i64(&lid.tree_size)?;
915        Ok(PyLandmarkID { log_id, tree_size })
916    }
917
918    /// :class:`LogID` identifying the landmark log.
919    #[getter]
920    fn log_id<'py>(&self, py: Python<'py>) -> Bound<'py, PyLogID> {
921        self.log_id.clone_ref(py).into_bound(py)
922    }
923
924    /// Tree size at the time the landmark certificate was issued.
925    #[getter]
926    fn tree_size(&self) -> i64 {
927        self.tree_size
928    }
929
930    fn __repr__(&self) -> String {
931        format!("LandmarkID(tree_size={})", self.tree_size)
932    }
933}
934
935// ── StandaloneCertificate ─────────────────────────────────────────────────────
936
937/// A full standalone Merkle Tree Certificate.
938///
939/// Contains the TBS certificate DER bytes (decodable with
940/// ``synta.Certificate``), the inclusion proof, subtree proof, cosigner
941/// subtree signatures, the signature algorithm OID, and the raw signature.
942#[pyclass(frozen, name = "StandaloneCertificate")]
943pub struct PyStandaloneCertificate {
944    tbs_certificate_der: Vec<u8>,
945    inclusion_proof: Py<PyInclusionProof>,
946    subtree_proof: Py<PySubtreeProof>,
947    subtree_signatures: Vec<Py<PySubtreeSignature>>,
948    signature_algorithm_oid: String,
949    signature: Vec<u8>,
950}
951
952#[pymethods]
953impl PyStandaloneCertificate {
954    /// Parse a DER-encoded ``StandaloneCertificate`` SEQUENCE.
955    #[staticmethod]
956    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
957        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
958        let sc = StandaloneCertificate::decode(&mut dec).map_err(SyntaErr)?;
959        let tbs_certificate_der = tbs_cert_to_der(&sc.tbs_certificate)?;
960        let inclusion_proof = make_inclusion_proof(py, sc.inclusion_proof)?;
961        let subtree_proof = make_subtree_proof(py, sc.subtree_proof)?;
962        let subtree_signatures = sc
963            .subtree_signatures
964            .into_iter()
965            .map(|ss| make_subtree_signature(py, ss))
966            .collect::<PyResult<Vec<_>>>()?;
967        let signature_algorithm_oid = alg_oid_str(&sc.signature_algorithm);
968        let signature = sc.signature.as_bytes().to_vec();
969        Ok(PyStandaloneCertificate {
970            tbs_certificate_der,
971            inclusion_proof,
972            subtree_proof,
973            subtree_signatures,
974            signature_algorithm_oid,
975            signature,
976        })
977    }
978
979    /// DER-encoded ``TBSCertificate``.  Pass to ``synta.Certificate.from_der()``
980    /// (after wrapping in a full certificate) or inspect fields via ``Decoder``.
981    #[getter]
982    fn tbs_certificate_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
983        PyBytes::new(py, &self.tbs_certificate_der)
984    }
985
986    /// :class:`InclusionProof` certifying the log entry.
987    #[getter]
988    fn inclusion_proof<'py>(&self, py: Python<'py>) -> Bound<'py, PyInclusionProof> {
989        self.inclusion_proof.clone_ref(py).into_bound(py)
990    }
991
992    /// :class:`SubtreeProof` for log compaction.
993    #[getter]
994    fn subtree_proof<'py>(&self, py: Python<'py>) -> Bound<'py, PySubtreeProof> {
995        self.subtree_proof.clone_ref(py).into_bound(py)
996    }
997
998    /// List of :class:`SubtreeSignature` cosignatures.
999    #[getter]
1000    fn subtree_signatures<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
1001        let elems: Vec<Bound<'_, PySubtreeSignature>> = self
1002            .subtree_signatures
1003            .iter()
1004            .map(|x| x.clone_ref(py).into_bound(py))
1005            .collect();
1006        PyList::new(py, elems)
1007    }
1008
1009    /// Dotted-decimal OID of the signature algorithm.
1010    #[getter]
1011    fn signature_algorithm_oid(&self) -> &str {
1012        &self.signature_algorithm_oid
1013    }
1014
1015    /// Raw signature bytes.
1016    #[getter]
1017    fn signature<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1018        PyBytes::new(py, &self.signature)
1019    }
1020
1021    fn __repr__(&self) -> String {
1022        format!(
1023            "StandaloneCertificate(algorithm='{}', sigs={})",
1024            self.signature_algorithm_oid,
1025            self.subtree_signatures.len()
1026        )
1027    }
1028}
1029
1030// ── LandmarkCertificate ───────────────────────────────────────────────────────
1031
1032/// A Merkle Tree Landmark Certificate.
1033///
1034/// Similar to :class:`StandaloneCertificate` but references a landmark log
1035/// via a :class:`LandmarkID` instead of a subtree proof and cosignatures.
1036#[pyclass(frozen, name = "LandmarkCertificate")]
1037pub struct PyLandmarkCertificate {
1038    tbs_certificate_der: Vec<u8>,
1039    inclusion_proof: Py<PyInclusionProof>,
1040    landmark_id: Py<PyLandmarkID>,
1041    signature_algorithm_oid: String,
1042    signature: Vec<u8>,
1043}
1044
1045#[pymethods]
1046impl PyLandmarkCertificate {
1047    /// Parse a DER-encoded ``LandmarkCertificate`` SEQUENCE.
1048    #[staticmethod]
1049    pub fn from_der(py: Python<'_>, data: &[u8]) -> PyResult<Self> {
1050        let mut dec = synta::Decoder::new(data, synta::Encoding::Der);
1051        let lc = LandmarkCertificate::decode(&mut dec).map_err(SyntaErr)?;
1052        let tbs_certificate_der = tbs_cert_to_der(&lc.tbs_certificate)?;
1053        let inclusion_proof = make_inclusion_proof(py, lc.inclusion_proof)?;
1054        let landmark_id = make_landmark_id(py, lc.landmark_id)?;
1055        let signature_algorithm_oid = alg_oid_str(&lc.signature_algorithm);
1056        let signature = lc.signature.as_bytes().to_vec();
1057        Ok(PyLandmarkCertificate {
1058            tbs_certificate_der,
1059            inclusion_proof,
1060            landmark_id,
1061            signature_algorithm_oid,
1062            signature,
1063        })
1064    }
1065
1066    /// DER-encoded ``TBSCertificate``.
1067    #[getter]
1068    fn tbs_certificate_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1069        PyBytes::new(py, &self.tbs_certificate_der)
1070    }
1071
1072    /// :class:`InclusionProof` certifying the log entry.
1073    #[getter]
1074    fn inclusion_proof<'py>(&self, py: Python<'py>) -> Bound<'py, PyInclusionProof> {
1075        self.inclusion_proof.clone_ref(py).into_bound(py)
1076    }
1077
1078    /// :class:`LandmarkID` referencing the landmark log.
1079    #[getter]
1080    fn landmark_id<'py>(&self, py: Python<'py>) -> Bound<'py, PyLandmarkID> {
1081        self.landmark_id.clone_ref(py).into_bound(py)
1082    }
1083
1084    /// Dotted-decimal OID of the signature algorithm.
1085    #[getter]
1086    fn signature_algorithm_oid(&self) -> &str {
1087        &self.signature_algorithm_oid
1088    }
1089
1090    /// Raw signature bytes.
1091    #[getter]
1092    fn signature<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
1093        PyBytes::new(py, &self.signature)
1094    }
1095
1096    fn __repr__(&self) -> String {
1097        format!(
1098            "LandmarkCertificate(algorithm='{}')",
1099            self.signature_algorithm_oid
1100        )
1101    }
1102}
1103
1104// ── Module registration ───────────────────────────────────────────────────────
1105
1106/// Register all MTC classes into the ``synta.mtc`` submodule.
1107/// Populate the `synta.mtc` module with all classes.
1108///
1109/// `m` is the pre-created `synta.mtc` submodule passed in from `lib.rs`.
1110pub fn register_mtc_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
1111    m.add_class::<PyProofNode>()?;
1112    m.add_class::<PySubtree>()?;
1113    m.add_class::<PySubtreeProof>()?;
1114    m.add_class::<PyInclusionProof>()?;
1115    m.add_class::<PyLogID>()?;
1116    m.add_class::<PyCosignerID>()?;
1117    m.add_class::<PyCheckpoint>()?;
1118    m.add_class::<PySubtreeSignature>()?;
1119    m.add_class::<PyTbsCertificateLogEntry>()?;
1120    m.add_class::<PyMerkleTreeCertEntry>()?;
1121    m.add_class::<PyLandmarkID>()?;
1122    m.add_class::<PyStandaloneCertificate>()?;
1123    m.add_class::<PyLandmarkCertificate>()?;
1124    Ok(())
1125}