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
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    m.add_function(wrap_pyfunction!(format_dn, m)?)?;
127    m.add_function(wrap_pyfunction!(format_dn_slash, m)?)?;
128    m.add_function(wrap_pyfunction!(find_extension_value, m)?)?;
129    m.add_function(wrap_pyfunction!(encode_general_names, m)?)?;
130    m.add_function(wrap_pyfunction!(signing_algorithm_der, m)?)?;
131    m.add_function(wrap_pyfunction!(key_usage_bit, m)?)?;
132    m.add_function(wrap_pyfunction!(decode_public_key_info, m)?)?;
133
134    // PublicKey and PrivateKey classes
135    m.add_class::<crypto_keys::PyPublicKey>()?;
136    m.add_class::<crypto_keys::PyPrivateKey>()?;
137
138    // Symmetric crypto submodule (synta.crypto)
139    crypto::register_crypto_module(m)?;
140
141    // X.509 extension value builders submodule (synta.ext)
142    ext_builders::register_ext_module(m)?;
143
144    // X.509 verification submodule (synta.x509)
145    x509_verification::register_x509_module(m)?;
146
147    // Add version
148    m.add("__version__", env!("CARGO_PKG_VERSION"))?;
149
150    Ok(())
151}
152
153/// Parse a DER-encoded GeneralNames SEQUENCE into ``(tag_number, content_bytes)`` pairs.
154///
155/// ``san_der`` must be the **complete DER bytes** of the ``SEQUENCE OF GeneralName``
156/// value — exactly what you get from the SAN extension's ``extn_value`` octet-string
157/// content, or from ``Certificate.get_extension_value_der("2.5.29.17")``.
158///
159/// Returns a ``list`` of ``(tag_number: int, content: bytes)`` tuples, one per
160/// ``GeneralName`` alternative.  Tag numbers follow RFC 5280:
161///
162/// * 0 — otherName (constructed; ``content`` is the full ``OtherNameValue`` TLV)
163/// * 1 — rfc822Name (email); ``content`` is raw IA5String bytes
164/// * 2 — dNSName; ``content`` is raw IA5String bytes
165/// * 3 — x400Address
166/// * 4 — directoryName; ``content`` is the Name SEQUENCE TLV — pass to ``parse_name_attrs()``
167/// * 5 — ediPartyName
168/// * 6 — uniformResourceIdentifier; ``content`` is raw IA5String bytes
169/// * 7 — iPAddress; ``content`` is 4 bytes (IPv4) or 16 bytes (IPv6)
170/// * 8 — registeredID; ``content`` is raw OID value bytes
171///
172/// Tag constants are available in the :mod:`synta.general_name` submodule
173/// (e.g. ``synta.general_name.DNS_NAME == 2``), making dispatch readable
174/// without hardcoded magic numbers:
175///
176/// ```python,ignore
177/// import ipaddress
178/// import synta.general_name as gn
179///
180/// san_der = cert.get_extension_value_der("2.5.29.17")
181/// for tag_num, content in synta.parse_general_names(san_der):
182///     if tag_num == gn.DNS_NAME:
183///         print("DNS:", content.decode("ascii"))
184///     elif tag_num == gn.IP_ADDRESS:
185///         print("IP:", ipaddress.ip_address(content))
186///     elif tag_num == gn.RFC822_NAME:
187///         print("email:", content.decode("ascii"))
188///     elif tag_num == gn.DIRECTORY_NAME:
189///         attrs = synta.parse_name_attrs(content)
190///         print("DirName:", attrs)
191///     elif tag_num == gn.URI:
192///         print("URI:", content.decode("ascii"))
193/// ```
194///
195/// Returns an empty list if ``san_der`` cannot be parsed as a DER SEQUENCE.
196#[pyfunction]
197fn parse_general_names<'py>(
198    py: Python<'py>,
199    san_der: &[u8],
200) -> PyResult<Bound<'py, pyo3::types::PyList>> {
201    use pyo3::types::{PyBytes, PyList, PyTuple};
202
203    let list = PyList::empty(py);
204    for (tag_num, content) in synta_certificate::parse_general_names(san_der) {
205        let tuple = PyTuple::new(
206            py,
207            [
208                tag_num.into_pyobject(py)?.into_any(),
209                PyBytes::new(py, &content).into_any(),
210            ],
211        )?;
212        list.append(tuple)?;
213    }
214    Ok(list)
215}
216
217/// Walk a DER-encoded X.500 Name SEQUENCE and return ``(dotted_oid, value_str)`` pairs.
218///
219/// ``name_der`` must be the **complete TLV** bytes of the Name SEQUENCE (tag + length
220/// + value), as returned by ``Certificate.issuer_raw_der`` or
221/// ``Certificate.subject_raw_der``, or from a ``directoryName`` entry in
222/// ``parse_general_names()``.
223///
224/// Returns a ``list`` of ``(oid: str, value: str)`` tuples in DER traversal order
225/// (outermost RDN first, innermost ATV first within each RDN).  The OID is always
226/// in dotted-decimal notation (e.g. ``"2.5.4.3"``).  The value string is decoded
227/// using the appropriate per-tag encoding: UTF-8 for most types, Latin-1 for
228/// TeletexString, UCS-2 big-endian for BMPString, and UCS-4 big-endian for
229/// UniversalString.
230///
231/// This replaces manual ``Decoder`` iteration over the Name structure and is the
232/// structured-data counterpart to the ``Certificate.issuer`` string property:
233///
234/// ```python
235/// # Inspect subject attributes directly:
236/// attrs = synta.parse_name_attrs(cert.subject_raw_der)
237/// # → [("2.5.4.6", "US"), ("2.5.4.10", "Example Corp"), ("2.5.4.3", "Root CA")]
238///
239/// # Build a cryptography.x509.Name for comparison or re-use:
240/// from cryptography.x509 import Name, NameAttribute, ObjectIdentifier
241/// name = Name([
242///     NameAttribute(ObjectIdentifier(oid), val)
243///     for oid, val in synta.parse_name_attrs(cert.subject_raw_der)
244/// ])
245/// ```
246///
247/// Returns an empty list if ``name_der`` cannot be parsed.
248#[pyfunction]
249fn parse_name_attrs<'py>(
250    py: Python<'py>,
251    name_der: &[u8],
252) -> PyResult<Bound<'py, pyo3::types::PyList>> {
253    use pyo3::types::{PyList, PyTuple};
254
255    let attrs = synta_certificate::name::parse_name_attrs(name_der);
256    let list = PyList::empty(py);
257    for (oid, value) in attrs {
258        let tuple = PyTuple::new(
259            py,
260            [
261                oid.into_pyobject(py)?.into_any(),
262                value.into_pyobject(py)?.into_any(),
263            ],
264        )?;
265        list.append(tuple)?;
266    }
267    Ok(list)
268}
269
270/// Encode DER bytes as a PEM block.
271///
272/// Returns :class:`bytes` containing a ``-----BEGIN {label}-----`` /
273/// ``-----END {label}-----`` block with standard 64-character base64 lines.
274/// This is the low-level inverse of :func:`pem_to_der`.
275///
276/// For serialising parsed objects use the class-level
277/// ``Certificate.to_pem()``, ``CertificationRequest.to_pem()``, etc., which
278/// fill in the correct label automatically.
279///
280/// ```python
281/// with open("cert.der", "rb") as f:
282///     der = f.read()
283/// pem = synta.der_to_pem(der, "CERTIFICATE")
284/// ```
285#[pyfunction]
286fn der_to_pem<'py>(py: Python<'py>, der: &[u8], label: &str) -> Bound<'py, pyo3::types::PyBytes> {
287    pyo3::types::PyBytes::new(py, &synta_certificate::der_to_pem(label, der))
288}
289
290/// Decode PEM blocks to DER bytes.
291///
292/// Strips ``-----BEGIN ...-----`` / ``-----END ...-----`` boundary lines and
293/// decodes the base64 body of every PEM block found in the input.  Implemented
294/// in pure Rust — no external dependencies required.
295///
296/// Always returns :class:`list` [:class:`bytes`] — one entry per PEM block.
297/// Raises :exc:`ValueError` if no PEM block is found.
298///
299/// ```python,ignore
300/// # Single block — index into the list:
301/// der = synta.pem_to_der(open("cert.pem", "rb").read())[0]
302/// cert = synta.Certificate.from_der(der)
303///
304/// # Bundle / chain:
305/// ders = synta.pem_to_der(open("bundle.pem", "rb").read())
306/// certs = [synta.Certificate.from_der(d) for d in ders]
307/// ```
308#[pyfunction]
309fn pem_to_der<'py>(
310    py: Python<'py>,
311    data: &[u8],
312) -> PyResult<pyo3::Bound<'py, pyo3::types::PyList>> {
313    let blocks = synta_certificate::pem_blocks(data);
314    if blocks.is_empty() {
315        return Err(pyo3::exceptions::PyValueError::new_err(
316            "no PEM block found in input",
317        ));
318    }
319    let list = pyo3::types::PyList::empty(py);
320    for (_, block) in &blocks {
321        list.append(pyo3::types::PyBytes::new(py, block))?;
322    }
323    Ok(list)
324}
325
326/// Compute a hash digest of arbitrary bytes.
327///
328/// Returns a :class:`bytes` object containing the raw (binary) digest.
329/// ``algorithm`` must be one of ``"sha1"``, ``"sha224"``, ``"sha256"``,
330/// ``"sha384"``, ``"sha512"``, or ``"md5"``.  Raises :exc:`ValueError` for
331/// unknown algorithm names or crypto backend errors.
332///
333/// ```python,ignore
334/// import synta
335///
336/// # Hash a certificate DER blob:
337/// digest_bytes = synta.digest("sha256", cert_der)
338/// print(digest_bytes.hex())
339///
340/// # Hash an arbitrary byte string:
341/// digest_bytes = synta.digest("sha1", b"hello world")
342/// ```
343#[pyfunction]
344fn digest<'py>(
345    py: Python<'py>,
346    algorithm: &str,
347    data: &[u8],
348) -> PyResult<pyo3::Bound<'py, pyo3::types::PyBytes>> {
349    use synta_certificate::{default_data_hasher, DataHasher};
350    let d = default_data_hasher()
351        .hash_data(algorithm, data)
352        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
353    Ok(pyo3::types::PyBytes::new(py, &d))
354}
355
356/// Format a DER-encoded X.500 Name as an RFC 4514 distinguished name string.
357///
358/// ``name_der`` must be the complete TLV bytes of the Name SEQUENCE (tag +
359/// length + value), as returned by :attr:`Certificate.issuer_raw_der` or
360/// :attr:`Certificate.subject_raw_der`.
361///
362/// Returns a string like ``"CN=example.com, O=Example Inc, C=US"``.
363/// Returns an empty string if ``name_der`` cannot be parsed.
364///
365/// ```python,ignore
366/// dn = synta.format_dn(cert.subject_raw_der)
367/// print(dn)  # CN=example.com, O=Example Inc, C=US
368/// ```
369#[pyfunction]
370fn format_dn(name_der: &[u8]) -> String {
371    synta_certificate::name::format_dn(name_der)
372}
373
374/// Format a DER-encoded X.500 Name in OpenSSL slash-separated form.
375///
376/// ``name_der`` must be the complete TLV bytes of the Name SEQUENCE (tag +
377/// length + value), as returned by :attr:`Certificate.issuer_raw_der` or
378/// :attr:`Certificate.subject_raw_der`.
379///
380/// Returns a string like ``"/C=US/O=Example Inc/CN=example.com"``.
381/// Returns an empty string if ``name_der`` cannot be parsed.
382///
383/// ```python,ignore
384/// dn = synta.format_dn_slash(cert.subject_raw_der)
385/// print(dn)  # /C=US/O=Example Inc/CN=example.com
386/// ```
387#[pyfunction]
388fn format_dn_slash(name_der: &[u8]) -> String {
389    synta_certificate::name::format_dn_slash(name_der)
390}
391
392/// Find the value bytes of an X.509v3 extension by OID.
393///
394/// ``ext_seq_der`` must be the complete DER bytes of the ``Extensions``
395/// SEQUENCE (i.e. the bytes captured by the ``extensions`` field of a
396/// parsed ``Certificate`` after the ``[3] EXPLICIT`` wrapper is stripped).
397/// Use :meth:`Certificate.get_extension_value_der` for the more common
398/// case of looking up an extension value in a certificate directly.
399///
400/// ``oid`` is either a dotted-decimal OID string (e.g. ``"2.5.29.17"``)
401/// or an :class:`ObjectIdentifier` instance.
402///
403/// Returns the extension value bytes (the content of the ``extnValue``
404/// OCTET STRING, without the OCTET STRING TLV wrapper), or ``None`` if
405/// no matching extension is present.  Raises :exc:`ValueError` if ``oid``
406/// is not a valid OID.
407///
408/// ```python,ignore
409/// ext_der = cert.get_extension_value_der("2.5.29.17")
410/// ```
411#[pyfunction]
412fn find_extension_value<'py>(
413    py: Python<'py>,
414    ext_seq_der: &[u8],
415    oid: &Bound<'_, PyAny>,
416) -> PyResult<Py<PyAny>> {
417    use std::str::FromStr;
418    use synta::ObjectIdentifier;
419
420    let oid_val: ObjectIdentifier =
421        if let Ok(oid_ref) = oid.extract::<pyo3::PyRef<crate::types::PyObjectIdentifier>>() {
422            oid_ref.inner.clone()
423        } else if let Ok(s) = oid.extract::<String>() {
424            ObjectIdentifier::from_str(&s)
425                .map_err(|_| pyo3::exceptions::PyValueError::new_err(format!("invalid OID: {s}")))?
426        } else {
427            return Err(pyo3::exceptions::PyTypeError::new_err(
428                "oid must be a str or ObjectIdentifier",
429            ));
430        };
431
432    match synta_certificate::find_extension_value(ext_seq_der, oid_val.components()) {
433        Some(bytes) => Ok(pyo3::types::PyBytes::new(py, bytes).into_any().unbind()),
434        None => Ok(py.None()),
435    }
436}
437
438/// Encode a list of ``(tag_number, value_bytes)`` pairs as a DER ``SEQUENCE OF GeneralName``.
439///
440/// ``entries`` must be a list of ``(tag_number: int, value: bytes)`` tuples in
441/// the same format returned by :func:`parse_general_names`.  Tag numbers follow
442/// RFC 5280 (see :mod:`synta.general_name` for named constants).
443///
444/// Returns the DER-encoded ``SEQUENCE OF GeneralName`` bytes on success, or
445/// ``None`` if any entry cannot be encoded.  Raises :exc:`ValueError` if the
446/// input is structurally invalid (e.g. not a list of 2-tuples).
447///
448/// ```python,ignore
449/// import synta
450/// import synta.general_name as gn
451///
452/// san_der = synta.encode_general_names([
453///     (gn.DNS_NAME, b"example.com"),
454///     (gn.IP_ADDRESS, b"\\xc0\\xa8\\x00\\x01"),  # 192.168.0.1
455/// ])
456/// ```
457#[pyfunction]
458fn encode_general_names<'py>(
459    py: Python<'py>,
460    entries: &Bound<'_, pyo3::types::PyList>,
461) -> PyResult<Py<PyAny>> {
462    let mut rust_entries: Vec<(u32, Vec<u8>)> = Vec::with_capacity(entries.len());
463    for item in entries.iter() {
464        let tuple = item.cast::<pyo3::types::PyTuple>().map_err(|_| {
465            pyo3::exceptions::PyValueError::new_err("each entry must be a (int, bytes) tuple")
466        })?;
467        if tuple.len() != 2 {
468            return Err(pyo3::exceptions::PyValueError::new_err(
469                "each entry must be a 2-tuple (tag_number, bytes)",
470            ));
471        }
472        let tag_num: u32 = tuple
473            .get_item(0)?
474            .extract()
475            .map_err(|_| pyo3::exceptions::PyValueError::new_err("tag_number must be an int"))?;
476        let value: Vec<u8> = tuple
477            .get_item(1)?
478            .extract()
479            .map_err(|_| pyo3::exceptions::PyValueError::new_err("value must be bytes"))?;
480        rust_entries.push((tag_num, value));
481    }
482
483    let refs: Vec<(u32, &[u8])> = rust_entries
484        .iter()
485        .map(|(t, v)| (*t, v.as_slice()))
486        .collect();
487
488    match synta_certificate::encode_general_names(&refs) {
489        Some(encoded) => Ok(pyo3::types::PyBytes::new(py, &encoded).into_any().unbind()),
490        None => Ok(py.None()),
491    }
492}
493
494/// Build the DER encoding of an ``AlgorithmIdentifier`` for signing.
495///
496/// ``key_oid`` is the public key algorithm OID — either a dotted-decimal
497/// string (e.g. ``"1.2.840.113549.1.1.1"`` for RSA) or an
498/// :class:`ObjectIdentifier` instance.
499///
500/// ``hash_algo`` is the hash algorithm name, e.g. ``"sha256"``, ``"sha384"``,
501/// or ``"sha512"``.
502///
503/// Returns the DER bytes of the ``AlgorithmIdentifier`` SEQUENCE, or ``None``
504/// if the key OID is not recognised or the hash algorithm is not valid for
505/// the key type.  Raises :exc:`ValueError` if ``key_oid`` is not a valid OID.
506///
507/// ```python,ignore
508/// alg_der = synta.signing_algorithm_der("1.2.840.113549.1.1.1", "sha256")
509/// # → DER for sha256WithRSAEncryption AlgorithmIdentifier
510/// ```
511#[pyfunction]
512fn signing_algorithm_der<'py>(
513    py: Python<'py>,
514    key_oid: &Bound<'_, PyAny>,
515    hash_algo: &str,
516) -> PyResult<Py<PyAny>> {
517    use std::str::FromStr;
518    use synta::ObjectIdentifier;
519
520    let oid_val: ObjectIdentifier =
521        if let Ok(oid_ref) = key_oid.extract::<pyo3::PyRef<crate::types::PyObjectIdentifier>>() {
522            oid_ref.inner.clone()
523        } else if let Ok(s) = key_oid.extract::<String>() {
524            ObjectIdentifier::from_str(&s)
525                .map_err(|_| pyo3::exceptions::PyValueError::new_err(format!("invalid OID: {s}")))?
526        } else {
527            return Err(pyo3::exceptions::PyTypeError::new_err(
528                "key_oid must be a str or ObjectIdentifier",
529            ));
530        };
531
532    match synta_certificate::signing_algorithm_der(&oid_val, hash_algo) {
533        Some(der) => Ok(pyo3::types::PyBytes::new(py, &der).into_any().unbind()),
534        None => Ok(py.None()),
535    }
536}
537
538/// Test whether a bit position is set in a KeyUsage BIT STRING value.
539///
540/// ``ku_value_bytes`` must be the raw value bytes of the KeyUsage BIT STRING
541/// (i.e. the bytes inside the OCTET STRING wrapper of the extension value,
542/// after decoding the BIT STRING tag and length — the first byte is the
543/// unused-bits count, followed by the named-bit flags).
544///
545/// ``bit_n`` is the named-bit index as defined in RFC 5280 §4.2.1.3.
546/// Named-bit constants are available in :mod:`synta.cert` (e.g.
547/// ``synta.cert.KEY_USAGE_DIGITAL_SIGNATURE == 0``).
548///
549/// Returns ``True`` if bit ``bit_n`` is set, ``False`` otherwise.
550///
551/// ```python,ignore
552/// ku_der = cert.get_extension_value_der("2.5.29.15")
553/// # bit 5 = keyCertSign
554/// is_ca = synta.key_usage_bit(ku_der, 5)
555/// ```
556#[pyfunction]
557fn key_usage_bit(ku_value_bytes: &[u8], bit_n: usize) -> PyResult<bool> {
558    let mut dec = synta::Decoder::new(ku_value_bytes, synta::Encoding::Der);
559    let ku: synta_certificate::KeyUsage = dec.decode().map_err(|e| {
560        pyo3::exceptions::PyValueError::new_err(format!("invalid KeyUsage DER: {e}"))
561    })?;
562    Ok(synta_certificate::key_usage_bit(&ku, bit_n))
563}
564
565/// Decode a DER-encoded ``SubjectPublicKeyInfo`` into a dictionary.
566///
567/// ``spki_der`` must be the complete DER bytes of the
568/// ``SubjectPublicKeyInfo`` SEQUENCE TLV, as returned by
569/// :attr:`Certificate.subject_public_key_info_der` or
570/// :meth:`PublicKey.to_der`.
571///
572/// Returns a :class:`dict` with at minimum these keys:
573///
574/// * ``"algorithm_oid"`` (:class:`str`) — dotted OID of the public-key algorithm
575/// * ``"key_bytes"`` (:class:`bytes`) — raw key bytes from the BIT STRING
576///
577/// For RSA keys the dict additionally contains:
578///
579/// * ``"modulus"`` (:class:`bytes`) — raw modulus bytes (may include 0x00 sign byte)
580/// * ``"exponent"`` (:class:`int`) — public exponent (typically 65537)
581/// * ``"bit_count"`` (:class:`int`) — key size in bits
582///
583/// For EC keys the dict additionally contains:
584///
585/// * ``"bit_count"`` (:class:`int`) — key size in bits
586/// * ``"curve_oid"`` (:class:`str`) — dotted OID of the named curve
587/// * ``"curve_short_name"`` (:class:`str` or ``None``) — short name, e.g. ``"prime256v1"``
588/// * ``"curve_nist_name"`` (:class:`str` or ``None``) — NIST name, e.g. ``"P-256"``
589///
590/// Raises :exc:`ValueError` if ``spki_der`` cannot be parsed.
591///
592/// ```python,ignore
593/// spki_der = cert.subject_public_key_info_der
594/// info = synta.decode_public_key_info(spki_der)
595/// print(info["algorithm_oid"])   # e.g. "1.2.840.10045.2.1" for EC
596/// print(info.get("curve_nist_name"))  # "P-256"
597/// ```
598#[pyfunction]
599fn decode_public_key_info<'py>(
600    py: Python<'py>,
601    spki_der: &[u8],
602) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
603    use pyo3::types::{PyBytes, PyDict};
604    use synta::{Decoder, Encoding};
605    use synta_certificate::SubjectPublicKeyInfo;
606
607    let mut dec = Decoder::new(spki_der, Encoding::Der);
608    let spki: SubjectPublicKeyInfo<'_> = dec
609        .decode()
610        .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("invalid SPKI DER: {e}")))?;
611
612    let alg_oid = spki
613        .algorithm
614        .algorithm
615        .components()
616        .iter()
617        .map(|n| n.to_string())
618        .collect::<Vec<_>>()
619        .join(".");
620    let key_bytes = spki.subject_public_key.as_bytes();
621    let key_bit_len = spki.subject_public_key.bit_len();
622
623    let info = synta_certificate::decode_public_key_info(
624        &spki.algorithm.algorithm,
625        spki.algorithm.parameters.as_ref(),
626        key_bytes,
627        key_bit_len,
628    );
629
630    let dict = PyDict::new(py);
631    dict.set_item("algorithm_oid", &alg_oid)?;
632
633    match info {
634        synta_certificate::PublicKeyInfo::Rsa {
635            modulus,
636            exponent,
637            bit_count,
638        } => {
639            dict.set_item("key_bytes", PyBytes::new(py, &modulus))?;
640            dict.set_item("modulus", PyBytes::new(py, &modulus))?;
641            dict.set_item("exponent", exponent)?;
642            dict.set_item("bit_count", bit_count)?;
643        }
644        synta_certificate::PublicKeyInfo::Ec {
645            key_bytes,
646            bit_count,
647            curve_short_name,
648            curve_nist_name,
649            curve_oid_str,
650        } => {
651            dict.set_item("key_bytes", PyBytes::new(py, &key_bytes))?;
652            dict.set_item("bit_count", bit_count)?;
653            dict.set_item("curve_oid", &curve_oid_str)?;
654            match curve_short_name {
655                Some(name) => dict.set_item("curve_short_name", name)?,
656                None => dict.set_item("curve_short_name", py.None())?,
657            }
658            match curve_nist_name {
659                Some(name) => dict.set_item("curve_nist_name", name)?,
660                None => dict.set_item("curve_nist_name", py.None())?,
661            }
662        }
663        synta_certificate::PublicKeyInfo::Unknown {
664            key_bytes,
665            bit_count,
666            ..
667        } => {
668            dict.set_item("key_bytes", PyBytes::new(py, &key_bytes))?;
669            dict.set_item("bit_count", bit_count)?;
670        }
671    }
672
673    Ok(dict)
674}