1#![allow(clippy::not_unsafe_ptr_arg_deref)]
5
6use std::ffi::CStr;
28use std::ffi::CString;
29use std::os::raw::c_char;
30
31use snafu::ResultExt;
32
33use crate::error;
34use crate::error::Result as TcResult;
35use crate::message::Message;
36use crate::party::{Party, PartyList};
37use crate::session::{Session, SessionParams};
38use crate::share::Share;
39
40pub enum FFITcSession {}
42
43#[repr(C)]
49pub struct CFMTcPartyList {
50 pub party_ids: *const *const c_char,
51 pub transport_endpoints: *const *const c_char,
52 pub count: u32,
53}
54
55#[repr(C)]
58pub struct CFMTcMessage {
59 pub from_party_id: *const c_char,
60 pub to_party_id: *const c_char,
61 pub round: u8,
62 pub payload: *const u8,
63 pub payload_len: u32,
64}
65
66#[repr(C)]
68pub struct CFMTcShare {
69 pub scheme: *const c_char,
70 pub bytes: *const u8,
71 pub len: u32,
72}
73
74#[repr(C)]
78pub struct CFMTcMessageArray {
79 pub items: *mut CFMTcMessage,
80 pub count: u32,
81}
82
83fn cstr_to_string(ptr: *const c_char, param: &'static str) -> TcResult<String> {
84 if ptr.is_null() {
85 return error::NullPointerSnafu { param }.fail();
86 }
87 unsafe { CStr::from_ptr(ptr) }
88 .to_str()
89 .context(error::InvalidUTF8Snafu {})
90 .map(str::to_string)
91}
92
93fn opt_cstr_to_string(ptr: *const c_char) -> TcResult<Option<String>> {
94 if ptr.is_null() {
95 return Ok(None);
96 }
97 unsafe { CStr::from_ptr(ptr) }
98 .to_str()
99 .context(error::InvalidUTF8Snafu {})
100 .map(|s| Some(s.to_string()))
101}
102
103fn party_list_from_c(cpl: &CFMTcPartyList) -> TcResult<PartyList> {
105 let count = cpl.count as usize;
106 let ids = if cpl.party_ids.is_null() {
107 return error::NullPointerSnafu { param: "party_ids" }.fail();
108 } else {
109 unsafe { std::slice::from_raw_parts(cpl.party_ids, count) }
110 };
111 let endpoints = if cpl.transport_endpoints.is_null() {
112 vec![std::ptr::null::<c_char>(); count]
114 } else {
115 unsafe { std::slice::from_raw_parts(cpl.transport_endpoints, count) }.to_vec()
116 };
117 if endpoints.len() != count {
118 return error::NullPointerSnafu {
119 param: "transport_endpoints",
120 }
121 .fail();
122 }
123 let mut parties = Vec::with_capacity(count);
124 for (i, id_ptr) in ids.iter().enumerate() {
125 let id = cstr_to_string(*id_ptr, "party_id")?;
126 let ep = if endpoints[i].is_null() {
127 None
128 } else {
129 Some(cstr_to_string(endpoints[i], "transport_endpoint")?)
130 };
131 parties.push(Party::new(id, ep));
132 }
133 Ok(PartyList::from_parties(parties))
134}
135
136fn share_from_c(cshare: *const CFMTcShare) -> TcResult<Option<Share>> {
138 if cshare.is_null() {
139 return Ok(None);
140 }
141 let cshare = unsafe { &*cshare };
142 let scheme = cstr_to_string(cshare.scheme, "share.scheme")?;
143 if cshare.bytes.is_null() && cshare.len != 0 {
144 return error::NullPointerSnafu {
145 param: "share.bytes",
146 }
147 .fail();
148 }
149 let bytes = if cshare.len == 0 {
150 Vec::new()
151 } else {
152 unsafe { std::slice::from_raw_parts(cshare.bytes, cshare.len as usize) }.to_vec()
153 };
154 Ok(Some(Share::new(scheme, bytes)))
155}
156
157fn share_to_c(share: Share) -> TcResult<*mut CFMTcShare> {
160 let scheme_c = CString::new(share.scheme()).context(error::NulByteSnafu {})?;
161 let bytes = share.into_bytes();
162 let bytes_len = bytes.len() as u32;
163 let bytes_box = bytes.into_boxed_slice();
164 let bytes_ptr = Box::into_raw(bytes_box) as *const u8;
165 let scheme_ptr = scheme_c.into_raw();
166 let cshare = Box::new(CFMTcShare {
167 scheme: scheme_ptr,
168 bytes: bytes_ptr,
169 len: bytes_len,
170 });
171 Ok(Box::into_raw(cshare))
172}
173
174fn messages_to_c_array(msgs: Vec<Message>) -> TcResult<*mut CFMTcMessageArray> {
179 let count = msgs.len() as u32;
180 if msgs.is_empty() {
181 let bundle = Box::new(CFMTcMessageArray {
182 items: std::ptr::null_mut(),
183 count: 0,
184 });
185 return Ok(Box::into_raw(bundle));
186 }
187 let mut items: Vec<CFMTcMessage> = Vec::with_capacity(msgs.len());
188 for m in msgs {
189 let from_c = CString::new(m.from_party_id).context(error::NulByteSnafu {})?;
190 let to_c = match m.to_party_id {
191 Some(t) => Some(CString::new(t).context(error::NulByteSnafu {})?),
192 None => None,
193 };
194 let payload_len = m.payload.len() as u32;
195 let payload_box = m.payload.into_boxed_slice();
196 let payload_ptr = Box::into_raw(payload_box) as *const u8;
197 let from_ptr = from_c.into_raw();
198 let to_ptr: Option<*mut c_char> = to_c.map(|c| c.into_raw());
199 items.push(CFMTcMessage {
201 from_party_id: from_ptr,
202 to_party_id: to_ptr.unwrap_or(std::ptr::null_mut()),
203 round: m.round,
204 payload: payload_ptr,
205 payload_len,
206 });
207 }
208 let items_box = items.into_boxed_slice();
209 let items_ptr = Box::into_raw(items_box) as *mut CFMTcMessage;
210 let bundle = Box::new(CFMTcMessageArray {
211 items: items_ptr,
212 count,
213 });
214 Ok(Box::into_raw(bundle))
215}
216
217unsafe fn free_message_fields(m: &CFMTcMessage) {
220 unsafe {
221 if !m.from_party_id.is_null() {
222 drop(CString::from_raw(m.from_party_id as *mut c_char));
223 }
224 if !m.to_party_id.is_null() {
225 drop(CString::from_raw(m.to_party_id as *mut c_char));
226 }
227 if m.payload_len != 0 && !m.payload.is_null() {
228 let slice =
229 std::slice::from_raw_parts_mut(m.payload as *mut u8, m.payload_len as usize);
230 drop(Box::from_raw(slice as *mut [u8]));
231 }
232 }
233}
234
235#[allow(dead_code)]
239fn code_of<T>(r: TcResult<T>) -> u32 {
240 match r {
241 Ok(_) => 0,
242 Err(e) => e.code(),
243 }
244}
245
246#[unsafe(no_mangle)]
255pub extern "C" fn cfm_tc_session_create(
256 out: *mut *mut FFITcSession,
257 scheme: *const c_char,
258 party_list: *const CFMTcPartyList,
259 threshold: u32,
260 this_party_idx: u32,
261 local_share: *const CFMTcShare,
262 message: *const u8,
263 message_len: u32,
264) -> u32 {
265 if out.is_null() {
266 return error::ErrorCode::NULL_POINTER.into();
267 }
268 let result = (|| -> TcResult<*mut FFITcSession> {
269 let scheme_name = cstr_to_string(scheme, "scheme")?;
270 if party_list.is_null() {
271 return error::NullPointerSnafu {
272 param: "party_list",
273 }
274 .fail();
275 }
276 let cpl = unsafe { &*party_list };
277 let parties = party_list_from_c(cpl)?;
278 let local_share = share_from_c(local_share)?;
279 let message = if message.is_null() || message_len == 0 {
280 None
281 } else {
282 Some(unsafe { std::slice::from_raw_parts(message, message_len as usize) }.to_vec())
283 };
284 let params = SessionParams {
285 scheme: scheme_name,
286 parties,
287 threshold,
288 this_party_idx: this_party_idx as usize,
289 local_share,
290 message,
291 };
292 let session = Session::create(¶ms)?;
293 Ok(Box::into_raw(Box::new(session)) as *mut FFITcSession)
294 })();
295 match result {
296 Ok(ptr) => {
297 unsafe { *out = ptr };
298 0
299 }
300 Err(e) => e.code(),
301 }
302}
303
304#[unsafe(no_mangle)]
312pub extern "C" fn cfm_tc_session_round(
313 session: *mut FFITcSession,
314 incoming: *const CFMTcMessage,
315 incoming_count: u32,
316 outgoing: *mut *mut CFMTcMessageArray,
317 outgoing_count: *mut u32,
318 complete: *mut u8,
319) -> u32 {
320 if session.is_null() {
321 return error::ErrorCode::NULL_POINTER.into();
322 }
323 if outgoing.is_null() || outgoing_count.is_null() || complete.is_null() {
324 return error::ErrorCode::NULL_POINTER.into();
325 }
326 let session = unsafe { &mut *(session as *mut Session) };
327
328 let incoming_vec: TcResult<Vec<Message>> = (|| {
330 if incoming.is_null() || incoming_count == 0 {
331 return Ok(Vec::new());
332 }
333 let slice = unsafe { std::slice::from_raw_parts(incoming, incoming_count as usize) };
334 let mut out = Vec::with_capacity(slice.len());
335 for cm in slice {
336 let from = cstr_to_string(cm.from_party_id, "incoming.from_party_id")?;
337 let to = opt_cstr_to_string(cm.to_party_id)?;
338 let payload = if cm.payload_len == 0 || cm.payload.is_null() {
339 Vec::new()
340 } else {
341 unsafe { std::slice::from_raw_parts(cm.payload, cm.payload_len as usize) }.to_vec()
342 };
343 out.push(Message {
344 from_party_id: from,
345 to_party_id: to,
346 round: cm.round,
347 payload,
348 });
349 }
350 Ok(out)
351 })();
352 let incoming_vec = match incoming_vec {
353 Ok(v) => v,
354 Err(e) => return e.code(),
355 };
356
357 match session.round_step(&incoming_vec) {
358 Ok(rr) => match messages_to_c_array(rr.outgoing) {
359 Ok(arr_ptr) => {
360 let arr = unsafe { &mut *arr_ptr };
361 unsafe {
362 *outgoing = arr_ptr;
363 *outgoing_count = arr.count;
364 *complete = if rr.complete { 1 } else { 0 };
365 }
366 0
367 }
368 Err(e) => e.code(),
369 },
370 Err(e) => e.code(),
371 }
372}
373
374#[unsafe(no_mangle)]
379pub extern "C" fn cfm_tc_session_result(
380 session: *mut FFITcSession,
381 out: *mut u8,
382 out_max: u32,
383 out_len: *mut u32,
384) -> u32 {
385 if session.is_null() {
386 return error::ErrorCode::NULL_POINTER.into();
387 }
388 if out_len.is_null() {
389 return error::ErrorCode::NULL_POINTER.into();
390 }
391 let session = unsafe { &*(session as *mut Session) };
392 match session.result() {
393 Ok(bytes) => {
394 let needed = bytes.len();
395 unsafe { *out_len = needed as u32 };
396 if (out_max as usize) < needed {
397 return error::ErrorCode::INSUFFICIENT_BUFFER.into();
398 }
399 if !out.is_null() {
400 unsafe {
401 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, needed);
402 }
403 }
404 0
405 }
406 Err(e) => e.code(),
407 }
408}
409
410#[unsafe(no_mangle)]
417pub extern "C" fn cfm_tc_dkg_output_share(
418 session: *mut FFITcSession,
419 share_out: *mut *mut CFMTcShare,
420 public_key_out: *mut u8,
421 pk_max: u32,
422 pk_len: *mut u32,
423) -> u32 {
424 if session.is_null() {
425 return error::ErrorCode::NULL_POINTER.into();
426 }
427 if share_out.is_null() || pk_len.is_null() {
428 return error::ErrorCode::NULL_POINTER.into();
429 }
430 let session = unsafe { &*(session as *mut Session) };
431
432 let pk = match session.dkg_public_key() {
434 Ok(b) => b,
435 Err(e) => return e.code(),
436 };
437 let needed = pk.len();
438 unsafe { *pk_len = needed as u32 };
439 if (pk_max as usize) < needed {
440 return error::ErrorCode::INSUFFICIENT_BUFFER.into();
441 }
442 if !public_key_out.is_null() {
443 unsafe {
444 std::ptr::copy_nonoverlapping(pk.as_ptr(), public_key_out, needed);
445 }
446 }
447
448 let empty_share = Share::new(session.scheme_name(), Vec::new());
454 match share_to_c(empty_share) {
455 Ok(ptr) => {
456 unsafe { *share_out = ptr };
457 0
458 }
459 Err(e) => e.code(),
460 }
461}
462
463#[unsafe(no_mangle)]
465pub extern "C" fn cfm_tc_session_destroy(session: *mut FFITcSession) {
466 if session.is_null() {
467 return;
468 }
469 unsafe {
470 let session = Box::from_raw(session as *mut Session);
471 let mut session = session;
472 session.destroy();
473 drop(session);
474 }
475}
476
477#[unsafe(no_mangle)]
480pub extern "C" fn cfm_tc_messages_destroy(arr: *mut CFMTcMessageArray) {
481 if arr.is_null() {
482 return;
483 }
484 unsafe {
485 let bundle = Box::from_raw(arr);
486 if bundle.count == 0 || bundle.items.is_null() {
487 return;
488 }
489 let items = std::slice::from_raw_parts_mut(bundle.items, bundle.count as usize);
490 for m in items.iter() {
491 free_message_fields(m);
492 }
493 let _ = Box::from_raw(items as *mut [CFMTcMessage]);
494 }
495}
496
497#[unsafe(no_mangle)]
500pub extern "C" fn cfm_tc_share_destroy(share: *mut CFMTcShare) {
501 if share.is_null() {
502 return;
503 }
504 unsafe {
505 let cshare = Box::from_raw(share);
506 if !cshare.scheme.is_null() {
507 drop(CString::from_raw(cshare.scheme as *mut c_char));
508 }
509 if cshare.len != 0 && !cshare.bytes.is_null() {
510 let slice =
511 std::slice::from_raw_parts_mut(cshare.bytes as *mut u8, cshare.len as usize);
512 let _ = Box::from_raw(slice as *mut [u8]);
513 }
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520 use crate::registry::{SessionImpl, TcScheme, TcSchemeKind};
521 use crate::session::SessionParams;
522
523 #[test]
525 fn party_list_round_trip_c() {
526 let id1 = CString::new("a").unwrap();
527 let id2 = CString::new("b").unwrap();
528 let ep1 = CString::new("quic://a:443").unwrap();
529 let ids = [id1.as_ptr(), id2.as_ptr()];
530 let eps = [ep1.as_ptr(), std::ptr::null()];
531 let cpl = CFMTcPartyList {
532 party_ids: ids.as_ptr(),
533 transport_endpoints: eps.as_ptr(),
534 count: 2,
535 };
536 let list = party_list_from_c(&cpl).expect("parse");
537 assert_eq!(list.len(), 2);
538 assert_eq!(list.get(0).unwrap().id, "a");
539 assert_eq!(
540 list.get(0).unwrap().transport_endpoint.as_deref(),
541 Some("quic://a:443")
542 );
543 assert_eq!(list.get(1).unwrap().id, "b");
544 assert!(list.get(1).unwrap().transport_endpoint.is_none());
545 }
546
547 struct FfiTestScheme;
550 impl TcScheme for FfiTestScheme {
551 fn name(&self) -> &'static str {
552 "ffi-test"
553 }
554 fn kind(&self) -> TcSchemeKind {
555 TcSchemeKind::Signature
556 }
557 fn create_session(
558 &self,
559 params: &SessionParams,
560 ) -> crate::error::Result<Box<dyn SessionImpl>> {
561 let our_id = params.parties.get(params.this_party_idx)?.id.clone();
562 Ok(Box::new(FfiTestSession {
563 our_id,
564 round_done: 0,
565 payload: params.message.clone().unwrap_or_default(),
566 }))
567 }
568 }
569 struct FfiTestSession {
570 our_id: String,
571 round_done: u8,
572 payload: Vec<u8>,
573 }
574 impl SessionImpl for FfiTestSession {
575 fn round(
576 &mut self,
577 _incoming: &[Message],
578 ) -> crate::error::Result<crate::registry::RoundResult> {
579 self.round_done += 1;
580 if self.round_done == 1 {
581 Ok(crate::registry::RoundResult::new(
582 vec![Message::broadcast(&self.our_id, 1, self.payload.clone())],
583 false,
584 ))
585 } else {
586 Ok(crate::registry::RoundResult::done())
587 }
588 }
589 fn result(&self) -> crate::error::Result<Vec<u8>> {
590 Ok(self.payload.clone())
591 }
592 fn destroy(&mut self) {
593 self.payload.fill(0);
594 }
595 }
596 inventory::submit! {
597 crate::registry::RegisteredScheme {
598 scheme: &FfiTestScheme as &dyn TcScheme
599 }
600 }
601
602 fn build_party_list_c() -> (
603 Vec<CString>,
604 Vec<CString>,
605 Vec<*const c_char>,
606 Vec<*const c_char>,
607 ) {
608 let ids = vec![
609 CString::new("a").unwrap(),
610 CString::new("b").unwrap(),
611 CString::new("c").unwrap(),
612 ];
613 let id_ptrs: Vec<*const c_char> = ids.iter().map(|c| c.as_ptr()).collect();
614 let eps: Vec<CString> = Vec::new();
615 let ep_ptrs: Vec<*const c_char> = vec![std::ptr::null(); 3];
616 (ids, eps, id_ptrs, ep_ptrs)
617 }
618
619 #[test]
620 fn ffi_full_session_lifecycle() {
621 let (_ids, _eps, id_ptrs, ep_ptrs) = build_party_list_c();
622 let cpl = CFMTcPartyList {
623 party_ids: id_ptrs.as_ptr(),
624 transport_endpoints: ep_ptrs.as_ptr(),
625 count: 3,
626 };
627 let scheme = CString::new("ffi-test").unwrap();
628 let msg = b"sign-this-message";
629 let mut session_ptr: *mut FFITcSession = std::ptr::null_mut();
630 let code = cfm_tc_session_create(
631 &mut session_ptr,
632 scheme.as_ptr(),
633 &cpl,
634 2,
635 0,
636 std::ptr::null(),
637 msg.as_ptr(),
638 msg.len() as u32,
639 );
640 assert_eq!(code, 0, "create should succeed");
641 assert!(!session_ptr.is_null());
642
643 let mut outgoing: *mut CFMTcMessageArray = std::ptr::null_mut();
645 let mut outgoing_count: u32 = 0;
646 let mut complete: u8 = 99;
647 let code = cfm_tc_session_round(
648 session_ptr,
649 std::ptr::null(),
650 0,
651 &mut outgoing,
652 &mut outgoing_count,
653 &mut complete,
654 );
655 assert_eq!(code, 0, "round 1 should succeed");
656 assert_eq!(complete, 0, "round 1 not complete");
657 assert_eq!(outgoing_count, 1);
658 assert!(!outgoing.is_null());
659 unsafe {
661 let arr = &*outgoing;
662 let m = &*arr.items.add(0);
663 assert_eq!(CStr::from_ptr(m.from_party_id).to_str().unwrap(), "a");
664 assert!(m.to_party_id.is_null(), "broadcast to_party_id is null");
665 assert_eq!(m.round, 1);
666 assert_eq!(m.payload_len, msg.len() as u32);
667 }
668 cfm_tc_messages_destroy(outgoing);
669
670 let code = cfm_tc_session_round(
672 session_ptr,
673 std::ptr::null(),
674 0,
675 &mut outgoing,
676 &mut outgoing_count,
677 &mut complete,
678 );
679 assert_eq!(code, 0, "round 2 should succeed");
680 assert_eq!(complete, 1, "round 2 complete");
681 assert_eq!(outgoing_count, 0);
682 cfm_tc_messages_destroy(outgoing);
683
684 let mut out_buf = [0u8; 64];
686 let mut out_len: u32 = 0;
687 let code = cfm_tc_session_result(
688 session_ptr,
689 out_buf.as_mut_ptr(),
690 out_buf.len() as u32,
691 &mut out_len,
692 );
693 assert_eq!(code, 0, "result should succeed");
694 assert_eq!(out_len as usize, msg.len());
695 assert_eq!(&out_buf[..out_len as usize], msg);
696
697 cfm_tc_session_destroy(session_ptr);
698 }
699
700 #[test]
701 fn ffi_session_result_insufficient_buffer() {
702 let (_ids, _eps, id_ptrs, ep_ptrs) = build_party_list_c();
703 let cpl = CFMTcPartyList {
704 party_ids: id_ptrs.as_ptr(),
705 transport_endpoints: ep_ptrs.as_ptr(),
706 count: 3,
707 };
708 let scheme = CString::new("ffi-test").unwrap();
709 let msg = b"twelve-bytes";
710 let mut session_ptr: *mut FFITcSession = std::ptr::null_mut();
711 cfm_tc_session_create(
712 &mut session_ptr,
713 scheme.as_ptr(),
714 &cpl,
715 2,
716 0,
717 std::ptr::null(),
718 msg.as_ptr(),
719 msg.len() as u32,
720 );
721 let mut outgoing: *mut CFMTcMessageArray = std::ptr::null_mut();
723 let mut outgoing_count: u32 = 0;
724 let mut complete: u8 = 0;
725 cfm_tc_session_round(
726 session_ptr,
727 std::ptr::null(),
728 0,
729 &mut outgoing,
730 &mut outgoing_count,
731 &mut complete,
732 );
733 cfm_tc_messages_destroy(outgoing);
734 cfm_tc_session_round(
735 session_ptr,
736 std::ptr::null(),
737 0,
738 &mut outgoing,
739 &mut outgoing_count,
740 &mut complete,
741 );
742 cfm_tc_messages_destroy(outgoing);
743
744 let mut out_buf = [0u8; 4];
746 let mut out_len: u32 = 0;
747 let code = cfm_tc_session_result(session_ptr, out_buf.as_mut_ptr(), 4, &mut out_len);
748 assert_eq!(code, error::ErrorCode::INSUFFICIENT_BUFFER as u32);
749 assert_eq!(out_len as usize, msg.len(), "out_len reports required size");
750
751 cfm_tc_session_destroy(session_ptr);
752 }
753
754 #[test]
755 fn ffi_create_rejects_null_out() {
756 let scheme = CString::new("ffi-test").unwrap();
757 let code = cfm_tc_session_create(
758 std::ptr::null_mut(),
759 scheme.as_ptr(),
760 std::ptr::null(),
761 1,
762 0,
763 std::ptr::null(),
764 std::ptr::null(),
765 0,
766 );
767 assert_eq!(code, error::ErrorCode::NULL_POINTER as u32);
768 }
769
770 #[test]
771 fn ffi_share_round_trip() {
772 let original = Share::new("ffi-test", vec![0xCA, 0xFE]);
773 let ptr = share_to_c(original).expect("to_c");
774 let back = share_from_c(ptr).expect("from_c").expect("Some");
775 cfm_tc_share_destroy(ptr);
776 assert_eq!(back.scheme(), "ffi-test");
777 assert_eq!(back.bytes(), &[0xCA, 0xFE]);
778 }
779
780 #[test]
781 fn ffi_destroy_null_is_safe() {
782 cfm_tc_session_destroy(std::ptr::null_mut());
783 cfm_tc_messages_destroy(std::ptr::null_mut());
784 cfm_tc_share_destroy(std::ptr::null_mut());
785 }
786}