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