synta-python 0.1.3

Python extension module for the synta ASN.1 library
Documentation
//! Python bindings for RFC 3279 algorithm parameter types.
//!
//! Exposes ``DssParms``, ``DssSigValue``, ``EcdsaSigValue``, and ``ECParameters``
//! as Python classes, along with the OID constants from the ``PKIXAlgs`` module.
//! All types are installed into the ``synta.pkixalgs`` submodule.

use std::sync::OnceLock;

use pyo3::prelude::*;
use pyo3::types::PyBytes;

use synta::traits::Encode;
use synta::{Decoder, Encoding};

use crate::error::SyntaErr;
use crate::types::PyObjectIdentifier;

// ── PyDssParms ────────────────────────────────────────────────────────────────

/// DSA domain parameters (RFC 3279 §2.3.2).
///
/// Carries the prime modulus ``p``, prime divisor ``q``, and generator ``g``
/// parameters for a DSA public key.  Decoded from the ``parameters`` field
/// of an ``AlgorithmIdentifier`` whose OID is ``id-dsa``.
///
/// ```python,ignore
/// import synta.pkixalgs as pa
/// parms = pa.DssParms.from_der(alg_id_params_der)
/// print(len(parms.p))  # byte length of p
/// ```
#[pyclass(frozen, name = "DssParms")]
pub struct PyDssParms {
    inner: synta_certificate::pkixalgs_types::DssParms,
}

#[pymethods]
impl PyDssParms {
    /// Parse a DER-encoded ``Dss-Parms`` SEQUENCE.
    ///
    /// :param data: DER bytes of the ``Dss-Parms`` SEQUENCE.
    /// :raises ValueError: if the bytes cannot be decoded.
    #[staticmethod]
    fn from_der(data: &[u8]) -> PyResult<Self> {
        let mut dec = Decoder::new(data, Encoding::Der);
        let inner = dec
            .decode::<synta_certificate::pkixalgs_types::DssParms>()
            .map_err(SyntaErr)?;
        Ok(Self { inner })
    }

    /// Return the DER encoding of this ``Dss-Parms`` SEQUENCE.
    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
        let mut enc = synta::Encoder::new(Encoding::Der);
        self.inner.encode(&mut enc).map_err(SyntaErr)?;
        Ok(PyBytes::new(py, &enc.finish().map_err(SyntaErr)?))
    }

    /// Prime modulus ``p`` (big-endian two's-complement bytes).
    #[getter]
    fn p<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, self.inner.p.as_bytes())
    }

    /// Prime divisor ``q`` (big-endian two's-complement bytes).
    #[getter]
    fn q<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, self.inner.q.as_bytes())
    }

    /// Generator ``g`` (big-endian two's-complement bytes).
    #[getter]
    fn g<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, self.inner.g.as_bytes())
    }

    fn __repr__(&self) -> String {
        format!(
            "DssParms(p=<{} bytes>, q=<{} bytes>, g=<{} bytes>)",
            self.inner.p.as_bytes().len(),
            self.inner.q.as_bytes().len(),
            self.inner.g.as_bytes().len(),
        )
    }
}

// ── PyDssSigValue ─────────────────────────────────────────────────────────────

/// DSA signature value (RFC 3279 §2.2.2).
///
/// Contains the integer pair ``(r, s)`` produced by the DSA signing operation.
#[pyclass(frozen, name = "DssSigValue")]
pub struct PyDssSigValue {
    inner: synta_certificate::pkixalgs_types::DssSigValue,
}

#[pymethods]
impl PyDssSigValue {
    /// Parse a DER-encoded ``Dss-Sig-Value`` SEQUENCE.
    #[staticmethod]
    fn from_der(data: &[u8]) -> PyResult<Self> {
        let mut dec = Decoder::new(data, Encoding::Der);
        let inner = dec
            .decode::<synta_certificate::pkixalgs_types::DssSigValue>()
            .map_err(SyntaErr)?;
        Ok(Self { inner })
    }

    /// Return the DER encoding of this ``Dss-Sig-Value`` SEQUENCE.
    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
        let mut enc = synta::Encoder::new(Encoding::Der);
        self.inner.encode(&mut enc).map_err(SyntaErr)?;
        Ok(PyBytes::new(py, &enc.finish().map_err(SyntaErr)?))
    }

    /// Signature integer ``r`` (big-endian two's-complement bytes).
    #[getter]
    fn r<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, self.inner.r.as_bytes())
    }

    /// Signature integer ``s`` (big-endian two's-complement bytes).
    #[getter]
    fn s<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, self.inner.s.as_bytes())
    }

    fn __repr__(&self) -> String {
        format!(
            "DssSigValue(r=<{} bytes>, s=<{} bytes>)",
            self.inner.r.as_bytes().len(),
            self.inner.s.as_bytes().len(),
        )
    }
}

