1use std::cell::RefCell;
46use std::ffi::{c_char, c_int, CStr, CString};
47use std::panic;
48
49use crate::chain::{DyoloChain, SystemClock};
50use crate::error::A1Error;
51use crate::identity::DyoloIdentity;
52use crate::intent::{Intent, MerkleProof};
53use crate::registry::{MemoryNonceStore, MemoryRevocationStore};
54
55thread_local! {
57 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
58}
59
60fn set_last_error(msg: impl Into<Vec<u8>>) {
61 LAST_ERROR.with(|e| {
62 let s =
63 CString::new(msg).unwrap_or_else(|_| CString::new("error contains nul byte").unwrap());
64 *e.borrow_mut() = Some(s);
65 });
66}
67
68fn a1_error_to_status(e: &A1Error) -> c_int {
69 match e {
70 A1Error::EmptyChain => A1Status::A1ErrEmptyChain as c_int,
71 A1Error::StorageFailure(_) => A1Status::A1ErrStorageFailure as c_int,
72 A1Error::RootMismatch => A1Status::A1ErrRootMismatch as c_int,
73 A1Error::BrokenLinkage(_) => A1Status::A1ErrBrokenLinkage as c_int,
74 A1Error::InvalidSignature(_) => A1Status::A1ErrInvalidSig as c_int,
75 A1Error::NotYetValid(..) => A1Status::A1ErrNotYetValid as c_int,
76 A1Error::Expired(..) => A1Status::A1ErrExpired as c_int,
77 A1Error::TemporalViolation(..) => A1Status::A1ErrTemporalViol as c_int,
78 A1Error::MaxDepthExceeded(..) => A1Status::A1ErrMaxDepth as c_int,
79 A1Error::InvalidSubScopeProof => A1Status::A1ErrInvalidProof as c_int,
80 A1Error::ScopeEscalation(_) => A1Status::A1ErrScopeEscal as c_int,
81 A1Error::UnauthorizedLeaf => A1Status::A1ErrUnauthorized as c_int,
82 A1Error::ScopeViolation => A1Status::A1ErrScopeViol as c_int,
83 A1Error::NonceReplay => A1Status::A1ErrNonceReplay as c_int,
84 A1Error::Revoked => A1Status::A1ErrRevoked as c_int,
85 A1Error::IntentNotFound => A1Status::A1ErrIntentNotFound as c_int,
86 A1Error::EmptyTree => A1Status::A1ErrEmptyTree as c_int,
87 A1Error::WireFormatError(_) => A1Status::A1ErrWireFormat as c_int,
88 A1Error::UnsupportedVersion { .. } => A1Status::A1ErrUnsupportedVer as c_int,
89 A1Error::PolicyViolation(_) => A1Status::A1ErrPolicyViolation as c_int,
90 A1Error::BatchItemFailed { .. } => A1Status::A1ErrBatchItemFailed as c_int,
91 A1Error::MacVerificationFailed => A1Status::A1ErrMacFailed as c_int,
92 A1Error::NamespaceMismatch { .. } => A1Status::A1ErrNamespaceMismatch as c_int,
93 A1Error::RateLimitExceeded => A1Status::A1ErrRateLimit as c_int,
94 A1Error::StorageUnhealthy(_) => A1Status::A1ErrStorageUnhealthy as c_int,
95 A1Error::PassportNarrowingViolation => A1Status::A1ErrPassportNarrowing as c_int,
96 _ => A1Status::A1ErrUnknown as c_int,
97 }
98}
99
100#[repr(C)]
108pub enum A1Status {
109 A1Ok = 0,
111 A1ErrEmptyChain = 1,
113 A1ErrStorageFailure = 2,
115 A1ErrRootMismatch = 3,
117 A1ErrBrokenLinkage = 4,
119 A1ErrInvalidSig = 5,
121 A1ErrNotYetValid = 6,
123 A1ErrExpired = 7,
125 A1ErrTemporalViol = 8,
127 A1ErrMaxDepth = 9,
129 A1ErrInvalidProof = 10,
131 A1ErrScopeEscal = 11,
133 A1ErrUnauthorized = 12,
135 A1ErrScopeViol = 13,
137 A1ErrNonceReplay = 14,
139 A1ErrRevoked = 15,
141 A1ErrIntentNotFound = 16,
143 A1ErrEmptyTree = 17,
145 A1ErrWireFormat = 18,
147 A1ErrUnsupportedVer = 19,
149 A1ErrPolicyViolation = 20,
151 A1ErrBatchItemFailed = 21,
153 A1ErrMacFailed = 22,
155 A1ErrNamespaceMismatch = 23,
157 A1ErrRateLimit = 24,
159 A1ErrStorageUnhealthy = 25,
161 A1ErrPassportNarrowing = 26,
163 A1ErrPanic = 98,
165 A1ErrUnknown = 99,
167}
168
169pub struct OpaqueIdentity(DyoloIdentity);
171
172pub struct OpaqueRevocationStore(MemoryRevocationStore);
174
175pub struct OpaqueNonceStore(MemoryNonceStore);
177
178#[allow(dead_code)]
180pub struct OpaqueChain {
181 chain: DyoloChain,
182 rev: MemoryRevocationStore,
183 nonces: MemoryNonceStore,
184}
185
186#[unsafe(no_mangle)]
206pub unsafe extern "C" fn dyolo_last_error() -> *const c_char {
207 LAST_ERROR.with(|e| e.borrow().as_ref().map_or(std::ptr::null(), |s| s.as_ptr()))
208}
209
210#[unsafe(no_mangle)]
218pub extern "C" fn dyolo_identity_generate() -> *mut OpaqueIdentity {
219 Box::into_raw(Box::new(OpaqueIdentity(DyoloIdentity::generate())))
220}
221
222#[unsafe(no_mangle)]
232pub unsafe extern "C" fn dyolo_identity_from_seed(seed: *const u8) -> *mut OpaqueIdentity {
233 if seed.is_null() {
234 set_last_error("dyolo_identity_from_seed: seed pointer is null");
235 return std::ptr::null_mut();
236 }
237 let bytes: [u8; 32] = unsafe { std::slice::from_raw_parts(seed, 32) }
238 .try_into()
239 .expect("seed is always 32 bytes");
240 Box::into_raw(Box::new(OpaqueIdentity(DyoloIdentity::from_signing_bytes(
241 &bytes,
242 ))))
243}
244
245#[unsafe(no_mangle)]
254pub unsafe extern "C" fn dyolo_identity_verifying_key(
255 identity: *const OpaqueIdentity,
256 out: *mut u8,
257) -> c_int {
258 if identity.is_null() || out.is_null() {
259 set_last_error("null pointer argument");
260 return A1Status::A1ErrUnknown as c_int;
261 }
262 let vk = unsafe { (*identity).0.verifying_key() };
263 unsafe { std::ptr::copy_nonoverlapping(vk.as_bytes().as_ptr(), out, 32) };
264 A1Status::A1Ok as c_int
265}
266
267#[unsafe(no_mangle)]
274pub unsafe extern "C" fn dyolo_identity_free(identity: *mut OpaqueIdentity) {
275 if !identity.is_null() {
276 let _ = unsafe { Box::from_raw(identity) };
277 }
278}
279
280#[unsafe(no_mangle)]
284pub extern "C" fn dyolo_revocation_store_new() -> *mut OpaqueRevocationStore {
285 Box::into_raw(Box::new(
286 OpaqueRevocationStore(MemoryRevocationStore::new()),
287 ))
288}
289
290#[unsafe(no_mangle)]
297pub unsafe extern "C" fn dyolo_revocation_store_free(store: *mut OpaqueRevocationStore) {
298 if !store.is_null() {
299 let _ = unsafe { Box::from_raw(store) };
300 }
301}
302
303#[unsafe(no_mangle)]
305pub extern "C" fn dyolo_nonce_store_new() -> *mut OpaqueNonceStore {
306 Box::into_raw(Box::new(OpaqueNonceStore(MemoryNonceStore::new())))
307}
308
309#[unsafe(no_mangle)]
316pub unsafe extern "C" fn dyolo_nonce_store_free(store: *mut OpaqueNonceStore) {
317 if !store.is_null() {
318 let _ = unsafe { Box::from_raw(store) };
319 }
320}
321
322#[unsafe(no_mangle)]
332pub unsafe extern "C" fn dyolo_cert_revoke(
333 store: *mut OpaqueRevocationStore,
334 fingerprint_hex: *const c_char,
335) -> c_int {
336 let result = panic::catch_unwind(|| {
337 if store.is_null() || fingerprint_hex.is_null() {
338 return Err("null pointer argument".to_string());
339 }
340 let fp_str = unsafe { CStr::from_ptr(fingerprint_hex) }
341 .to_str()
342 .map_err(|e| format!("invalid utf-8: {e}"))?;
343 let fp_bytes: [u8; 32] = hex::decode(fp_str)
344 .map_err(|e| format!("invalid hex: {e}"))?
345 .try_into()
346 .map_err(|_| "fingerprint must be 32 bytes".to_string())?;
347
348 use crate::registry::RevocationStore;
349 unsafe { &(*store).0 }
350 .revoke(&fp_bytes)
351 .map_err(|e| e.to_string())
352 });
353
354 match result {
355 Ok(Ok(())) => A1Status::A1Ok as c_int,
356 Ok(Err(msg)) => {
357 set_last_error(msg);
358 A1Status::A1ErrUnknown as c_int
359 }
360 Err(_) => {
361 set_last_error("internal panic");
362 A1Status::A1ErrPanic as c_int
363 }
364 }
365}
366
367#[cfg(feature = "wire")]
389#[unsafe(no_mangle)]
390pub unsafe extern "C" fn dyolo_authorize_json(
391 rev_store: *mut OpaqueRevocationStore,
392 nonce_store: *mut OpaqueNonceStore,
393 chain_json: *const c_char,
394 agent_pk_hex: *const c_char,
395 intent_action: *const c_char,
396 mac_key: *const u8,
397 out_buf: *mut c_char,
398 out_buf_len: usize,
399) -> c_int {
400 let result = panic::catch_unwind(|| -> Result<String, (c_int, String)> {
401 if chain_json.is_null()
403 || agent_pk_hex.is_null()
404 || intent_action.is_null()
405 || mac_key.is_null()
406 || out_buf.is_null()
407 || out_buf_len == 0
408 {
409 return Err((
410 A1Status::A1ErrUnknown as c_int,
411 "null or zero-length argument".to_string(),
412 ));
413 }
414
415 if rev_store.is_null() || nonce_store.is_null() {
416 return Err((
417 A1Status::A1ErrUnknown as c_int,
418 "null store pointer argument".to_string(),
419 ));
420 }
421
422 let chain_str = unsafe { CStr::from_ptr(chain_json) }
423 .to_str()
424 .map_err(|e| {
425 (
426 A1Status::A1ErrUnknown as c_int,
427 format!("chain_json is not valid UTF-8: {e}"),
428 )
429 })?;
430
431 let pk_hex = unsafe { CStr::from_ptr(agent_pk_hex) }
432 .to_str()
433 .map_err(|e| {
434 (
435 A1Status::A1ErrUnknown as c_int,
436 format!("agent_pk_hex is not valid UTF-8: {e}"),
437 )
438 })?;
439
440 let action = unsafe { CStr::from_ptr(intent_action) }
441 .to_str()
442 .map_err(|e| {
443 (
444 A1Status::A1ErrUnknown as c_int,
445 format!("intent_action is not valid UTF-8: {e}"),
446 )
447 })?;
448
449 let mac: [u8; 32] = unsafe { std::slice::from_raw_parts(mac_key, 32) }
450 .try_into()
451 .map_err(|_| {
452 (
453 A1Status::A1ErrUnknown as c_int,
454 "mac_key must be 32 bytes".to_string(),
455 )
456 })?;
457
458 let pk_bytes: [u8; 32] = hex::decode(pk_hex)
460 .map_err(|e| {
461 (
462 A1Status::A1ErrUnknown as c_int,
463 format!("invalid agent_pk_hex: {e}"),
464 )
465 })?
466 .try_into()
467 .map_err(|_| {
468 (
469 A1Status::A1ErrUnknown as c_int,
470 "agent_pk must be 32 bytes".to_string(),
471 )
472 })?;
473 let agent_pk = ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes).map_err(|e| {
474 (
475 A1Status::A1ErrUnknown as c_int,
476 format!("invalid agent public key: {e}"),
477 )
478 })?;
479
480 let signed: crate::wire::SignedChain = serde_json::from_str(chain_str).map_err(|e| {
482 (
483 A1Status::A1ErrWireFormat as c_int,
484 format!("chain_json parse error: {e}"),
485 )
486 })?;
487
488 #[allow(deprecated)]
489 let chain = signed.into_chain().map_err(|e| {
490 (
491 A1Status::A1ErrWireFormat as c_int,
492 format!("chain conversion error: {e}"),
493 )
494 })?;
495
496 let intent = Intent::new(action).map_err(|e| {
498 (
499 A1Status::A1ErrUnknown as c_int,
500 format!("intent error: {e}"),
501 )
502 })?;
503 let intent_hash = intent.hash();
504
505 let action_result = chain
506 .authorize(
507 &agent_pk,
508 &intent_hash,
509 &MerkleProof::default(), &SystemClock,
511 unsafe { &(*rev_store).0 },
512 unsafe { &(*nonce_store).0 },
513 )
514 .map_err(|e| (a1_error_to_status(&e), e.to_string()))?;
515
516 let token = crate::wire::VerifiedToken::sign(&action_result.receipt, &mac);
517 serde_json::to_string(&token).map_err(|e| {
518 (
519 A1Status::A1ErrUnknown as c_int,
520 format!("token serialization: {e}"),
521 )
522 })
523 });
524
525 match result {
526 Ok(Ok(json)) => {
527 let cstr = CString::new(json).unwrap_or_default();
528 let bytes = cstr.as_bytes_with_nul();
529 if bytes.len() > out_buf_len {
530 set_last_error(format!(
531 "output buffer too small: need {}, got {out_buf_len}",
532 bytes.len()
533 ));
534 return A1Status::A1ErrUnknown as c_int;
535 }
536 unsafe {
537 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_buf as *mut u8, bytes.len())
538 };
539 A1Status::A1Ok as c_int
540 }
541 Ok(Err((code, msg))) => {
542 set_last_error(msg);
543 code
544 }
545 Err(_panic) => {
546 set_last_error("internal panic in dyolo_authorize_json");
547 A1Status::A1ErrPanic as c_int
548 }
549 }
550}
551
552#[cfg(feature = "wire")]
563#[unsafe(no_mangle)]
564pub unsafe extern "C" fn dyolo_authorize_receipt_json(
565 rev_store: *mut OpaqueRevocationStore,
566 nonce_store: *mut OpaqueNonceStore,
567 chain_json: *const c_char,
568 agent_pk_hex: *const c_char,
569 intent_action: *const c_char,
570 out_buf: *mut c_char,
571 out_buf_len: usize,
572) -> c_int {
573 let result = panic::catch_unwind(|| -> Result<String, (c_int, String)> {
574 if chain_json.is_null()
575 || agent_pk_hex.is_null()
576 || intent_action.is_null()
577 || out_buf.is_null()
578 || out_buf_len == 0
579 || rev_store.is_null()
580 || nonce_store.is_null()
581 {
582 return Err((
583 A1Status::A1ErrUnknown as c_int,
584 "null or zero-length argument".to_string(),
585 ));
586 }
587
588 let chain_str = unsafe { CStr::from_ptr(chain_json) }
589 .to_str()
590 .map_err(|e| (A1Status::A1ErrUnknown as c_int, format!("chain_json: {e}")))?;
591 let pk_hex = unsafe { CStr::from_ptr(agent_pk_hex) }
592 .to_str()
593 .map_err(|e| {
594 (
595 A1Status::A1ErrUnknown as c_int,
596 format!("agent_pk_hex: {e}"),
597 )
598 })?;
599 let action = unsafe { CStr::from_ptr(intent_action) }
600 .to_str()
601 .map_err(|e| {
602 (
603 A1Status::A1ErrUnknown as c_int,
604 format!("intent_action: {e}"),
605 )
606 })?;
607
608 let pk_bytes: [u8; 32] = hex::decode(pk_hex)
609 .map_err(|e| {
610 (
611 A1Status::A1ErrUnknown as c_int,
612 format!("invalid agent_pk_hex: {e}"),
613 )
614 })?
615 .try_into()
616 .map_err(|_| {
617 (
618 A1Status::A1ErrUnknown as c_int,
619 "agent_pk must be 32 bytes".to_string(),
620 )
621 })?;
622 let agent_pk = ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes).map_err(|e| {
623 (
624 A1Status::A1ErrUnknown as c_int,
625 format!("invalid agent public key: {e}"),
626 )
627 })?;
628
629 let signed: crate::wire::SignedChain = serde_json::from_str(chain_str).map_err(|e| {
630 (
631 A1Status::A1ErrWireFormat as c_int,
632 format!("chain_json parse error: {e}"),
633 )
634 })?;
635
636 #[allow(deprecated)]
637 let chain = signed
638 .into_chain()
639 .map_err(|e| (A1Status::A1ErrWireFormat as c_int, format!("{e}")))?;
640
641 let intent = Intent::new(action).map_err(|e| {
642 (
643 A1Status::A1ErrUnknown as c_int,
644 format!("intent error: {e}"),
645 )
646 })?;
647 let intent_hash = intent.hash();
648
649 let authorized = chain
650 .authorize(
651 &agent_pk,
652 &intent_hash,
653 &MerkleProof::default(),
654 &SystemClock,
655 unsafe { &(*rev_store).0 },
656 unsafe { &(*nonce_store).0 },
657 )
658 .map_err(|e| (a1_error_to_status(&e), e.to_string()))?;
659
660 serde_json::to_string(&authorized.receipt).map_err(|e| {
661 (
662 A1Status::A1ErrUnknown as c_int,
663 format!("receipt serialization: {e}"),
664 )
665 })
666 });
667
668 match result {
669 Ok(Ok(json)) => {
670 let cstr = CString::new(json).unwrap_or_default();
671 let bytes = cstr.as_bytes_with_nul();
672 if bytes.len() > out_buf_len {
673 set_last_error(format!(
674 "output buffer too small: need {}, got {out_buf_len}",
675 bytes.len()
676 ));
677 return A1Status::A1ErrUnknown as c_int;
678 }
679 unsafe {
680 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_buf as *mut u8, bytes.len())
681 };
682 A1Status::A1Ok as c_int
683 }
684 Ok(Err((code, msg))) => {
685 set_last_error(msg);
686 code
687 }
688 Err(_) => {
689 set_last_error("internal panic in dyolo_authorize_receipt_json");
690 A1Status::A1ErrPanic as c_int
691 }
692 }
693}
694
695#[cfg(feature = "wire")]
704#[unsafe(no_mangle)]
705pub unsafe extern "C" fn dyolo_authorize_with_proof_json(
706 rev_store: *mut OpaqueRevocationStore,
707 nonce_store: *mut OpaqueNonceStore,
708 chain_json: *const c_char,
709 agent_pk_hex: *const c_char,
710 intent_action: *const c_char,
711 proof_json: *const c_char,
712 mac_key: *const u8,
713 out_buf: *mut c_char,
714 out_buf_len: usize,
715) -> c_int {
716 let result = panic::catch_unwind(|| {
717 if chain_json.is_null()
718 || agent_pk_hex.is_null()
719 || intent_action.is_null()
720 || mac_key.is_null()
721 || out_buf.is_null()
722 || out_buf_len == 0
723 || proof_json.is_null()
724 || rev_store.is_null()
725 || nonce_store.is_null()
726 {
727 return Err("null argument".to_string());
728 }
729
730 let proof_str = unsafe { CStr::from_ptr(proof_json) }
731 .to_str()
732 .map_err(|e| e.to_string())?;
733 let proof: MerkleProof = serde_json::from_str(proof_str).map_err(|e| e.to_string())?;
734
735 let chain_str = unsafe { CStr::from_ptr(chain_json) }
736 .to_str()
737 .map_err(|e| e.to_string())?;
738 let action = unsafe { CStr::from_ptr(intent_action) }
739 .to_str()
740 .map_err(|e| e.to_string())?;
741
742 let pk_hex = unsafe { CStr::from_ptr(agent_pk_hex) }
743 .to_str()
744 .map_err(|e| e.to_string())?;
745 let pk_bytes: [u8; 32] = hex::decode(pk_hex)
746 .map_err(|e| e.to_string())?
747 .try_into()
748 .map_err(|_| "32 bytes".to_string())?;
749 let agent_pk =
750 ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes).map_err(|e| e.to_string())?;
751
752 let mac: [u8; 32] = unsafe { std::slice::from_raw_parts(mac_key, 32) }
753 .try_into()
754 .map_err(|_| "32 bytes".to_string())?;
755
756 let signed: crate::wire::SignedChain =
757 serde_json::from_str(chain_str).map_err(|e| e.to_string())?;
758
759 #[allow(deprecated)]
760 let chain = signed.into_chain().map_err(|e| e.to_string())?;
761
762 let intent = Intent::new(action).map_err(|e| e.to_string())?;
763 let intent_hash = intent.hash();
764
765 let action_result = chain
766 .authorize(
767 &agent_pk,
768 &intent_hash,
769 &proof,
770 &SystemClock,
771 unsafe { &(*rev_store).0 },
772 unsafe { &(*nonce_store).0 },
773 )
774 .map_err(|e| e.to_string())?;
775
776 let token = crate::wire::VerifiedToken::sign(&action_result.receipt, &mac);
777 serde_json::to_string(&token).map_err(|e| e.to_string())
778 });
779
780 match result {
781 Ok(Ok(json)) => {
782 let cstr = CString::new(json).unwrap_or_default();
783 let bytes = cstr.as_bytes_with_nul();
784 if bytes.len() > out_buf_len {
785 return A1Status::A1ErrUnknown as c_int;
786 }
787 unsafe {
788 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out_buf as *mut u8, bytes.len())
789 };
790 A1Status::A1Ok as c_int
791 }
792 Ok(Err(msg)) => {
793 set_last_error(msg);
794 A1Status::A1ErrUnknown as c_int
795 }
796 Err(_) => A1Status::A1ErrPanic as c_int,
797 }
798}
799
800#[unsafe(no_mangle)]
808pub extern "C" fn dyolo_version() -> *const c_char {
809 concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr().cast()
811}