_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 /// Load a public key from a DER-encoded SubjectPublicKeyInfo structure.
59 ///
60 /// ```python,ignore
61 /// with open("pubkey.der", "rb") as f:
62 /// pub = synta.PublicKey.from_der(f.read())
63 /// ```
64 #[staticmethod]
65 fn from_der(data: &[u8]) -> PyResult<Self> {
66 let inner =
67 BackendPublicKey::from_der(data).map_err(|e| PyValueError::new_err(format!("{e}")))?;
68 Ok(Self { inner })
69 }
70
71 /// Serialize this public key to PEM-encoded SubjectPublicKeyInfo.
72 ///
73 /// ```python,ignore
74 /// pem = pub.to_pem()
75 /// open("pubkey.pem", "wb").write(pem)
76 /// ```
77 fn to_pem<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
78 let pem = self
79 .inner
80 .to_pem()
81 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
82 Ok(PyBytes::new(py, &pem))
83 }
84
85 /// Serialize this public key to DER-encoded SubjectPublicKeyInfo.
86 ///
87 /// ```python,ignore
88 /// der = pub.to_der()
89 /// open("pubkey.der", "wb").write(der)
90 /// ```
91 fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
92 let der = self
93 .inner
94 .to_der()
95 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
96 Ok(PyBytes::new(py, &der))
97 }
98
99 /// The key algorithm as a lowercase string.
100 ///
101 /// Returns one of ``"rsa"``, ``"ec"``, ``"ed25519"``, ``"ed448"``,
102 /// ``"dsa"``, or ``"unknown"``.
103 #[getter]
104 fn key_type(&self) -> &'static str {
105 self.inner.key_type()
106 }
107
108 /// The key size in bits, or ``None`` for EdDSA keys.
109 ///
110 /// For RSA this is the modulus bit-length; for EC this is the field
111 /// bit-length. Returns ``None`` for Ed25519 and Ed448.
112 #[getter]
113 fn key_size(&self) -> Option<i64> {
114 self.inner.key_bit_size()
115 }
116
117 /// The RSA modulus ``n`` as big-endian bytes, or ``None`` for non-RSA keys.
118 #[getter]
119 fn modulus<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
120 match self
121 .inner
122 .rsa_modulus()
123 .map_err(|e| PyValueError::new_err(format!("{e}")))?
124 {
125 Some(n) => Ok(Some(PyBytes::new(py, &n))),
126 None => Ok(None),
127 }
128 }
129
130 /// The RSA public exponent ``e`` as big-endian bytes, or ``None`` for
131 /// non-RSA keys.
132 ///
133 /// The most common value is ``b'\x01\x00\x01'`` (65537).
134 #[getter]
135 fn public_exponent<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
136 match self
137 .inner
138 .rsa_public_exponent()
139 .map_err(|e| PyValueError::new_err(format!("{e}")))?
140 {
141 Some(e) => Ok(Some(PyBytes::new(py, &e))),
142 None => Ok(None),
143 }
144 }
145
146 /// The NIST curve name for EC keys, or ``None`` for non-EC keys.
147 ///
148 /// Returns ``"P-256"``, ``"P-384"``, ``"P-521"``, or ``"unknown"`` for
149 /// EC keys on other curves.
150 #[getter]
151 fn curve_name(&self) -> PyResult<Option<&'static str>> {
152 self.inner
153 .ec_curve_name()
154 .map_err(|e| PyValueError::new_err(format!("{e}")))
155 }
156
157 /// The affine X coordinate of the EC public key as big-endian bytes, or
158 /// ``None`` for non-EC keys.
159 #[getter]
160 fn x<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
161 match self
162 .inner
163 .ec_affine_coordinates()
164 .map_err(|e| PyValueError::new_err(format!("{e}")))?
165 {
166 Some((xv, _)) => Ok(Some(PyBytes::new(py, &xv))),
167 None => Ok(None),
168 }
169 }
170
171 /// The affine Y coordinate of the EC public key as big-endian bytes, or
172 /// ``None`` for non-EC keys.
173 #[getter]
174 fn y<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
175 match self
176 .inner
177 .ec_affine_coordinates()
178 .map_err(|e| PyValueError::new_err(format!("{e}")))?
179 {
180 Some((_, yv)) => Ok(Some(PyBytes::new(py, &yv))),
181 None => Ok(None),
182 }
183 }
184
185 /// Encrypt ``plaintext`` with RSA-OAEP using the specified hash algorithm.
186 ///
187 /// ``hash_algorithm`` must be one of ``"sha1"``, ``"sha224"``,
188 /// ``"sha256"``, ``"sha384"``, or ``"sha512"``.
189 ///
190 /// Raises :exc:`ValueError` if this key is not an RSA key.
191 ///
192 /// ```python,ignore
193 /// ct = pub.rsa_oaep_encrypt(b"secret data", "sha256")
194 /// ```
195 #[pyo3(signature = (plaintext, hash_algorithm = "sha256"))]
196 fn rsa_oaep_encrypt<'py>(
197 &self,
198 py: Python<'py>,
199 plaintext: &[u8],
200 hash_algorithm: &str,
201 ) -> PyResult<Bound<'py, PyBytes>> {
202 use synta_certificate::OpensslRsaOaepEncryptor;
203 let ct = OpensslRsaOaepEncryptor::new(self.inner.spki_der(), hash_algorithm)
204 .map_err(|e| PyValueError::new_err(format!("{e}")))?
205 .encrypt_key(plaintext)
206 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
207 Ok(PyBytes::new(py, &ct))
208 }
209
210 /// Encrypt ``plaintext`` with RSA PKCS\#1 v1.5 padding.
211 ///
212 /// Raises :exc:`ValueError` if this key is not an RSA key.
213 ///
214 /// ```python,ignore
215 /// ct = pub.rsa_pkcs1v15_encrypt(b"secret data")
216 /// ```
217 fn rsa_pkcs1v15_encrypt<'py>(
218 &self,
219 py: Python<'py>,
220 plaintext: &[u8],
221 ) -> PyResult<Bound<'py, PyBytes>> {
222 use synta_certificate::OpensslRsaPkcs1Encryptor;
223 let ct = OpensslRsaPkcs1Encryptor::new(self.inner.spki_der())
224 .map_err(|e| PyValueError::new_err(format!("{e}")))?
225 .encrypt_key(plaintext)
226 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
227 Ok(PyBytes::new(py, &ct))
228 }
229
230 /// Verify a signature over ``data``.
231 ///
232 /// ``algorithm`` is the hash algorithm used during signing. It must be one
233 /// of ``"sha1"``, ``"sha224"``, ``"sha256"``, ``"sha384"``, or
234 /// ``"sha512"`` for RSA (PKCS\#1 v1.5) and ECDSA keys. For Ed25519 and
235 /// Ed448 keys pass ``None`` (or omit the argument) — no pre-hash is used.
236 ///
237 /// Raises :exc:`ValueError` if the signature is invalid or the algorithm
238 /// combination is unsupported.
239 ///
240 /// ```python,ignore
241 /// pub.verify_signature(sig, data, "sha256") # RSA or ECDSA
242 /// ed_pub.verify_signature(sig, data) # Ed25519 / Ed448
243 /// ```
244 #[pyo3(signature = (signature, data, algorithm = None))]
245 fn verify_signature(
246 &self,
247 signature: &[u8],
248 data: &[u8],
249 algorithm: Option<&str>,
250 ) -> PyResult<()> {
251 self.inner
252 .verify_message(data, signature, algorithm)
253 .map_err(|e| PyValueError::new_err(format!("{e}")))
254 }
255
256 fn __repr__(&self) -> String {
257 let kt = self.inner.key_type();
258 let bits = match kt {
259 "ed25519" | "ed448" => String::new(),
260 _ => self
261 .inner
262 .key_bit_size()
263 .map(|b| format!(", key_size={b}"))
264 .unwrap_or_default(),
265 };
266 format!("PublicKey(key_type={kt:?}{bits})")
267 }
268}
269
270// ── PrivateKey ────────────────────────────────────────────────────────────────
271
272/// An asymmetric private key.
273///
274/// Supports RSA, EC (P-256, P-384, P-521), Ed25519, Ed448, and DSA keys.
275/// Load from PEM (optionally password-protected) or unencrypted PKCS\#8 DER;
276/// serialize back to PEM (optionally encrypted with AES-256-CBC) or
277/// unencrypted PKCS\#8 DER. RSA keys can decrypt ciphertext with OAEP or
278/// PKCS\#1 v1.5 padding.
279///
280/// ```python,ignore
281/// import synta
282///
283/// # Load an encrypted RSA private key from PEM:
284/// with open("rsa_key.pem", "rb") as f:
285/// priv = synta.PrivateKey.from_pem(f.read(), password=b"secret")
286///
287/// # Extract the public key:
288/// pub = priv.public_key
289///
290/// # Decrypt RSA-OAEP ciphertext:
291/// plaintext = priv.rsa_oaep_decrypt(ciphertext, "sha256")
292/// ```
293#[pyclass(frozen, name = "PrivateKey")]
294pub struct PyPrivateKey {
295 pub(crate) inner: BackendPrivateKey,
296}
297
298#[pymethods]
299impl PyPrivateKey {
300 /// Load a private key from PEM-encoded data.
301 ///
302 /// Supports RSA, EC, Ed25519, Ed448, and DSA keys in both PKCS\#8
303 /// (``-----BEGIN PRIVATE KEY-----``) and traditional
304 /// (``-----BEGIN RSA PRIVATE KEY-----`` etc.) PEM formats.
305 ///
306 /// If the PEM block is password-protected, pass the passphrase as
307 /// ``password``.
308 ///
309 /// ```python,ignore
310 /// # Unencrypted key:
311 /// priv = synta.PrivateKey.from_pem(open("key.pem", "rb").read())
312 ///
313 /// # Encrypted key:
314 /// priv = synta.PrivateKey.from_pem(open("key.pem", "rb").read(), password=b"pass")
315 /// ```
316 #[staticmethod]
317 #[pyo3(signature = (data, password = None))]
318 fn from_pem(data: &[u8], password: Option<&[u8]>) -> PyResult<Self> {
319 let inner = BackendPrivateKey::from_pem(data, password)
320 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
321 Ok(Self { inner })
322 }
323
324 /// Load an unencrypted private key from PKCS\#8 DER bytes.
325 ///
326 /// ```python,ignore
327 /// with open("key.der", "rb") as f:
328 /// priv = synta.PrivateKey.from_der(f.read())
329 /// ```
330 #[staticmethod]
331 fn from_der(data: &[u8]) -> PyResult<Self> {
332 let inner =
333 BackendPrivateKey::from_der(data).map_err(|e| PyValueError::new_err(format!("{e}")))?;
334 Ok(Self { inner })
335 }
336
337 /// Serialize this private key to PEM-encoded PKCS\#8.
338 ///
339 /// If ``password`` is provided the output is encrypted with AES-256-CBC.
340 ///
341 /// ```python,ignore
342 /// # Unencrypted:
343 /// pem = priv.to_pem()
344 ///
345 /// # Encrypted:
346 /// pem = priv.to_pem(password=b"my-passphrase")
347 /// ```
348 #[pyo3(signature = (password = None))]
349 fn to_pem<'py>(
350 &self,
351 py: Python<'py>,
352 password: Option<&[u8]>,
353 ) -> PyResult<Bound<'py, PyBytes>> {
354 let pem = self
355 .inner
356 .to_pem(password)
357 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
358 Ok(PyBytes::new(py, &pem))
359 }
360
361 /// Serialize this private key to unencrypted PKCS\#8 DER.
362 ///
363 /// ```python,ignore
364 /// der = priv.to_der()
365 /// open("key.der", "wb").write(der)
366 /// ```
367 fn to_der<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
368 let der = self
369 .inner
370 .to_der()
371 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
372 Ok(PyBytes::new(py, &der))
373 }
374
375 /// Serialize this private key to encrypted PKCS\#8 DER
376 /// (``EncryptedPrivateKeyInfo``, RFC 5958 §3).
377 ///
378 /// ```python,ignore
379 /// der = priv.to_pkcs8_encrypted(b"my-passphrase")
380 /// open("key.p8e", "wb").write(der)
381 ///
382 /// # Round-trip:
383 /// priv2 = synta.PrivateKey.from_pkcs8_encrypted(der, b"my-passphrase")
384 /// assert priv2.to_der() == priv.to_der()
385 /// ```
386 fn to_pkcs8_encrypted<'py>(
387 &self,
388 py: Python<'py>,
389 password: &[u8],
390 ) -> PyResult<Bound<'py, PyBytes>> {
391 let der = self
392 .inner
393 .to_pkcs8_encrypted(password)
394 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
395 Ok(PyBytes::new(py, &der))
396 }
397
398 /// Load a private key from an encrypted PKCS\#8 DER blob
399 /// (``EncryptedPrivateKeyInfo``).
400 ///
401 /// ```python,ignore
402 /// der = open("key.p8e", "rb").read()
403 /// priv = synta.PrivateKey.from_pkcs8_encrypted(der, b"my-passphrase")
404 /// ```
405 #[staticmethod]
406 fn from_pkcs8_encrypted(data: &[u8], password: &[u8]) -> PyResult<Self> {
407 let inner = BackendPrivateKey::from_pkcs8_encrypted(data, password)
408 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
409 Ok(Self { inner })
410 }
411
412 /// The key algorithm as a lowercase string.
413 ///
414 /// Returns one of ``"rsa"``, ``"ec"``, ``"ed25519"``, ``"ed448"``,
415 /// ``"dsa"``, or ``"unknown"``.
416 #[getter]
417 fn key_type(&self) -> &'static str {
418 self.inner.key_type()
419 }
420
421 /// The key size in bits, or ``None`` for EdDSA keys.
422 #[getter]
423 fn key_size(&self) -> Option<i64> {
424 self.inner.key_bit_size()
425 }
426
427 /// The public key corresponding to this private key.
428 ///
429 /// ```python,ignore
430 /// pub = priv.public_key
431 /// ct = pub.rsa_oaep_encrypt(b"data", "sha256")
432 /// ```
433 #[getter]
434 fn public_key(&self) -> PyResult<PyPublicKey> {
435 let bpk = self
436 .inner
437 .public_key()
438 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
439 Ok(PyPublicKey { inner: bpk })
440 }
441
442 /// Decrypt ``ciphertext`` with RSA-OAEP using the specified hash algorithm.
443 ///
444 /// ``hash_algorithm`` must be one of ``"sha1"``, ``"sha224"``,
445 /// ``"sha256"``, ``"sha384"``, or ``"sha512"``.
446 ///
447 /// Raises :exc:`ValueError` if this key is not an RSA key.
448 ///
449 /// ```python,ignore
450 /// plaintext = priv.rsa_oaep_decrypt(ciphertext, "sha256")
451 /// ```
452 #[pyo3(signature = (ciphertext, hash_algorithm = "sha256"))]
453 fn rsa_oaep_decrypt<'py>(
454 &self,
455 py: Python<'py>,
456 ciphertext: &[u8],
457 hash_algorithm: &str,
458 ) -> PyResult<Bound<'py, PyBytes>> {
459 use synta_certificate::OpensslRsaOaepDecryptor;
460 let pkcs8 = self
461 .inner
462 .to_der()
463 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
464 let pt = OpensslRsaOaepDecryptor::new(&pkcs8, hash_algorithm)
465 .map_err(|e| PyValueError::new_err(format!("{e}")))?
466 .decrypt_key(ciphertext)
467 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
468 Ok(PyBytes::new(py, &pt))
469 }
470
471 /// Decrypt ``ciphertext`` with RSA PKCS\#1 v1.5 padding.
472 ///
473 /// Raises :exc:`ValueError` if this key is not an RSA key.
474 ///
475 /// ```python,ignore
476 /// plaintext = priv.rsa_pkcs1v15_decrypt(ciphertext)
477 /// ```
478 fn rsa_pkcs1v15_decrypt<'py>(
479 &self,
480 py: Python<'py>,
481 ciphertext: &[u8],
482 ) -> PyResult<Bound<'py, PyBytes>> {
483 use synta_certificate::OpensslRsaPkcs1Decryptor;
484 let pkcs8 = self
485 .inner
486 .to_der()
487 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
488 let pt = OpensslRsaPkcs1Decryptor::new(&pkcs8)
489 .map_err(|e| PyValueError::new_err(format!("{e}")))?
490 .decrypt_key(ciphertext)
491 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
492 Ok(PyBytes::new(py, &pt))
493 }
494
495 /// Generate a new RSA private key.
496 ///
497 /// ``key_size`` is the modulus bit-length (e.g. 2048, 3072, 4096).
498 /// ``public_exponent`` defaults to 65537.
499 ///
500 /// ```python,ignore
501 /// priv = synta.PrivateKey.generate_rsa(2048)
502 /// ```
503 #[staticmethod]
504 #[pyo3(signature = (key_size, public_exponent = 65537))]
505 fn generate_rsa(key_size: u32, public_exponent: u32) -> PyResult<Self> {
506 let inner = BackendPrivateKey::generate_rsa(key_size, public_exponent)
507 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
508 Ok(Self { inner })
509 }
510
511 /// Generate a new EC private key on the specified named curve.
512 ///
513 /// ``curve`` must be one of ``"P-256"``, ``"P-384"``, or ``"P-521"``.
514 /// Raises :exc:`ValueError` for unknown curve names.
515 ///
516 /// ```python,ignore
517 /// priv = synta.PrivateKey.generate_ec("P-256")
518 /// ```
519 #[staticmethod]
520 #[pyo3(signature = (curve = "P-256"))]
521 fn generate_ec(curve: &str) -> PyResult<Self> {
522 let inner = BackendPrivateKey::generate_ec(curve)
523 .map_err(|e| PyValueError::new_err(format!("{e}")))?;
524 Ok(Self { inner })
525 }
526
527 fn __repr__(&self) -> String {
528 let kt = self.inner.key_type();
529 let bits = match kt {
530 "ed25519" | "ed448" => String::new(),
531 _ => self
532 .inner
533 .key_bit_size()
534 .map(|b| format!(", key_size={b}"))
535 .unwrap_or_default(),
536 };
537 format!("PrivateKey(key_type={kt:?}{bits})")
538 }
539}
540
541// ── PrivateKey trait impl ─────────────────────────────────────────────────────
542
543/// Implement the backend-agnostic [`synta_certificate::PrivateKey`] trait for
544/// [`PyPrivateKey`] by delegating to [`synta_certificate::BackendPrivateKey`].
545///
546/// This allows Python binding code (e.g. `cert_builder.rs`) to call
547/// `key.as_signer(algorithm)` without importing backend-specific types.
548impl synta_certificate::PrivateKey for PyPrivateKey {
549 fn public_key_spki_der(&self) -> Result<Vec<u8>, synta_certificate::PrivateKeyError> {
550 self.inner.public_key_spki_der()
551 }
552
553 fn as_signer(&self, algorithm: &str) -> Box<dyn synta_certificate::ErasedCertificateSigner> {
554 self.inner.as_signer(algorithm)
555 }
556}