synta-python 0.2.5

Python extension module for the synta ASN.1 library
Documentation
//! Typed Python classes for each `GeneralName` variant (RFC 5280 §4.2.1.6).
//!
//! Each variant of the ASN.1 `GeneralName` CHOICE is represented as a
//! separate frozen pyclass in the `synta.general_name` submodule.  These
//! types are constructed by [`py_from_general_name`], which is called from
//! the Python binding whenever typed `GeneralName` objects are needed.

use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyList, PyModule};

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

// ── OtherName ─────────────────────────────────────────────────────────────────

/// An `otherName [0]` GeneralName.
///
/// Carries an arbitrary typed value identified by ``type_id`` (the OID) and
/// ``value`` (the full DER TLV of the ``[0] EXPLICIT ANY`` field).
#[pyclass(frozen, name = "OtherName", module = "synta.general_name")]
pub struct PyOtherName {
    pub type_id: synta::ObjectIdentifier,
    /// DER bytes of the value Element (tag + length + contents).
    pub value: Vec<u8>,
}

#[pymethods]
impl PyOtherName {
    /// OID identifying the OtherName type.
    #[getter]
    fn type_id<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
        Py::new(py, PyObjectIdentifier::from_oid(self.type_id.clone())).map(|p| p.into_bound(py))
    }

    /// Raw DER bytes of the value field (tag + length + value).
    #[getter]
    fn value<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, &self.value)
    }

    fn __repr__(&self) -> String {
        format!("OtherName(type_id={})", self.type_id)
    }
}

// ── RFC822Name ────────────────────────────────────────────────────────────────

/// An `rfc822Name [1]` GeneralName (e-mail address).
#[pyclass(frozen, name = "RFC822Name", module = "synta.general_name")]
pub struct PyRfc822Name {
    pub value: String,
}

#[pymethods]
impl PyRfc822Name {
    /// The e-mail address string.
    #[getter]
    fn value(&self) -> &str {
        &self.value
    }

    fn __repr__(&self) -> String {
        format!("RFC822Name(value={:?})", self.value)
    }
}

// ── DNSName ───────────────────────────────────────────────────────────────────

/// A `dNSName [2]` GeneralName (DNS host name).
#[pyclass(frozen, name = "DNSName", module = "synta.general_name")]
pub struct PyDnsName {
    pub value: String,
}

#[pymethods]
impl PyDnsName {
    /// The DNS name string.
    #[getter]
    fn value(&self) -> &str {
        &self.value
    }

    fn __repr__(&self) -> String {
        format!("DNSName(value={:?})", self.value)
    }
}

// ── X400Address ───────────────────────────────────────────────────────────────

/// An `x400Address [3]` GeneralName (raw DER, unusual in practice).
#[pyclass(frozen, name = "X400Address", module = "synta.general_name")]
pub struct PyX400Address {
    pub raw_der: Vec<u8>,
}

#[pymethods]
impl PyX400Address {
    /// Raw DER bytes of the ORAddress value.
    #[getter]
    fn raw_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, &self.raw_der)
    }

    fn __repr__(&self) -> String {
        format!("X400Address(raw_der=<{} bytes>)", self.raw_der.len())
    }
}

// ── DirectoryName ─────────────────────────────────────────────────────────────

/// A `directoryName [4]` GeneralName (X.500 distinguished name).
///
/// The ``name_der`` field contains the raw DER encoding of the `Name`
/// SEQUENCE.  Pass it to :func:`~synta.parse_name_attrs` to decode the
/// attribute list.
#[pyclass(frozen, name = "DirectoryName", module = "synta.general_name")]
pub struct PyDirectoryName {
    /// Raw DER bytes of the Name SEQUENCE.
    pub name_der: Vec<u8>,
}

#[pymethods]
impl PyDirectoryName {
    /// Raw DER bytes of the Name SEQUENCE (starts with ``0x30``).
    #[getter]
    fn name_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, &self.name_der)
    }

    fn __repr__(&self) -> String {
        let dn = synta_certificate::name::format_dn(&self.name_der);
        format!("DirectoryName(name={dn:?})")
    }
}

// ── EDIPartyName ──────────────────────────────────────────────────────────────

