use std::cell::RefCell;
use std::ffi::{c_char, c_int, CStr, CString};
use std::panic;
use crate::chain::{DyoloChain, SystemClock};
use crate::error::A1Error;
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);
});
}
fn a1_error_to_status(e: &A1Error) -> c_int {
match e {
A1Error::EmptyChain => A1Status::A1ErrEmptyChain as c_int,
A1Error::StorageFailure(_) => A1Status::A1ErrStorageFailure as c_int,
A1Error::RootMismatch => A1Status::A1ErrRootMismatch as c_int,
A1Error::BrokenLinkage(_) => A1Status::A1ErrBrokenLinkage as c_int,
A1Error::InvalidSignature(_) => A1Status::A1ErrInvalidSig as c_int,
A1Error::NotYetValid(..) => A1Status::A1ErrNotYetValid as c_int,
A1Error::Expired(..) => A1Status::A1ErrExpired as c_int,
A1Error::TemporalViolation(..) => A1Status::A1ErrTemporalViol as c_int,
A1Error::MaxDepthExceeded(..) => A1Status::A1ErrMaxDepth as c_int,
A1Error::InvalidSubScopeProof => A1Status::A1ErrInvalidProof as c_int,
A1Error::ScopeEscalation(_) => A1Status::A1ErrScopeEscal as c_int,
A1Error::UnauthorizedLeaf => A1Status::A1ErrUnauthorized as c_int,
A1Error::ScopeViolation => A1Status::A1ErrScopeViol as c_int,
A1Error::NonceReplay => A1Status::A1ErrNonceReplay as c_int,
A1Error::Revoked => A1Status::A1ErrRevoked as c_int,
A1Error::IntentNotFound => A1Status::A1ErrIntentNotFound as c_int,
A1Error::EmptyTree => A1Status::A1ErrEmptyTree as c_int,
A1Error::WireFormatError(_) => A1Status::A1ErrWireFormat as c_int,
A1Error::UnsupportedVersion { .. } => A1Status::A1ErrUnsupportedVer as c_int,
A1Error::PolicyViolation(_) => A1Status::A1ErrPolicyViolation as c_int,
A1Error::BatchItemFailed { .. } => A1Status::A1ErrBatchItemFailed as c_int,
A1Error::MacVerificationFailed => A1Status::A1ErrMacFailed as c_int,
A1Error::NamespaceMismatch { .. } => A1Status::A1ErrNamespaceMismatch as c_int,
A1Error::RateLimitExceeded => A1Status::A1ErrRateLimit as c_int,
A1Error::StorageUnhealthy(_) => A1Status::A1ErrStorageUnhealthy as c_int,
A1Error::PassportNarrowingViolation => A1Status::A1ErrPassportNarrowing as c_int,
_ => A1Status::A1ErrUnknown as c_int,
}
}
#[repr(C)]
pub enum A1Status {
A1Ok = 0,
A1ErrEmptyChain = 1,
A1ErrStorageFailure = 2,
A1ErrRootMismatch = 3,
A1ErrBrokenLinkage = 4,
A1ErrInvalidSig = 5,
A1ErrNotYetValid = 6,
A1ErrExpired = 7,
A1ErrTemporalViol = 8,
A1ErrMaxDepth = 9,
A1ErrInvalidProof = 10,
A1ErrScopeEscal = 11,
A1ErrUnauthorized = 12,
A1ErrScopeViol = 13,
A1ErrNonceReplay = 14,
A1ErrRevoked = 15,
A1ErrIntentNotFound = 16,
A1ErrEmptyTree = 17,
A1ErrWireFormat = 18,
A1ErrUnsupportedVer = 19,
A1ErrPolicyViolation = 20,
A1ErrBatchItemFailed = 21,
A1ErrMacFailed = 22,
A1ErrNamespaceMismatch = 23,
A1ErrRateLimit = 24,
A1ErrStorageUnhealthy = 25,
A1ErrPassportNarrowing = 26,
A1ErrPanic = 98,
A1ErrUnknown = 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 A1Status::A1ErrUnknown as c_int;
}
let vk = unsafe { (*identity).0.verifying_key() };
unsafe { std::ptr::copy_nonoverlapping(vk.as_bytes().as_ptr(), out, 32) };
A1Status::A1Ok 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(())) => A1Status::A1Ok as c_int,
Ok(Err(msg)) => {
set_last_error(msg);
A1Status::A1ErrUnknown as c_int
}
Err(_) => {
set_last_error("internal panic");
A1Status::A1ErrPanic 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(|| -> Result<String, (c_int, String)> {
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((
A1Status::A1ErrUnknown as c_int,
"null or zero-length argument".to_string(),
));
}
if rev_store.is_null() || nonce_store.is_null() {
return Err((
A1Status::A1ErrUnknown as c_int,
"null store pointer argument".to_string(),
));
}
let chain_str = unsafe { CStr::from_ptr(chain_json) }
.to_str()
.map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
format!("chain_json is not valid UTF-8: {e}"),
)
})?;
let pk_hex = unsafe { CStr::from_ptr(agent_pk_hex) }
.to_str()
.map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
format!("agent_pk_hex is not valid UTF-8: {e}"),
)
})?;
let action = unsafe { CStr::from_ptr(intent_action) }
.to_str()
.map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
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(|_| {
(
A1Status::A1ErrUnknown as c_int,
"mac_key must be 32 bytes".to_string(),
)
})?;
let pk_bytes: [u8; 32] = hex::decode(pk_hex)
.map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
format!("invalid agent_pk_hex: {e}"),
)
})?
.try_into()
.map_err(|_| {
(
A1Status::A1ErrUnknown as c_int,
"agent_pk must be 32 bytes".to_string(),
)
})?;
let agent_pk = ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes).map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
format!("invalid agent public key: {e}"),
)
})?;
let signed: crate::wire::SignedChain = serde_json::from_str(chain_str).map_err(|e| {
(
A1Status::A1ErrWireFormat as c_int,
format!("chain_json parse error: {e}"),
)
})?;
#[allow(deprecated)]
let chain = signed.into_chain().map_err(|e| {
(
A1Status::A1ErrWireFormat as c_int,
format!("chain conversion error: {e}"),
)
})?;
let intent = Intent::new(action).map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
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| (a1_error_to_status(&e), e.to_string()))?;
let token = crate::wire::VerifiedToken::sign(&action_result.receipt, &mac);
serde_json::to_string(&token).map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
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 A1Status::A1ErrUnknown as c_int;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_buf as *mut u8, bytes.len())
};
A1Status::A1Ok as c_int
}
Ok(Err((code, msg))) => {
set_last_error(msg);
code
}
Err(_panic) => {
set_last_error("internal panic in dyolo_authorize_json");
A1Status::A1ErrPanic 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(|| -> Result<String, (c_int, String)> {
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((
A1Status::A1ErrUnknown as c_int,
"null or zero-length argument".to_string(),
));
}
let chain_str = unsafe { CStr::from_ptr(chain_json) }
.to_str()
.map_err(|e| (A1Status::A1ErrUnknown as c_int, format!("chain_json: {e}")))?;
let pk_hex = unsafe { CStr::from_ptr(agent_pk_hex) }
.to_str()
.map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
format!("agent_pk_hex: {e}"),
)
})?;
let action = unsafe { CStr::from_ptr(intent_action) }
.to_str()
.map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
format!("intent_action: {e}"),
)
})?;
let pk_bytes: [u8; 32] = hex::decode(pk_hex)
.map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
format!("invalid agent_pk_hex: {e}"),
)
})?
.try_into()
.map_err(|_| {
(
A1Status::A1ErrUnknown as c_int,
"agent_pk must be 32 bytes".to_string(),
)
})?;
let agent_pk = ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes).map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
format!("invalid agent public key: {e}"),
)
})?;
let signed: crate::wire::SignedChain = serde_json::from_str(chain_str).map_err(|e| {
(
A1Status::A1ErrWireFormat as c_int,
format!("chain_json parse error: {e}"),
)
})?;
#[allow(deprecated)]
let chain = signed
.into_chain()
.map_err(|e| (A1Status::A1ErrWireFormat as c_int, format!("{e}")))?;
let intent = Intent::new(action).map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
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| (a1_error_to_status(&e), e.to_string()))?;
serde_json::to_string(&authorized.receipt).map_err(|e| {
(
A1Status::A1ErrUnknown as c_int,
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 A1Status::A1ErrUnknown as c_int;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_buf as *mut u8, bytes.len())
};
A1Status::A1Ok as c_int
}
Ok(Err((code, msg))) => {
set_last_error(msg);
code
}
Err(_) => {
set_last_error("internal panic in dyolo_authorize_receipt_json");
A1Status::A1ErrPanic 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 A1Status::A1ErrUnknown as c_int;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_buf as *mut u8, bytes.len())
};
A1Status::A1Ok as c_int
}
Ok(Err(msg)) => {
set_last_error(msg);
A1Status::A1ErrUnknown as c_int
}
Err(_) => A1Status::A1ErrPanic as c_int,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn dyolo_version() -> *const c_char {
concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr().cast()
}