Skip to main content

_synta/
crypto_keys.rs

1//! Python bindings for generic public and private key operations.
2//!
3//! Exposes [`PyPublicKey`] and [`PyPrivateKey`] as pyo3 classes supporting
4//! RSA, EC, EdDSA, and DSA keys via the `synta-certificate` backend traits.
5//! No direct `openssl::*` imports are used here.
6
7use pyo3::exceptions::PyValueError;
8use pyo3::prelude::*;
9use pyo3::types::PyBytes;
10use synta_certificate::{BackendPrivateKey, BackendPublicKey, PrivateKey};
11
12// ── PublicKey ─────────────────────────────────────────────────────────────────
13
14/// An asymmetric public key.
15///
16/// Supports RSA, EC (P-256, P-384, P-521), Ed25519, Ed448, and DSA keys.
17/// Load from PEM or SubjectPublicKeyInfo DER; serialize back to PEM or DER.
18/// RSA keys can encrypt data with OAEP or PKCS\#1 v1.5 padding.
19///
20/// ```python,ignore
21/// import synta
22///
23/// # Load an RSA public key from a PEM file:
24/// with open("rsa_pub.pem", "rb") as f:
25///     pub = synta.PublicKey.from_pem(f.read())
26/// print(pub.key_type)   # "rsa"
27/// print(pub.key_size)   # e.g. 2048
28///
29/// # Encrypt with OAEP (SHA-256):
30/// ct = pub.rsa_oaep_encrypt(b"secret", "sha256")
31///
32/// # Load an EC public key from SPKI DER:
33/// ec_pub = synta.PublicKey.from_der(spki_der)
34/// print(ec_pub.curve_name)  # "P-256"
35/// ```
36#[pyclass(frozen, name = "PublicKey")]
37pub struct PyPublicKey {
38    pub(crate) inner: BackendPublicKey,
39}
40
41#[pymethods]
42impl PyPublicKey {
43    /// Load a public key from PEM-encoded SubjectPublicKeyInfo data.
44    ///
45    /// Supports RSA, EC (P-256, P-384, P-521), Ed25519, Ed448, and DSA keys.
46    ///
47    /// ```python,ignore
48    /// with open("pubkey.pem", "rb") as f:
49    ///     pub = synta.PublicKey.from_pem(f.read())
50    /// ```
51    #[staticmethod]
52    fn from_pem(data: &[u8]) -> PyResult<Self> {
53        let inner =
54            BackendPublicKey::from_pem(data).map_err(|e| PyValueError::new_err(format!("{e}")))?;
55        Ok(Self { inner })
56    }
57
58    /// Construct an RSA public key from raw big-endian modulus *n* and public-exponent *e* bytes.
59    ///
60    /// This is the inverse of the :attr:`modulus` and :attr:`public_exponent` getters.
61    /// Raises :exc:`ValueError` if the inputs do not encode a valid RSA key.
62    ///
63    /// ```python,ignore
64    /// # n and e are big-endian bytes (e.g. extracted from a PKCS#11 token)
65    /// pub = synta.PublicKey.from_rsa_components(n, e)
66    /// assert pub.key_type == "rsa"
67    /// ```
68    #[staticmethod]
69    fn from_rsa_components(n: &[u8], e: &[u8]) -> PyResult<Self> {
70        let inner = BackendPublicKey::from_rsa_components(n, e)
71            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
72        Ok(Self { inner })
73    }
74
75    /// Construct an EC public key from affine coordinates *x* and *y* (big-endian
76    /// bytes) and a NIST curve name (``"P-256"``, ``"P-384"``, or ``"P-521"``).
77    ///
78    /// This is the inverse of the :attr:`x`, :attr:`y`, and :attr:`curve_name` getters.
79    /// Raises :exc:`ValueError` for unknown curve names or invalid coordinates.
80    ///
81    /// ```python,ignore
82    /// pub = synta.PublicKey.from_ec_components(x_bytes, y_bytes, "P-256")
83    /// assert pub.key_type == "ec"
84    /// ```
85    #[staticmethod]
86    fn from_ec_components(x: &[u8], y: &[u8], curve: &str) -> PyResult<Self> {
87        let inner = BackendPublicKey::from_ec_components(x, y, curve)
88            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
89        Ok(Self { inner })
90    }
91
92    /// Load a public key from a DER-encoded SubjectPublicKeyInfo structure.
93    ///
94    /// ```python,ignore
95    /// with open("pubkey.der", "rb") as f:
96    ///     pub = synta.PublicKey.from_der(f.read())
97    /// ```
98    #[staticmethod]
99    fn from_der(data: &[u8]) -> PyResult<Self> {
100        let inner =
101            BackendPublicKey::from_der(data).map_err(|e| PyValueError::new_err(format!("{e}")))?;
102        Ok(Self { inner })
103    }
104
105    /// Serialize this public key to PEM-encoded SubjectPublicKeyInfo.
106    ///
107    /// ```python,ignore
108    /// pem = pub.to_pem()
109    /// open("pubkey.pem", "wb").write(pem)
110    /// ```
111    fn to_pem<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
112        let pem = self
113            .inner
114            .to_pem()
115            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
116        Ok(PyBytes::new(py, &pem))
117    }
118
119    /// Serialize this public key to DER-encoded SubjectPublicKeyInfo.
120    ///
121    /// ```python,ignore
122    /// der = pub.to_der()
123    /// open("pubkey.der", "wb").write(der)
124    /// ```
125    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
126        let der = self
127            .inner
128            .to_der()
129            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
130        Ok(PyBytes::new(py, &der))
131    }
132
133    /// The key algorithm as a lowercase string.
134    ///
135    /// Returns one of ``"rsa"``, ``"ec"``, ``"ed25519"``, ``"ed448"``,
136    /// ``"dsa"``, or ``"unknown"``.
137    #[getter]
138    fn key_type(&self) -> &'static str {
139        self.inner.key_type()
140    }
141
142    /// The key size in bits, or ``None`` for EdDSA keys.
143    ///
144    /// For RSA this is the modulus bit-length; for EC this is the field
145    /// bit-length.  Returns ``None`` for Ed25519 and Ed448.
146    #[getter]
147    fn key_size(&self) -> Option<i64> {
148        self.inner.key_bit_size()
149    }
150
151    /// The RSA modulus ``n`` as big-endian bytes, or ``None`` for non-RSA keys.
152    #[getter]
153    fn modulus<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
154        match self
155            .inner
156            .rsa_modulus()
157            .map_err(|e| PyValueError::new_err(format!("{e}")))?
158        {
159            Some(n) => Ok(Some(PyBytes::new(py, &n))),
160            None => Ok(None),
161        }
162    }
163
164    /// The RSA public exponent ``e`` as big-endian bytes, or ``None`` for
165    /// non-RSA keys.
166    ///
167    /// The most common value is ``b'\x01\x00\x01'`` (65537).
168    #[getter]
169    fn public_exponent<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
170        match self
171            .inner
172            .rsa_public_exponent()
173            .map_err(|e| PyValueError::new_err(format!("{e}")))?
174        {
175            Some(e) => Ok(Some(PyBytes::new(py, &e))),
176            None => Ok(None),
177        }
178    }
179
180    /// The NIST curve name for EC keys, or ``None`` for non-EC keys.
181    ///
182    /// Returns ``"P-256"``, ``"P-384"``, ``"P-521"``, or ``"unknown"`` for
183    /// EC keys on other curves.
184    #[getter]
185    fn curve_name(&self) -> PyResult<Option<&'static str>> {
186        self.inner
187            .ec_curve_name()
188            .map_err(|e| PyValueError::new_err(format!("{e}")))
189    }
190
191    /// The affine X coordinate of the EC public key as big-endian bytes, or
192    /// ``None`` for non-EC keys.
193    #[getter]
194    fn x<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
195        match self
196            .inner
197            .ec_affine_coordinates()
198            .map_err(|e| PyValueError::new_err(format!("{e}")))?
199        {
200            Some((xv, _)) => Ok(Some(PyBytes::new(py, &xv))),
201            None => Ok(None),
202        }
203    }
204
205    /// The affine Y coordinate of the EC public key as big-endian bytes, or
206    /// ``None`` for non-EC keys.
207    #[getter]
208    fn y<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
209        match self
210            .inner
211            .ec_affine_coordinates()
212            .map_err(|e| PyValueError::new_err(format!("{e}")))?
213        {
214            Some((_, yv)) => Ok(Some(PyBytes::new(py, &yv))),
215            None => Ok(None),
216        }
217    }
218
219    /// Encrypt ``plaintext`` with RSA-OAEP using the specified hash algorithm.
220    ///
221    /// ``hash_algorithm`` must be one of ``"sha1"``, ``"sha224"``,
222    /// ``"sha256"``, ``"sha384"``, or ``"sha512"``.
223    ///
224    /// Raises :exc:`ValueError` if this key is not an RSA key.
225    ///
226    /// ```python,ignore
227    /// ct = pub.rsa_oaep_encrypt(b"secret data", "sha256")
228    /// ```
229    #[pyo3(signature = (plaintext, hash_algorithm = "sha256"))]
230    fn rsa_oaep_encrypt<'py>(
231        &self,
232        py: Python<'py>,
233        plaintext: &[u8],
234        hash_algorithm: &str,
235    ) -> PyResult<Bound<'py, PyBytes>> {
236        let ct = self
237            .inner
238            .rsa_oaep_encrypt(plaintext, hash_algorithm)
239            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
240        Ok(PyBytes::new(py, &ct))
241    }
242
243    /// Encrypt ``plaintext`` with RSA PKCS\#1 v1.5 padding.
244    ///
245    /// Raises :exc:`ValueError` if this key is not an RSA key.
246    ///
247    /// ```python,ignore
248    /// ct = pub.rsa_pkcs1v15_encrypt(b"secret data")
249    /// ```
250    fn rsa_pkcs1v15_encrypt<'py>(
251        &self,
252        py: Python<'py>,
253        plaintext: &[u8],
254    ) -> PyResult<Bound<'py, PyBytes>> {
255        let ct = self
256            .inner
257            .rsa_pkcs1v15_encrypt(plaintext)
258            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
259        Ok(PyBytes::new(py, &ct))
260    }
261
262    /// Verify a signature over ``data``.
263    ///
264    /// ``algorithm`` is the hash algorithm used during signing.  It must be one
265    /// of ``"sha1"``, ``"sha224"``, ``"sha256"``, ``"sha384"``, or
266    /// ``"sha512"`` for RSA (PKCS\#1 v1.5) and ECDSA keys.  For Ed25519,
267    /// Ed448, and ML-DSA keys pass ``None`` (or omit the argument) — no
268    /// pre-hash is used.
269    ///
270    /// ``context`` is the ML-DSA context string (FIPS 204 domain separator).
271    /// It defaults to ``b""`` (empty context, equivalent to omitting the
272    /// context).  Ignored for non-ML-DSA keys.
273    ///
274    /// Raises :exc:`ValueError` if the signature is invalid or the algorithm
275    /// combination is unsupported.
276    ///
277    /// ```python,ignore
278    /// pub.verify_signature(sig, data, "sha256")              # RSA or ECDSA
279    /// ed_pub.verify_signature(sig, data)                     # Ed25519 / Ed448
280    /// ml_dsa_pub.verify_signature(sig, data)                 # ML-DSA (empty context)
281    /// ml_dsa_pub.verify_signature(sig, data, context=b"app") # ML-DSA with context
282    /// ```
283    #[pyo3(signature = (signature, data, algorithm = None, context = None))]
284    fn verify_signature(
285        &self,
286        signature: &[u8],
287        data: &[u8],
288        algorithm: Option<&str>,
289        context: Option<&[u8]>,
290    ) -> PyResult<()> {
291        let kt = self.inner.key_type();
292        if matches!(kt, "ml-dsa-44" | "ml-dsa-65" | "ml-dsa-87") {
293            self.inner
294                .verify_ml_dsa_with_context(data, signature, context.unwrap_or(b""))
295                .map_err(|e| PyValueError::new_err(format!("{e}")))?;
296            return Ok(());
297        }
298        self.inner
299            .verify_message(data, signature, algorithm)
300            .map_err(|e| PyValueError::new_err(format!("{e}")))
301    }
302
303    /// Verify an X.509 certificate signature given raw DER-encoded components.
304    ///
305    /// This is the low-level counterpart to :meth:`verify_signature`.  Instead
306    /// of an algorithm name string, it accepts a DER-encoded
307    /// ``AlgorithmIdentifier`` as found in an X.509 certificate or CRL
308    /// ``signatureAlgorithm`` field.  The active crypto backend (NSS when the
309    /// ``nss`` feature is compiled in, otherwise OpenSSL) is used for
310    /// verification.
311    ///
312    /// :param tbs_der: DER bytes of the ``TBSCertificate`` (or ``TBSCertList``,
313    ///     ``BasicOCSPResponse``, etc.) — the bytes that were signed.
314    /// :param sig_alg_der: DER bytes of the ``AlgorithmIdentifier`` SEQUENCE
315    ///     from the outer certificate structure.
316    /// :param signature: Raw signature bytes (the BIT STRING value, i.e. the
317    ///     payload without the tag/length/unused-bits byte).
318    /// :raises ValueError: if the signature is invalid or the algorithm is
319    ///     unsupported by the active backend.
320    ///
321    /// ```python,ignore
322    /// # Verify the signature on a parsed certificate using its own fields:
323    /// pub.verify_certificate_signature(tbs_der, sig_alg_der, sig_bytes)
324    /// ```
325    fn verify_certificate_signature(
326        &self,
327        tbs_der: &[u8],
328        sig_alg_der: &[u8],
329        signature: &[u8],
330    ) -> PyResult<()> {
331        self.inner
332            .verify_signature(tbs_der, sig_alg_der, signature)
333            .map_err(|e| PyValueError::new_err(format!("{e}")))
334    }
335
336    /// ML-KEM encapsulation: generate a shared secret and a ciphertext.
337    ///
338    /// Returns a ``(ciphertext, shared_secret)`` tuple.  The holder of the
339    /// corresponding private key can call :meth:`PrivateKey.kem_decapsulate`
340    /// with ``ciphertext`` to recover ``shared_secret``.
341    ///
342    /// :raises ValueError: if this key is not an ML-KEM public key.
343    ///
344    /// ```python,ignore
345    /// ct, ss = pub.kem_encapsulate()
346    /// ss2    = priv.kem_decapsulate(ct)
347    /// assert ss == ss2
348    /// ```
349    fn kem_encapsulate<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, pyo3::types::PyTuple>> {
350        use pyo3::types::PyTuple;
351        let (ct, ss) = self
352            .inner
353            .ml_kem_encapsulate()
354            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
355        let items = [PyBytes::new(py, &ct), PyBytes::new(py, &ss)];
356        PyTuple::new(py, items)
357    }
358
359    /// Composite ML-KEM encapsulation (draft-ietf-lamps-pq-composite-kem-18):
360    /// generate a shared secret and a ciphertext.
361    ///
362    /// Returns a ``(ciphertext, shared_secret)`` tuple.  The holder of the
363    /// corresponding private key can call
364    /// :meth:`PrivateKey.composite_kem_decapsulate` with ``ciphertext`` to
365    /// recover ``shared_secret``.
366    ///
367    /// :raises ValueError: if this key is not a recognized composite ML-KEM
368    ///     public key.
369    ///
370    /// ```python,ignore
371    /// ct, ss = pub.composite_kem_encapsulate()
372    /// ss2    = priv.composite_kem_decapsulate(ct)
373    /// assert ss == ss2
374    /// ```
375    fn composite_kem_encapsulate<'py>(
376        &self,
377        py: Python<'py>,
378    ) -> PyResult<Bound<'py, pyo3::types::PyTuple>> {
379        use pyo3::types::PyTuple;
380        let (ct, ss) = self
381            .inner
382            .composite_kem_encapsulate()
383            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
384        let items = [PyBytes::new(py, &ct), PyBytes::new(py, &ss)];
385        PyTuple::new(py, items)
386    }
387
388    fn __repr__(&self) -> String {
389        let kt = self.inner.key_type();
390        let bits = match kt {
391            "ed25519" | "ed448" | "ml-dsa-44" | "ml-dsa-65" | "ml-dsa-87" | "ml-kem-512"
392            | "ml-kem-768" | "ml-kem-1024" => String::new(),
393            _ => self
394                .inner
395                .key_bit_size()
396                .map(|b| format!(", key_size={b}"))
397                .unwrap_or_default(),
398        };
399        format!("PublicKey(key_type={kt:?}{bits})")
400    }
401}
402
403// ── PrivateKey ────────────────────────────────────────────────────────────────
404
405/// An asymmetric private key.
406///
407/// Supports RSA, EC (P-256, P-384, P-521), Ed25519, Ed448, and DSA keys.
408/// Load from PEM (optionally password-protected) or unencrypted PKCS\#8 DER;
409/// serialize back to PEM (optionally encrypted with AES-256-CBC) or
410/// unencrypted PKCS\#8 DER.  RSA keys can decrypt ciphertext with OAEP or
411/// PKCS\#1 v1.5 padding.
412///
413/// ```python,ignore
414/// import synta
415///
416/// # Load an encrypted RSA private key from PEM:
417/// with open("rsa_key.pem", "rb") as f:
418///     priv = synta.PrivateKey.from_pem(f.read(), password=b"secret")
419///
420/// # Extract the public key:
421/// pub = priv.public_key
422///
423/// # Decrypt RSA-OAEP ciphertext:
424/// plaintext = priv.rsa_oaep_decrypt(ciphertext, "sha256")
425/// ```
426#[pyclass(frozen, name = "PrivateKey")]
427pub struct PyPrivateKey {
428    pub(crate) inner: BackendPrivateKey,
429}
430
431#[pymethods]
432impl PyPrivateKey {
433    /// Load a private key from PEM-encoded data.
434    ///
435    /// Supports RSA, EC, Ed25519, Ed448, and DSA keys in both PKCS\#8
436    /// (``-----BEGIN PRIVATE KEY-----``) and traditional
437    /// (``-----BEGIN RSA PRIVATE KEY-----`` etc.) PEM formats.
438    ///
439    /// If the PEM block is password-protected, pass the passphrase as
440    /// ``password``.
441    ///
442    /// ```python,ignore
443    /// # Unencrypted key:
444    /// priv = synta.PrivateKey.from_pem(open("key.pem", "rb").read())
445    ///
446    /// # Encrypted key:
447    /// priv = synta.PrivateKey.from_pem(open("key.pem", "rb").read(), password=b"pass")
448    /// ```
449    #[staticmethod]
450    #[pyo3(signature = (data, password = None))]
451    fn from_pem(data: &[u8], password: Option<&[u8]>) -> PyResult<Self> {
452        let inner = BackendPrivateKey::from_pem(data, password)
453            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
454        Ok(Self { inner })
455    }
456
457    /// Load an unencrypted private key from PKCS\#8 DER bytes.
458    ///
459    /// ```python,ignore
460    /// with open("key.der", "rb") as f:
461    ///     priv = synta.PrivateKey.from_der(f.read())
462    /// ```
463    #[staticmethod]
464    fn from_der(data: &[u8]) -> PyResult<Self> {
465        let inner =
466            BackendPrivateKey::from_der(data).map_err(|e| PyValueError::new_err(format!("{e}")))?;
467        Ok(Self { inner })
468    }
469
470    /// Serialize this private key to PEM-encoded PKCS\#8.
471    ///
472    /// If ``password`` is provided the output is encrypted with AES-256-CBC.
473    ///
474    /// ```python,ignore
475    /// # Unencrypted:
476    /// pem = priv.to_pem()
477    ///
478    /// # Encrypted:
479    /// pem = priv.to_pem(password=b"my-passphrase")
480    /// ```
481    #[pyo3(signature = (password = None))]
482    fn to_pem<'py>(
483        &self,
484        py: Python<'py>,
485        password: Option<&[u8]>,
486    ) -> PyResult<Bound<'py, PyBytes>> {
487        let pem = self
488            .inner
489            .to_pem(password)
490            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
491        Ok(PyBytes::new(py, &pem))
492    }
493
494    /// Serialize this private key to unencrypted PKCS\#8 DER.
495    ///
496    /// ```python,ignore
497    /// der = priv.to_der()
498    /// open("key.der", "wb").write(der)
499    /// ```
500    fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
501        let der = self
502            .inner
503            .to_der()
504            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
505        Ok(PyBytes::new(py, &der))
506    }
507
508    /// Serialize this private key to encrypted PKCS\#8 DER
509    /// (``EncryptedPrivateKeyInfo``, RFC 5958 §3).
510    ///
511    /// ```python,ignore
512    /// der = priv.to_pkcs8_encrypted(b"my-passphrase")
513    /// open("key.p8e", "wb").write(der)
514    ///
515    /// # Round-trip:
516    /// priv2 = synta.PrivateKey.from_pkcs8_encrypted(der, b"my-passphrase")
517    /// assert priv2.to_der() == priv.to_der()
518    /// ```
519    fn to_pkcs8_encrypted<'py>(
520        &self,
521        py: Python<'py>,
522        password: &[u8],
523    ) -> PyResult<Bound<'py, PyBytes>> {
524        let der = self
525            .inner
526            .to_pkcs8_encrypted(password)
527            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
528        Ok(PyBytes::new(py, &der))
529    }
530
531    /// Load a private key from an encrypted PKCS\#8 DER blob
532    /// (``EncryptedPrivateKeyInfo``).
533    ///
534    /// ```python,ignore
535    /// der = open("key.p8e", "rb").read()
536    /// priv = synta.PrivateKey.from_pkcs8_encrypted(der, b"my-passphrase")
537    /// ```
538    #[staticmethod]
539    fn from_pkcs8_encrypted(data: &[u8], password: &[u8]) -> PyResult<Self> {
540        let inner = BackendPrivateKey::from_pkcs8_encrypted(data, password)
541            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
542        Ok(Self { inner })
543    }
544
545    /// Load a private key from a PKCS#11 URI (RFC 7512).
546    ///
547    /// Requires the OpenSSL PKCS#11 provider or NSS to be configured.
548    /// The URI has the form ``pkcs11:token=MyToken;id=%01%02%03;pin-value=1234``.
549    ///
550    /// Raises :exc:`ValueError` if the URI cannot be parsed or the key cannot be loaded.
551    #[staticmethod]
552    #[cfg(any(feature = "openssl", feature = "nss"))]
553    fn from_pkcs11_uri(uri: &str) -> PyResult<Self> {
554        let inner = BackendPrivateKey::from_pkcs11_uri(uri)
555            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
556        Ok(Self { inner })
557    }
558
559    /// The key algorithm as a lowercase string.
560    ///
561    /// Returns one of ``"rsa"``, ``"ec"``, ``"ed25519"``, ``"ed448"``,
562    /// ``"dsa"``, or ``"unknown"``.
563    #[getter]
564    fn key_type(&self) -> &'static str {
565        self.inner.key_type()
566    }
567
568    /// The key size in bits, or ``None`` for EdDSA keys.
569    #[getter]
570    fn key_size(&self) -> Option<i64> {
571        self.inner.key_bit_size()
572    }
573
574    /// The public key corresponding to this private key.
575    ///
576    /// ```python,ignore
577    /// pub = priv.public_key
578    /// ct = pub.rsa_oaep_encrypt(b"data", "sha256")
579    /// ```
580    #[getter]
581    fn public_key(&self) -> PyResult<PyPublicKey> {
582        let bpk = self
583            .inner
584            .public_key()
585            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
586        Ok(PyPublicKey { inner: bpk })
587    }
588
589    /// Decrypt ``ciphertext`` with RSA-OAEP using the specified hash algorithm.
590    ///
591    /// ``hash_algorithm`` must be one of ``"sha1"``, ``"sha224"``,
592    /// ``"sha256"``, ``"sha384"``, or ``"sha512"``.
593    ///
594    /// Raises :exc:`ValueError` if this key is not an RSA key.
595    ///
596    /// ```python,ignore
597    /// plaintext = priv.rsa_oaep_decrypt(ciphertext, "sha256")
598    /// ```
599    #[pyo3(signature = (ciphertext, hash_algorithm = "sha256"))]
600    fn rsa_oaep_decrypt<'py>(
601        &self,
602        py: Python<'py>,
603        ciphertext: &[u8],
604        hash_algorithm: &str,
605    ) -> PyResult<Bound<'py, PyBytes>> {
606        let pt = self
607            .inner
608            .rsa_oaep_decrypt(ciphertext, hash_algorithm)
609            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
610        Ok(PyBytes::new(py, &pt))
611    }
612
613    /// Decrypt ``ciphertext`` with RSA PKCS\#1 v1.5 padding.
614    ///
615    /// Raises :exc:`ValueError` if this key is not an RSA key.
616    ///
617    /// ```python,ignore
618    /// plaintext = priv.rsa_pkcs1v15_decrypt(ciphertext)
619    /// ```
620    fn rsa_pkcs1v15_decrypt<'py>(
621        &self,
622        py: Python<'py>,
623        ciphertext: &[u8],
624    ) -> PyResult<Bound<'py, PyBytes>> {
625        let pt = self
626            .inner
627            .rsa_pkcs1v15_decrypt(ciphertext)
628            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
629        Ok(PyBytes::new(py, &pt))
630    }
631
632    /// Generate a new RSA private key.
633    ///
634    /// ``key_size`` is the modulus bit-length (e.g. 2048, 3072, 4096).
635    /// ``public_exponent`` defaults to 65537.
636    ///
637    /// ```python,ignore
638    /// priv = synta.PrivateKey.generate_rsa(2048)
639    /// ```
640    #[staticmethod]
641    #[pyo3(signature = (key_size, public_exponent = 65537))]
642    fn generate_rsa(key_size: u32, public_exponent: u32) -> PyResult<Self> {
643        let inner = BackendPrivateKey::generate_rsa(key_size, public_exponent)
644            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
645        Ok(Self { inner })
646    }
647
648    /// Generate a new EC private key on the specified named curve.
649    ///
650    /// ``curve`` must be one of ``"P-256"``, ``"P-384"``, or ``"P-521"``.
651    /// Raises :exc:`ValueError` for unknown curve names.
652    ///
653    /// ```python,ignore
654    /// priv = synta.PrivateKey.generate_ec("P-256")
655    /// ```
656    #[staticmethod]
657    #[pyo3(signature = (curve = "P-256"))]
658    fn generate_ec(curve: &str) -> PyResult<Self> {
659        let inner = BackendPrivateKey::generate_ec(curve)
660            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
661        Ok(Self { inner })
662    }
663
664    /// Generate a new Ed25519 private key (RFC 8032).
665    ///
666    /// ```python,ignore
667    /// priv = synta.PrivateKey.generate_ed25519()
668    /// pub  = priv.public_key
669    /// ```
670    #[staticmethod]
671    fn generate_ed25519() -> PyResult<Self> {
672        let inner = BackendPrivateKey::generate_ed25519()
673            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
674        Ok(Self { inner })
675    }
676
677    /// Generate a new Ed448 private key (RFC 8032).
678    ///
679    /// ```python,ignore
680    /// priv = synta.PrivateKey.generate_ed448()
681    /// pub  = priv.public_key
682    /// ```
683    #[staticmethod]
684    fn generate_ed448() -> PyResult<Self> {
685        let inner = BackendPrivateKey::generate_ed448()
686            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
687        Ok(Self { inner })
688    }
689
690    /// Generate a new ML-DSA private key (FIPS 204).
691    ///
692    /// ``parameter_set`` must be one of ``"ML-DSA-44"``, ``"ML-DSA-65"``, or
693    /// ``"ML-DSA-87"``.  Requires OpenSSL 3.5 or newer.
694    ///
695    /// ```python,ignore
696    /// priv = synta.PrivateKey.generate_ml_dsa("ML-DSA-65")
697    /// pub  = priv.public_key
698    /// sig  = priv.sign(message)
699    /// ```
700    #[staticmethod]
701    fn generate_ml_dsa(parameter_set: &str) -> PyResult<Self> {
702        let inner = BackendPrivateKey::generate_ml_dsa(parameter_set)
703            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
704        Ok(Self { inner })
705    }
706
707    /// Generate a new composite ML-DSA private key (draft-ietf-lamps-pq-composite-sigs-19).
708    ///
709    /// ``sub_arc`` selects the composite variant by its OID sub-arc (37–54):
710    ///
711    /// | sub_arc | Algorithm |
712    /// |---------|-----------|
713    /// | 37 | MLDSA44-RSA2048-PSS-SHA256 |
714    /// | 38 | MLDSA44-RSA2048-PKCS15-SHA256 |
715    /// | 39 | MLDSA44-Ed25519-SHA512 |
716    /// | 40 | MLDSA44-ECDSA-P256-SHA256 |
717    /// | 41 | MLDSA65-RSA3072-PSS-SHA512 |
718    /// | 42 | MLDSA65-RSA3072-PKCS15-SHA512 |
719    /// | 43 | MLDSA65-RSA4096-PSS-SHA512 |
720    /// | 44 | MLDSA65-RSA4096-PKCS15-SHA512 |
721    /// | 45 | MLDSA65-ECDSA-P256-SHA512 |
722    /// | 46 | MLDSA65-ECDSA-P384-SHA512 |
723    /// | 47 | MLDSA65-ECDSA-brainpoolP256r1-SHA512 |
724    /// | 48 | MLDSA65-Ed25519-SHA512 |
725    /// | 49 | MLDSA87-ECDSA-P384-SHA512 |
726    /// | 50 | MLDSA87-ECDSA-brainpoolP384r1-SHA512 |
727    /// | 51 | MLDSA87-Ed448-SHAKE256 |
728    /// | 52 | MLDSA87-RSA3072-PSS-SHA512 |
729    /// | 53 | MLDSA87-RSA4096-PSS-SHA512 |
730    /// | 54 | MLDSA87-ECDSA-P521-SHA512 |
731    ///
732    /// Requires OpenSSL 3.3+ (with ML-DSA support) and the ``pqc`` Cargo
733    /// feature, or NSS.
734    ///
735    /// ```python,ignore
736    /// # Generate MLDSA65-ECDSA-P256-SHA512 (sub_arc=45)
737    /// priv = synta.PrivateKey.generate_composite_ml_dsa(45)
738    /// ```
739    #[staticmethod]
740    fn generate_composite_ml_dsa(sub_arc: u32) -> PyResult<Self> {
741        let inner = synta_certificate::BackendPrivateKey::generate_composite_ml_dsa(sub_arc)
742            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
743        Ok(Self { inner })
744    }
745
746    /// Generate a new composite ML-KEM private key (draft-ietf-lamps-pq-composite-kem-18).
747    ///
748    /// ``sub_arc`` selects the composite variant by its OID sub-arc (55–66):
749    ///
750    /// | sub_arc | Algorithm |
751    /// |---------|-----------|
752    /// | 55 | MLKEM768-RSA2048-SHA3-256 |
753    /// | 56 | MLKEM768-RSA3072-SHA3-256 |
754    /// | 57 | MLKEM768-RSA4096-SHA3-256 |
755    /// | 58 | MLKEM768-X25519-SHA3-256 |
756    /// | 59 | MLKEM768-ECDH-P256-SHA3-256 |
757    /// | 60 | MLKEM768-ECDH-P384-SHA3-256 |
758    /// | 61 | MLKEM768-ECDH-brainpoolP256r1-SHA3-256 |
759    /// | 62 | MLKEM1024-RSA3072-SHA3-256 |
760    /// | 63 | MLKEM1024-ECDH-P384-SHA3-256 |
761    /// | 64 | MLKEM1024-ECDH-brainpoolP384r1-SHA3-256 |
762    /// | 65 | MLKEM1024-X448-SHA3-256 |
763    /// | 66 | MLKEM1024-ECDH-P521-SHA3-256 |
764    ///
765    /// Requires OpenSSL 3.2+ (with ML-KEM support) and the ``pqc`` Cargo
766    /// feature.  Unlike composite ML-DSA, there is no NSS backend for
767    /// composite ML-KEM.
768    ///
769    /// ```python,ignore
770    /// # Generate MLKEM768-ECDH-P256-SHA3-256 (sub_arc=59)
771    /// priv = synta.PrivateKey.generate_composite_kem(59)
772    /// pub  = priv.public_key
773    /// ct, ss  = pub.composite_kem_encapsulate()
774    /// ss2     = priv.composite_kem_decapsulate(ct)
775    /// assert ss == ss2
776    /// ```
777    #[staticmethod]
778    fn generate_composite_kem(sub_arc: u32) -> PyResult<Self> {
779        let inner = synta_certificate::BackendPrivateKey::generate_composite_kem(sub_arc)
780            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
781        Ok(Self { inner })
782    }
783
784    /// Generate a new ML-KEM private key (FIPS 203).
785    ///
786    /// ``parameter_set`` must be one of ``"ML-KEM-512"``, ``"ML-KEM-768"``, or
787    /// ``"ML-KEM-1024"``.  Requires OpenSSL 3.5 or newer.
788    ///
789    /// ```python,ignore
790    /// priv = synta.PrivateKey.generate_ml_kem("ML-KEM-768")
791    /// pub  = priv.public_key
792    /// ct, ss = pub.kem_encapsulate()
793    /// ss2    = priv.kem_decapsulate(ct)
794    /// assert ss == ss2
795    /// ```
796    #[staticmethod]
797    fn generate_ml_kem(parameter_set: &str) -> PyResult<Self> {
798        let inner = BackendPrivateKey::generate_ml_kem(parameter_set)
799            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
800        Ok(Self { inner })
801    }
802
803    /// ML-KEM decapsulation: recover the shared secret from ``ciphertext``.
804    ///
805    /// The ``ciphertext`` must have been produced by the peer calling
806    /// :meth:`PublicKey.kem_encapsulate` on the corresponding public key.
807    ///
808    /// :raises ValueError: if this key is not an ML-KEM key or decapsulation fails.
809    ///
810    /// ```python,ignore
811    /// shared_secret = priv.kem_decapsulate(ciphertext)
812    /// ```
813    fn kem_decapsulate<'py>(
814        &self,
815        py: Python<'py>,
816        ciphertext: &[u8],
817    ) -> PyResult<Bound<'py, PyBytes>> {
818        let ss = self
819            .inner
820            .ml_kem_decapsulate(ciphertext)
821            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
822        Ok(PyBytes::new(py, &ss))
823    }
824
825    /// Composite ML-KEM decapsulation: recover the shared secret from ``ciphertext``.
826    ///
827    /// The ``ciphertext`` must have been produced by the peer calling
828    /// :meth:`PublicKey.composite_kem_encapsulate` on the corresponding
829    /// public key.
830    ///
831    /// :raises ValueError: if this key is not a composite ML-KEM key or
832    ///     decapsulation fails.
833    ///
834    /// ```python,ignore
835    /// shared_secret = priv.composite_kem_decapsulate(ciphertext)
836    /// ```
837    fn composite_kem_decapsulate<'py>(
838        &self,
839        py: Python<'py>,
840        ciphertext: &[u8],
841    ) -> PyResult<Bound<'py, PyBytes>> {
842        let ss = self
843            .inner
844            .composite_kem_decapsulate(ciphertext)
845            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
846        Ok(PyBytes::new(py, &ss))
847    }
848
849    /// Sign ``data`` with this private key and return the raw signature bytes.
850    ///
851    /// ``algorithm`` is the hash algorithm used during signing (e.g.
852    /// ``"sha256"`` for RSA PKCS\#1 v1.5 and ECDSA).  For Ed25519, Ed448,
853    /// and ML-DSA keys pass ``None`` (or omit the argument) — no pre-hash is
854    /// used.
855    ///
856    /// ``context`` is the ML-DSA context string (FIPS 204 domain separator).
857    /// It defaults to ``b""`` (empty context, equivalent to omitting the
858    /// context).  Ignored for non-ML-DSA keys.
859    ///
860    /// This method signs arbitrary bytes; it is the caller's responsibility to
861    /// hash the data if required by the algorithm (Ed25519 / Ed448 / ML-DSA
862    /// hash internally and must receive the original message, not a pre-hash).
863    ///
864    /// :raises ValueError: if the algorithm is unknown or signing fails.
865    ///
866    /// ```python,ignore
867    /// priv = synta.PrivateKey.generate_ec("P-256")
868    /// sig  = priv.sign(tbs_der, "sha256")
869    /// priv.public_key.verify_signature(sig, tbs_der, "sha256")
870    ///
871    /// ml_priv = synta.PrivateKey.generate_ml_dsa("ML-DSA-65")
872    /// sig = ml_priv.sign(message, context=b"my-app")
873    /// ml_priv.public_key.verify_signature(sig, message, context=b"my-app")
874    /// ```
875    #[pyo3(signature = (data, algorithm = None, context = None))]
876    fn sign<'py>(
877        &self,
878        py: Python<'py>,
879        data: &[u8],
880        algorithm: Option<&str>,
881        context: Option<&[u8]>,
882    ) -> PyResult<Bound<'py, PyBytes>> {
883        let kt = self.inner.key_type();
884        if matches!(kt, "ml-dsa-44" | "ml-dsa-65" | "ml-dsa-87") {
885            let sig = self
886                .inner
887                .sign_ml_dsa_with_context(data, context.unwrap_or(b""))
888                .map_err(|e| PyValueError::new_err(format!("{e}")))?;
889            return Ok(PyBytes::new(py, &sig));
890        }
891        let alg = algorithm.unwrap_or("sha256");
892        let signer = self.inner.as_signer(alg);
893        let sig = signer
894            .sign_tbs_erased(data)
895            .map_err(|e| PyValueError::new_err(format!("{e}")))?;
896        Ok(PyBytes::new(py, &sig))
897    }
898
899    fn __repr__(&self) -> String {
900        let kt = self.inner.key_type();
901        let bits = match kt {
902            "ed25519" | "ed448" | "ml-dsa-44" | "ml-dsa-65" | "ml-dsa-87" | "ml-kem-512"
903            | "ml-kem-768" | "ml-kem-1024" => String::new(),
904            _ => self
905                .inner
906                .key_bit_size()
907                .map(|b| format!(", key_size={b}"))
908                .unwrap_or_default(),
909        };
910        format!("PrivateKey(key_type={kt:?}{bits})")
911    }
912}
913
914// ── PrivateKey trait impl ─────────────────────────────────────────────────────
915
916/// Implement the backend-agnostic [`synta_certificate::PrivateKey`] trait for
917/// [`PyPrivateKey`] by delegating to [`synta_certificate::BackendPrivateKey`].
918///
919/// This allows Python binding code (e.g. `cert_builder.rs`) to call
920/// `key.as_signer(algorithm)` without importing backend-specific types.
921impl synta_certificate::PrivateKey for PyPrivateKey {
922    fn public_key_spki_der(&self) -> Result<Vec<u8>, synta_certificate::PrivateKeyError> {
923        self.inner.public_key_spki_der()
924    }
925
926    fn as_signer(&self, algorithm: &str) -> Box<dyn synta_certificate::ErasedCertificateSigner> {
927        self.inner.as_signer(algorithm)
928    }
929}