/// An `ediPartyName [5]` GeneralName (raw DER, unusual in practice).
#[pyclass(frozen, name = "EDIPartyName", module = "synta.general_name")]
pub struct PyEdiPartyName {
    pub raw_der: Vec<u8>,
}

#[pymethods]
impl PyEdiPartyName {
    /// Raw DER bytes of the EDIPartyName value.
    #[getter]
    fn raw_der<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, &self.raw_der)
    }

    fn __repr__(&self) -> String {
        format!("EDIPartyName(raw_der=<{} bytes>)", self.raw_der.len())
    }
}

// ── UniformResourceIdentifier ─────────────────────────────────────────────────

/// A `uniformResourceIdentifier [6]` GeneralName (URI).
#[pyclass(
    frozen,
    name = "UniformResourceIdentifier",
    module = "synta.general_name"
)]
pub struct PyUri {
    pub value: String,
}

#[pymethods]
impl PyUri {
    /// The URI string.
    #[getter]
    fn value(&self) -> &str {
        &self.value
    }

    fn __repr__(&self) -> String {
        format!("UniformResourceIdentifier(value={:?})", self.value)
    }
}

// ── IPAddress ─────────────────────────────────────────────────────────────────

/// An `iPAddress [7]` GeneralName.
///
/// ``address`` is 4 raw bytes for IPv4 or 16 raw bytes for IPv6.
#[pyclass(frozen, name = "IPAddress", module = "synta.general_name")]
pub struct PyIPAddress {
    pub address: Vec<u8>,
}

#[pymethods]
impl PyIPAddress {
    /// Raw address bytes (4 bytes for IPv4, 16 bytes for IPv6).
    #[getter]
    fn address<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, &self.address)
    }

    /// The IP address as a Python :class:`ipaddress.IPv4Address` or
    /// :class:`ipaddress.IPv6Address`, matching the ``cryptography`` library API.
    ///
    /// ```python,ignore
    /// import ipaddress
    /// gn = ...  # IPAddress general name
    /// assert isinstance(gn.value, (ipaddress.IPv4Address, ipaddress.IPv6Address))
    /// ```
    #[getter]
    fn value<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
        let ipaddress = PyModule::import(py, "ipaddress")?;
        let cls = match self.address.len() {
            4 => ipaddress.getattr("IPv4Address")?,
            16 => ipaddress.getattr("IPv6Address")?,
            n => {
                return Err(pyo3::exceptions::PyValueError::new_err(format!(
                    "invalid IP address length: {n} (expected 4 or 16)"
                )))
            }
        };
        cls.call1((PyBytes::new(py, &self.address),))
    }

    fn __repr__(&self) -> String {
        match self.address.len() {
            4 => format!(
                "IPAddress(address={}.{}.{}.{})",
                self.address[0], self.address[1], self.address[2], self.address[3]
            ),
            16 => {
                let parts: Vec<String> = self
                    .address
                    .chunks(2)
                    .map(|c| format!("{:02x}{:02x}", c[0], c[1]))
                    .collect();
                format!("IPAddress(address={})", parts.join(":"))
            }
            n => format!("IPAddress(address=<{n} bytes>)"),
        }
    }
}

// ── RegisteredID ──────────────────────────────────────────────────────────────

/// A `registeredID [8]` GeneralName (OID).
#[pyclass(frozen, name = "RegisteredID", module = "synta.general_name")]
pub struct PyRegisteredId {
    pub oid: synta::ObjectIdentifier,
}

#[pymethods]
impl PyRegisteredId {
    /// The registered OID.
    #[getter]
    fn oid<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyObjectIdentifier>> {
        Py::new(py, PyObjectIdentifier::from_oid(self.oid.clone())).map(|p| p.into_bound(py))
    }

    fn __repr__(&self) -> String {
        format!("RegisteredID(oid={})", self.oid)
    }
}

// ── Converter helper ──────────────────────────────────────────────────────────

/// Encode an ASN.1 `Element` to DER bytes (no generated `to_der` for Element).
///
/// Returns `None` if encoding fails.
fn encode_element(elem: &synta::Element<'_>) -> Option<Vec<u8>> {
    use synta::traits::Encode;
    let mut enc = synta::Encoder::new(synta::Encoding::Der);
    elem.encode(&mut enc).ok()?;
    enc.finish().ok()
}