// ── PyEcdsaSigValue ───────────────────────────────────────────────────────────

/// ECDSA signature value (RFC 3279 §2.2.3, X9.62).
///
/// Contains the integer pair ``(r, s)`` produced by the ECDSA signing
/// operation.  Typically found as the ``subjectPublicKey`` bit-string content
/// inside an X.509 certificate's ``AlgorithmIdentifier`` for ECDSA.
///
/// ```python,ignore
/// import synta.pkixalgs as pa
/// sig = pa.EcdsaSigValue.from_der(signature_bytes)
/// r_bytes, s_bytes = sig.r, sig.s
/// ```
#[pyclass(frozen, name = "EcdsaSigValue")]
pub struct PyEcdsaSigValue {
    inner: synta_certificate::pkixalgs_types::EcdsaSigValue,
}

#[pymethods]
impl PyEcdsaSigValue {
    /// Parse a DER-encoded ``ECDSA-Sig-Value`` SEQUENCE.
    #[staticmethod]
    fn from_der(data: &[u8]) -> PyResult<Self> {
        let mut dec = Decoder::new(data, Encoding::Der);
        let inner = dec
            .decode::<synta_certificate::pkixalgs_types::EcdsaSigValue>()
            .map_err(SyntaErr)?;
        Ok(Self { inner })
    }

    /// Return the DER encoding of this ``ECDSA-Sig-Value`` SEQUENCE.
    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
        let mut enc = synta::Encoder::new(Encoding::Der);
        self.inner.encode(&mut enc).map_err(SyntaErr)?;
        Ok(PyBytes::new(py, &enc.finish().map_err(SyntaErr)?))
    }

    /// Signature integer ``r`` (big-endian two's-complement bytes).
    #[getter]
    fn r<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, self.inner.r.as_bytes())
    }

    /// Signature integer ``s`` (big-endian two's-complement bytes).
    #[getter]
    fn s<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, self.inner.s.as_bytes())
    }

    fn __repr__(&self) -> String {
        format!(
            "EcdsaSigValue(r=<{} bytes>, s=<{} bytes>)",
            self.inner.r.as_bytes().len(),
            self.inner.s.as_bytes().len(),
        )
    }
}

// ── PyECParameters ────────────────────────────────────────────────────────────

/// EC domain parameters (RFC 3279 §2.3.5, X9.62).
///
/// A CHOICE with three alternatives:
///
/// * ``namedCurve`` — an OID identifying a well-known curve (most common in X.509)
/// * ``ecParameters`` — explicit ``SpecifiedECDomain`` (rarely used in PKI)
/// * ``implicitlyCA`` — NULL (inherit parameters from the CA certificate)
///
/// Use :attr:`arm` to determine which alternative is present, and
/// :attr:`named_curve_oid` to obtain the OID for the ``namedCurve`` arm.
///
/// ```python,ignore
/// import synta.pkixalgs as pa
/// params = pa.ECParameters.from_der(alg_params_der)
/// if params.arm == "namedCurve":
///     print(params.named_curve_oid)
/// ```
#[pyclass(frozen, name = "ECParameters")]
pub struct PyECParameters {
    _data: Py<PyBytes>,
    raw: &'static [u8],
    inner: OnceLock<Box<synta_certificate::pkixalgs_types::ECParameters<'static>>>,
}

impl PyECParameters {
    fn params(&self) -> PyResult<&synta_certificate::pkixalgs_types::ECParameters<'static>> {
        if let Some(v) = self.inner.get() {
            return Ok(v.as_ref());
        }
        let mut dec = Decoder::new(self.raw, Encoding::Der);
        let decoded = dec
            .decode::<synta_certificate::pkixalgs_types::ECParameters<'_>>()
            .map_err(SyntaErr)?;
        // SAFETY: raw is pinned for the lifetime of self (kept alive by _data).
        let decoded: synta_certificate::pkixalgs_types::ECParameters<'static> =
            unsafe { std::mem::transmute(decoded) };
        let _ = self.inner.set(Box::new(decoded));
        Ok(self.inner.get().unwrap().as_ref())
    }
}

#[pymethods]
impl PyECParameters {
    /// Parse a DER-encoded ``ECParameters`` CHOICE.
    #[staticmethod]
    fn from_der(py: Python<'_>, data: Bound<'_, PyBytes>) -> PyResult<Self> {
        let py_bytes = data.unbind();
        // Validate before storing
        {
            let raw = py_bytes.as_bytes(py);
            Decoder::new(raw, Encoding::Der)
                .decode::<synta_certificate::pkixalgs_types::ECParameters<'_>>()
                .map_err(SyntaErr)?;
        }
        let raw: &'static [u8] = unsafe { std::mem::transmute(py_bytes.as_bytes(py)) };
        Ok(Self {
            _data: py_bytes,
            raw,
            inner: OnceLock::new(),
        })
    }

