use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use synta_certificate::crypto::{KeySpec, TokenManager};
use synta_certificate::pkcs11_mgmt::Pkcs11Manager;
use synta_certificate::{merge_object_label, Pkcs11Uri, Pkcs11UriAttributes};
use crate::crypto_keys::PyPrivateKey;
#[pyclass(frozen, name = "SlotInfo")]
pub struct PySlotInfo {
pub(crate) slot_id: u64,
pub(crate) token_label: String,
pub(crate) manufacturer_id: String,
pub(crate) model: String,
pub(crate) serial_number: String,
pub(crate) flags: u64,
}
#[pymethods]
impl PySlotInfo {
#[getter]
fn slot_id(&self) -> u64 {
self.slot_id
}
#[getter]
fn token_label(&self) -> &str {
&self.token_label
}
#[getter]
fn manufacturer_id(&self) -> &str {
&self.manufacturer_id
}
#[getter]
fn model(&self) -> &str {
&self.model
}
#[getter]
fn serial_number(&self) -> &str {
&self.serial_number
}
#[getter]
fn flags(&self) -> u64 {
self.flags
}
fn __repr__(&self) -> String {
format!(
"SlotInfo(slot_id={}, token_label={:?}, manufacturer_id={:?})",
self.slot_id, self.token_label, self.manufacturer_id
)
}
}
#[pyclass(frozen, name = "KeyInfo")]
pub struct PyKeyInfo {
pub(crate) label: String,
pub(crate) id: Vec<u8>,
pub(crate) key_type: String,
pub(crate) key_bits: u32,
}
#[pymethods]
impl PyKeyInfo {
#[getter]
fn label(&self) -> &str {
&self.label
}
#[getter]
fn id<'py>(&self, py: Python<'py>) -> Bound<'py, pyo3::types::PyBytes> {
pyo3::types::PyBytes::new(py, &self.id)
}
#[getter]
fn key_type(&self) -> &str {
&self.key_type
}
#[getter]
fn key_bits(&self) -> u32 {
self.key_bits
}
fn __repr__(&self) -> String {
format!(
"KeyInfo(label={:?}, key_type={:?}, key_bits={})",
self.label, self.key_type, self.key_bits
)
}
}
#[pyclass(frozen, name = "Pkcs11Token")]
pub struct PyPkcs11Token {
mgr: Pkcs11Manager,
uri: String,
attrs: Pkcs11UriAttributes,
}
fn make_mgr(uri: &Pkcs11Uri, module: Option<&str>) -> PyResult<Pkcs11Manager> {
match module {
Some(path) => Pkcs11Manager::new(path).map_err(|e| PyValueError::new_err(format!("{e}"))),
None => Pkcs11Manager::from_uri(uri).map_err(|e| PyValueError::new_err(format!("{e}"))),
}
}
fn redact_pin(uri: &str) -> String {
let body = uri.strip_prefix("pkcs11:").unwrap_or(uri);
let (path, query) = body.split_once('?').unwrap_or((body, ""));
if query.is_empty() {
return uri.to_owned();
}
let redacted: Vec<&str> = query
.split('&')
.map(|seg| {
if seg.starts_with("pin-value=") {
"pin-value=***"
} else {
seg
}
})
.collect();
format!("pkcs11:{path}?{}", redacted.join("&"))
}
#[pymethods]
impl PyPkcs11Token {
#[new]
#[pyo3(signature = (uri, module = None))]
fn new(uri: &str, module: Option<&str>) -> PyResult<Self> {
let parsed = Pkcs11Uri::parse(uri).ok_or_else(|| {
PyValueError::new_err(format!("invalid PKCS#11 URI: {:?}", redact_pin(uri)))
})?;
let mgr = make_mgr(&parsed, module)?;
let attrs = parsed.attrs;
Ok(Self {
mgr,
uri: uri.to_owned(),
attrs,
})
}
fn find_key(&self, object_uri: &str) -> PyResult<bool> {
let mut uri = Pkcs11Uri::parse(object_uri).ok_or_else(|| {
PyValueError::new_err(format!("invalid PKCS#11 URI: {:?}", redact_pin(object_uri)))
})?;
if uri.attrs.token.is_none() {
uri.attrs.token = self.attrs.token.clone();
}
if !uri.attrs.has_pin() {
uri.attrs
.set_pin_value(self.attrs.pin_value().map(|s| s.to_owned()));
}
self.mgr
.find_key(&uri)
.map_err(|e| PyValueError::new_err(format!("{e}")))
}
fn list_keys(&self) -> PyResult<Vec<PyKeyInfo>> {
let token_name = self
.attrs
.token
.as_deref()
.ok_or_else(|| PyValueError::new_err("Pkcs11Token URI must contain 'token='"))?;
let pin = self.attrs.pin_value();
let keys = self
.mgr
.list_keys(token_name, pin)
.map_err(|e| PyValueError::new_err(format!("{e}")))?;
Ok(keys
.into_iter()
.map(|k| PyKeyInfo {
label: k.label,
id: k.id,
key_type: k.key_type,
key_bits: k.key_bits,
})
.collect())
}
fn delete_key(&self, object_uri: &str) -> PyResult<()> {
let mut uri = Pkcs11Uri::parse(object_uri).ok_or_else(|| {
PyValueError::new_err(format!("invalid PKCS#11 URI: {:?}", redact_pin(object_uri)))
})?;
if uri.attrs.token.is_none() {
uri.attrs.token = self.attrs.token.clone();
}
if !uri.attrs.has_pin() {
uri.attrs
.set_pin_value(self.attrs.pin_value().map(|s| s.to_owned()));
}
self.mgr
.delete_key(&uri)
.map_err(|e| PyValueError::new_err(format!("{e}")))
}
#[pyo3(signature = (key_type, param, label, extractable = false))]
fn generate_key_pair(
&self,
key_type: &str,
param: Bound<'_, PyAny>,
label: &str,
extractable: bool,
) -> PyResult<PyPrivateKey> {
let spec = build_key_spec(key_type, ¶m)?;
let full_uri_str = merge_object_label(&self.uri, label);
let full_uri = Pkcs11Uri::parse(&full_uri_str).ok_or_else(|| {
PyValueError::new_err(format!("failed to build URI with label '{label}'"))
})?;
let inner = self
.mgr
.generate_key_pair_in_token(&spec, &full_uri, extractable)
.map_err(|e| PyValueError::new_err(format!("{e}")))?;
Ok(PyPrivateKey { inner })
}
fn __repr__(&self) -> String {
format!("Pkcs11Token(uri={:?})", redact_pin(&self.uri))
}
}
#[pyfunction]
#[pyo3(signature = (module = None))]
pub fn list_slots(module: Option<&str>) -> PyResult<Vec<PySlotInfo>> {
let mgr = if let Some(path) = module {
Pkcs11Manager::new(path).map_err(|e| PyValueError::new_err(format!("{e}")))?
} else {
Pkcs11Manager::from_env().map_err(|e| PyValueError::new_err(format!("{e}")))?
};
let slots = mgr
.list_slots()
.map_err(|e| PyValueError::new_err(format!("{e}")))?;
Ok(slots
.into_iter()
.map(|s| PySlotInfo {
slot_id: s.slot_id,
token_label: s.token_label,
manufacturer_id: s.manufacturer_id,
model: s.model,
serial_number: s.serial_number,
flags: s.flags,
})
.collect())
}
fn build_key_spec(key_type: &str, param: &Bound<'_, PyAny>) -> PyResult<KeySpec> {
match key_type.to_ascii_lowercase().as_str() {
"rsa" => {
let bits: u32 = param.extract().map_err(|e| {
PyValueError::new_err(format!("RSA key requires an integer bit-length as param: {e}"))
})?;
Ok(KeySpec::Rsa(bits))
}
"ec" => {
let curve: String = param.extract().map_err(|e| {
PyValueError::new_err(format!(
"EC key requires a curve name string as param (e.g. 'P-256'): {e}"
))
})?;
Ok(KeySpec::Ec(curve))
}
"ed25519" => Ok(KeySpec::Ed25519),
"ed448" => Ok(KeySpec::Ed448),
"ml-dsa" => {
let ps: String = param.extract().map_err(|e| {
PyValueError::new_err(format!(
"ML-DSA key requires a parameter-set string (e.g. 'ML-DSA-65'): {e}"
))
})?;
Ok(KeySpec::MlDsa(ps))
}
"ml-kem" => {
let ps: String = param.extract().map_err(|e| {
PyValueError::new_err(format!(
"ML-KEM key requires a parameter-set string (e.g. 'ML-KEM-768'): {e}"
))
})?;
Ok(KeySpec::MlKem(ps))
}
other => Err(PyValueError::new_err(format!(
"unsupported key type {other:?}; expected 'rsa', 'ec', 'ed25519', 'ed448', 'ml-dsa', or 'ml-kem'"
))),
}
}
pub fn register_pkcs11_module(parent: &Bound<'_, PyModule>) -> PyResult<()> {
let py = parent.py();
let m = PyModule::new(py, "pkcs11")?;
m.add_class::<PySlotInfo>()?;
m.add_class::<PyKeyInfo>()?;
m.add_class::<PyPkcs11Token>()?;
m.add_function(wrap_pyfunction!(list_slots, &m)?)?;
crate::install_submodule(
parent,
&m,
"synta.pkcs11",
Some("PKCS#11 token management: slot listing, key find/list/delete/generate."),
)
}