/// Convert a Rust `GeneralName<'_>` to the appropriate Python general-name object.
///
/// Returns a `Bound<'py, PyAny>` pointing to the concrete typed class
/// (`PyOtherName`, `PyDnsName`, etc.) registered in `synta.general_name`.
pub fn py_from_general_name<'py>(
    py: Python<'py>,
    gn: &synta_certificate::GeneralName<'_>,
) -> PyResult<Bound<'py, PyAny>> {
    use synta_certificate::GeneralName;

    match gn {
        GeneralName::OtherName(on) => {
            let value = encode_element(&on.value).ok_or_else(|| {
                pyo3::exceptions::PyValueError::new_err(
                    "failed to encode OtherName value element to DER",
                )
            })?;
            let obj = Py::new(
                py,
                PyOtherName {
                    type_id: on.type_id.clone(),
                    value,
                },
            )?;
            Ok(obj.into_bound(py).into_any())
        }
        GeneralName::Rfc822Name(ia5) => {
            let obj = Py::new(
                py,
                PyRfc822Name {
                    value: ia5.as_str().to_string(),
                },
            )?;
            Ok(obj.into_bound(py).into_any())
        }
        GeneralName::DNSName(ia5) => {
            let obj = Py::new(
                py,
                PyDnsName {
                    value: ia5.as_str().to_string(),
                },
            )?;
            Ok(obj.into_bound(py).into_any())
        }
        GeneralName::X400Address(elem) => {
            let raw_der = encode_element(elem).ok_or_else(|| {
                pyo3::exceptions::PyValueError::new_err(
                    "failed to encode X400Address element to DER",
                )
            })?;
            let obj = Py::new(py, PyX400Address { raw_der })?;
            Ok(obj.into_bound(py).into_any())
        }
        GeneralName::DirectoryName(name) => {
            let name_der = name.to_der().map_err(SyntaErr)?;
            let obj = Py::new(py, PyDirectoryName { name_der })?;
            Ok(obj.into_bound(py).into_any())
        }
        GeneralName::EdiPartyName(epn) => {
            let raw_der = epn.to_der().map_err(SyntaErr)?;
            let obj = Py::new(py, PyEdiPartyName { raw_der })?;
            Ok(obj.into_bound(py).into_any())
        }
        GeneralName::UniformResourceIdentifier(ia5) => {
            let obj = Py::new(
                py,
                PyUri {
                    value: ia5.as_str().to_string(),
                },
            )?;
            Ok(obj.into_bound(py).into_any())
        }
        GeneralName::IPAddress(oct) => {
            let obj = Py::new(
                py,
                PyIPAddress {
                    address: oct.as_bytes().to_vec(),
                },
            )?;
            Ok(obj.into_bound(py).into_any())
        }
        GeneralName::RegisteredID(oid) => {
            let obj = Py::new(py, PyRegisteredId { oid: oid.clone() })?;
            Ok(obj.into_bound(py).into_any())
        }
    }
}

/// Decode a DER-encoded `SEQUENCE OF GeneralName` into a Python list of
/// typed general-name objects.
///
/// `raw` must be the complete DER bytes of the `SEQUENCE OF GeneralName`
/// value (i.e. the `extn_value` OCTET STRING content for SAN or IAN).
///
/// Returns a Python `list` of typed objects: :class:`DNSName`,
/// :class:`RFC822Name`, :class:`OtherName`, etc.
pub fn decode_general_names_to_py<'py>(
    py: Python<'py>,
    raw: &[u8],
) -> PyResult<Bound<'py, PyList>> {
    use synta_certificate::GeneralName;

    let mut decoder = synta::Decoder::new(raw, synta::Encoding::Der);
    let gns: Vec<GeneralName<'_>> = decoder.decode().map_err(|e| {
        pyo3::exceptions::PyValueError::new_err(format!(
            "failed to decode SEQUENCE OF GeneralName: {e}"
        ))
    })?;

    let list = PyList::empty(py);
    for gn in &gns {
        list.append(py_from_general_name(py, gn)?)?;
    }
    Ok(list)
}