use std::cell::RefCell;
use std::ffi::{c_char, c_int, CStr, CString};
use std::panic;
use crate::chain::{DyoloChain, SystemClock};
use crate::error::KyaError;
use crate::identity::DyoloIdentity;
use crate::intent::{Intent, MerkleProof};
use crate::registry::{MemoryNonceStore, MemoryRevocationStore};
thread_local! {
static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
}
fn set_last_error(msg: impl Into<Vec<u8>>) {
LAST_ERROR.with(|e| {
let s =
CString::new(msg).unwrap_or_else(|_| CString::new("error contains nul byte").unwrap());
*e.borrow_mut() = Some(s);
});
}
#[allow(dead_code)]
fn kya_error_to_status(e: &KyaError) -> c_int {
match e {
KyaError::EmptyChain => 1,
KyaError::StorageFailure(_) => 2,
KyaError::RootMismatch => 3,
KyaError::BrokenLinkage(_) => 4,
KyaError::InvalidSignature(_) => 5,
KyaError::NotYetValid(..) => 6,
KyaError::Expired(..) => 7,
KyaError::TemporalViolation(..) => 8,
KyaError::MaxDepthExceeded(..) => 9,
KyaError::InvalidSubScopeProof => 10,
KyaError::ScopeEscalation(_) => 11,
KyaError::UnauthorizedLeaf => 12,
KyaError::ScopeViolation => 13,
KyaError::NonceReplay => 14,
KyaError::Revoked => 15,
KyaError::IntentNotFound => 16,
KyaError::EmptyTree => 17,
KyaError::WireFormatError(_) => 18,
KyaError::UnsupportedVersion { .. } => 19,
_ => 99,
}
}
#[repr(C)]
pub enum KyaStatus {
KyaOk = 0,
KyaErrEmptyChain = 1,
KyaErrStorageFailure = 2,
KyaErrRootMismatch = 3,
KyaErrBrokenLinkage = 4,
KyaErrInvalidSig = 5,
KyaErrNotYetValid = 6,
KyaErrExpired = 7,
KyaErrTemporalViol = 8,
KyaErrMaxDepth = 9,
KyaErrInvalidProof = 10,
KyaErrScopeEscal = 11,
KyaErrUnauthorized = 12,
KyaErrScopeViol = 13,
KyaErrNonceReplay = 14,
KyaErrRevoked = 15,
KyaErrIntentNotFound = 16,
KyaErrEmptyTree = 17,
KyaErrWireFormat = 18,
KyaErrUnsupportedVer = 19,
KyaErrPanic = 98,
KyaErrUnknown = 99,
}
pub struct OpaqueIdentity(DyoloIdentity);
pub struct OpaqueRevocationStore(MemoryRevocationStore);
pub struct OpaqueNonceStore(MemoryNonceStore);
#[allow(dead_code)]
pub struct OpaqueChain {
chain: DyoloChain,
rev: MemoryRevocationStore,
nonces: MemoryNonceStore,
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dyolo_last_error() -> *const c_char {
LAST_ERROR.with(|e| e.borrow().as_ref().map_or(std::ptr::null(), |s| s.as_ptr()))
}
#[unsafe(no_mangle)]
pub extern "C" fn dyolo_identity_generate() -> *mut OpaqueIdentity {
Box::into_raw(Box::new(OpaqueIdentity(DyoloIdentity::generate())))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dyolo_identity_from_seed(seed: *const u8) -> *mut OpaqueIdentity {
if seed.is_null() {
set_last_error("dyolo_identity_from_seed: seed pointer is null");
return std::ptr::null_mut();
}
let bytes: [u8; 32] = unsafe { std::slice::from_raw_parts(seed, 32) }
.try_into()
.expect("seed is always 32 bytes");
Box::into_raw(Box::new(OpaqueIdentity(DyoloIdentity::from_signing_bytes(
&bytes,
))))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dyolo_identity_verifying_key(
identity: *const OpaqueIdentity,
out: *mut u8,
) -> c_int {
if identity.is_null() || out.is_null() {
set_last_error("null pointer argument");
return KyaStatus::KyaErrUnknown as c_int;
}
let vk = unsafe { (*identity).0.verifying_key() };
unsafe { std::ptr::copy_nonoverlapping(vk.as_bytes().as_ptr(), out, 32) };
KyaStatus::KyaOk as c_int
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dyolo_identity_free(identity: *mut OpaqueIdentity) {
if !identity.is_null() {
let _ = unsafe { Box::from_raw(identity) };
}
}
#[unsafe(no_mangle)]
pub extern "C" fn dyolo_revocation_store_new() -> *mut OpaqueRevocationStore {
Box::into_raw(Box::new(
OpaqueRevocationStore(MemoryRevocationStore::new()),
))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dyolo_revocation_store_free(store: *mut OpaqueRevocationStore) {
if !store.is_null() {
let _ = unsafe { Box::from_raw(store) };
}
}
#[unsafe(no_mangle)]
pub extern "C" fn dyolo_nonce_store_new() -> *mut OpaqueNonceStore {
Box::into_raw(Box::new(OpaqueNonceStore(MemoryNonceStore::new())))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dyolo_nonce_store_free(store: *mut OpaqueNonceStore) {
if !store.is_null() {
let _ = unsafe { Box::from_raw(store) };
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dyolo_cert_revoke(
store: *mut OpaqueRevocationStore,
fingerprint_hex: *const c_char,
) -> c_int {
let result = panic::catch_unwind(|| {
if store.is_null() || fingerprint_hex.is_null() {
return Err("null pointer argument".to_string());
}
let fp_str = unsafe { CStr::from_ptr(fingerprint_hex) }
.to_str()
.map_err(|e| format!("invalid utf-8: {e}"))?;
let fp_bytes: [u8; 32] = hex::decode(fp_str)
.map_err(|e| format!("invalid hex: {e}"))?
.try_into()
.map_err(|_| "fingerprint must be 32 bytes".to_string())?;
use crate::registry::RevocationStore;
unsafe { &(*store).0 }
.revoke(&fp_bytes)
.map_err(|e| e.to_string())
});
match result {
Ok(Ok(())) => KyaStatus::KyaOk as c_int,
Ok(Err(msg)) => {
set_last_error(msg);
KyaStatus::KyaErrUnknown as c_int
}
Err(_) => {
set_last_error("internal panic");
KyaStatus::KyaErrPanic as c_int
}
}
}
#[cfg(feature = "wire")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dyolo_authorize_json(
rev_store: *mut OpaqueRevocationStore,
nonce_store: *mut OpaqueNonceStore,
chain_json: *const c_char,
agent_pk_hex: *const c_char,
intent_action: *const c_char,
mac_key: *const u8,
out_buf: *mut c_char,
out_buf_len: usize,
) -> c_int {
let result = panic::catch_unwind(|| {
if chain_json.is_null()
|| agent_pk_hex.is_null()
|| intent_action.is_null()
|| mac_key.is_null()
|| out_buf.is_null()
|| out_buf_len == 0
{
return Err("null or zero-length argument".to_string());
}
if rev_store.is_null() || nonce_store.is_null() {
return Err("null store pointer argument".to_string());
}
let chain_str = unsafe { CStr::from_ptr(chain_json) }
.to_str()
.map_err(|e| format!("chain_json is not valid UTF-8: {e}"))?;
let pk_hex = unsafe { CStr::from_ptr(agent_pk_hex) }
.to_str()
.map_err(|e| format!("agent_pk_hex is not valid UTF-8: {e}"))?;
let action = unsafe { CStr::from_ptr(intent_action) }
.to_str()
.map_err(|e| format!("intent_action is not valid UTF-8: {e}"))?;
let mac: [u8; 32] = unsafe { std::slice::from_raw_parts(mac_key, 32) }
.try_into()
.map_err(|_| "mac_key must be 32 bytes".to_string())?;
let pk_bytes: [u8; 32] = hex::decode(pk_hex)
.map_err(|e| format!("invalid agent_pk_hex: {e}"))?
.try_into()
.map_err(|_| "agent_pk must be 32 bytes".to_string())?;
let agent_pk = ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes)
.map_err(|e| format!("invalid agent public key: {e}"))?;
let signed: crate::wire::SignedChain =
serde_json::from_str(chain_str).map_err(|e| format!("chain_json parse error: {e}"))?;
#[allow(deprecated)]
let chain = signed
.into_chain()
.map_err(|e| format!("chain conversion error: {e}"))?;
let intent = Intent::new(action).map_err(|e| format!("intent error: {e}"))?;
let intent_hash = intent.hash();
let action_result = chain
.authorize(
&agent_pk,
&intent_hash,
&MerkleProof::default(), &SystemClock,
unsafe { &(*rev_store).0 },
unsafe { &(*nonce_store).0 },
)
.map_err(|e| format!("{e}"))?;
let token = crate::wire::VerifiedToken::sign(&action_result.receipt, &mac);
serde_json::to_string(&token).map_err(|e| format!("token serialization: {e}"))
});
match result {
Ok(Ok(json)) => {
let cstr = CString::new(json).unwrap_or_default();
let bytes = cstr.as_bytes_with_nul();
if bytes.len() > out_buf_len {
set_last_error(format!(
"output buffer too small: need {}, got {out_buf_len}",
bytes.len()
));
return KyaStatus::KyaErrUnknown as c_int;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_buf as *mut u8, bytes.len())
};
KyaStatus::KyaOk as c_int
}
Ok(Err(msg)) => {
set_last_error(msg);
KyaStatus::KyaErrUnknown as c_int
}
Err(_panic) => {
set_last_error("internal panic in dyolo_authorize_json");
KyaStatus::KyaErrPanic as c_int
}
}
}
#[cfg(feature = "wire")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dyolo_authorize_receipt_json(
rev_store: *mut OpaqueRevocationStore,
nonce_store: *mut OpaqueNonceStore,
chain_json: *const c_char,
agent_pk_hex: *const c_char,
intent_action: *const c_char,
out_buf: *mut c_char,
out_buf_len: usize,
) -> c_int {
let result = panic::catch_unwind(|| {
if chain_json.is_null()
|| agent_pk_hex.is_null()
|| intent_action.is_null()
|| out_buf.is_null()
|| out_buf_len == 0
|| rev_store.is_null()
|| nonce_store.is_null()
{
return Err("null or zero-length argument".to_string());
}
let chain_str = unsafe { CStr::from_ptr(chain_json) }
.to_str()
.map_err(|e| format!("chain_json: {e}"))?;
let pk_hex = unsafe { CStr::from_ptr(agent_pk_hex) }
.to_str()
.map_err(|e| format!("agent_pk_hex: {e}"))?;
let action = unsafe { CStr::from_ptr(intent_action) }
.to_str()
.map_err(|e| format!("intent_action: {e}"))?;
let pk_bytes: [u8; 32] = hex::decode(pk_hex)
.map_err(|e| format!("invalid agent_pk_hex: {e}"))?
.try_into()
.map_err(|_| "agent_pk must be 32 bytes".to_string())?;
let agent_pk = ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes)
.map_err(|e| format!("invalid agent public key: {e}"))?;
let signed: crate::wire::SignedChain =
serde_json::from_str(chain_str).map_err(|e| format!("chain_json parse error: {e}"))?;
#[allow(deprecated)]
let chain = signed.into_chain().map_err(|e| format!("{e}"))?;
let intent = Intent::new(action).map_err(|e| format!("intent error: {e}"))?;
let intent_hash = intent.hash();
let authorized = chain
.authorize(
&agent_pk,
&intent_hash,
&MerkleProof::default(),
&SystemClock,
unsafe { &(*rev_store).0 },
unsafe { &(*nonce_store).0 },
)
.map_err(|e| format!("{e}"))?;
serde_json::to_string(&authorized.receipt)
.map_err(|e| format!("receipt serialization: {e}"))
});
match result {
Ok(Ok(json)) => {
let cstr = CString::new(json).unwrap_or_default();
let bytes = cstr.as_bytes_with_nul();
if bytes.len() > out_buf_len {
set_last_error(format!(
"output buffer too small: need {}, got {out_buf_len}",
bytes.len()
));
return KyaStatus::KyaErrUnknown as c_int;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_buf as *mut u8, bytes.len())
};
KyaStatus::KyaOk as c_int
}
Ok(Err(msg)) => {
set_last_error(msg);
KyaStatus::KyaErrUnknown as c_int
}
Err(_) => {
set_last_error("internal panic");
KyaStatus::KyaErrPanic as c_int
}
}
}
#[cfg(feature = "wire")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dyolo_authorize_with_proof_json(
rev_store: *mut OpaqueRevocationStore,
nonce_store: *mut OpaqueNonceStore,
chain_json: *const c_char,
agent_pk_hex: *const c_char,
intent_action: *const c_char,
proof_json: *const c_char,
mac_key: *const u8,
out_buf: *mut c_char,
out_buf_len: usize,
) -> c_int {
let result = panic::catch_unwind(|| {
if chain_json.is_null()
|| agent_pk_hex.is_null()
|| intent_action.is_null()
|| mac_key.is_null()
|| out_buf.is_null()
|| out_buf_len == 0
|| proof_json.is_null()
|| rev_store.is_null()
|| nonce_store.is_null()
{
return Err("null argument".to_string());
}
let proof_str = unsafe { CStr::from_ptr(proof_json) }
.to_str()
.map_err(|e| e.to_string())?;
let proof: MerkleProof = serde_json::from_str(proof_str).map_err(|e| e.to_string())?;
let chain_str = unsafe { CStr::from_ptr(chain_json) }
.to_str()
.map_err(|e| e.to_string())?;
let action = unsafe { CStr::from_ptr(intent_action) }
.to_str()
.map_err(|e| e.to_string())?;
let pk_hex = unsafe { CStr::from_ptr(agent_pk_hex) }
.to_str()
.map_err(|e| e.to_string())?;
let pk_bytes: [u8; 32] = hex::decode(pk_hex)
.map_err(|e| e.to_string())?
.try_into()
.map_err(|_| "32 bytes".to_string())?;
let agent_pk =
ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes).map_err(|e| e.to_string())?;
let mac: [u8; 32] = unsafe { std::slice::from_raw_parts(mac_key, 32) }
.try_into()
.map_err(|_| "32 bytes".to_string())?;
let signed: crate::wire::SignedChain =
serde_json::from_str(chain_str).map_err(|e| e.to_string())?;
#[allow(deprecated)]
let chain = signed.into_chain().map_err(|e| e.to_string())?;
let intent = Intent::new(action).map_err(|e| e.to_string())?;
let intent_hash = intent.hash();
let action_result = chain
.authorize(
&agent_pk,
&intent_hash,
&proof,
&SystemClock,
unsafe { &(*rev_store).0 },
unsafe { &(*nonce_store).0 },
)
.map_err(|e| e.to_string())?;
let token = crate::wire::VerifiedToken::sign(&action_result.receipt, &mac);
serde_json::to_string(&token).map_err(|e| e.to_string())
});
match result {
Ok(Ok(json)) => {
let cstr = CString::new(json).unwrap_or_default();
let bytes = cstr.as_bytes_with_nul();
if bytes.len() > out_buf_len {
return KyaStatus::KyaErrUnknown as c_int;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_buf as *mut u8, bytes.len())
};
KyaStatus::KyaOk as c_int
}
Ok(Err(msg)) => {
set_last_error(msg);
KyaStatus::KyaErrUnknown as c_int
}
Err(_) => KyaStatus::KyaErrPanic as c_int,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn dyolo_version() -> *const c_char {
concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr().cast()
}