1use std::panic::{catch_unwind, AssertUnwindSafe};
15use std::ptr;
16
17use pamoja_security::{DeviceIdentity, PublicIdentity, Signature};
18
19use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
20
21pub const PAMOJA_KEY_LEN: usize = 32;
23
24pub const PAMOJA_SIGNATURE_LEN: usize = 64;
26
27pub const PAMOJA_FINGERPRINT_LEN: usize = 16;
29
30pub struct PamojaDeviceIdentity {
32 pub(crate) inner: DeviceIdentity,
33}
34
35#[no_mangle]
48pub unsafe extern "C" fn pamoja_device_identity_new(
49 seed: *const u8,
50 seed_len: usize,
51) -> *mut PamojaDeviceIdentity {
52 let bytes = match read_bytes(seed, seed_len) {
53 Ok(bytes) => bytes,
54 Err(_) => return ptr::null_mut(),
55 };
56 let Ok(seed) = <[u8; PAMOJA_KEY_LEN]>::try_from(bytes.as_slice()) else {
57 set_last_error(format!("seed must be exactly {PAMOJA_KEY_LEN} bytes"));
58 return ptr::null_mut();
59 };
60 Box::into_raw(Box::new(PamojaDeviceIdentity {
61 inner: DeviceIdentity::from_seed(&seed),
62 }))
63}
64
65#[no_mangle]
77pub unsafe extern "C" fn pamoja_device_identity_public_key(
78 identity: *const PamojaDeviceIdentity,
79 out_public_key: *mut u8,
80) -> PamojaStatus {
81 let Some(identity) = identity_handle(identity) else {
82 return PamojaStatus::InvalidArgument;
83 };
84 if out_public_key.is_null() {
85 set_last_error("out_public_key must not be null".to_owned());
86 return PamojaStatus::InvalidArgument;
87 }
88 let key = identity.inner.public().to_bytes();
89 ptr::copy_nonoverlapping(key.as_ptr(), out_public_key, PAMOJA_KEY_LEN);
90 PamojaStatus::Ok
91}
92
93#[no_mangle]
107pub unsafe extern "C" fn pamoja_device_identity_sign(
108 identity: *const PamojaDeviceIdentity,
109 payload: *const u8,
110 payload_len: usize,
111 out_signature: *mut u8,
112) -> PamojaStatus {
113 let Some(identity) = identity_handle(identity) else {
114 return PamojaStatus::InvalidArgument;
115 };
116 if out_signature.is_null() {
117 set_last_error("out_signature must not be null".to_owned());
118 return PamojaStatus::InvalidArgument;
119 }
120 let payload = match read_bytes(payload, payload_len) {
121 Ok(payload) => payload,
122 Err(status) => return status,
123 };
124 match catch_unwind(AssertUnwindSafe(|| {
125 identity.inner.sign(&payload).to_bytes()
126 })) {
127 Ok(signature) => {
128 ptr::copy_nonoverlapping(signature.as_ptr(), out_signature, PAMOJA_SIGNATURE_LEN);
129 PamojaStatus::Ok
130 }
131 Err(_) => {
132 set_last_error("panic at the FFI boundary".to_owned());
133 PamojaStatus::Panic
134 }
135 }
136}
137
138#[no_mangle]
156pub unsafe extern "C" fn pamoja_device_identity_sign_message(
157 identity: *const PamojaDeviceIdentity,
158 payload: *const u8,
159 payload_len: usize,
160 out_buffer: *mut *mut PamojaBuffer,
161) -> PamojaStatus {
162 let Some(identity) = identity_handle(identity) else {
163 return PamojaStatus::InvalidArgument;
164 };
165 if out_buffer.is_null() {
166 set_last_error("out_buffer must not be null".to_owned());
167 return PamojaStatus::InvalidArgument;
168 }
169 let payload = match read_bytes(payload, payload_len) {
170 Ok(payload) => payload,
171 Err(status) => return status,
172 };
173 match catch_unwind(AssertUnwindSafe(|| identity.inner.sign_message(&payload))) {
174 Ok(message) => {
175 *out_buffer = PamojaBuffer::into_raw(message);
176 PamojaStatus::Ok
177 }
178 Err(_) => {
179 set_last_error("panic at the FFI boundary".to_owned());
180 PamojaStatus::Panic
181 }
182 }
183}
184
185#[no_mangle]
201pub unsafe extern "C" fn pamoja_public_identity_verify_message(
202 public_key: *const u8,
203 message: *const u8,
204 message_len: usize,
205 out_buffer: *mut *mut PamojaBuffer,
206) -> PamojaStatus {
207 if out_buffer.is_null() {
208 set_last_error("out_buffer must not be null".to_owned());
209 return PamojaStatus::InvalidArgument;
210 }
211 let public = match read_public(public_key) {
212 Ok(public) => public,
213 Err(status) => return status,
214 };
215 let message = match read_bytes(message, message_len) {
216 Ok(message) => message,
217 Err(status) => return status,
218 };
219 match public.verify_message(&message) {
220 Ok(payload) => {
221 *out_buffer = PamojaBuffer::into_raw(payload.to_vec());
222 PamojaStatus::Ok
223 }
224 Err(error) => {
225 set_last_error(error.to_string());
226 PamojaStatus::from_error(&error)
227 }
228 }
229}
230
231#[no_mangle]
240pub unsafe extern "C" fn pamoja_device_identity_free(identity: *mut PamojaDeviceIdentity) {
241 if !identity.is_null() {
242 drop(Box::from_raw(identity));
243 }
244}
245
246#[no_mangle]
262pub unsafe extern "C" fn pamoja_public_identity_fingerprint(
263 public_key: *const u8,
264 out_fingerprint: *mut u8,
265) -> PamojaStatus {
266 let public = match read_public(public_key) {
267 Ok(public) => public,
268 Err(status) => return status,
269 };
270 if out_fingerprint.is_null() {
271 set_last_error("out_fingerprint must not be null".to_owned());
272 return PamojaStatus::InvalidArgument;
273 }
274 let fingerprint = public.fingerprint();
275 ptr::copy_nonoverlapping(
276 fingerprint.as_ptr(),
277 out_fingerprint,
278 PAMOJA_FINGERPRINT_LEN,
279 );
280 PamojaStatus::Ok
281}
282
283#[no_mangle]
298pub unsafe extern "C" fn pamoja_public_identity_verify(
299 public_key: *const u8,
300 payload: *const u8,
301 payload_len: usize,
302 signature: *const u8,
303) -> PamojaStatus {
304 let public = match read_public(public_key) {
305 Ok(public) => public,
306 Err(status) => return status,
307 };
308 let payload = match read_bytes(payload, payload_len) {
309 Ok(payload) => payload,
310 Err(status) => return status,
311 };
312 let signature_bytes = match read_bytes(signature, PAMOJA_SIGNATURE_LEN) {
313 Ok(bytes) => bytes,
314 Err(status) => return status,
315 };
316 let Ok(signature_bytes) = <[u8; PAMOJA_SIGNATURE_LEN]>::try_from(signature_bytes.as_slice())
317 else {
318 set_last_error(format!(
319 "signature must be exactly {PAMOJA_SIGNATURE_LEN} bytes"
320 ));
321 return PamojaStatus::InvalidArgument;
322 };
323 let signature = Signature::from_bytes(&signature_bytes);
324
325 match public.verify(&payload, &signature) {
326 Ok(()) => PamojaStatus::Ok,
327 Err(error) => {
328 set_last_error(error.to_string());
329 PamojaStatus::from_error(&error)
330 }
331 }
332}
333
334pub(crate) unsafe fn identity_handle<'a>(
340 identity: *const PamojaDeviceIdentity,
341) -> Option<&'a PamojaDeviceIdentity> {
342 if identity.is_null() {
343 set_last_error("identity must not be null".to_owned());
344 return None;
345 }
346 Some(&*identity)
347}
348
349pub(crate) unsafe fn read_public(public_key: *const u8) -> Result<PublicIdentity, PamojaStatus> {
356 let bytes = read_bytes(public_key, PAMOJA_KEY_LEN)?;
357 let Ok(bytes) = <[u8; PAMOJA_KEY_LEN]>::try_from(bytes.as_slice()) else {
358 set_last_error(format!("public key must be exactly {PAMOJA_KEY_LEN} bytes"));
359 return Err(PamojaStatus::InvalidArgument);
360 };
361 PublicIdentity::from_bytes(&bytes).map_err(|error| {
362 let status = PamojaStatus::from_error(&error);
363 set_last_error(error.to_string());
364 status
365 })
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 fn identity(seed: u8) -> *mut PamojaDeviceIdentity {
374 let seed = [seed; PAMOJA_KEY_LEN];
375 let handle = unsafe { pamoja_device_identity_new(seed.as_ptr(), seed.len()) };
377 assert!(!handle.is_null());
378 handle
379 }
380
381 #[test]
382 fn a_signature_verifies_against_its_signer() {
383 let device = identity(1);
384 let mut public = [0u8; PAMOJA_KEY_LEN];
385 let mut signature = [0u8; PAMOJA_SIGNATURE_LEN];
386 let payload = b"reading";
387
388 unsafe {
390 assert_eq!(
391 pamoja_device_identity_public_key(device, public.as_mut_ptr()),
392 PamojaStatus::Ok
393 );
394 assert_eq!(
395 pamoja_device_identity_sign(
396 device,
397 payload.as_ptr(),
398 payload.len(),
399 signature.as_mut_ptr()
400 ),
401 PamojaStatus::Ok
402 );
403 assert_eq!(
404 pamoja_public_identity_verify(
405 public.as_ptr(),
406 payload.as_ptr(),
407 payload.len(),
408 signature.as_ptr()
409 ),
410 PamojaStatus::Ok
411 );
412 pamoja_device_identity_free(device);
413 }
414 }
415
416 #[test]
417 fn a_tampered_payload_fails_with_an_auth_status() {
418 let device = identity(2);
419 let mut public = [0u8; PAMOJA_KEY_LEN];
420 let mut signature = [0u8; PAMOJA_SIGNATURE_LEN];
421 let payload = b"reading";
422
423 unsafe {
425 pamoja_device_identity_public_key(device, public.as_mut_ptr());
426 pamoja_device_identity_sign(
427 device,
428 payload.as_ptr(),
429 payload.len(),
430 signature.as_mut_ptr(),
431 );
432 let tampered = b"reading!";
433 assert_eq!(
434 pamoja_public_identity_verify(
435 public.as_ptr(),
436 tampered.as_ptr(),
437 tampered.len(),
438 signature.as_ptr()
439 ),
440 PamojaStatus::Auth
441 );
442 pamoja_device_identity_free(device);
443 }
444 }
445
446 #[test]
447 fn a_seed_of_the_wrong_length_is_rejected() {
448 let seed = [7u8; 16];
449 let handle = unsafe { pamoja_device_identity_new(seed.as_ptr(), seed.len()) };
451 assert!(handle.is_null());
452 }
453
454 #[test]
455 fn the_fingerprint_is_lowercase_hex() {
456 let device = identity(3);
457 let mut public = [0u8; PAMOJA_KEY_LEN];
458 let mut fingerprint = [0u8; PAMOJA_FINGERPRINT_LEN];
459
460 unsafe {
462 pamoja_device_identity_public_key(device, public.as_mut_ptr());
463 assert_eq!(
464 pamoja_public_identity_fingerprint(public.as_ptr(), fingerprint.as_mut_ptr()),
465 PamojaStatus::Ok
466 );
467 pamoja_device_identity_free(device);
468 }
469 assert!(fingerprint
470 .iter()
471 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte)));
472 }
473
474 #[test]
475 fn calls_on_a_null_identity_are_rejected() {
476 let mut public = [0u8; PAMOJA_KEY_LEN];
477 unsafe {
479 assert_eq!(
480 pamoja_device_identity_public_key(ptr::null(), public.as_mut_ptr()),
481 PamojaStatus::InvalidArgument
482 );
483 pamoja_device_identity_free(ptr::null_mut());
485 }
486 }
487}