    /// Return the DER encoding of this ``ECParameters`` value.
    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
        let mut enc = synta::Encoder::new(Encoding::Der);
        self.params()?.encode(&mut enc).map_err(SyntaErr)?;
        Ok(PyBytes::new(py, &enc.finish().map_err(SyntaErr)?))
    }

    /// Which CHOICE arm is present: ``"namedCurve"``, ``"ecParameters"``, or
    /// ``"implicitlyCA"``.
    #[getter]
    fn arm(&self) -> PyResult<&'static str> {
        use synta_certificate::pkixalgs_types::ECParameters::*;
        Ok(match self.params()? {
            NamedCurve(_) => "namedCurve",
            EcParameters(_) => "ecParameters",
            ImplicitlyCA(_) => "implicitlyCA",
        })
    }

    /// The named-curve OID, or ``None`` if the arm is not ``namedCurve``.
    #[getter]
    fn named_curve_oid(&self, py: Python<'_>) -> PyResult<Option<Py<PyObjectIdentifier>>> {
        use synta_certificate::pkixalgs_types::ECParameters::*;
        match self.params()? {
            NamedCurve(oid) => {
                let obj = Py::new(py, PyObjectIdentifier::from_oid(oid.clone()))?;
                Ok(Some(obj))
            }
            _ => Ok(None),
        }
    }

    fn __repr__(&self) -> PyResult<String> {
        Ok(format!("ECParameters(arm={})", self.arm()?))
    }
}

// ── register_pkixalgs_submodule ───────────────────────────────────────────────

/// Build and register the ``synta.pkixalgs`` submodule.
pub(super) fn register_pkixalgs_submodule(parent: &Bound<'_, PyModule>) -> PyResult<()> {
    let py = parent.py();
    let m = PyModule::new(py, "pkixalgs")?;

    m.add_class::<PyDssParms>()?;
    m.add_class::<PyDssSigValue>()?;
    m.add_class::<PyEcdsaSigValue>()?;
    m.add_class::<PyECParameters>()?;

    // ── DSA / DH OIDs ────────────────────────────────────────────────────────
    m.add(
        "ID_DSA",
        super::oid_const(py, synta_certificate::pkixalgs_types::ID_DSA),
    )?;
    m.add(
        "ID_DSA_WITH_SHA1",
        super::oid_const(py, synta_certificate::pkixalgs_types::ID_DSA_WITH_SHA1),
    )?;
    m.add(
        "DHPUBLICNUMBER",
        super::oid_const(py, synta_certificate::pkixalgs_types::DHPUBLICNUMBER),
    )?;

    // ── EC / ECDSA OIDs ──────────────────────────────────────────────────────
    m.add(
        "ID_EC_PUBLIC_KEY",
        super::oid_const(py, synta_certificate::pkixalgs_types::ID_EC_PUBLIC_KEY),
    )?;
    m.add(
        "ECDSA_WITH_SHA1",
        super::oid_const(py, synta_certificate::pkixalgs_types::ECDSA_WITH_SHA1),
    )?;
    m.add(
        "ECDSA_WITH_SHA256",
        super::oid_const(py, synta_certificate::pkixalgs_types::ECDSA_WITH_SHA256),
    )?;
    m.add(
        "ECDSA_WITH_SHA384",
        super::oid_const(py, synta_certificate::pkixalgs_types::ECDSA_WITH_SHA384),
    )?;
    m.add(
        "ECDSA_WITH_SHA512",
        super::oid_const(py, synta_certificate::pkixalgs_types::ECDSA_WITH_SHA512),
    )?;

    // ── Named curve OIDs ─────────────────────────────────────────────────────
    m.add(
        "PRIME192V1",
        super::oid_const(py, synta_certificate::pkixalgs_types::PRIME192V1),
    )?;
    m.add(
        "PRIME256V1",
        super::oid_const(py, synta_certificate::pkixalgs_types::PRIME256V1),
    )?;
    m.add(
        "SECP224R1",
        super::oid_const(py, synta_certificate::pkixalgs_types::SECP224R1),
    )?;
    m.add(
        "SECP384R1",
        super::oid_const(py, synta_certificate::pkixalgs_types::SECP384R1),
    )?;
    m.add(
        "SECP521R1",
        super::oid_const(py, synta_certificate::pkixalgs_types::SECP521R1),
    )?;

    crate::install_submodule(
        parent,
        &m,
        "synta.pkixalgs",
        Some(concat!(
            "synta.pkixalgs — RFC 3279 algorithm parameter types.\n\n",
            "Provides DssParms, DssSigValue, EcdsaSigValue, and ECParameters\n",
            "for decoding DSA/DH domain parameters and DSA/ECDSA signature values,\n",
            "along with OID constants for DSA, DH, EC, and named-curve algorithms.",
        )),
    )
}