_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, KeyDecryptor, KeyEncryptor};
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 use synta_certificate::OpensslRsaOaepEncryptor;
237 let ct = OpensslRsaOaepEncryptor::new(self.inner.spki_der(), hash_algorithm)
238 .map_err(|e| PyValueError::new_err(format!("{e}")))?
239 .encrypt_key(plaintext)
240 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
241 Ok(PyBytes::new(py, &ct))
242 }
243
244 /// Encrypt ``plaintext`` with RSA PKCS\#1 v1.5 padding.
245 ///
246 /// Raises :exc:`ValueError` if this key is not an RSA key.
247 ///
248 /// ```python,ignore
249 /// ct = pub.rsa_pkcs1v15_encrypt(b"secret data")
250 /// ```
251 fn rsa_pkcs1v15_encrypt<'py>(
252 &self,
253 py: Python<'py>,
254 plaintext: &[u8],
255 ) -> PyResult<Bound<'py, PyBytes>> {
256 use synta_certificate::OpensslRsaPkcs1Encryptor;
257 let ct = OpensslRsaPkcs1Encryptor::new(self.inner.spki_der())
258 .map_err(|e| PyValueError::new_err(format!("{e}")))?
259 .encrypt_key(plaintext)
260 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
261 Ok(PyBytes::new(py, &ct))
262 }
263
264 /// Verify a signature over ``data``.
265 ///
266 /// ``algorithm`` is the hash algorithm used during signing. It must be one
267 /// of ``"sha1"``, ``"sha224"``, ``"sha256"``, ``"sha384"``, or
268 /// ``"sha512"`` for RSA (PKCS\#1 v1.5) and ECDSA keys. For Ed25519 and
269 /// Ed448 keys pass ``None`` (or omit the argument) — no pre-hash is used.
270 ///
271 /// Raises :exc:`ValueError` if the signature is invalid or the algorithm
272 /// combination is unsupported.
273 ///
274 /// ```python,ignore
275 /// pub.verify_signature(sig, data, "sha256") # RSA or ECDSA
276 /// ed_pub.verify_signature(sig, data) # Ed25519 / Ed448
277 /// ```
278 #[pyo3(signature = (signature, data, algorithm = None))]
279 fn verify_signature(
280 &self,
281 signature: &[u8],
282 data: &[u8],
283 algorithm: Option<&str>,
284 ) -> PyResult<()> {
285 self.inner
286 .verify_message(data, signature, algorithm)
287 .map_err(|e| PyValueError::new_err(format!("{e}")))
288 }
289
290 fn __repr__(&self) -> String {
291 let kt = self.inner.key_type();
292 let bits = match kt {
293 "ed25519" | "ed448" => String::new(),
294 _ => self
295 .inner
296 .key_bit_size()
297 .map(|b| format!(", key_size={b}"))
298 .unwrap_or_default(),
299 };
300 format!("PublicKey(key_type={kt:?}{bits})")
301 }
302}
303
304// ── PrivateKey ────────────────────────────────────────────────────────────────
305
306/// An asymmetric private key.
307///
308/// Supports RSA, EC (P-256, P-384, P-521), Ed25519, Ed448, and DSA keys.
309/// Load from PEM (optionally password-protected) or unencrypted PKCS\#8 DER;
310/// serialize back to PEM (optionally encrypted with AES-256-CBC) or
311/// unencrypted PKCS\#8 DER. RSA keys can decrypt ciphertext with OAEP or
312/// PKCS\#1 v1.5 padding.
313///
314/// ```python,ignore
315/// import synta
316///
317/// # Load an encrypted RSA private key from PEM:
318/// with open("rsa_key.pem", "rb") as f:
319/// priv = synta.PrivateKey.from_pem(f.read(), password=b"secret")
320///
321/// # Extract the public key:
322/// pub = priv.public_key
323///
324/// # Decrypt RSA-OAEP ciphertext:
325/// plaintext = priv.rsa_oaep_decrypt(ciphertext, "sha256")
326/// ```
327#[pyclass(frozen, name = "PrivateKey")]
328pub struct PyPrivateKey {
329 pub(crate) inner: BackendPrivateKey,
330}
331
332#[pymethods]
333impl PyPrivateKey {
334 /// Load a private key from PEM-encoded data.
335 ///
336 /// Supports RSA, EC, Ed25519, Ed448, and DSA keys in both PKCS\#8
337 /// (``-----BEGIN PRIVATE KEY-----``) and traditional
338 /// (``-----BEGIN RSA PRIVATE KEY-----`` etc.) PEM formats.
339 ///
340 /// If the PEM block is password-protected, pass the passphrase as
341 /// ``password``.
342 ///
343 /// ```python,ignore
344 /// # Unencrypted key:
345 /// priv = synta.PrivateKey.from_pem(open("key.pem", "rb").read())
346 ///
347 /// # Encrypted key:
348 /// priv = synta.PrivateKey.from_pem(open("key.pem", "rb").read(), password=b"pass")
349 /// ```
350 #[staticmethod]
351 #[pyo3(signature = (data, password = None))]
352 fn from_pem(data: &[u8], password: Option<&[u8]>) -> PyResult<Self> {
353 let inner = BackendPrivateKey::from_pem(data, password)
354 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
355 Ok(Self { inner })
356 }
357
358 /// Load an unencrypted private key from PKCS\#8 DER bytes.
359 ///
360 /// ```python,ignore
361 /// with open("key.der", "rb") as f:
362 /// priv = synta.PrivateKey.from_der(f.read())
363 /// ```
364 #[staticmethod]
365 fn from_der(data: &[u8]) -> PyResult<Self> {
366 let inner =
367 BackendPrivateKey::from_der(data).map_err(|e| PyValueError::new_err(format!("{e}")))?;
368 Ok(Self { inner })
369 }
370
371 /// Serialize this private key to PEM-encoded PKCS\#8.
372 ///
373 /// If ``password`` is provided the output is encrypted with AES-256-CBC.
374 ///
375 /// ```python,ignore
376 /// # Unencrypted:
377 /// pem = priv.to_pem()
378 ///
379 /// # Encrypted:
380 /// pem = priv.to_pem(password=b"my-passphrase")
381 /// ```
382 #[pyo3(signature = (password = None))]
383 fn to_pem<'py>(
384 &self,
385 py: Python<'py>,
386 password: Option<&[u8]>,
387 ) -> PyResult<Bound<'py, PyBytes>> {
388 let pem = self
389 .inner
390 .to_pem(password)
391 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
392 Ok(PyBytes::new(py, &pem))
393 }
394
395 /// Serialize this private key to unencrypted PKCS\#8 DER.
396 ///
397 /// ```python,ignore
398 /// der = priv.to_der()
399 /// open("key.der", "wb").write(der)
400 /// ```
401 fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
402 let der = self
403 .inner
404 .to_der()
405 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
406 Ok(PyBytes::new(py, &der))
407 }
408
409 /// Serialize this private key to encrypted PKCS\#8 DER
410 /// (``EncryptedPrivateKeyInfo``, RFC 5958 §3).
411 ///
412 /// ```python,ignore
413 /// der = priv.to_pkcs8_encrypted(b"my-passphrase")
414 /// open("key.p8e", "wb").write(der)
415 ///
416 /// # Round-trip:
417 /// priv2 = synta.PrivateKey.from_pkcs8_encrypted(der, b"my-passphrase")
418 /// assert priv2.to_der() == priv.to_der()
419 /// ```
420 fn to_pkcs8_encrypted<'py>(
421 &self,
422 py: Python<'py>,
423 password: &[u8],
424 ) -> PyResult<Bound<'py, PyBytes>> {
425 let der = self
426 .inner
427 .to_pkcs8_encrypted(password)
428 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
429 Ok(PyBytes::new(py, &der))
430 }
431
432 /// Load a private key from an encrypted PKCS\#8 DER blob
433 /// (``EncryptedPrivateKeyInfo``).
434 ///
435 /// ```python,ignore
436 /// der = open("key.p8e", "rb").read()
437 /// priv = synta.PrivateKey.from_pkcs8_encrypted(der, b"my-passphrase")
438 /// ```
439 #[staticmethod]
440 fn from_pkcs8_encrypted(data: &[u8], password: &[u8]) -> PyResult<Self> {
441 let inner = BackendPrivateKey::from_pkcs8_encrypted(data, password)
442 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
443 Ok(Self { inner })
444 }
445
446 /// The key algorithm as a lowercase string.
447 ///
448 /// Returns one of ``"rsa"``, ``"ec"``, ``"ed25519"``, ``"ed448"``,
449 /// ``"dsa"``, or ``"unknown"``.
450 #[getter]
451 fn key_type(&self) -> &'static str {
452 self.inner.key_type()
453 }
454
455 /// The key size in bits, or ``None`` for EdDSA keys.
456 #[getter]
457 fn key_size(&self) -> Option<i64> {
458 self.inner.key_bit_size()
459 }
460
461 /// The public key corresponding to this private key.
462 ///
463 /// ```python,ignore
464 /// pub = priv.public_key
465 /// ct = pub.rsa_oaep_encrypt(b"data", "sha256")
466 /// ```
467 #[getter]
468 fn public_key(&self) -> PyResult<PyPublicKey> {
469 let bpk = self
470 .inner
471 .public_key()
472 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
473 Ok(PyPublicKey { inner: bpk })
474 }
475
476 /// Decrypt ``ciphertext`` with RSA-OAEP using the specified hash algorithm.
477 ///
478 /// ``hash_algorithm`` must be one of ``"sha1"``, ``"sha224"``,
479 /// ``"sha256"``, ``"sha384"``, or ``"sha512"``.
480 ///
481 /// Raises :exc:`ValueError` if this key is not an RSA key.
482 ///
483 /// ```python,ignore
484 /// plaintext = priv.rsa_oaep_decrypt(ciphertext, "sha256")
485 /// ```
486 #[pyo3(signature = (ciphertext, hash_algorithm = "sha256"))]
487 fn rsa_oaep_decrypt<'py>(
488 &self,
489 py: Python<'py>,
490 ciphertext: &[u8],
491 hash_algorithm: &str,
492 ) -> PyResult<Bound<'py, PyBytes>> {
493 use synta_certificate::OpensslRsaOaepDecryptor;
494 let pkcs8 = self
495 .inner
496 .to_der()
497 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
498 let pt = OpensslRsaOaepDecryptor::new(&pkcs8, hash_algorithm)
499 .map_err(|e| PyValueError::new_err(format!("{e}")))?
500 .decrypt_key(ciphertext)
501 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
502 Ok(PyBytes::new(py, &pt))
503 }
504
505 /// Decrypt ``ciphertext`` with RSA PKCS\#1 v1.5 padding.
506 ///
507 /// Raises :exc:`ValueError` if this key is not an RSA key.
508 ///
509 /// ```python,ignore
510 /// plaintext = priv.rsa_pkcs1v15_decrypt(ciphertext)
511 /// ```
512 fn rsa_pkcs1v15_decrypt<'py>(
513 &self,
514 py: Python<'py>,
515 ciphertext: &[u8],
516 ) -> PyResult<Bound<'py, PyBytes>> {
517 use synta_certificate::OpensslRsaPkcs1Decryptor;
518 let pkcs8 = self
519 .inner
520 .to_der()
521 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
522 let pt = OpensslRsaPkcs1Decryptor::new(&pkcs8)
523 .map_err(|e| PyValueError::new_err(format!("{e}")))?
524 .decrypt_key(ciphertext)
525 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
526 Ok(PyBytes::new(py, &pt))
527 }
528
529 /// Generate a new RSA private key.
530 ///
531 /// ``key_size`` is the modulus bit-length (e.g. 2048, 3072, 4096).
532 /// ``public_exponent`` defaults to 65537.
533 ///
534 /// ```python,ignore
535 /// priv = synta.PrivateKey.generate_rsa(2048)
536 /// ```
537 #[staticmethod]
538 #[pyo3(signature = (key_size, public_exponent = 65537))]
539 fn generate_rsa(key_size: u32, public_exponent: u32) -> PyResult<Self> {
540 let inner = BackendPrivateKey::generate_rsa(key_size, public_exponent)
541 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
542 Ok(Self { inner })
543 }
544
545 /// Generate a new EC private key on the specified named curve.
546 ///
547 /// ``curve`` must be one of ``"P-256"``, ``"P-384"``, or ``"P-521"``.
548 /// Raises :exc:`ValueError` for unknown curve names.
549 ///
550 /// ```python,ignore
551 /// priv = synta.PrivateKey.generate_ec("P-256")
552 /// ```
553 #[staticmethod]
554 #[pyo3(signature = (curve = "P-256"))]
555 fn generate_ec(curve: &str) -> PyResult<Self> {
556 let inner = BackendPrivateKey::generate_ec(curve)
557 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
558 Ok(Self { inner })
559 }
560
561 fn __repr__(&self) -> String {
562 let kt = self.inner.key_type();
563 let bits = match kt {
564 "ed25519" | "ed448" => String::new(),
565 _ => self
566 .inner
567 .key_bit_size()
568 .map(|b| format!(", key_size={b}"))
569 .unwrap_or_default(),
570 };
571 format!("PrivateKey(key_type={kt:?}{bits})")
572 }
573}
574
575// ── PrivateKey trait impl ─────────────────────────────────────────────────────
576
577/// Implement the backend-agnostic [`synta_certificate::PrivateKey`] trait for
578/// [`PyPrivateKey`] by delegating to [`synta_certificate::BackendPrivateKey`].
579///
580/// This allows Python binding code (e.g. `cert_builder.rs`) to call
581/// `key.as_signer(algorithm)` without importing backend-specific types.
582impl synta_certificate::PrivateKey for PyPrivateKey {
583 fn public_key_spki_der(&self) -> Result<Vec<u8>, synta_certificate::PrivateKeyError> {
584 self.inner.public_key_spki_der()
585 }
586
587 fn as_signer(&self, algorithm: &str) -> Box<dyn synta_certificate::ErasedCertificateSigner> {
588 self.inner.as_signer(algorithm)
589 }
590}