use pyo3::prelude::*;
use pyo3::types::PyBytes;
use synta::{Decoder, Encoding};
use crate::types::PyObjectIdentifier;
#[pyclass(frozen, name = "MlDsaPrivateKey")]
pub struct PyMlDsaPrivateKey {
parameter_set: Py<PyObjectIdentifier>,
kind: &'static str,
seed: Option<Py<PyBytes>>,
expanded_key: Option<Py<PyBytes>>,
}
#[pymethods]
impl PyMlDsaPrivateKey {
#[staticmethod]
fn from_der(
py: Python<'_>,
algorithm_oid: &Bound<'_, PyAny>,
data: &Bound<'_, PyBytes>,
) -> PyResult<Self> {
use synta_certificate::oids;
use synta_certificate::{
MlDsa44PrivateKey, MlDsa44PrivateKeyBoth, MlDsa65PrivateKey, MlDsa65PrivateKeyBoth,
MlDsa87PrivateKey, MlDsa87PrivateKeyBoth,
};
let oid = super::oid_from_pyany(algorithm_oid)?;
let comps = oid.components();
let raw = data.as_bytes();
let mut decoder = Decoder::new(raw, Encoding::Der);
macro_rules! to_py {
($slice:expr) => {
PyBytes::new(py, $slice).unbind()
};
}
let (kind, seed, expanded_key) = if comps == oids::ML_DSA_44 {
let key: MlDsa44PrivateKey<'_> = decoder
.decode()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
match key {
MlDsa44PrivateKey::Seed(s) => ("seed", Some(to_py!(s.as_bytes())), None),
MlDsa44PrivateKey::ExpandedKey(ek) => {
("expanded_key", None, Some(to_py!(ek.as_bytes())))
}
MlDsa44PrivateKey::Both(MlDsa44PrivateKeyBoth {
seed: s,
expanded_key: ek,
}) => (
"both",
Some(to_py!(s.as_bytes())),
Some(to_py!(ek.as_bytes())),
),
}
} else if comps == oids::ML_DSA_65 {
let key: MlDsa65PrivateKey<'_> = decoder
.decode()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
match key {
MlDsa65PrivateKey::Seed(s) => ("seed", Some(to_py!(s.as_bytes())), None),
MlDsa65PrivateKey::ExpandedKey(ek) => {
("expanded_key", None, Some(to_py!(ek.as_bytes())))
}
MlDsa65PrivateKey::Both(MlDsa65PrivateKeyBoth {
seed: s,
expanded_key: ek,
}) => (
"both",
Some(to_py!(s.as_bytes())),
Some(to_py!(ek.as_bytes())),
),
}
} else if comps == oids::ML_DSA_87 {
let key: MlDsa87PrivateKey<'_> = decoder
.decode()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
match key {
MlDsa87PrivateKey::Seed(s) => ("seed", Some(to_py!(s.as_bytes())), None),
MlDsa87PrivateKey::ExpandedKey(ek) => {
("expanded_key", None, Some(to_py!(ek.as_bytes())))
}
MlDsa87PrivateKey::Both(MlDsa87PrivateKeyBoth {
seed: s,
expanded_key: ek,
}) => (
"both",
Some(to_py!(s.as_bytes())),
Some(to_py!(ek.as_bytes())),
),
}
} else {
return Err(pyo3::exceptions::PyValueError::new_err(format!(
"unknown ML-DSA algorithm OID: {oid}; expected one of ML_DSA_44, ML_DSA_65, ML_DSA_87",
)));
};
let ps = Py::new(py, PyObjectIdentifier::from_oid(oid))
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
Ok(Self {
parameter_set: ps,
kind,
seed,
expanded_key,
})
}
#[getter]
fn parameter_set<'py>(&self, py: Python<'py>) -> Bound<'py, PyObjectIdentifier> {
self.parameter_set.bind(py).clone()
}
#[getter]
fn kind(&self) -> &str {
self.kind
}
#[getter]
fn seed<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
self.seed.as_ref().map(|b| b.bind(py).clone())
}
#[getter]
fn expanded_key<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyBytes>> {
self.expanded_key.as_ref().map(|b| b.bind(py).clone())
}
fn __repr__(&self, py: Python<'_>) -> String {
let oid_str = self.parameter_set.bind(py).borrow().inner.to_string();
format!(
"MlDsaPrivateKey(parameter_set={oid_str:?}, kind={:?})",
self.kind
)
}
}
#[pyclass(frozen, name = "MlDsaPublicKey")]
pub struct PyMlDsaPublicKey {
parameter_set: Py<PyObjectIdentifier>,
key: Py<PyBytes>,
}
#[pymethods]
impl PyMlDsaPublicKey {
#[staticmethod]
fn from_bytes(
py: Python<'_>,
algorithm_oid: &Bound<'_, PyAny>,
data: &Bound<'_, PyBytes>,
) -> PyResult<Self> {
use synta_certificate::oids;
let oid = super::oid_from_pyany(algorithm_oid)?;
let comps = oid.components();
let raw = data.as_bytes();
let expected_len = if comps == oids::ML_DSA_44 {
1312usize
} else if comps == oids::ML_DSA_65 {
1952
} else if comps == oids::ML_DSA_87 {
2592
} else {
return Err(pyo3::exceptions::PyValueError::new_err(format!(
"unknown ML-DSA algorithm OID: {oid}; \
expected one of ML_DSA_44, ML_DSA_65, ML_DSA_87",
)));
};
if raw.len() != expected_len {
return Err(pyo3::exceptions::PyValueError::new_err(format!(
"ML-DSA public key has wrong length: expected {expected_len} bytes, got {}",
raw.len()
)));
}
let ps = Py::new(py, PyObjectIdentifier::from_oid(oid))
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
Ok(Self {
parameter_set: ps,
key: PyBytes::new(py, raw).unbind(),
})
}
#[getter]
fn parameter_set<'py>(&self, py: Python<'py>) -> Bound<'py, PyObjectIdentifier> {
self.parameter_set.bind(py).clone()
}
#[getter]
fn key<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
self.key.bind(py).clone()
}
fn __len__(&self, py: Python<'_>) -> usize {
self.key.bind(py).as_bytes().len()
}
fn __repr__(&self, py: Python<'_>) -> String {
let oid_str = self.parameter_set.bind(py).borrow().inner.to_string();
let key_len = self.key.bind(py).as_bytes().len();
format!("MlDsaPublicKey(parameter_set={oid_str:?}, key=<{key_len} bytes>)")
}
}
#[pyclass(frozen, name = "RsaPublicKey")]
pub struct PyRsaPublicKey {
modulus: Py<PyBytes>,
public_exponent: Py<PyBytes>,
}
#[pymethods]
impl PyRsaPublicKey {
#[staticmethod]
fn from_der(py: Python<'_>, data: &Bound<'_, PyBytes>) -> PyResult<Self> {
use synta_certificate::pkcs1_types::RsaPublicKey;
let raw = data.as_bytes();
let mut decoder = Decoder::new(raw, Encoding::Der);
let key: RsaPublicKey = decoder
.decode()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
Ok(Self {
modulus: PyBytes::new(py, key.modulus.as_bytes()).unbind(),
public_exponent: PyBytes::new(py, key.public_exponent.as_bytes()).unbind(),
})
}
#[getter]
fn modulus<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
self.modulus.bind(py).clone()
}
#[getter]
fn public_exponent<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
self.public_exponent.bind(py).clone()
}
fn __repr__(&self, py: Python<'_>) -> String {
let mod_len = self.modulus.bind(py).as_bytes().len();
format!("RsaPublicKey(modulus=<{mod_len} bytes>)")
}
}
#[pyclass(frozen, name = "RsassaPssParams")]
pub struct PyRsassaPssParams {
hash_algorithm: Option<Py<PyObjectIdentifier>>,
mask_gen_algorithm: Option<Py<PyObjectIdentifier>>,
mask_gen_hash_algorithm: Option<Py<PyObjectIdentifier>>,
salt_length: Option<i64>,
trailer_field: Option<i64>,
}
#[pymethods]
impl PyRsassaPssParams {
#[staticmethod]
fn from_der(py: Python<'_>, data: &Bound<'_, PyBytes>) -> PyResult<Self> {
use synta::{Encode, Encoder as SyntaEncoder};
use synta_certificate::pkcs1_types::RsassaPssParams;
let raw = data.as_bytes();
let mut decoder = Decoder::new(raw, Encoding::Der);
let params: RsassaPssParams<'_> = decoder
.decode()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
let mk_oid = |oid: synta::ObjectIdentifier| -> PyResult<Py<PyObjectIdentifier>> {
Py::new(py, PyObjectIdentifier::from_oid(oid))
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
};
let hash_algorithm = params
.hash_algorithm
.map(|a| mk_oid(a.algorithm))
.transpose()?;
let (mask_gen_algorithm, mask_gen_hash_algorithm) = match params.mask_gen_algorithm {
None => (None, None),
Some(mga) => {
let mga_oid = mk_oid(mga.algorithm)?;
let inner_hash_oid = mga.parameters.and_then(|elem| {
let mut enc = SyntaEncoder::new(Encoding::Der);
elem.encode(&mut enc).ok()?;
let bytes = enc.finish().ok()?;
let mut dec = Decoder::new(bytes.as_slice(), Encoding::Der);
let alg_id: synta_certificate::AlgorithmIdentifier<'_> = dec.decode().ok()?;
Some(alg_id.algorithm)
});
let inner = inner_hash_oid.map(mk_oid).transpose()?;
(Some(mga_oid), inner)
}
};
let salt_length = params
.salt_length
.map(|i| {
i.as_i64()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
})
.transpose()?;
let trailer_field = params
.trailer_field
.map(|i| {
i.as_i64()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
})
.transpose()?;
Ok(Self {
hash_algorithm,
mask_gen_algorithm,
mask_gen_hash_algorithm,
salt_length,
trailer_field,
})
}
#[getter]
fn hash_algorithm<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyObjectIdentifier>> {
self.hash_algorithm.as_ref().map(|o| o.bind(py).clone())
}
#[getter]
fn mask_gen_algorithm<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyObjectIdentifier>> {
self.mask_gen_algorithm.as_ref().map(|o| o.bind(py).clone())
}
#[getter]
fn mask_gen_hash_algorithm<'py>(
&self,
py: Python<'py>,
) -> Option<Bound<'py, PyObjectIdentifier>> {
self.mask_gen_hash_algorithm
.as_ref()
.map(|o| o.bind(py).clone())
}
#[getter]
fn salt_length(&self) -> Option<i64> {
self.salt_length
}
#[getter]
fn trailer_field(&self) -> Option<i64> {
self.trailer_field
}
fn __repr__(&self, py: Python<'_>) -> String {
let hash = self
.hash_algorithm
.as_ref()
.map(|o| o.bind(py).borrow().inner.to_string())
.unwrap_or_else(|| "SHA-1 (default)".to_string());
let salt = self.salt_length.unwrap_or(20);
format!("RsassaPssParams(hash_algorithm={hash:?}, salt_length={salt})")
}
}