_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}