1use std::panic::{catch_unwind, AssertUnwindSafe};
14use std::ptr;
15
16use pamoja_audit::{verify_chain, AuditLog, Entry, Verifier};
17
18use crate::security::{identity_handle, read_public, PamojaDeviceIdentity};
19use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
20
21pub const PAMOJA_AUDIT_DIGEST_LEN: usize = 32;
23
24pub const PAMOJA_AUDIT_SIGNATURE_LEN: usize = 64;
26
27pub struct PamojaAuditEntry {
32 entry: Entry,
33}
34
35pub struct PamojaAuditLog {
41 log: AuditLog,
42}
43
44pub struct PamojaAuditVerifier {
49 verifier: Verifier,
50}
51
52#[no_mangle]
70pub unsafe extern "C" fn pamoja_audit_log_new(
71 identity: *const PamojaDeviceIdentity,
72) -> *mut PamojaAuditLog {
73 let Some(handle) = identity_handle(identity) else {
74 return ptr::null_mut();
75 };
76 Box::into_raw(Box::new(PamojaAuditLog {
77 log: AuditLog::new(handle.inner.clone()),
78 }))
79}
80
81#[no_mangle]
102pub unsafe extern "C" fn pamoja_audit_log_resume(
103 identity: *const PamojaDeviceIdentity,
104 last: *const PamojaAuditEntry,
105) -> *mut PamojaAuditLog {
106 let Some(handle) = identity_handle(identity) else {
107 return ptr::null_mut();
108 };
109 let Some(last) = entry_handle(last) else {
110 return ptr::null_mut();
111 };
112 Box::into_raw(Box::new(PamojaAuditLog {
113 log: AuditLog::resume(handle.inner.clone(), &last.entry),
114 }))
115}
116
117#[no_mangle]
136pub unsafe extern "C" fn pamoja_audit_log_append(
137 log: *mut PamojaAuditLog,
138 payload: *const u8,
139 payload_len: usize,
140) -> *mut PamojaAuditEntry {
141 if log.is_null() {
142 set_last_error("log must not be null".to_owned());
143 return ptr::null_mut();
144 }
145 let Ok(payload) = read_bytes(payload, payload_len) else {
146 return ptr::null_mut();
147 };
148 let result = catch_unwind(AssertUnwindSafe(|| (*log).log.append(&payload)));
149 match result {
150 Ok(entry) => Box::into_raw(Box::new(PamojaAuditEntry { entry })),
151 Err(_) => {
152 set_last_error("append panicked".to_owned());
153 ptr::null_mut()
154 }
155 }
156}
157
158#[no_mangle]
167pub unsafe extern "C" fn pamoja_audit_log_free(log: *mut PamojaAuditLog) {
168 if !log.is_null() {
169 drop(Box::from_raw(log));
170 }
171}
172
173#[no_mangle]
190pub unsafe extern "C" fn pamoja_audit_entry_from_bytes(
191 bytes: *const u8,
192 len: usize,
193) -> *mut PamojaAuditEntry {
194 let Ok(bytes) = read_bytes(bytes, len) else {
195 return ptr::null_mut();
196 };
197 match Entry::from_bytes(&bytes) {
198 Ok(entry) => Box::into_raw(Box::new(PamojaAuditEntry { entry })),
199 Err(error) => {
200 set_last_error(error.to_string());
201 ptr::null_mut()
202 }
203 }
204}
205
206#[no_mangle]
221pub unsafe extern "C" fn pamoja_audit_entry_to_bytes(
222 entry: *const PamojaAuditEntry,
223) -> *mut PamojaBuffer {
224 let Some(entry) = entry_handle(entry) else {
225 return ptr::null_mut();
226 };
227 PamojaBuffer::into_raw(entry.entry.to_bytes())
228}
229
230#[no_mangle]
244pub unsafe extern "C" fn pamoja_audit_entry_index(entry: *const PamojaAuditEntry) -> u64 {
245 match entry_handle(entry) {
246 Some(entry) => entry.entry.index(),
247 None => 0,
248 }
249}
250
251#[no_mangle]
269pub unsafe extern "C" fn pamoja_audit_entry_previous(
270 entry: *const PamojaAuditEntry,
271 out_previous: *mut u8,
272) -> PamojaStatus {
273 let Some(entry) = entry_handle(entry) else {
274 return PamojaStatus::InvalidArgument;
275 };
276 write_digest(entry.entry.previous(), out_previous, "out_previous")
277}
278
279#[no_mangle]
295pub unsafe extern "C" fn pamoja_audit_entry_digest(
296 entry: *const PamojaAuditEntry,
297 out_digest: *mut u8,
298) -> PamojaStatus {
299 let Some(entry) = entry_handle(entry) else {
300 return PamojaStatus::InvalidArgument;
301 };
302 write_digest(entry.entry.digest(), out_digest, "out_digest")
303}
304
305#[no_mangle]
321pub unsafe extern "C" fn pamoja_audit_entry_signature(
322 entry: *const PamojaAuditEntry,
323 out_signature: *mut u8,
324) -> PamojaStatus {
325 let Some(entry) = entry_handle(entry) else {
326 return PamojaStatus::InvalidArgument;
327 };
328 if out_signature.is_null() {
329 set_last_error("out_signature must not be null".to_owned());
330 return PamojaStatus::InvalidArgument;
331 }
332 let bytes = entry.entry.signature().to_bytes();
333 ptr::copy_nonoverlapping(bytes.as_ptr(), out_signature, PAMOJA_AUDIT_SIGNATURE_LEN);
334 PamojaStatus::Ok
335}
336
337#[no_mangle]
352pub unsafe extern "C" fn pamoja_audit_entry_payload(
353 entry: *const PamojaAuditEntry,
354) -> *mut PamojaBuffer {
355 let Some(entry) = entry_handle(entry) else {
356 return ptr::null_mut();
357 };
358 PamojaBuffer::into_raw(entry.entry.payload().to_vec())
359}
360
361#[no_mangle]
370pub unsafe extern "C" fn pamoja_audit_entry_free(entry: *mut PamojaAuditEntry) {
371 if !entry.is_null() {
372 drop(Box::from_raw(entry));
373 }
374}
375
376#[no_mangle]
391pub unsafe extern "C" fn pamoja_audit_verifier_new(
392 public_key: *const u8,
393) -> *mut PamojaAuditVerifier {
394 let Ok(public) = read_public(public_key) else {
395 return ptr::null_mut();
396 };
397 Box::into_raw(Box::new(PamojaAuditVerifier {
398 verifier: Verifier::new(public),
399 }))
400}
401
402#[no_mangle]
423pub unsafe extern "C" fn pamoja_audit_verifier_check(
424 verifier: *mut PamojaAuditVerifier,
425 entry: *const PamojaAuditEntry,
426) -> PamojaStatus {
427 if verifier.is_null() {
428 set_last_error("verifier must not be null".to_owned());
429 return PamojaStatus::InvalidArgument;
430 }
431 let Some(entry) = entry_handle(entry) else {
432 return PamojaStatus::InvalidArgument;
433 };
434 match (*verifier).verifier.check(&entry.entry) {
435 Ok(()) => PamojaStatus::Ok,
436 Err(error) => {
437 let status = PamojaStatus::from_error(&error);
438 set_last_error(error.to_string());
439 status
440 }
441 }
442}
443
444#[no_mangle]
453pub unsafe extern "C" fn pamoja_audit_verifier_free(verifier: *mut PamojaAuditVerifier) {
454 if !verifier.is_null() {
455 drop(Box::from_raw(verifier));
456 }
457}
458
459#[no_mangle]
478pub unsafe extern "C" fn pamoja_audit_verify_chain(
479 public_key: *const u8,
480 entries: *const *const PamojaAuditEntry,
481 count: usize,
482) -> PamojaStatus {
483 let public = match read_public(public_key) {
484 Ok(public) => public,
485 Err(status) => return status,
486 };
487 if count != 0 && entries.is_null() {
488 set_last_error("entries must not be null when count is non-zero".to_owned());
489 return PamojaStatus::InvalidArgument;
490 }
491
492 let mut owned = Vec::with_capacity(count);
493 for offset in 0..count {
494 let Some(entry) = entry_handle(*entries.add(offset)) else {
495 return PamojaStatus::InvalidArgument;
496 };
497 owned.push(entry.entry.clone());
498 }
499
500 match verify_chain(&public, &owned) {
501 Ok(()) => PamojaStatus::Ok,
502 Err(error) => {
503 let status = PamojaStatus::from_error(&error);
504 set_last_error(error.to_string());
505 status
506 }
507 }
508}
509
510unsafe fn entry_handle<'a>(entry: *const PamojaAuditEntry) -> Option<&'a PamojaAuditEntry> {
516 if entry.is_null() {
517 set_last_error("entry must not be null".to_owned());
518 return None;
519 }
520 Some(&*entry)
521}
522
523unsafe fn write_digest(
530 digest: [u8; PAMOJA_AUDIT_DIGEST_LEN],
531 out: *mut u8,
532 name: &str,
533) -> PamojaStatus {
534 if out.is_null() {
535 set_last_error(format!("{name} must not be null"));
536 return PamojaStatus::InvalidArgument;
537 }
538 ptr::copy_nonoverlapping(digest.as_ptr(), out, PAMOJA_AUDIT_DIGEST_LEN);
539 PamojaStatus::Ok
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545 use crate::security::{
546 pamoja_device_identity_free, pamoja_device_identity_new, PAMOJA_KEY_LEN,
547 };
548 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
549
550 unsafe fn signer(seed: u8) -> (*mut PamojaDeviceIdentity, [u8; PAMOJA_KEY_LEN]) {
552 let seed = [seed; PAMOJA_KEY_LEN];
553 let identity = pamoja_device_identity_new(seed.as_ptr(), seed.len());
554 assert!(!identity.is_null());
555 let mut public = [0u8; PAMOJA_KEY_LEN];
556 assert_eq!(
557 crate::security::pamoja_device_identity_public_key(identity, public.as_mut_ptr()),
558 PamojaStatus::Ok
559 );
560 (identity, public)
561 }
562
563 unsafe fn take(buffer: *mut PamojaBuffer) -> Vec<u8> {
565 assert!(!buffer.is_null());
566 let bytes =
567 std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
568 .to_vec();
569 pamoja_buffer_free(buffer);
570 bytes
571 }
572
573 #[test]
574 fn a_chain_verifies_entry_by_entry() {
575 unsafe {
576 let (identity, public) = signer(7);
577 let log = pamoja_audit_log_new(identity);
578 let verifier = pamoja_audit_verifier_new(public.as_ptr());
579
580 for index in 0..3u64 {
581 let payload = [index as u8; 4];
582 let entry = pamoja_audit_log_append(log, payload.as_ptr(), payload.len());
583 assert_eq!(pamoja_audit_entry_index(entry), index);
584 assert_eq!(
585 pamoja_audit_verifier_check(verifier, entry),
586 PamojaStatus::Ok
587 );
588 assert_eq!(take(pamoja_audit_entry_payload(entry)), payload);
589 pamoja_audit_entry_free(entry);
590 }
591
592 pamoja_audit_verifier_free(verifier);
593 pamoja_audit_log_free(log);
594 pamoja_device_identity_free(identity);
595 }
596 }
597
598 #[test]
599 fn an_altered_record_breaks_the_chain() {
600 unsafe {
601 let (identity, public) = signer(9);
602 let log = pamoja_audit_log_new(identity);
603
604 let first = pamoja_audit_log_append(log, b"open".as_ptr(), 4);
605 let second = pamoja_audit_log_append(log, b"shut".as_ptr(), 4);
606
607 let mut bytes = take(pamoja_audit_entry_to_bytes(second));
608 let last = bytes.len() - 1;
609 bytes[last] ^= 0xff;
610 let tampered = pamoja_audit_entry_from_bytes(bytes.as_ptr(), bytes.len());
611 assert!(!tampered.is_null());
612
613 let chain = [first.cast_const(), tampered.cast_const()];
614 assert_eq!(
615 pamoja_audit_verify_chain(public.as_ptr(), chain.as_ptr(), chain.len()),
616 PamojaStatus::Auth
617 );
618
619 let honest = [first.cast_const(), second.cast_const()];
620 assert_eq!(
621 pamoja_audit_verify_chain(public.as_ptr(), honest.as_ptr(), honest.len()),
622 PamojaStatus::Ok
623 );
624
625 pamoja_audit_entry_free(tampered);
626 pamoja_audit_entry_free(second);
627 pamoja_audit_entry_free(first);
628 pamoja_audit_log_free(log);
629 pamoja_device_identity_free(identity);
630 }
631 }
632
633 #[test]
634 fn a_resumed_log_continues_the_chain() {
635 unsafe {
636 let (identity, public) = signer(11);
637 let first_log = pamoja_audit_log_new(identity);
638 let first = pamoja_audit_log_append(first_log, b"boot".as_ptr(), 4);
639
640 let resumed = pamoja_audit_log_resume(identity, first);
641 let second = pamoja_audit_log_append(resumed, b"read".as_ptr(), 4);
642 assert_eq!(pamoja_audit_entry_index(second), 1);
643
644 let mut previous = [0u8; PAMOJA_AUDIT_DIGEST_LEN];
645 let mut digest = [0u8; PAMOJA_AUDIT_DIGEST_LEN];
646 assert_eq!(
647 pamoja_audit_entry_previous(second, previous.as_mut_ptr()),
648 PamojaStatus::Ok
649 );
650 assert_eq!(
651 pamoja_audit_entry_digest(first, digest.as_mut_ptr()),
652 PamojaStatus::Ok
653 );
654 assert_eq!(previous, digest);
655
656 let chain = [first.cast_const(), second.cast_const()];
657 assert_eq!(
658 pamoja_audit_verify_chain(public.as_ptr(), chain.as_ptr(), chain.len()),
659 PamojaStatus::Ok
660 );
661
662 pamoja_audit_entry_free(second);
663 pamoja_audit_entry_free(first);
664 pamoja_audit_log_free(resumed);
665 pamoja_audit_log_free(first_log);
666 pamoja_device_identity_free(identity);
667 }
668 }
669
670 #[test]
671 fn a_null_handle_is_refused_rather_than_dereferenced() {
672 unsafe {
673 assert!(pamoja_audit_log_new(ptr::null()).is_null());
674 assert!(pamoja_audit_entry_to_bytes(ptr::null()).is_null());
675 assert_eq!(pamoja_audit_entry_index(ptr::null()), 0);
676 assert_eq!(
677 pamoja_audit_verifier_check(ptr::null_mut(), ptr::null()),
678 PamojaStatus::InvalidArgument
679 );
680 pamoja_audit_entry_free(ptr::null_mut());
681 pamoja_audit_log_free(ptr::null_mut());
682 pamoja_audit_verifier_free(ptr::null_mut());
683 }
684 }
685}