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