Skip to main content

_synta/
lib.rs

1//! Python bindings for Synta ASN.1 library
2//!
3//! This module provides PyO3-based Python bindings for the Synta ASN.1 library,
4//! enabling high-performance ASN.1 parsing and encoding from Python.
5
6use pyo3::prelude::*;
7
8pub mod certificate;
9pub mod crypto;
10pub mod crypto_keys;
11pub mod decoder;
12pub mod encoder;
13pub mod error;
14pub mod ext_builders;
15pub mod krb5;
16pub mod otp;
17pub mod pkinit;
18pub mod types;
19pub mod x509_verification;
20
21// Re-export for convenience
22pub use certificate::*;
23pub use decoder::*;
24pub use encoder::*;
25pub use error::*;
26pub use types::*;
27
28/// Register a submodule into both the parent module and ``sys.modules``.
29///
30/// Sets the module's ``__name__`` to ``qualified_name`` (e.g. ``"synta.oids"``)
31/// and optionally its ``__doc__``, adds it as an attribute on ``parent`` under
32/// the leaf name (the part after the last ``'.'``), and installs it in
33/// ``sys.modules[qualified_name]``.
34///
35/// This ensures ``import synta.oids``, ``from synta import oids``, and
36/// ``repr(synta.oids)`` all behave correctly.
37pub(crate) fn install_submodule<'py>(
38    parent: &Bound<'py, PyModule>,
39    m: &Bound<'py, PyModule>,
40    qualified_name: &str,
41    doc: Option<&str>,
42) -> PyResult<()> {
43    let py = parent.py();
44    m.setattr("__name__", qualified_name)?;
45    if let Some(docstr) = doc {
46        m.setattr("__doc__", docstr)?;
47    }
48    let (package, leaf) = qualified_name
49        .rsplit_once('.')
50        .unwrap_or(("", qualified_name));
51    m.setattr("__package__", package)?;
52    let sys_modules = py.import("sys")?.getattr("modules")?;
53    sys_modules.set_item(qualified_name, m)?;
54    parent.add(leaf, m)?;
55    Ok(())
56}
57
58/// ASN.1 encoding rules
59///
60/// Specifies which encoding rules to use when encoding or decoding ASN.1 data.
61#[pyclass(name = "Encoding")]
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum PyEncoding {
64    /// Distinguished Encoding Rules (deterministic subset of BER)
65    DER,
66    /// Basic Encoding Rules (most flexible)
67    BER,
68    /// Canonical Encoding Rules (similar to DER but for streaming)
69    CER,
70}
71
72impl From<PyEncoding> for synta::Encoding {
73    fn from(enc: PyEncoding) -> Self {
74        match enc {
75            PyEncoding::DER => synta::Encoding::Der,
76            PyEncoding::BER => synta::Encoding::Ber,
77            PyEncoding::CER => synta::Encoding::Cer,
78        }
79    }
80}
81
82impl From<synta::Encoding> for PyEncoding {
83    fn from(enc: synta::Encoding) -> Self {
84        match enc {
85            synta::Encoding::Der => PyEncoding::DER,
86            synta::Encoding::Ber => PyEncoding::BER,
87            synta::Encoding::Cer => PyEncoding::CER,
88        }
89    }
90}
91
92/// Synta: High-performance ASN.1 parser and encoder
93///
94/// This module provides ASN.1 parsing, decoding, and encoding capabilities
95/// with support for DER (Distinguished Encoding Rules) and BER (Basic Encoding Rules).
96///
97/// Example:
98///     >>> import synta
99///     >>> # Decode an integer
100///     >>> decoder = synta.Decoder(b'\\x02\\x01\\x2A', synta.Encoding.DER)
101///     >>> integer = decoder.decode_integer()
102///     >>> print(integer.to_int())
103///     42
104#[pymodule]
105fn _synta(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
106    // Add encoding enum
107    m.add_class::<PyEncoding>()?;
108
109    // Add error types
110    m.add("SyntaError", py.get_type::<SyntaError>())?;
111
112    // Add decoder and encoder
113    m.add_class::<PyDecoder>()?;
114    m.add_class::<PyEncoder>()?;
115
116    // Add primitive types
117    m.add_class::<PyInteger>()?;
118    m.add_class::<PyOctetString>()?;
119    m.add_class::<PyBitString>()?;
120    m.add_class::<PyBoolean>()?;
121    m.add_class::<PyReal>()?;
122    m.add_class::<PyUtcTime>()?;
123    m.add_class::<PyGeneralizedTime>()?;
124    m.add_class::<PyNull>()?;
125    m.add_class::<PyUtf8String>()?;
126    m.add_class::<PyPrintableString>()?;
127    m.add_class::<PyIA5String>()?;
128    // New string types
129    m.add_class::<PyNumericString>()?;
130    m.add_class::<PyTeletexString>()?;
131    m.add_class::<PyVisibleString>()?;
132    m.add_class::<PyGeneralString>()?;
133    m.add_class::<PyUniversalString>()?;
134    m.add_class::<PyBmpString>()?;
135    m.add_class::<PyTaggedElement>()?;
136    m.add_class::<PyRawElement>()?;
137
138    // Add certificate types (Certificate, CertificationRequest, CertificateList, OCSPResponse)
139    // and the pem_to_der helper function.
140    certificate::register_module(m)?;
141    m.add_function(wrap_pyfunction!(pem_to_der, m)?)?;
142    m.add_function(wrap_pyfunction!(der_to_pem, m)?)?;
143    m.add_function(wrap_pyfunction!(parse_general_names, m)?)?;
144    m.add_function(wrap_pyfunction!(parse_name_attrs, m)?)?;
145    m.add_function(wrap_pyfunction!(encode_extended_key_usage, m)?)?;
146    m.add_function(wrap_pyfunction!(encode_subject_alt_names, m)?)?;
147    m.add_function(wrap_pyfunction!(name_der_equal, m)?)?;
148    m.add_function(wrap_pyfunction!(digest, m)?)?;
149
150    // PublicKey and PrivateKey classes
151    m.add_class::<crypto_keys::PyPublicKey>()?;
152    m.add_class::<crypto_keys::PyPrivateKey>()?;
153
154    // Symmetric crypto submodule (synta.crypto)
155    crypto::register_crypto_module(m)?;
156
157    // X.509 extension value builders submodule (synta.ext)
158    ext_builders::register_ext_module(m)?;
159
160    // Kerberos V5 submodule (synta.krb5)
161    krb5::register_krb5_module(m)?;
162
163    // X.509 verification submodule (synta.x509)
164    x509_verification::register_x509_module(m)?;
165
166    // Add version
167    m.add("__version__", env!("CARGO_PKG_VERSION"))?;
168
169    Ok(())
170}
171
172/// Parse a DER-encoded GeneralNames SEQUENCE into ``(tag_number, content_bytes)`` pairs.
173///
174/// ``san_der`` must be the **complete DER bytes** of the ``SEQUENCE OF GeneralName``
175/// value — exactly what you get from the SAN extension's ``extn_value`` octet-string
176/// content, or from ``Certificate.get_extension_value_der("2.5.29.17")``.
177///
178/// Returns a ``list`` of ``(tag_number: int, content: bytes)`` tuples, one per
179/// ``GeneralName`` alternative.  Tag numbers follow RFC 5280:
180///
181/// * 0 — otherName (constructed; ``content`` is the full ``OtherNameValue`` TLV)
182/// * 1 — rfc822Name (email); ``content`` is raw IA5String bytes
183/// * 2 — dNSName; ``content`` is raw IA5String bytes
184/// * 3 — x400Address
185/// * 4 — directoryName; ``content`` is the Name SEQUENCE TLV — pass to ``parse_name_attrs()``
186/// * 5 — ediPartyName
187/// * 6 — uniformResourceIdentifier; ``content`` is raw IA5String bytes
188/// * 7 — iPAddress; ``content`` is 4 bytes (IPv4) or 16 bytes (IPv6)
189/// * 8 — registeredID; ``content`` is raw OID value bytes
190///
191/// Tag constants are available in the :mod:`synta.general_name` submodule
192/// (e.g. ``synta.general_name.DNS_NAME == 2``), making dispatch readable
193/// without hardcoded magic numbers:
194///
195/// ```python,ignore
196/// import ipaddress
197/// import synta.general_name as gn
198///
199/// san_der = cert.get_extension_value_der("2.5.29.17")
200/// for tag_num, content in synta.parse_general_names(san_der):
201///     if tag_num == gn.DNS_NAME:
202///         print("DNS:", content.decode("ascii"))
203///     elif tag_num == gn.IP_ADDRESS:
204///         print("IP:", ipaddress.ip_address(content))
205///     elif tag_num == gn.RFC822_NAME:
206///         print("email:", content.decode("ascii"))
207///     elif tag_num == gn.DIRECTORY_NAME:
208///         attrs = synta.parse_name_attrs(content)
209///         print("DirName:", attrs)
210///     elif tag_num == gn.URI:
211///         print("URI:", content.decode("ascii"))
212/// ```
213///
214/// Returns an empty list if ``san_der`` cannot be parsed as a DER SEQUENCE.
215#[pyfunction]
216fn parse_general_names<'py>(
217    py: Python<'py>,
218    san_der: &[u8],
219) -> PyResult<Bound<'py, pyo3::types::PyList>> {
220    use pyo3::types::{PyBytes, PyList, PyTuple};
221
222    let list = PyList::empty(py);
223    for (tag_num, content) in synta_certificate::parse_general_names(san_der) {
224        let tuple = PyTuple::new(
225            py,
226            [
227                tag_num.into_pyobject(py)?.into_any(),
228                PyBytes::new(py, &content).into_any(),
229            ],
230        )?;
231        list.append(tuple)?;
232    }
233    Ok(list)
234}
235
236/// Walk a DER-encoded X.500 Name SEQUENCE and return ``(dotted_oid, value_str)`` pairs.
237///
238/// ``name_der`` must be the **complete TLV** bytes of the Name SEQUENCE (tag + length
239/// + value), as returned by ``Certificate.issuer_raw_der`` or
240/// ``Certificate.subject_raw_der``, or from a ``directoryName`` entry in
241/// ``parse_general_names()``.
242///
243/// Returns a ``list`` of ``(oid: str, value: str)`` tuples in DER traversal order
244/// (outermost RDN first, innermost ATV first within each RDN).  The OID is always
245/// in dotted-decimal notation (e.g. ``"2.5.4.3"``).  The value string is decoded
246/// using the appropriate per-tag encoding: UTF-8 for most types, Latin-1 for
247/// TeletexString, UCS-2 big-endian for BMPString, and UCS-4 big-endian for
248/// UniversalString.
249///
250/// This replaces manual ``Decoder`` iteration over the Name structure and is the
251/// structured-data counterpart to the ``Certificate.issuer`` string property:
252///
253/// ```python
254/// # Inspect subject attributes directly:
255/// attrs = synta.parse_name_attrs(cert.subject_raw_der)
256/// # → [("2.5.4.6", "US"), ("2.5.4.10", "Example Corp"), ("2.5.4.3", "Root CA")]
257///
258/// # Build a cryptography.x509.Name for comparison or re-use:
259/// from cryptography.x509 import Name, NameAttribute, ObjectIdentifier
260/// name = Name([
261///     NameAttribute(ObjectIdentifier(oid), val)
262///     for oid, val in synta.parse_name_attrs(cert.subject_raw_der)
263/// ])
264/// ```
265///
266/// Returns an empty list if ``name_der`` cannot be parsed.
267#[pyfunction]
268fn parse_name_attrs<'py>(
269    py: Python<'py>,
270    name_der: &[u8],
271) -> PyResult<Bound<'py, pyo3::types::PyList>> {
272    use pyo3::types::{PyList, PyTuple};
273
274    let attrs = synta_certificate::name::parse_name_attrs(name_der);
275    let list = PyList::empty(py);
276    for (oid, value) in attrs {
277        let tuple = PyTuple::new(
278            py,
279            [
280                oid.into_pyobject(py)?.into_any(),
281                value.into_pyobject(py)?.into_any(),
282            ],
283        )?;
284        list.append(tuple)?;
285    }
286    Ok(list)
287}
288
289/// Encode DER bytes as a PEM block.
290///
291/// Returns :class:`bytes` containing a ``-----BEGIN {label}-----`` /
292/// ``-----END {label}-----`` block with standard 64-character base64 lines.
293/// This is the low-level inverse of :func:`pem_to_der`.
294///
295/// For serialising parsed objects use the class-level
296/// ``Certificate.to_pem()``, ``CertificationRequest.to_pem()``, etc., which
297/// fill in the correct label automatically.
298///
299/// ```python
300/// with open("cert.der", "rb") as f:
301///     der = f.read()
302/// pem = synta.der_to_pem(der, "CERTIFICATE")
303/// ```
304#[pyfunction]
305fn der_to_pem<'py>(py: Python<'py>, der: &[u8], label: &str) -> Bound<'py, pyo3::types::PyBytes> {
306    pyo3::types::PyBytes::new(py, &synta_certificate::der_to_pem(label, der))
307}
308
309/// Decode PEM blocks to DER bytes.
310///
311/// Strips ``-----BEGIN ...-----`` / ``-----END ...-----`` boundary lines and
312/// decodes the base64 body of every PEM block found in the input.  Implemented
313/// in pure Rust — no external dependencies required.
314///
315/// Always returns :class:`list` [:class:`bytes`] — one entry per PEM block.
316/// Raises :exc:`ValueError` if no PEM block is found.
317///
318/// ```python,ignore
319/// # Single block — index into the list:
320/// der = synta.pem_to_der(open("cert.pem", "rb").read())[0]
321/// cert = synta.Certificate.from_der(der)
322///
323/// # Bundle / chain:
324/// ders = synta.pem_to_der(open("bundle.pem", "rb").read())
325/// certs = [synta.Certificate.from_der(d) for d in ders]
326/// ```
327#[pyfunction]
328fn pem_to_der<'py>(
329    py: Python<'py>,
330    data: &[u8],
331) -> PyResult<pyo3::Bound<'py, pyo3::types::PyList>> {
332    let blocks = synta_certificate::pem_blocks(data);
333    if blocks.is_empty() {
334        return Err(pyo3::exceptions::PyValueError::new_err(
335            "no PEM block found in input",
336        ));
337    }
338    let list = pyo3::types::PyList::empty(py);
339    for (_, block) in &blocks {
340        list.append(pyo3::types::PyBytes::new(py, block))?;
341    }
342    Ok(list)
343}
344
345/// Compute a hash digest of arbitrary bytes.
346///
347/// Returns a :class:`bytes` object containing the raw (binary) digest.
348/// ``algorithm`` must be one of ``"sha1"``, ``"sha224"``, ``"sha256"``,
349/// ``"sha384"``, ``"sha512"``, or ``"md5"``.  Raises :exc:`ValueError` for
350/// unknown algorithm names or crypto backend errors.
351///
352/// ```python,ignore
353/// import synta
354///
355/// # Hash a certificate DER blob:
356/// digest_bytes = synta.digest("sha256", cert_der)
357/// print(digest_bytes.hex())
358///
359/// # Hash an arbitrary byte string:
360/// digest_bytes = synta.digest("sha1", b"hello world")
361/// ```
362#[pyfunction]
363fn digest<'py>(
364    py: Python<'py>,
365    algorithm: &str,
366    data: &[u8],
367) -> PyResult<pyo3::Bound<'py, pyo3::types::PyBytes>> {
368    use synta_certificate::{default_data_hasher, DataHasher};
369    let d = default_data_hasher()
370        .hash_data(algorithm, data)
371        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
372    Ok(pyo3::types::PyBytes::new(py, &d))
373}