synta-python-mtc 0.3.1

Python extension module for synta Merkle Tree Certificates types
Documentation
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use synta::traits::Decode;

use super::proof::PyHashAlgorithm;

// ── Module-level utility functions ────────────────────────────────────────────

/// Extract the leaf index from a composite MTC serial number (draft-04 §6.1).
///
/// The serial number encodes ``(log_number << 48) | log_entry_index``.
/// This function returns the lower 48 bits, which is the log entry index.
///
/// ```python,ignore
/// import synta.mtc as mtc
///
/// serial = (3 << 48) | 42  # log_number=3, leaf_index=42
/// assert mtc.extract_leaf_index(serial) == 42
/// ```
#[pyfunction]
pub fn extract_leaf_index(serial: u64) -> u64 {
    serial & 0xFFFF_FFFF_FFFF
}

/// Extract the log number from a composite MTC serial number (draft-04 §6.1).
///
/// The serial number encodes ``(log_number << 48) | log_entry_index``.
/// This function returns the upper 16 bits, which is the log number.
///
/// ```python,ignore
/// import synta.mtc as mtc
///
/// serial = (3 << 48) | 42  # log_number=3, leaf_index=42
/// assert mtc.extract_log_number(serial) == 3
/// ```
#[pyfunction]
pub fn extract_log_number(serial: u64) -> u64 {
    serial >> 48
}

/// Build the DER-encoded value for the ``id-pe-mtcCertificationAuthority``
/// extension (draft-04 §5.5).
///
/// The extension MUST be marked critical in certificates issued by an
/// MTC-capable CA.  It advertises the hash algorithm, signature algorithm,
/// and minimum serial number used by the CA's MTC logs.
///
/// :param hash_alg: :class:`HashAlgorithm` used by all CA logs.
/// :param sig_alg_der: DER-encoded ``AlgorithmIdentifier`` SEQUENCE TLV for
///     the CA cosigner signature algorithm.
/// :param min_serial: Minimum allowed serial number.
/// :returns: ``(extension_value_der, critical)`` where ``critical`` is
///     always ``True`` per spec §5.5.
/// :raises ValueError: if *sig_alg_der* cannot be decoded or encoding fails.
#[pyfunction]
pub fn build_mtc_ca_extension<'py>(
    py: Python<'py>,
    hash_alg: &PyHashAlgorithm,
    sig_alg_der: &[u8],
    min_serial: u64,
) -> PyResult<(Bound<'py, PyBytes>, bool)> {
    use synta_certificate::AlgorithmIdentifier;

    let mut dec = synta::Decoder::new(sig_alg_der, synta::Encoding::Der);
    let sig_alg = AlgorithmIdentifier::decode(&mut dec)
        .map_err(|e| PyValueError::new_err(format!("invalid AlgorithmIdentifier DER: {e}")))?;

    let der = synta_mtc::builder::ca_extension::build_mtc_ca_extension_from_hash(
        hash_alg.inner,
        &sig_alg,
        min_serial,
        u64::MAX, // max_serial: use maximum value (no upper limit)
    )
    .map_err(|e| PyValueError::new_err(e.to_string()))?;

    // Per spec §5.5 this extension MUST always be critical.
    Ok((PyBytes::new(py, &der), true))
}

/// Parse the ``id-pe-mtcCertificationAuthority`` extension from a
/// DER-encoded X.509 certificate (draft-04 §5.5).
///
/// Searches the certificate's extensions for
/// ``id-pe-mtcCertificationAuthority``.
///
/// :param cert_der: DER-encoded ``Certificate`` bytes (the full certificate,
///     not just the extension value).
/// :returns: ``(hash_algorithm, min_serial, max_serial)`` where
///     ``hash_algorithm`` is a :class:`HashAlgorithm`, ``min_serial`` is the
///     minimum allowed serial number, and ``max_serial`` is the maximum allowed
///     serial number. Returns ``None`` if the extension is absent.
/// :raises ValueError: if *cert_der* is malformed or the extension value
///     cannot be decoded.
#[pyfunction]
#[allow(clippy::type_complexity)]
pub fn parse_mtc_ca_extension<'py>(
    py: Python<'py>,
    cert_der: &[u8],
) -> PyResult<Option<(Py<PyHashAlgorithm>, u64, u64)>> {
    use synta_mtc::builder::ca_extension::parse_mtc_ca_extension as rust_parse;

    let parsed = rust_parse(cert_der).map_err(|e| PyValueError::new_err(e.to_string()))?;

    match parsed {
        None => Ok(None),
        Some(ext) => {
            // The hash algorithm is already resolved in the parsed extension
            let py_hash_alg = Py::new(
                py,
                PyHashAlgorithm {
                    inner: ext.log_hash,
                },
            )?;

            Ok(Some((py_hash_alg, ext.min_serial, ext.max_serial)))
        }
    }
}