877 lines
30 KiB
Rust
877 lines
30 KiB
Rust
use pyo3::exceptions::PyValueError;
|
|
use pyo3::prelude::*;
|
|
use pyo3::types::PyBytes;
|
|
use rand::rngs::OsRng;
|
|
|
|
use crate::credential::{self, bbs, SIGNATURE_LEN};
|
|
use crate::credential::registry as reg;
|
|
use crate::issuance;
|
|
use crate::transport::handshake::HANDSHAKE_MSG_LEN;
|
|
|
|
fn to_py_err(e: crate::Error) -> PyErr {
|
|
PyValueError::new_err(e.to_string())
|
|
}
|
|
|
|
fn to_32(bytes: &[u8], name: &str) -> PyResult<[u8; 32]> {
|
|
bytes.try_into().map_err(|_| PyValueError::new_err(format!("{name} must be 32 bytes")))
|
|
}
|
|
|
|
fn bytes_to_hex(b: &[u8]) -> String {
|
|
b.iter().map(|byte| format!("{byte:02x}")).collect()
|
|
}
|
|
|
|
// ── Ristretto Keypair (transport identity) ───────────────────────────
|
|
|
|
#[pyclass(name = "Keypair")]
|
|
pub struct PyKeypair {
|
|
inner: Option<credential::Keypair>,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl PyKeypair {
|
|
#[new]
|
|
fn new() -> Self {
|
|
PyKeypair {
|
|
inner: Some(credential::Keypair::generate(&mut OsRng)),
|
|
}
|
|
}
|
|
|
|
fn public_key(&self) -> PyResult<PyPublicKey> {
|
|
let kp = self.inner.as_ref().ok_or_else(|| {
|
|
PyValueError::new_err("keypair was consumed by Node")
|
|
})?;
|
|
Ok(PyPublicKey {
|
|
inner: *kp.public(),
|
|
})
|
|
}
|
|
|
|
fn sign<'py>(&self, py: Python<'py>, msg: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
|
let kp = self.inner.as_ref().ok_or_else(|| {
|
|
PyValueError::new_err("keypair was consumed by Node")
|
|
})?;
|
|
let sig = kp.sign(msg);
|
|
Ok(PyBytes::new(py, &sig.to_bytes()))
|
|
}
|
|
|
|
fn secret_key_bytes<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
|
|
let kp = self.inner.as_ref().ok_or_else(|| {
|
|
PyValueError::new_err("keypair was consumed by Node")
|
|
})?;
|
|
Ok(PyBytes::new(py, &kp.secret_key_bytes()))
|
|
}
|
|
|
|
#[staticmethod]
|
|
fn from_secret_key(bytes: &[u8]) -> PyResult<Self> {
|
|
if bytes.len() != 32 {
|
|
return Err(PyValueError::new_err("secret key must be 32 bytes"));
|
|
}
|
|
let arr: [u8; 32] = bytes.try_into().unwrap();
|
|
let inner = credential::Keypair::from_secret_key_bytes(&arr).map_err(to_py_err)?;
|
|
Ok(PyKeypair {
|
|
inner: Some(inner),
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Ristretto PublicKey ──────────────────────────────────────────────
|
|
|
|
#[pyclass(name = "PublicKey")]
|
|
#[derive(Clone)]
|
|
pub struct PyPublicKey {
|
|
inner: credential::PublicKey,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl PyPublicKey {
|
|
fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
PyBytes::new(py, &self.inner.to_bytes())
|
|
}
|
|
|
|
#[staticmethod]
|
|
fn from_bytes(bytes: &[u8]) -> PyResult<Self> {
|
|
if bytes.len() != 32 {
|
|
return Err(PyValueError::new_err("public key must be 32 bytes"));
|
|
}
|
|
let arr: [u8; 32] = bytes.try_into().unwrap();
|
|
Ok(PyPublicKey {
|
|
inner: credential::PublicKey::from_bytes(arr).map_err(to_py_err)?,
|
|
})
|
|
}
|
|
|
|
fn __eq__(&self, other: &PyPublicKey) -> bool {
|
|
self.inner == other.inner
|
|
}
|
|
|
|
fn __hash__(&self) -> u64 {
|
|
let b = self.inner.to_bytes();
|
|
u64::from_le_bytes(b[..8].try_into().unwrap())
|
|
}
|
|
|
|
fn verify(&self, msg: &[u8], signature: &[u8]) -> PyResult<bool> {
|
|
if signature.len() != SIGNATURE_LEN {
|
|
return Err(PyValueError::new_err("signature must be 64 bytes"));
|
|
}
|
|
let sig_arr: [u8; SIGNATURE_LEN] = signature.try_into().unwrap();
|
|
let sig = credential::Signature::from_bytes(&sig_arr).map_err(to_py_err)?;
|
|
match self.inner.verify(msg, &sig) {
|
|
Ok(()) => Ok(true),
|
|
Err(_) => Ok(false),
|
|
}
|
|
}
|
|
|
|
fn __repr__(&self) -> String {
|
|
let hex = bytes_to_hex(&self.inner.to_bytes());
|
|
format!("PublicKey({hex})")
|
|
}
|
|
}
|
|
|
|
// ── BBS+ Issuer ──────────────────────────────────────────────────────
|
|
|
|
#[pyclass(name = "BbsIssuer")]
|
|
pub struct PyBbsIssuer {
|
|
inner: bbs::IssuerKeyPair,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl PyBbsIssuer {
|
|
#[new]
|
|
fn new() -> PyResult<Self> {
|
|
let inner = bbs::IssuerKeyPair::generate().map_err(to_py_err)?;
|
|
Ok(Self { inner })
|
|
}
|
|
|
|
#[staticmethod]
|
|
fn from_secret_key(bytes: &[u8]) -> PyResult<Self> {
|
|
let inner = bbs::IssuerKeyPair::from_secret_key_bytes(bytes).map_err(to_py_err)?;
|
|
Ok(Self { inner })
|
|
}
|
|
|
|
fn public_key(&self) -> PyBbsPublicKey {
|
|
PyBbsPublicKey { inner: self.inner.public_key() }
|
|
}
|
|
|
|
fn secret_key_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
PyBytes::new(py, &self.inner.secret_key_bytes())
|
|
}
|
|
|
|
fn issue_blind<'py>(
|
|
&self,
|
|
py: Python<'py>,
|
|
commitment_with_proof: &[u8],
|
|
role_id: &[u8],
|
|
epoch: u64,
|
|
) -> PyResult<Bound<'py, PyBytes>> {
|
|
if role_id.len() != 32 {
|
|
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
|
}
|
|
let mut rid = [0u8; 32];
|
|
rid.copy_from_slice(role_id);
|
|
let sig = self.inner.issue_blind(commitment_with_proof, &rid, epoch).map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, &sig))
|
|
}
|
|
}
|
|
|
|
// ── BBS+ Public Key ──────────────────────────────────────────────────
|
|
|
|
#[pyclass(name = "BbsPublicKey")]
|
|
#[derive(Clone)]
|
|
pub struct PyBbsPublicKey {
|
|
inner: bbs::IssuerPublicKey,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl PyBbsPublicKey {
|
|
fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
PyBytes::new(py, &self.inner.to_bytes())
|
|
}
|
|
|
|
#[staticmethod]
|
|
fn from_bytes(bytes: &[u8]) -> PyResult<Self> {
|
|
let inner = bbs::IssuerPublicKey::from_bytes(bytes).map_err(to_py_err)?;
|
|
Ok(Self { inner })
|
|
}
|
|
}
|
|
|
|
// ── BBS+ Blind Request ──────────────────────────────────────────────
|
|
|
|
#[pyclass(name = "BlindRequest")]
|
|
pub struct PyBlindRequest {
|
|
commitment_with_proof: Vec<u8>,
|
|
prover_blind_bytes: Vec<u8>,
|
|
member_secret: Vec<u8>,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl PyBlindRequest {
|
|
fn commitment_with_proof<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
PyBytes::new(py, &self.commitment_with_proof)
|
|
}
|
|
|
|
fn prover_blind<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
PyBytes::new(py, &self.prover_blind_bytes)
|
|
}
|
|
|
|
fn member_secret<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
PyBytes::new(py, &self.member_secret)
|
|
}
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn prepare_blind_request() -> PyResult<PyBlindRequest> {
|
|
let req = bbs::prepare_blind_request().map_err(to_py_err)?;
|
|
Ok(PyBlindRequest {
|
|
commitment_with_proof: req.commitment_with_proof,
|
|
prover_blind_bytes: req.prover_blind.to_bytes().to_vec(),
|
|
member_secret: req.member_secret,
|
|
})
|
|
}
|
|
|
|
// ── BBS+ Credential ─────────────────────────────────────────────────
|
|
|
|
#[pyclass(name = "Credential")]
|
|
pub struct PyCredential {
|
|
inner: bbs::Credential,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl PyCredential {
|
|
#[staticmethod]
|
|
fn finalize(
|
|
blind_sig_bytes: &[u8],
|
|
member_secret: &[u8],
|
|
prover_blind: &[u8],
|
|
role_id: &[u8],
|
|
epoch: u64,
|
|
pk: &PyBbsPublicKey,
|
|
) -> PyResult<Self> {
|
|
if role_id.len() != 32 {
|
|
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
|
}
|
|
if prover_blind.len() != 32 {
|
|
return Err(PyValueError::new_err("prover_blind must be 32 bytes"));
|
|
}
|
|
let mut rid = [0u8; 32];
|
|
rid.copy_from_slice(role_id);
|
|
|
|
let pb_arr: [u8; 32] = prover_blind.try_into().unwrap();
|
|
let blind_factor = zkryptium::bbsplus::commitment::BlindFactor::from_bytes(&pb_arr)
|
|
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
|
|
let inner = bbs::Credential::finalize(
|
|
blind_sig_bytes,
|
|
member_secret.to_vec(),
|
|
blind_factor,
|
|
rid,
|
|
epoch,
|
|
&pk.inner,
|
|
).map_err(to_py_err)?;
|
|
|
|
Ok(Self { inner })
|
|
}
|
|
|
|
fn present<'py>(&self, py: Python<'py>, nonce: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
|
let pres = self.inner.present(nonce).map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, pres.to_bytes()))
|
|
}
|
|
|
|
fn role_id<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
PyBytes::new(py, self.inner.role_id())
|
|
}
|
|
|
|
fn epoch(&self) -> u64 {
|
|
self.inner.epoch()
|
|
}
|
|
}
|
|
|
|
// ── BBS+ Role ID helper ─────────────────────────────────────────────
|
|
|
|
#[pyfunction]
|
|
fn role_id<'py>(py: Python<'py>, name: &str) -> Bound<'py, PyBytes> {
|
|
let rid = bbs::role_id(name);
|
|
PyBytes::new(py, &rid)
|
|
}
|
|
|
|
// ── RoleRegistry (BBS+ server-side) ──────────────────────────────────
|
|
|
|
#[pyclass(name = "RoleRegistry")]
|
|
pub struct PyRoleRegistry {
|
|
inner: credential::RoleRegistry,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl PyRoleRegistry {
|
|
#[new]
|
|
fn new() -> Self {
|
|
PyRoleRegistry {
|
|
inner: credential::RoleRegistry::new(),
|
|
}
|
|
}
|
|
|
|
fn register_role(&mut self, role_id: &[u8], pk: &PyBbsPublicKey, epoch: u64) -> PyResult<()> {
|
|
if role_id.len() != 32 {
|
|
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
|
}
|
|
let mut rid = [0u8; 32];
|
|
rid.copy_from_slice(role_id);
|
|
self.inner.register_role(rid, pk.inner.clone(), epoch);
|
|
Ok(())
|
|
}
|
|
|
|
fn set_epoch(&mut self, role_id: &[u8], epoch: u64) -> PyResult<()> {
|
|
if role_id.len() != 32 {
|
|
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
|
}
|
|
let mut rid = [0u8; 32];
|
|
rid.copy_from_slice(role_id);
|
|
self.inner.set_epoch(&rid, epoch).map_err(to_py_err)
|
|
}
|
|
|
|
fn verify_presentation(&self, role_id: &[u8], proof_bytes: &[u8], nonce: &[u8]) -> PyResult<bool> {
|
|
if role_id.len() != 32 {
|
|
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
|
}
|
|
if proof_bytes.len() > crate::node::MAX_BBS_AUTH_PROOF_BYTES {
|
|
return Ok(false);
|
|
}
|
|
let mut rid = [0u8; 32];
|
|
rid.copy_from_slice(role_id);
|
|
let pres = bbs::Presentation::from_bytes(proof_bytes.to_vec());
|
|
match self.inner.verify_presentation(&rid, &pres, nonce) {
|
|
Ok(()) => Ok(true),
|
|
Err(crate::Error::InvalidPresentation | crate::Error::RoleNotRegistered) => Ok(false),
|
|
Err(e) => Err(to_py_err(e)),
|
|
}
|
|
}
|
|
|
|
fn has_role(&self, role_id: &[u8]) -> PyResult<bool> {
|
|
if role_id.len() != 32 {
|
|
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
|
}
|
|
let mut rid = [0u8; 32];
|
|
rid.copy_from_slice(role_id);
|
|
Ok(self.inner.has_role(&rid))
|
|
}
|
|
}
|
|
|
|
// ── Registry State (client-managed) ──────────────────────────────────
|
|
|
|
#[pyclass(name = "RegistryState")]
|
|
pub struct PyRegistryState {
|
|
inner_bytes: Vec<u8>,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl PyRegistryState {
|
|
#[staticmethod]
|
|
fn build(
|
|
admin_issuer_pk: &PyBbsPublicKey,
|
|
issuance_pk: &[u8],
|
|
version: u64,
|
|
prev_state_hash: &[u8],
|
|
roles: Vec<(Vec<u8>, PyBbsPublicKey, u64)>,
|
|
) -> PyResult<Self> {
|
|
if issuance_pk.len() != 32 {
|
|
return Err(PyValueError::new_err("issuance_pk must be 32 bytes"));
|
|
}
|
|
if prev_state_hash.len() != 32 {
|
|
return Err(PyValueError::new_err("prev_state_hash must be 32 bytes"));
|
|
}
|
|
let mut iss_pk = [0u8; 32];
|
|
iss_pk.copy_from_slice(issuance_pk);
|
|
let mut prev = [0u8; 32];
|
|
prev.copy_from_slice(prev_state_hash);
|
|
|
|
let entries: PyResult<Vec<reg::RoleEntry>> = roles.into_iter().map(|(rid, pk, epoch)| {
|
|
if rid.len() != 32 {
|
|
return Err(PyValueError::new_err("role_id must be 32 bytes"));
|
|
}
|
|
let mut role_id = [0u8; 32];
|
|
role_id.copy_from_slice(&rid);
|
|
Ok(reg::RoleEntry { role_id, issuer_pk: pk.inner.clone(), epoch })
|
|
}).collect();
|
|
|
|
let state = reg::RegistryState::new(
|
|
admin_issuer_pk.inner.clone(), iss_pk, version, prev, entries?,
|
|
);
|
|
Ok(PyRegistryState { inner_bytes: state.serialize() })
|
|
}
|
|
|
|
fn serialize<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
PyBytes::new(py, &self.inner_bytes)
|
|
}
|
|
|
|
#[staticmethod]
|
|
fn deserialize(data: &[u8]) -> PyResult<Self> {
|
|
let _ = reg::RegistryState::deserialize(data).map_err(to_py_err)?;
|
|
Ok(PyRegistryState { inner_bytes: data.to_vec() })
|
|
}
|
|
|
|
fn registry_id<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
|
|
let state = reg::RegistryState::deserialize(&self.inner_bytes).map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, &state.registry_id))
|
|
}
|
|
|
|
fn version(&self) -> PyResult<u64> {
|
|
let state = reg::RegistryState::deserialize(&self.inner_bytes).map_err(to_py_err)?;
|
|
Ok(state.version)
|
|
}
|
|
|
|
fn state_hash<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
let h = reg::RegistryState::state_hash(&self.inner_bytes);
|
|
PyBytes::new(py, &h)
|
|
}
|
|
|
|
fn certify<'py>(&self, py: Python<'py>, admin_credential: &PyCredential) -> PyResult<Bound<'py, PyBytes>> {
|
|
let state = reg::RegistryState::deserialize(&self.inner_bytes).map_err(to_py_err)?;
|
|
let cert = state.certify(&admin_credential.inner, &self.inner_bytes).map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, cert.to_bytes()))
|
|
}
|
|
|
|
#[staticmethod]
|
|
fn verify_cert(admin_issuer_pk: &PyBbsPublicKey, state_cert: &[u8], state_bytes: &[u8]) -> PyResult<bool> {
|
|
let cert = bbs::Presentation::from_bytes(state_cert.to_vec());
|
|
match reg::RegistryState::verify_cert(&admin_issuer_pk.inner, &cert, state_bytes) {
|
|
Ok(()) => Ok(true),
|
|
Err(_) => Ok(false),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn registry_id<'py>(py: Python<'py>, admin_issuer_pk: &PyBbsPublicKey) -> Bound<'py, PyBytes> {
|
|
let rid = reg::registry_id(&admin_issuer_pk.inner);
|
|
PyBytes::new(py, &rid)
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn admin_role_id<'py>(py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
let rid = reg::admin_role_id();
|
|
PyBytes::new(py, &rid)
|
|
}
|
|
|
|
// ── Registry Manager (server-side) ──────────────────────────────────
|
|
|
|
#[pyclass(name = "RegistryManager")]
|
|
pub struct PyRegistryManager {
|
|
inner: crate::registry_manager::RegistryManager,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl PyRegistryManager {
|
|
#[new]
|
|
fn new() -> Self {
|
|
PyRegistryManager {
|
|
inner: crate::registry_manager::RegistryManager::new(),
|
|
}
|
|
}
|
|
|
|
fn create<'py>(&mut self, py: Python<'py>, state_bytes: &[u8], state_cert: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
|
let rid = self.inner.create(state_bytes, state_cert).map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, &rid))
|
|
}
|
|
|
|
/// Load certified registry state from disk (any version); see [`RegistryManager::restore`].
|
|
fn restore<'py>(&mut self, py: Python<'py>, state_bytes: &[u8], state_cert: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
|
let rid = self.inner.restore(state_bytes, state_cert).map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, &rid))
|
|
}
|
|
|
|
fn update(&mut self, registry_id: &[u8], state_bytes: &[u8], state_cert: &[u8]) -> PyResult<()> {
|
|
let rid = to_32(registry_id, "registry_id")?;
|
|
self.inner.update(&rid, state_bytes, state_cert).map_err(to_py_err)
|
|
}
|
|
|
|
fn get<'py>(&self, py: Python<'py>, registry_id: &[u8]) -> PyResult<(Bound<'py, PyBytes>, Bound<'py, PyBytes>)> {
|
|
let rid = to_32(registry_id, "registry_id")?;
|
|
let (state_bytes, cert_bytes) = self.inner.get(&rid).map_err(to_py_err)?;
|
|
Ok((PyBytes::new(py, state_bytes), PyBytes::new(py, cert_bytes)))
|
|
}
|
|
|
|
fn has_registry(&self, registry_id: &[u8]) -> PyResult<bool> {
|
|
let rid = to_32(registry_id, "registry_id")?;
|
|
Ok(self.inner.has_registry(&rid))
|
|
}
|
|
|
|
fn verify_admin(&self, registry_id: &[u8], proof_bytes: &[u8], nonce: &[u8]) -> PyResult<bool> {
|
|
if proof_bytes.len() > crate::node::MAX_BBS_AUTH_PROOF_BYTES {
|
|
return Ok(false);
|
|
}
|
|
let rid = to_32(registry_id, "registry_id")?;
|
|
match self.inner.verify_admin(&rid, proof_bytes, nonce) {
|
|
Ok(()) => Ok(true),
|
|
Err(crate::Error::InvalidPresentation | crate::Error::RegistryNotFound) => Ok(false),
|
|
Err(e) => Err(to_py_err(e)),
|
|
}
|
|
}
|
|
|
|
fn verify_presentation(&self, registry_id: &[u8], role_id: &[u8], proof_bytes: &[u8], nonce: &[u8]) -> PyResult<bool> {
|
|
if proof_bytes.len() > crate::node::MAX_BBS_AUTH_PROOF_BYTES {
|
|
return Ok(false);
|
|
}
|
|
let rid = to_32(registry_id, "registry_id")?;
|
|
let roid = to_32(role_id, "role_id")?;
|
|
let pres = bbs::Presentation::from_bytes(proof_bytes.to_vec());
|
|
match self.inner.verify_presentation(&rid, &roid, &pres, nonce) {
|
|
Ok(()) => Ok(true),
|
|
Err(crate::Error::InvalidPresentation | crate::Error::RoleNotRegistered | crate::Error::RegistryNotFound) => Ok(false),
|
|
Err(e) => Err(to_py_err(e)),
|
|
}
|
|
}
|
|
|
|
fn queue_issuance_request(
|
|
&mut self,
|
|
registry_id: &[u8],
|
|
request_id: &[u8],
|
|
role_id: &[u8],
|
|
eph_pk: &[u8],
|
|
encrypted_commitment: &[u8],
|
|
) -> PyResult<()> {
|
|
let rid = to_32(registry_id, "registry_id")?;
|
|
let req_id = to_32(request_id, "request_id")?;
|
|
let roid = to_32(role_id, "role_id")?;
|
|
let epk = to_32(eph_pk, "eph_pk")?;
|
|
self.inner.queue_issuance_request(&rid, req_id, roid, epk, encrypted_commitment.to_vec())
|
|
.map_err(to_py_err)
|
|
}
|
|
|
|
fn take_pending_requests(&mut self, py: Python<'_>, registry_id: &[u8]) -> PyResult<Vec<PyObject>> {
|
|
let rid = to_32(registry_id, "registry_id")?;
|
|
let items = self.inner.take_pending_requests(&rid).map_err(to_py_err)?;
|
|
let result: Vec<PyObject> = items.into_iter().map(|(req_id, req)| {
|
|
let tuple = (
|
|
PyBytes::new(py, &req_id).into_any(),
|
|
PyBytes::new(py, &req.role_id).into_any(),
|
|
PyBytes::new(py, &req.eph_pk).into_any(),
|
|
PyBytes::new(py, &req.encrypted_commitment).into_any(),
|
|
);
|
|
tuple.into_pyobject(py).unwrap().into_any().unbind()
|
|
}).collect();
|
|
Ok(result)
|
|
}
|
|
|
|
fn grant_credential(&mut self, registry_id: &[u8], request_id: &[u8], encrypted_blind_sig: &[u8]) -> PyResult<()> {
|
|
let rid = to_32(registry_id, "registry_id")?;
|
|
let req_id = to_32(request_id, "request_id")?;
|
|
self.inner.grant_credential(&rid, req_id, encrypted_blind_sig.to_vec())
|
|
.map_err(to_py_err)
|
|
}
|
|
|
|
fn take_granted_credential<'py>(&mut self, py: Python<'py>, registry_id: &[u8], request_id: &[u8]) -> PyResult<Option<Bound<'py, PyBytes>>> {
|
|
let rid = to_32(registry_id, "registry_id")?;
|
|
let req_id = to_32(request_id, "request_id")?;
|
|
let result = self.inner.take_granted_credential(&rid, &req_id).map_err(to_py_err)?;
|
|
Ok(result.map(|data| PyBytes::new(py, &data)))
|
|
}
|
|
}
|
|
|
|
// ── Issuance E2E Encryption ──────────────────────────────────────────
|
|
|
|
#[pyclass(name = "IssuanceKeypair")]
|
|
pub struct PyIssuanceKeypair {
|
|
inner: issuance::IssuanceKeypair,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl PyIssuanceKeypair {
|
|
#[new]
|
|
fn new() -> Self {
|
|
PyIssuanceKeypair {
|
|
inner: issuance::IssuanceKeypair::generate(&mut OsRng),
|
|
}
|
|
}
|
|
|
|
#[staticmethod]
|
|
fn from_secret(bytes: &[u8]) -> PyResult<Self> {
|
|
if bytes.len() != 32 {
|
|
return Err(PyValueError::new_err("issuance secret key must be 32 bytes"));
|
|
}
|
|
let arr: [u8; 32] = bytes.try_into().unwrap();
|
|
Ok(PyIssuanceKeypair {
|
|
inner: issuance::IssuanceKeypair::from_secret_bytes(&arr),
|
|
})
|
|
}
|
|
|
|
fn public_key_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
PyBytes::new(py, &self.inner.public_key_bytes())
|
|
}
|
|
|
|
fn secret_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
PyBytes::new(py, &self.inner.secret_bytes())
|
|
}
|
|
|
|
fn decrypt<'py>(&self, py: Python<'py>, eph_pk: &[u8], ciphertext: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
|
let epk = to_32(eph_pk, "eph_pk")?;
|
|
let plaintext = self.inner.decrypt(&epk, ciphertext).map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, &plaintext))
|
|
}
|
|
|
|
fn encrypt<'py>(&self, py: Python<'py>, eph_pk: &[u8], plaintext: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
|
let epk = to_32(eph_pk, "eph_pk")?;
|
|
let ciphertext = self.inner.encrypt(&epk, plaintext).map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, &ciphertext))
|
|
}
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn encrypt_for_admin<'py>(py: Python<'py>, admin_issuance_pk: &[u8], plaintext: &[u8]) -> PyResult<(Bound<'py, PyBytes>, Bound<'py, PyBytes>)> {
|
|
let pk = to_32(admin_issuance_pk, "admin_issuance_pk")?;
|
|
let (eph_pk, ciphertext) = issuance::encrypt_for_admin(OsRng, &pk, plaintext).map_err(to_py_err)?;
|
|
Ok((PyBytes::new(py, &eph_pk), PyBytes::new(py, &ciphertext)))
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn decrypt_from_admin<'py>(py: Python<'py>, eph_secret: &[u8], admin_issuance_pk: &[u8], ciphertext: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
|
let sec = to_32(eph_secret, "eph_secret")?;
|
|
let pk = to_32(admin_issuance_pk, "admin_issuance_pk")?;
|
|
let plaintext = issuance::decrypt_from_admin(&sec, &pk, ciphertext).map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, &plaintext))
|
|
}
|
|
|
|
// ── Session ──────────────────────────────────────────────────────────
|
|
|
|
#[pyclass(name = "Session")]
|
|
pub struct PySession {
|
|
inner: crate::transport::Session,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl PySession {
|
|
fn transcript_hash<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
|
|
PyBytes::new(py, self.inner.transcript_hash())
|
|
}
|
|
|
|
fn encrypt<'py>(&mut self, py: Python<'py>, plaintext: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
|
let packet = self.inner.encrypt(plaintext).map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, &packet))
|
|
}
|
|
|
|
fn decrypt<'py>(&mut self, py: Python<'py>, packet: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
|
let plaintext = self.inner.decrypt(packet).map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, &plaintext))
|
|
}
|
|
}
|
|
|
|
// ── Node (high-level API) ────────────────────────────────────────────
|
|
|
|
#[pyclass(name = "Node")]
|
|
pub struct PyNode {
|
|
inner: crate::node::Node,
|
|
}
|
|
|
|
#[pyclass(name = "PendingConnect")]
|
|
pub struct PyPendingConnect {
|
|
inner: Option<crate::node::PendingConnect>,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl PyNode {
|
|
#[new]
|
|
fn new(keypair: &mut PyKeypair) -> PyResult<Self> {
|
|
let kp = keypair.inner.take().ok_or_else(|| {
|
|
PyValueError::new_err("keypair already consumed")
|
|
})?;
|
|
Ok(PyNode {
|
|
inner: crate::node::Node::new(kp),
|
|
})
|
|
}
|
|
|
|
fn public_key(&self) -> PyPublicKey {
|
|
PyPublicKey {
|
|
inner: *self.inner.public_key(),
|
|
}
|
|
}
|
|
|
|
fn connect(&self) -> (PyPendingConnect, Vec<u8>) {
|
|
let (pending, msg) = self.inner.connect(OsRng);
|
|
(
|
|
PyPendingConnect {
|
|
inner: Some(pending),
|
|
},
|
|
msg.to_vec(),
|
|
)
|
|
}
|
|
|
|
/// Complete handshake: verify server identity, then produce BBS+ auth.
|
|
fn complete_connect(
|
|
&self,
|
|
pending: &mut PyPendingConnect,
|
|
response_msg: &[u8],
|
|
identity_proof: &[u8],
|
|
expected_server_pk: &PyPublicKey,
|
|
credential: &PyCredential,
|
|
) -> PyResult<(PySession, Vec<u8>)> {
|
|
let p = pending
|
|
.inner
|
|
.take()
|
|
.ok_or_else(|| PyValueError::new_err("PendingConnect already consumed"))?;
|
|
|
|
if response_msg.len() != HANDSHAKE_MSG_LEN {
|
|
return Err(PyValueError::new_err("response_msg must be 32 bytes"));
|
|
}
|
|
let msg: [u8; HANDSHAKE_MSG_LEN] = response_msg.try_into().unwrap();
|
|
|
|
let (session, auth_packet) = self
|
|
.inner
|
|
.complete_connect(
|
|
p,
|
|
&msg,
|
|
identity_proof,
|
|
&expected_server_pk.inner,
|
|
&credential.inner,
|
|
)
|
|
.map_err(to_py_err)?;
|
|
|
|
Ok((PySession { inner: session }, auth_packet))
|
|
}
|
|
|
|
/// Produce encrypted identity proof (server signs transcript with long-term key).
|
|
fn prove_identity<'py>(
|
|
&self,
|
|
py: Python<'py>,
|
|
session: &mut PySession,
|
|
) -> PyResult<Bound<'py, PyBytes>> {
|
|
let proof = self.inner.prove_identity(&mut session.inner).map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, &proof))
|
|
}
|
|
|
|
fn accept(&self, init_msg: &[u8]) -> PyResult<(PySession, Vec<u8>)> {
|
|
if init_msg.len() != HANDSHAKE_MSG_LEN {
|
|
return Err(PyValueError::new_err("init_msg must be 32 bytes"));
|
|
}
|
|
let msg: [u8; HANDSHAKE_MSG_LEN] = init_msg.try_into().unwrap();
|
|
let (session, response) = self.inner.accept(OsRng, &msg).map_err(to_py_err)?;
|
|
Ok((PySession { inner: session }, response.to_vec()))
|
|
}
|
|
|
|
/// Verify encrypted BBS+ auth packet. Returns the role_id (32 bytes) on success.
|
|
fn verify_auth<'py>(
|
|
&self,
|
|
py: Python<'py>,
|
|
session: &mut PySession,
|
|
encrypted_auth: &[u8],
|
|
registry: &PyRoleRegistry,
|
|
) -> PyResult<Bound<'py, PyBytes>> {
|
|
let rid = self
|
|
.inner
|
|
.verify_auth(&mut session.inner, encrypted_auth, ®istry.inner)
|
|
.map_err(to_py_err)?;
|
|
Ok(PyBytes::new(py, &rid))
|
|
}
|
|
|
|
/// Complete handshake verifying server identity only (no BBS+ auth).
|
|
fn complete_connect_anon(
|
|
&self,
|
|
pending: &mut PyPendingConnect,
|
|
response_msg: &[u8],
|
|
identity_proof: &[u8],
|
|
expected_server_pk: &PyPublicKey,
|
|
) -> PyResult<PySession> {
|
|
let p = pending
|
|
.inner
|
|
.take()
|
|
.ok_or_else(|| PyValueError::new_err("PendingConnect already consumed"))?;
|
|
|
|
if response_msg.len() != HANDSHAKE_MSG_LEN {
|
|
return Err(PyValueError::new_err("response_msg must be 32 bytes"));
|
|
}
|
|
let msg: [u8; HANDSHAKE_MSG_LEN] = response_msg.try_into().unwrap();
|
|
|
|
let session = self
|
|
.inner
|
|
.complete_connect_anon(
|
|
p,
|
|
&msg,
|
|
identity_proof,
|
|
&expected_server_pk.inner,
|
|
)
|
|
.map_err(to_py_err)?;
|
|
|
|
Ok(PySession { inner: session })
|
|
}
|
|
|
|
/// Complete handshake for a client-managed registry. Includes
|
|
/// registry_id in the auth packet.
|
|
fn complete_connect_managed(
|
|
&self,
|
|
pending: &mut PyPendingConnect,
|
|
response_msg: &[u8],
|
|
identity_proof: &[u8],
|
|
expected_server_pk: &PyPublicKey,
|
|
credential: &PyCredential,
|
|
registry_id: &[u8],
|
|
) -> PyResult<(PySession, Vec<u8>)> {
|
|
let p = pending
|
|
.inner
|
|
.take()
|
|
.ok_or_else(|| PyValueError::new_err("PendingConnect already consumed"))?;
|
|
|
|
if response_msg.len() != HANDSHAKE_MSG_LEN {
|
|
return Err(PyValueError::new_err("response_msg must be 32 bytes"));
|
|
}
|
|
let msg: [u8; HANDSHAKE_MSG_LEN] = response_msg.try_into().unwrap();
|
|
let rid = to_32(registry_id, "registry_id")?;
|
|
|
|
let (session, auth_packet) = self
|
|
.inner
|
|
.complete_connect_managed(
|
|
p,
|
|
&msg,
|
|
identity_proof,
|
|
&expected_server_pk.inner,
|
|
&credential.inner,
|
|
&rid,
|
|
)
|
|
.map_err(to_py_err)?;
|
|
|
|
Ok((PySession { inner: session }, auth_packet))
|
|
}
|
|
|
|
/// Verify managed-registry auth packet. Returns (registry_id, role_id).
|
|
fn verify_auth_managed<'py>(
|
|
&self,
|
|
py: Python<'py>,
|
|
session: &mut PySession,
|
|
encrypted_auth: &[u8],
|
|
manager: &PyRegistryManager,
|
|
) -> PyResult<(Bound<'py, PyBytes>, Bound<'py, PyBytes>)> {
|
|
let (reg_id, role_id) = self
|
|
.inner
|
|
.verify_auth_managed(&mut session.inner, encrypted_auth, &manager.inner)
|
|
.map_err(to_py_err)?;
|
|
Ok((PyBytes::new(py, ®_id), PyBytes::new(py, &role_id)))
|
|
}
|
|
}
|
|
|
|
// ── Module ───────────────────────────────────────────────────────────
|
|
|
|
#[pymodule]
|
|
fn _zkac(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
m.add("MAX_BBS_AUTH_PROOF_BYTES", crate::node::MAX_BBS_AUTH_PROOF_BYTES)?;
|
|
// Transport identity (ristretto255)
|
|
m.add_class::<PyKeypair>()?;
|
|
m.add_class::<PyPublicKey>()?;
|
|
// BBS+ anonymous credentials
|
|
m.add_class::<PyBbsIssuer>()?;
|
|
m.add_class::<PyBbsPublicKey>()?;
|
|
m.add_class::<PyBlindRequest>()?;
|
|
m.add_class::<PyCredential>()?;
|
|
m.add_function(wrap_pyfunction!(prepare_blind_request, m)?)?;
|
|
m.add_function(wrap_pyfunction!(role_id, m)?)?;
|
|
// Server registry (static, server-configured)
|
|
m.add_class::<PyRoleRegistry>()?;
|
|
// Client-managed registries
|
|
m.add_class::<PyRegistryState>()?;
|
|
m.add_class::<PyRegistryManager>()?;
|
|
m.add_function(wrap_pyfunction!(registry_id, m)?)?;
|
|
m.add_function(wrap_pyfunction!(admin_role_id, m)?)?;
|
|
// E2E-encrypted issuance
|
|
m.add_class::<PyIssuanceKeypair>()?;
|
|
m.add_function(wrap_pyfunction!(encrypt_for_admin, m)?)?;
|
|
m.add_function(wrap_pyfunction!(decrypt_from_admin, m)?)?;
|
|
// Transport
|
|
m.add_class::<PySession>()?;
|
|
m.add_class::<PyNode>()?;
|
|
m.add_class::<PyPendingConnect>()?;
|
|
Ok(())
|
|
}
|