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