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