Skip to main content

_synta/
crypto_keys.rs

1//! Python bindings for generic public and private key operations.
2//!
3//! Exposes [`PyPublicKey`] and [`PyPrivateKey`] as pyo3 classes supporting
4//! RSA, EC, EdDSA, and DSA keys via the `synta-certificate` backend traits.
5//! No direct `openssl::*` imports are used here.
6
7use pyo3::exceptions::PyValueError;
8use pyo3::prelude::*;
9use pyo3::types::PyBytes;
10use synta_certificate::{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}