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