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