Skip to main content

confium_store/
ffi.rs

1//! FFI surface for the Store crate.
2//!
3//! Exposes the `cfm_keystore_*` C ABI. The wire protocol mirrors
4//! `TODO.finalize/12-keystore-interface.md`. Key material is opaque
5//! (`*mut c_void`) — the Store never interprets key bytes, it only
6//! indexes and returns handles owned by the caller (typically the
7//! Engine's `keyfmt` interface).
8//!
9//! Entry points follow the conventions established by `confium-core`'s
10//! FFI layer: raw-pointer parameters, null-checked on entry, and a
11//! `u32` result encoding either `0` for success or a numeric
12//! [`crate::error::ErrorCode`].
13
14use std::collections::HashMap;
15use std::ffi::CStr;
16use std::ffi::c_void;
17use std::os::raw::c_char;
18
19use snafu::{ResultExt, ensure};
20
21use crate::backend::Options;
22use crate::error::{InvalidUTF8Snafu, NullPointerSnafu, Result};
23use crate::keystore::Keystore;
24
25/// Opaque handle returned to C callers. Never dereferenced by C; only
26/// passed back into the FFI.
27pub enum FFIKeystore {}
28
29/// Opaque key iterator handle.
30pub enum FFIKeyIterator {}
31
32/// Opaque key handle. The Store treats this as a caller-owned token; it
33/// stores and returns the pointer verbatim and never dereferences it.
34pub type FFIKey = c_void;
35
36// --- helpers --------------------------------------------------------------
37
38/// Null-check that returns an `Error` rather than a numeric code — used
39/// by the inner `_` functions that return `Result`.
40fn require<T>(ptr: *const T, param: &'static str) -> Result<()> {
41    if ptr.is_null() {
42        Err(NullPointerSnafu { param }.build())
43    } else {
44        Ok(())
45    }
46}
47
48/// Copy a NUL-terminated C string into an owned `String`, surfacing
49/// invalid UTF-8 as [`Error::InvalidUTF8`].
50fn cstring(cstr: *const c_char, param: &'static str) -> Result<String> {
51    require(cstr, param)?;
52    unsafe {
53        CStr::from_ptr(cstr)
54            .to_str()
55            .context(InvalidUTF8Snafu {})
56            .map(str::to_string)
57    }
58}
59
60/// Build an empty [`Options`] from a possibly-null pointer. The current
61/// wire protocol passes options opaquely; for now we accept `NULL` and
62/// produce an empty map. A richer option model can be layered in
63/// without changing the entry-point signatures.
64fn options_from_ptr(_opts: *const c_void) -> Options {
65    // The Store's Options is `HashMap<String, String>`. The Engine
66    // passes a typed `*const Options`; here we deliberately stay
67    // decoupled and treat the pointer as opaque. When the keystore
68    // becomes a loaded plugin, the loader will translate the Engine's
69    // option map into this crate's `Options` before calling `open`.
70    HashMap::new()
71}
72
73// --- create / destroy -----------------------------------------------------
74
75fn cfm_keystore_create_(
76    out: *mut *mut FFIKeystore,
77    backend_name: *const c_char,
78    opts: *const c_void,
79) -> Result<()> {
80    require(out, "out")?;
81    require(backend_name, "backend_name")?;
82    let name = cstring(backend_name, "backend_name")?;
83    let opts = options_from_ptr(opts);
84    let ks = Keystore::new(&name, &opts)?;
85    // SAFETY: the caller receives a heap-allocated handle; they must
86    // return it via `cfm_keystore_destroy`.
87    unsafe {
88        *out = Box::into_raw(Box::new(ks)) as *mut FFIKeystore;
89    }
90    Ok(())
91}
92
93/// Create a keystore backed by `backend_name` (e.g. `"memory"`).
94///
95/// Returns `0` on success, or a numeric
96/// [`ErrorCode`](crate::error::ErrorCode) on failure.
97#[unsafe(no_mangle)]
98pub extern "C" fn cfm_keystore_create(
99    out: *mut *mut FFIKeystore,
100    backend_name: *const c_char,
101    opts: *const c_void,
102) -> u32 {
103    cfm_keystore_create_(out, backend_name, opts).map_or_else(|e| e.code(), |_| 0)
104}
105
106/// Drop a keystore handle. Safe to call with `NULL`.
107#[unsafe(no_mangle)]
108pub extern "C" fn cfm_keystore_destroy(ks: *mut FFIKeystore) {
109    if !ks.is_null() {
110        // SAFETY: the handle was produced by `cfm_keystore_create` as a
111        // `Box::into_raw`; reclaiming it here is the matching drop.
112        unsafe {
113            drop(Box::from_raw(ks as *mut Keystore));
114        }
115    }
116}
117
118// --- private compartment --------------------------------------------------
119
120fn keystore_mut(ks: *mut FFIKeystore) -> Result<&'static mut Keystore> {
121    require(ks, "ks")?;
122    // SAFETY: the caller owns the handle and guarantees exclusive access
123    // for the duration of this mutable call. The `'static` lifetime is
124    // a convenience so the inner functions can return a borrow — the
125    // caller must not retain the reference beyond the FFI call.
126    Ok(unsafe { &mut *(ks as *mut Keystore) })
127}
128
129fn keystore_ref(ks: *mut FFIKeystore) -> Result<&'static Keystore> {
130    require(ks, "ks")?;
131    Ok(unsafe { &*(ks as *mut Keystore) })
132}
133
134/// Insert a secret key into the private compartment.
135#[unsafe(no_mangle)]
136pub extern "C" fn cfm_keystore_put_secret(
137    ks: *mut FFIKeystore,
138    module_id: *const c_char,
139    app_id: *const c_char,
140    key_id: *const c_char,
141    secret_key: *mut FFIKey,
142) -> u32 {
143    let inner = || -> Result<()> {
144        let ks = keystore_mut(ks)?;
145        let module = cstring(module_id, "module_id")?;
146        let app = cstring(app_id, "app_id")?;
147        let key_id = cstring(key_id, "key_id")?;
148        require(secret_key, "secret_key")?;
149        ks.put_secret(&module, &app, &key_id, secret_key)
150    };
151    inner().map_or_else(|e| e.code(), |_| 0)
152}
153
154/// Fetch a secret key from the private compartment.
155///
156/// On success writes the opaque key handle into `*out`.
157#[unsafe(no_mangle)]
158pub extern "C" fn cfm_keystore_get_secret(
159    ks: *mut FFIKeystore,
160    module_id: *const c_char,
161    app_id: *const c_char,
162    key_id: *const c_char,
163    out: *mut *mut FFIKey,
164) -> u32 {
165    let inner = || -> Result<()> {
166        let ks = keystore_ref(ks)?;
167        let module = cstring(module_id, "module_id")?;
168        let app = cstring(app_id, "app_id")?;
169        let key_id = cstring(key_id, "key_id")?;
170        require(out, "out")?;
171        let key = ks.get_secret(&module, &app, &key_id)?;
172        unsafe {
173            *out = key as *mut FFIKey;
174        }
175        Ok(())
176    };
177    inner().map_or_else(|e| e.code(), |_| 0)
178}
179
180// --- public compartment ---------------------------------------------------
181
182/// Insert a public key into the public compartment with a detached
183/// identity signature.
184#[unsafe(no_mangle)]
185pub extern "C" fn cfm_keystore_put_public(
186    ks: *mut FFIKeystore,
187    module_id: *const c_char,
188    app_id: *const c_char,
189    identity: *const c_char,
190    public_key: *mut FFIKey,
191    signature: *const u8,
192    sig_len: u32,
193) -> u32 {
194    let inner = || -> Result<()> {
195        let ks = keystore_mut(ks)?;
196        let module = cstring(module_id, "module_id")?;
197        let app = cstring(app_id, "app_id")?;
198        let identity = cstring(identity, "identity")?;
199        require(public_key, "public_key")?;
200        // signature may legitimately be empty (sig_len == 0); a NULL
201        // pointer with non-zero length is an error.
202        let sig: &[u8] = if signature.is_null() {
203            ensure!(sig_len == 0, NullPointerSnafu { param: "signature" });
204            &[]
205        } else {
206            // SAFETY: the caller vouches for `sig_len` bytes being
207            // readable from `signature`.
208            unsafe { std::slice::from_raw_parts(signature, sig_len as usize) }
209        };
210        ks.put_public(&module, &app, &identity, public_key, sig)
211    };
212    inner().map_or_else(|e| e.code(), |_| 0)
213}
214
215/// Fetch a public key from the public compartment by identity.
216///
217/// On success writes the opaque key handle into `*out`.
218#[unsafe(no_mangle)]
219pub extern "C" fn cfm_keystore_get_public(
220    ks: *mut FFIKeystore,
221    module_id: *const c_char,
222    app_id: *const c_char,
223    identity: *const c_char,
224    out: *mut *mut FFIKey,
225) -> u32 {
226    let inner = || -> Result<()> {
227        let ks = keystore_ref(ks)?;
228        let module = cstring(module_id, "module_id")?;
229        let app = cstring(app_id, "app_id")?;
230        let identity = cstring(identity, "identity")?;
231        require(out, "out")?;
232        let (key, _sig) = ks.get_public(&module, &app, &identity)?;
233        unsafe {
234            *out = key as *mut FFIKey;
235        }
236        Ok(())
237    };
238    inner().map_or_else(|e| e.code(), |_| 0)
239}
240
241// --- remote signing -------------------------------------------------------
242
243/// Out-buffer convention for byte results: the caller passes a
244/// destination pointer plus its capacity; on success `out` receives a
245/// freshly heap-allocated buffer (freed with `CFM_FREE` semantics by
246/// the embedding engine) and `out_len` its length.
247#[allow(clippy::missing_safety_doc)]
248#[unsafe(no_mangle)]
249pub unsafe extern "C" fn cfm_keystore_sign(
250    ks: *mut FFIKeystore,
251    module_id: *const c_char,
252    app_id: *const c_char,
253    key_id: *const c_char,
254    algorithm: *const c_char,
255    message: *const u8,
256    message_len: u32,
257    out: *mut *mut u8,
258    out_len: *mut u32,
259) -> u32 {
260    let inner = || -> Result<()> {
261        let ks = keystore_ref(ks)?;
262        let module = cstring(module_id, "module_id")?;
263        let app = cstring(app_id, "app_id")?;
264        let key_id = cstring(key_id, "key_id")?;
265        let algorithm = cstring(algorithm, "algorithm")?;
266        require(message, "message")?;
267        require(out, "out")?;
268        require(out_len, "out_len")?;
269        ensure!(
270            message_len > 0,
271            NullPointerSnafu {
272                param: "message_len"
273            }
274        );
275        // SAFETY: the caller guarantees `message` points to
276        // `message_len` readable bytes for the duration of the call.
277        let bytes = unsafe { std::slice::from_raw_parts(message, message_len as usize) };
278        let sig = ks.sign(&module, &app, &key_id, &algorithm, bytes)?;
279        let len = u32::try_from(sig.len()).map_err(|_| crate::error::Error::Wrapped {
280            message: "signature exceeds u32 length".to_string(),
281        })?;
282        // SAFETY: `out`/`out_len` are caller-provided writable slots;
283        // the buffer is released by the embedding engine's free.
284        unsafe {
285            *out = Box::into_raw(sig.into_boxed_slice()) as *mut u8;
286            *out_len = len;
287        }
288        Ok(())
289    };
290    inner().map_or_else(|e| e.code(), |_| 0)
291}
292
293// --- enumeration ----------------------------------------------------------
294
295/// Snapshot of one entry yielded by an iterator.
296struct IterEntry {
297    key: *mut c_void,
298    /// The index string (`key_id` for private, canonical identity for
299    /// public). Kept so a future `iterator_next_with_index` entry point
300    /// can surface it without an extra enumerate round-trip; the current
301    /// `cfm_keystore_iterator_next` only returns the key handle.
302    #[allow(dead_code)]
303    index: String,
304}
305
306/// Backing storage for an iterator handle. Owns a `Vec` snapshot taken
307/// at enumerate time — iteration does not hold a borrow on the
308/// keystore, so the caller may continue mutating the store while
309/// iterating over a past snapshot.
310pub struct KeyIterator {
311    entries: std::vec::IntoIter<IterEntry>,
312}
313
314fn cfm_keystore_enumerate_(
315    ks: *mut FFIKeystore,
316    module_id: *const c_char,
317    app_id: *const c_char,
318    compartment: u32,
319    out: *mut *mut FFIKeyIterator,
320) -> Result<()> {
321    let ks = keystore_ref(ks)?;
322    let module = cstring(module_id, "module_id")?;
323    let app = cstring(app_id, "app_id")?;
324    require(out, "out")?;
325    let comp = crate::backend::Compartment::from_wire(compartment)?;
326    let raw = ks.enumerate(&module, &app, comp)?;
327    let entries: Vec<IterEntry> = raw
328        .into_iter()
329        .map(|(key, index)| IterEntry { key, index })
330        .collect();
331    let it = KeyIterator {
332        entries: entries.into_iter(),
333    };
334    unsafe {
335        *out = Box::into_raw(Box::new(it)) as *mut FFIKeyIterator;
336    }
337    Ok(())
338}
339
340/// Enumerate entries in one compartment of one `(module, app)` scope.
341///
342/// `compartment`: `0` = public, `1` = private. The returned iterator
343/// holds a snapshot; mutating the keystore during iteration is safe.
344#[unsafe(no_mangle)]
345pub extern "C" fn cfm_keystore_enumerate(
346    ks: *mut FFIKeystore,
347    module_id: *const c_char,
348    app_id: *const c_char,
349    compartment: u32,
350    out: *mut *mut FFIKeyIterator,
351) -> u32 {
352    cfm_keystore_enumerate_(ks, module_id, app_id, compartment, out)
353        .map_or_else(|e| e.code(), |_| 0)
354}
355
356/// Advance the iterator. Returns `0` and writes the next key handle into
357/// `*out`, or `Error::ValueNotFound` when the iterator is exhausted (in
358/// which case `*out` is left untouched).
359#[unsafe(no_mangle)]
360pub extern "C" fn cfm_keystore_iterator_next(
361    it: *mut FFIKeyIterator,
362    out: *mut *mut FFIKey,
363) -> u32 {
364    let inner = || -> Result<()> {
365        require(it, "it")?;
366        require(out, "out")?;
367        // SAFETY: handle produced by `cfm_keystore_enumerate`.
368        let it = unsafe { &mut *(it as *mut KeyIterator) };
369        match it.entries.next() {
370            Some(entry) => {
371                unsafe {
372                    *out = entry.key as *mut FFIKey;
373                }
374                Ok(())
375            }
376            None => Err(crate::error::ValueNotFoundSnafu.build()),
377        }
378    };
379    inner().map_or_else(|e| e.code(), |_| 0)
380}
381
382/// Drop an iterator handle. Safe to call with `NULL`.
383#[unsafe(no_mangle)]
384pub extern "C" fn cfm_keystore_iterator_destroy(it: *mut FFIKeyIterator) {
385    if !it.is_null() {
386        // SAFETY: handle produced by `cfm_keystore_enumerate`.
387        unsafe {
388            drop(Box::from_raw(it as *mut KeyIterator));
389        }
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use crate::error::Error;
397    use std::ffi::CString;
398    use std::ptr;
399
400    fn sentinel(n: usize) -> *mut FFIKey {
401        n as *mut FFIKey
402    }
403
404    // Leaked CString raw pointers are acceptable in tests; the process
405    // exits shortly after. Using `CString::into_raw` avoids the borrow
406    // checker fighting the FFI call boundary.
407
408    #[test]
409    fn create_and_destroy_memory_keystore() {
410        let mut ks: *mut FFIKeystore = ptr::null_mut();
411        let name = CString::new("memory").unwrap().into_raw();
412        let rc = cfm_keystore_create(&mut ks, name, ptr::null());
413        assert_eq!(rc, 0, "create should succeed");
414        assert!(!ks.is_null());
415        cfm_keystore_destroy(ks);
416    }
417
418    #[test]
419    fn create_unknown_backend_returns_error() {
420        let mut ks: *mut FFIKeystore = ptr::null_mut();
421        let name = CString::new("nope").unwrap().into_raw();
422        let rc = cfm_keystore_create(&mut ks, name, ptr::null());
423        assert_eq!(
424            rc,
425            Error::UnknownBackend {
426                name: String::new()
427            }
428            .code()
429        );
430        assert!(ks.is_null());
431    }
432
433    #[test]
434    fn put_get_secret_round_trip() {
435        let mut ks: *mut FFIKeystore = ptr::null_mut();
436        let name = CString::new("memory").unwrap().into_raw();
437        cfm_keystore_create(&mut ks, name, ptr::null());
438        let key = sentinel(0x1000);
439
440        let m = CString::new("mod").unwrap().into_raw();
441        let a = CString::new("app").unwrap().into_raw();
442        let k = CString::new("key-1").unwrap().into_raw();
443        let rc = cfm_keystore_put_secret(ks, m, a, k, key);
444        assert_eq!(rc, 0, "put_secret");
445
446        let m = CString::new("mod").unwrap().into_raw();
447        let a = CString::new("app").unwrap().into_raw();
448        let k = CString::new("key-1").unwrap().into_raw();
449        let mut out: *mut FFIKey = ptr::null_mut();
450        let rc = cfm_keystore_get_secret(ks, m, a, k, &mut out);
451        assert_eq!(rc, 0, "get_secret");
452        assert_eq!(out, key);
453
454        cfm_keystore_destroy(ks);
455    }
456
457    #[test]
458    fn get_secret_missing_returns_value_not_found() {
459        let mut ks: *mut FFIKeystore = ptr::null_mut();
460        let name = CString::new("memory").unwrap().into_raw();
461        cfm_keystore_create(&mut ks, name, ptr::null());
462
463        let m = CString::new("mod").unwrap().into_raw();
464        let a = CString::new("app").unwrap().into_raw();
465        let k = CString::new("missing").unwrap().into_raw();
466        let mut out: *mut FFIKey = ptr::null_mut();
467        let rc = cfm_keystore_get_secret(ks, m, a, k, &mut out);
468        assert_eq!(rc, Error::ValueNotFound.code());
469    }
470
471    #[test]
472    fn put_get_public_round_trip() {
473        let mut ks: *mut FFIKeystore = ptr::null_mut();
474        let name = CString::new("memory").unwrap().into_raw();
475        cfm_keystore_create(&mut ks, name, ptr::null());
476
477        let key = sentinel(0x2000);
478        let sig = [0xDEu8, 0xAD, 0xBE, 0xEF];
479        let m = CString::new("mod").unwrap().into_raw();
480        let a = CString::new("app").unwrap().into_raw();
481        let id = CString::new("email:alice@example.com").unwrap().into_raw();
482        let rc = cfm_keystore_put_public(ks, m, a, id, key, sig.as_ptr(), sig.len() as u32);
483        assert_eq!(rc, 0, "put_public");
484
485        let m = CString::new("mod").unwrap().into_raw();
486        let a = CString::new("app").unwrap().into_raw();
487        let id = CString::new("email:alice@example.com").unwrap().into_raw();
488        let mut out: *mut FFIKey = ptr::null_mut();
489        let rc = cfm_keystore_get_public(ks, m, a, id, &mut out);
490        assert_eq!(rc, 0, "get_public");
491        assert_eq!(out, key);
492
493        cfm_keystore_destroy(ks);
494    }
495
496    #[test]
497    fn enumerate_private_then_exhaust() {
498        let mut ks: *mut FFIKeystore = ptr::null_mut();
499        let name = CString::new("memory").unwrap().into_raw();
500        cfm_keystore_create(&mut ks, name, ptr::null());
501
502        for (kid, key) in [("a", sentinel(1)), ("b", sentinel(2))] {
503            let m = CString::new("mod").unwrap().into_raw();
504            let a = CString::new("app").unwrap().into_raw();
505            let k = CString::new(kid).unwrap().into_raw();
506            let rc = cfm_keystore_put_secret(ks, m, a, k, key);
507            assert_eq!(rc, 0);
508        }
509
510        let m = CString::new("mod").unwrap().into_raw();
511        let a = CString::new("app").unwrap().into_raw();
512        let mut it: *mut FFIKeyIterator = ptr::null_mut();
513        let rc = cfm_keystore_enumerate(ks, m, a, 1, &mut it); // 1 = private
514        assert_eq!(rc, 0);
515
516        let mut seen = Vec::new();
517        loop {
518            let mut out: *mut FFIKey = ptr::null_mut();
519            let rc = cfm_keystore_iterator_next(it, &mut out);
520            if rc == Error::ValueNotFound.code() {
521                break;
522            }
523            assert_eq!(rc, 0);
524            seen.push(out as usize);
525        }
526        assert_eq!(seen.len(), 2);
527        cfm_keystore_iterator_destroy(it);
528        cfm_keystore_destroy(ks);
529    }
530
531    #[test]
532    fn enumerate_invalid_compartment_returns_error() {
533        let mut ks: *mut FFIKeystore = ptr::null_mut();
534        let name = CString::new("memory").unwrap().into_raw();
535        cfm_keystore_create(&mut ks, name, ptr::null());
536
537        let m = CString::new("mod").unwrap().into_raw();
538        let a = CString::new("app").unwrap().into_raw();
539        let mut it: *mut FFIKeyIterator = ptr::null_mut();
540        let rc = cfm_keystore_enumerate(ks, m, a, 99, &mut it);
541        assert_eq!(rc, crate::error::ErrorCode::INVALID_COMPARTMENT as u32);
542    }
543
544    #[test]
545    fn filesystem_open_succeeds() {
546        // The filesystem backend now opens against a real root directory.
547        // Without a configured root it falls back to the default under
548        // $HOME, so this asserts the FFI create path no longer surfaces
549        // NotImplemented.
550        let mut ks: *mut FFIKeystore = ptr::null_mut();
551        let name = CString::new("filesystem").unwrap().into_raw();
552        let rc = cfm_keystore_create(&mut ks, name, ptr::null());
553        assert_eq!(rc, 0, "filesystem create should succeed");
554        assert!(!ks.is_null());
555        cfm_keystore_destroy(ks);
556    }
557
558    #[test]
559    fn null_keystore_pointer_returns_null_pointer_code() {
560        let m = CString::new("mod").unwrap().into_raw();
561        let a = CString::new("app").unwrap().into_raw();
562        let k = CString::new("key-1").unwrap().into_raw();
563        let rc = cfm_keystore_put_secret(ptr::null_mut(), m, a, k, sentinel(1));
564        assert_eq!(rc, crate::error::ErrorCode::NULL_POINTER as u32);
565    }
566
567    #[test]
568    fn destroy_null_is_safe() {
569        // Must not crash.
570        cfm_keystore_destroy(ptr::null_mut());
571        cfm_keystore_iterator_destroy(ptr::null_mut());
572    }
573}