1use std::ptr;
16
17use pamoja_update::{
18 Boot, Delegation, Device, Envelope, Manifest, MemoryStore, PayloadFormat, Refusal, SlotRecord,
19 SlotState, SlotStore, Transfer, Updater, DELEGATION_MAX, DIGEST_LEN, ENVELOPE_MAX, ID_LEN,
20 MANIFEST_MAX, STRUCTURE_VERSION,
21};
22use pamoja_update::{ImageVerifier, Verified};
23
24use crate::security::{identity_handle, read_public, PamojaDeviceIdentity, PAMOJA_KEY_LEN};
25use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
26
27pub const PAMOJA_UPDATE_ID_LEN: usize = 16;
29
30pub const PAMOJA_UPDATE_DIGEST_LEN: usize = 32;
32
33pub const PAMOJA_UPDATE_STRUCTURE_VERSION: u8 = 1;
35
36const _: () = assert!(PAMOJA_UPDATE_ID_LEN == ID_LEN);
39const _: () = assert!(PAMOJA_UPDATE_DIGEST_LEN == DIGEST_LEN);
40const _: () = assert!(PAMOJA_UPDATE_STRUCTURE_VERSION == STRUCTURE_VERSION);
41
42pub const PAMOJA_UPDATE_FORMAT_RAW: u8 = 1;
44
45#[repr(C)]
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct PamojaManifest {
49 pub structure_version: u8,
51 pub sequence: u64,
54 pub vendor_id: [u8; PAMOJA_UPDATE_ID_LEN],
56 pub class_id: [u8; PAMOJA_UPDATE_ID_LEN],
58 pub format: u8,
61 pub storage: u8,
63 pub digest: [u8; PAMOJA_UPDATE_DIGEST_LEN],
65 pub size: u32,
67 pub expires: u64,
70}
71
72#[repr(C)]
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct PamojaDelegation {
76 pub epoch: u64,
79 pub release_key: [u8; PAMOJA_KEY_LEN],
81 pub expires: u64,
84}
85
86#[repr(C)]
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub struct PamojaDevice {
90 pub vendor_id: [u8; PAMOJA_UPDATE_ID_LEN],
92 pub class_id: [u8; PAMOJA_UPDATE_ID_LEN],
94 pub anchor: [u8; PAMOJA_KEY_LEN],
96}
97
98#[repr(C)]
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub enum PamojaSlotState {
102 Empty = 0,
104 Receiving = 1,
106 Staged = 2,
108 Pending = 3,
110 Confirmed = 4,
112 Failed = 5,
114}
115
116#[repr(C)]
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct PamojaSlotRecord {
120 pub state: PamojaSlotState,
122 pub sequence: u64,
124 pub size: u32,
126 pub digest: [u8; PAMOJA_UPDATE_DIGEST_LEN],
128 pub written: u32,
131}
132
133#[repr(C)]
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub enum PamojaBootAction {
137 Confirmed = 0,
139 Trying = 1,
141 Reverted = 2,
143}
144
145#[repr(C)]
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub struct PamojaBoot {
149 pub action: PamojaBootAction,
151 pub slot: u8,
154 pub fallback: u8,
157}
158
159pub struct PamojaImageVerifier {
165 verifier: ImageVerifier,
166}
167
168pub struct PamojaUpdater {
173 updater: Updater<MemoryStore>,
174 staging: Option<Staging>,
175}
176
177struct Staging {
183 envelope: Vec<u8>,
184 now: Option<u64>,
185 transfer: Option<Transfer>,
186}
187
188fn resume<'a>(
190 updater: &'a mut Updater<MemoryStore>,
191 open: &mut Staging,
192) -> Result<pamoja_update::Staging<'a, MemoryStore>, Refusal> {
193 match open.transfer.take() {
194 Some(transfer) => updater.resume_from(&open.envelope, open.now, transfer),
195 None => updater.resume_at(&open.envelope, open.now),
196 }
197}
198
199#[no_mangle]
211pub extern "C" fn pamoja_manifest_encode(manifest: PamojaManifest) -> *mut PamojaBuffer {
212 let Ok(manifest) = rust_manifest(manifest) else {
213 return ptr::null_mut();
214 };
215 let mut buf = [0u8; MANIFEST_MAX];
216 match manifest.encode(&mut buf) {
217 Ok(written) => PamojaBuffer::into_raw(buf[..written].to_vec()),
218 Err(refusal) => {
219 refuse(refusal);
220 ptr::null_mut()
221 }
222 }
223}
224
225#[no_mangle]
245pub unsafe extern "C" fn pamoja_manifest_decode(
246 bytes: *const u8,
247 len: usize,
248 out_manifest: *mut PamojaManifest,
249) -> PamojaStatus {
250 let bytes = match read_bytes(bytes, len) {
251 Ok(bytes) => bytes,
252 Err(status) => return status,
253 };
254 if out_manifest.is_null() {
255 set_last_error("out_manifest must not be null".to_owned());
256 return PamojaStatus::InvalidArgument;
257 }
258 match Manifest::decode(&bytes) {
259 Ok(manifest) => {
260 *out_manifest = boundary_manifest(&manifest);
261 PamojaStatus::Ok
262 }
263 Err(refusal) => refuse(refusal),
264 }
265}
266
267#[no_mangle]
285pub unsafe extern "C" fn pamoja_manifest_sign(
286 manifest: PamojaManifest,
287 author: *const PamojaDeviceIdentity,
288) -> *mut PamojaBuffer {
289 let Ok(manifest) = rust_manifest(manifest) else {
290 return ptr::null_mut();
291 };
292 let Some(author) = identity_handle(author) else {
293 return ptr::null_mut();
294 };
295 let mut buf = [0u8; ENVELOPE_MAX];
296 match manifest.sign(&author.inner, &mut buf) {
297 Ok(written) => PamojaBuffer::into_raw(buf[..written].to_vec()),
298 Err(refusal) => {
299 refuse(refusal);
300 ptr::null_mut()
301 }
302 }
303}
304
305#[no_mangle]
325pub unsafe extern "C" fn pamoja_envelope_verify(
326 bytes: *const u8,
327 len: usize,
328 public_key: *const u8,
329 out_manifest: *mut PamojaManifest,
330) -> PamojaStatus {
331 let bytes = match read_bytes(bytes, len) {
332 Ok(bytes) => bytes,
333 Err(status) => return status,
334 };
335 let public = match read_public(public_key) {
336 Ok(public) => public,
337 Err(status) => return status,
338 };
339 if out_manifest.is_null() {
340 set_last_error("out_manifest must not be null".to_owned());
341 return PamojaStatus::InvalidArgument;
342 }
343
344 let envelope = match Envelope::decode(&bytes) {
345 Ok(envelope) => envelope,
346 Err(refusal) => return refuse(refusal),
347 };
348 match envelope.verify(&public) {
349 Ok(manifest) => {
350 *out_manifest = boundary_manifest(&manifest);
351 PamojaStatus::Ok
352 }
353 Err(refusal) => refuse(refusal),
354 }
355}
356
357#[no_mangle]
378pub unsafe extern "C" fn pamoja_envelope_body(bytes: *const u8, len: usize) -> *mut PamojaBuffer {
379 let Ok(bytes) = read_bytes(bytes, len) else {
380 return ptr::null_mut();
381 };
382 match Envelope::decode(&bytes) {
383 Ok(envelope) => PamojaBuffer::into_raw(envelope.body().to_vec()),
384 Err(refusal) => {
385 refuse(refusal);
386 ptr::null_mut()
387 }
388 }
389}
390
391#[no_mangle]
407pub unsafe extern "C" fn pamoja_delegation_sign(
408 delegation: PamojaDelegation,
409 anchor: *const PamojaDeviceIdentity,
410) -> *mut PamojaBuffer {
411 let Some(anchor) = identity_handle(anchor) else {
412 return ptr::null_mut();
413 };
414 let delegation = Delegation {
415 epoch: delegation.epoch,
416 release_key: delegation.release_key,
417 expires: delegation.expires,
418 };
419 let mut buf = [0u8; DELEGATION_MAX];
420 match delegation.sign(&anchor.inner, &mut buf) {
421 Ok(written) => PamojaBuffer::into_raw(buf[..written].to_vec()),
422 Err(refusal) => {
423 refuse(refusal);
424 ptr::null_mut()
425 }
426 }
427}
428
429#[no_mangle]
449pub unsafe extern "C" fn pamoja_delegation_open(
450 bytes: *const u8,
451 len: usize,
452 anchor_public_key: *const u8,
453 out_delegation: *mut PamojaDelegation,
454) -> PamojaStatus {
455 let bytes = match read_bytes(bytes, len) {
456 Ok(bytes) => bytes,
457 Err(status) => return status,
458 };
459 let anchor = match read_public(anchor_public_key) {
460 Ok(anchor) => anchor,
461 Err(status) => return status,
462 };
463 if out_delegation.is_null() {
464 set_last_error("out_delegation must not be null".to_owned());
465 return PamojaStatus::InvalidArgument;
466 }
467 match Delegation::open(&bytes, &anchor) {
468 Ok(delegation) => {
469 *out_delegation = boundary_delegation(&delegation);
470 PamojaStatus::Ok
471 }
472 Err(refusal) => refuse(refusal),
473 }
474}
475
476#[no_mangle]
497pub unsafe extern "C" fn pamoja_image_digest(
498 image: *const u8,
499 image_len: usize,
500 out_digest: *mut u8,
501) -> PamojaStatus {
502 if out_digest.is_null() {
503 set_last_error("out_digest must not be null".to_owned());
504 return PamojaStatus::InvalidArgument;
505 }
506 let image = match read_bytes(image, image_len) {
507 Ok(image) => image,
508 Err(status) => return status,
509 };
510 let digest = pamoja_update::image_digest(&image);
511 core::ptr::copy_nonoverlapping(digest.as_ptr(), out_digest, digest.len());
512 PamojaStatus::Ok
513}
514
515#[no_mangle]
517pub extern "C" fn pamoja_image_verifier_new(manifest: PamojaManifest) -> *mut PamojaImageVerifier {
518 let Ok(manifest) = rust_manifest(manifest) else {
519 return ptr::null_mut();
520 };
521 Box::into_raw(Box::new(PamojaImageVerifier {
522 verifier: ImageVerifier::new(&manifest),
523 }))
524}
525
526#[no_mangle]
545pub unsafe extern "C" fn pamoja_image_verifier_update(
546 verifier: *mut PamojaImageVerifier,
547 chunk: *const u8,
548 len: usize,
549) -> PamojaStatus {
550 if verifier.is_null() {
551 set_last_error("verifier must not be null".to_owned());
552 return PamojaStatus::InvalidArgument;
553 }
554 let chunk = match read_bytes(chunk, len) {
555 Ok(chunk) => chunk,
556 Err(status) => return status,
557 };
558 match (*verifier).verifier.update(&chunk) {
559 Ok(()) => PamojaStatus::Ok,
560 Err(refusal) => refuse(refusal),
561 }
562}
563
564#[no_mangle]
587pub unsafe extern "C" fn pamoja_image_verifier_finish(
588 verifier: *mut PamojaImageVerifier,
589 out_size: *mut u32,
590 out_digest: *mut u8,
591) -> PamojaStatus {
592 if verifier.is_null() {
593 set_last_error("verifier must not be null".to_owned());
594 return PamojaStatus::InvalidArgument;
595 }
596 let owned = Box::from_raw(verifier);
597 match owned.verifier.finish() {
598 Ok(verified) => {
599 write_verified(&verified, out_size, out_digest);
600 PamojaStatus::Ok
601 }
602 Err(refusal) => refuse(refusal),
603 }
604}
605
606#[no_mangle]
615pub unsafe extern "C" fn pamoja_image_verifier_free(verifier: *mut PamojaImageVerifier) {
616 if !verifier.is_null() {
617 drop(Box::from_raw(verifier));
618 }
619}
620
621#[no_mangle]
634pub extern "C" fn pamoja_updater_new(
635 device: PamojaDevice,
636 slot_count: u8,
637 slot_capacity: u32,
638) -> *mut PamojaUpdater {
639 let anchor = unsafe { read_public(device.anchor.as_ptr()) };
642 let Ok(anchor) = anchor else {
643 return ptr::null_mut();
644 };
645 let device = Device {
646 vendor_id: device.vendor_id,
647 class_id: device.class_id,
648 anchor,
649 };
650 Box::into_raw(Box::new(PamojaUpdater {
651 updater: Updater::new(device, MemoryStore::new(slot_count, slot_capacity)),
652 staging: None,
653 }))
654}
655
656#[no_mangle]
678pub unsafe extern "C" fn pamoja_updater_adopt(
679 updater: *mut PamojaUpdater,
680 bytes: *const u8,
681 len: usize,
682 has_now: bool,
683 now: u64,
684 out_delegation: *mut PamojaDelegation,
685) -> PamojaStatus {
686 if updater.is_null() {
687 set_last_error("updater must not be null".to_owned());
688 return PamojaStatus::InvalidArgument;
689 }
690 let bytes = match read_bytes(bytes, len) {
691 Ok(bytes) => bytes,
692 Err(status) => return status,
693 };
694 match (*updater).updater.adopt(&bytes, clock(has_now, now)) {
695 Ok(delegation) => {
696 if !out_delegation.is_null() {
697 *out_delegation = boundary_delegation(&delegation);
698 }
699 PamojaStatus::Ok
700 }
701 Err(refusal) => refuse(refusal),
702 }
703}
704
705#[no_mangle]
722pub unsafe extern "C" fn pamoja_updater_delegation(
723 updater: *const PamojaUpdater,
724 out_delegation: *mut PamojaDelegation,
725) -> bool {
726 if updater.is_null() {
727 return false;
728 }
729 match (*updater).updater.delegation() {
730 Some(delegation) => {
731 if !out_delegation.is_null() {
732 *out_delegation = boundary_delegation(&delegation);
733 }
734 true
735 }
736 None => false,
737 }
738}
739
740#[no_mangle]
756pub unsafe extern "C" fn pamoja_updater_installed_sequence(
757 updater: *const PamojaUpdater,
758 out_sequence: *mut u64,
759) -> PamojaStatus {
760 if updater.is_null() || out_sequence.is_null() {
761 set_last_error("updater and out_sequence must not be null".to_owned());
762 return PamojaStatus::InvalidArgument;
763 }
764 match (*updater).updater.installed_sequence() {
765 Ok(sequence) => {
766 *out_sequence = sequence;
767 PamojaStatus::Ok
768 }
769 Err(refusal) => refuse(refusal),
770 }
771}
772
773#[no_mangle]
790pub unsafe extern "C" fn pamoja_updater_slot_record(
791 updater: *const PamojaUpdater,
792 slot: u8,
793 out_record: *mut PamojaSlotRecord,
794) -> PamojaStatus {
795 if updater.is_null() || out_record.is_null() {
796 set_last_error("updater and out_record must not be null".to_owned());
797 return PamojaStatus::InvalidArgument;
798 }
799 match (*updater).updater.store().record(slot) {
800 Ok(record) => {
801 *out_record = boundary_record(&record);
802 PamojaStatus::Ok
803 }
804 Err(refusal) => refuse(refusal),
805 }
806}
807
808#[no_mangle]
822pub unsafe extern "C" fn pamoja_updater_slot_count(updater: *const PamojaUpdater) -> u8 {
823 if updater.is_null() {
824 return 0;
825 }
826 (*updater).updater.store().slot_count()
827}
828
829#[no_mangle]
848pub unsafe extern "C" fn pamoja_updater_provision(
849 updater: *mut PamojaUpdater,
850 slot: u8,
851 sequence: u64,
852) -> PamojaStatus {
853 if updater.is_null() {
854 set_last_error("updater must not be null".to_owned());
855 return PamojaStatus::InvalidArgument;
856 }
857 match (*updater).updater.provision(slot, sequence) {
858 Ok(()) => PamojaStatus::Ok,
859 Err(refusal) => refuse(refusal),
860 }
861}
862
863#[allow(clippy::too_many_arguments)]
887#[no_mangle]
888pub unsafe extern "C" fn pamoja_updater_stage(
889 updater: *mut PamojaUpdater,
890 envelope: *const u8,
891 envelope_len: usize,
892 image: *const u8,
893 image_len: usize,
894 has_now: bool,
895 now: u64,
896 out_slot: *mut u8,
897) -> PamojaStatus {
898 if updater.is_null() {
899 set_last_error("updater must not be null".to_owned());
900 return PamojaStatus::InvalidArgument;
901 }
902 let envelope = match read_bytes(envelope, envelope_len) {
903 Ok(envelope) => envelope,
904 Err(status) => return status,
905 };
906 let image = match read_bytes(image, image_len) {
907 Ok(image) => image,
908 Err(status) => return status,
909 };
910 match (*updater)
911 .updater
912 .stage_at(&envelope, &image, clock(has_now, now))
913 {
914 Ok(slot) => {
915 write_slot(slot, out_slot);
916 PamojaStatus::Ok
917 }
918 Err(refusal) => refuse(refusal),
919 }
920}
921
922#[no_mangle]
953pub unsafe extern "C" fn pamoja_updater_begin(
954 updater: *mut PamojaUpdater,
955 envelope: *const u8,
956 envelope_len: usize,
957 has_now: bool,
958 now: u64,
959 out_slot: *mut u8,
960) -> PamojaStatus {
961 if updater.is_null() {
962 set_last_error("updater must not be null".to_owned());
963 return PamojaStatus::InvalidArgument;
964 }
965 let envelope = match read_bytes(envelope, envelope_len) {
966 Ok(envelope) => envelope,
967 Err(status) => return status,
968 };
969 let now = clock(has_now, now);
970
971 let handle = &mut *updater;
972 let staging = match handle.updater.begin_at(&envelope, now) {
973 Ok(staging) => staging,
974 Err(refusal) => return refuse(refusal),
975 };
976 let slot = staging.manifest().storage;
977 let transfer = Some(staging.detach());
978 handle.staging = Some(Staging {
979 envelope,
980 now,
981 transfer,
982 });
983 write_slot(slot, out_slot);
984 PamojaStatus::Ok
985}
986
987#[no_mangle]
1004pub unsafe extern "C" fn pamoja_updater_write(
1005 updater: *mut PamojaUpdater,
1006 chunk: *const u8,
1007 len: usize,
1008) -> PamojaStatus {
1009 if updater.is_null() {
1010 set_last_error("updater must not be null".to_owned());
1011 return PamojaStatus::InvalidArgument;
1012 }
1013 let chunk = match read_bytes(chunk, len) {
1014 Ok(chunk) => chunk,
1015 Err(status) => return status,
1016 };
1017 let handle = &mut *updater;
1018 let Some(open) = open_transfer(&mut handle.staging) else {
1019 return PamojaStatus::InvalidArgument;
1020 };
1021 let mut staging = match resume(&mut handle.updater, open) {
1022 Ok(staging) => staging,
1023 Err(refusal) => return refuse(refusal),
1024 };
1025 if let Err(refusal) = staging.write(&chunk) {
1026 return refuse(refusal);
1027 }
1028 open.transfer = Some(staging.detach());
1029 PamojaStatus::Ok
1030}
1031
1032#[no_mangle]
1049pub unsafe extern "C" fn pamoja_updater_progress(
1050 updater: *mut PamojaUpdater,
1051 out_written: *mut u32,
1052 out_total: *mut u32,
1053) -> PamojaStatus {
1054 if updater.is_null() {
1055 set_last_error("updater must not be null".to_owned());
1056 return PamojaStatus::InvalidArgument;
1057 }
1058 let handle = &mut *updater;
1059 let Some(open) = open_transfer(&mut handle.staging) else {
1060 return PamojaStatus::InvalidArgument;
1061 };
1062 let staging = match resume(&mut handle.updater, open) {
1063 Ok(staging) => staging,
1064 Err(refusal) => return refuse(refusal),
1065 };
1066 let (written, total) = staging.progress();
1067 open.transfer = Some(staging.detach());
1068 if !out_written.is_null() {
1069 *out_written = written;
1070 }
1071 if !out_total.is_null() {
1072 *out_total = total;
1073 }
1074 PamojaStatus::Ok
1075}
1076
1077#[no_mangle]
1094pub unsafe extern "C" fn pamoja_updater_finish(
1095 updater: *mut PamojaUpdater,
1096 out_slot: *mut u8,
1097) -> PamojaStatus {
1098 if updater.is_null() {
1099 set_last_error("updater must not be null".to_owned());
1100 return PamojaStatus::InvalidArgument;
1101 }
1102 let handle = &mut *updater;
1103 let Some(open) = open_transfer(&mut handle.staging) else {
1104 return PamojaStatus::InvalidArgument;
1105 };
1106 let outcome = resume(&mut handle.updater, open).and_then(|staging| staging.finish());
1107 match outcome {
1108 Ok(slot) => {
1109 handle.staging = None;
1110 write_slot(slot, out_slot);
1111 PamojaStatus::Ok
1112 }
1113 Err(refusal) => refuse(refusal),
1114 }
1115}
1116
1117#[no_mangle]
1138pub unsafe extern "C" fn pamoja_updater_on_boot(
1139 updater: *mut PamojaUpdater,
1140 out_boot: *mut PamojaBoot,
1141) -> PamojaStatus {
1142 if updater.is_null() || out_boot.is_null() {
1143 set_last_error("updater and out_boot must not be null".to_owned());
1144 return PamojaStatus::InvalidArgument;
1145 }
1146 match (*updater).updater.on_boot() {
1147 Ok(boot) => {
1148 *out_boot = boundary_boot(boot);
1149 PamojaStatus::Ok
1150 }
1151 Err(refusal) => refuse(refusal),
1152 }
1153}
1154
1155#[no_mangle]
1171pub unsafe extern "C" fn pamoja_updater_confirm(
1172 updater: *mut PamojaUpdater,
1173 out_slot: *mut u8,
1174) -> PamojaStatus {
1175 if updater.is_null() {
1176 set_last_error("updater must not be null".to_owned());
1177 return PamojaStatus::InvalidArgument;
1178 }
1179 match (*updater).updater.confirm() {
1180 Ok(slot) => {
1181 write_slot(slot, out_slot);
1182 PamojaStatus::Ok
1183 }
1184 Err(refusal) => refuse(refusal),
1185 }
1186}
1187
1188#[no_mangle]
1205pub unsafe extern "C" fn pamoja_updater_revert(
1206 updater: *mut PamojaUpdater,
1207 out_slot: *mut u8,
1208) -> PamojaStatus {
1209 if updater.is_null() {
1210 set_last_error("updater must not be null".to_owned());
1211 return PamojaStatus::InvalidArgument;
1212 }
1213 match (*updater).updater.revert() {
1214 Ok(slot) => {
1215 write_slot(slot, out_slot);
1216 PamojaStatus::Ok
1217 }
1218 Err(refusal) => refuse(refusal),
1219 }
1220}
1221
1222#[no_mangle]
1231pub unsafe extern "C" fn pamoja_updater_free(updater: *mut PamojaUpdater) {
1232 if !updater.is_null() {
1233 drop(Box::from_raw(updater));
1234 }
1235}
1236
1237fn refuse(refusal: Refusal) -> PamojaStatus {
1239 let error = pamoja_core::Error::from(refusal);
1240 let status = PamojaStatus::from_error(&error);
1241 set_last_error(refusal.reason().to_owned());
1242 status
1243}
1244
1245pub const PAMOJA_UPDATE_BLOCK_HEADER_LEN: usize = 6;
1247
1248const _: () = assert!(PAMOJA_UPDATE_BLOCK_HEADER_LEN == pamoja_update::block::HEADER_LEN);
1251
1252#[no_mangle]
1283pub unsafe extern "C" fn pamoja_update_block_frame(
1284 envelope: *const u8,
1285 envelope_len: usize,
1286 image: *const u8,
1287 image_len: usize,
1288 out_block: *mut u8,
1289 capacity: usize,
1290 out_len: *mut usize,
1291) -> PamojaStatus {
1292 let (Ok(envelope), Ok(image), false, false) = (
1293 read_bytes(envelope, envelope_len),
1294 read_bytes(image, image_len),
1295 out_block.is_null(),
1296 out_len.is_null(),
1297 ) else {
1298 set_last_error("a required argument was null".to_owned());
1299 return PamojaStatus::InvalidArgument;
1300 };
1301 let out = core::slice::from_raw_parts_mut(out_block, capacity);
1302 match pamoja_update::block::frame(&envelope, &image, out) {
1303 Ok(written) => {
1304 *out_len = written;
1305 PamojaStatus::Ok
1306 }
1307 Err(refusal) => refuse(refusal),
1308 }
1309}
1310
1311#[no_mangle]
1336pub unsafe extern "C" fn pamoja_update_block_split(
1337 block: *const u8,
1338 block_len: usize,
1339 out_envelope_at: *mut usize,
1340 out_envelope_len: *mut usize,
1341 out_image_at: *mut usize,
1342 out_image_len: *mut usize,
1343) -> PamojaStatus {
1344 let Ok(bytes) = read_bytes(block, block_len) else {
1345 set_last_error("a required argument was null".to_owned());
1346 return PamojaStatus::InvalidArgument;
1347 };
1348 match pamoja_update::block::split(&bytes) {
1349 Ok((envelope, image)) => {
1350 if !out_envelope_at.is_null() {
1351 *out_envelope_at = PAMOJA_UPDATE_BLOCK_HEADER_LEN;
1352 }
1353 if !out_envelope_len.is_null() {
1354 *out_envelope_len = envelope.len();
1355 }
1356 if !out_image_at.is_null() {
1357 *out_image_at = PAMOJA_UPDATE_BLOCK_HEADER_LEN + envelope.len();
1358 }
1359 if !out_image_len.is_null() {
1360 *out_image_len = image.len();
1361 }
1362 PamojaStatus::Ok
1363 }
1364 Err(refusal) => refuse(refusal),
1365 }
1366}
1367
1368fn clock(has_now: bool, now: u64) -> Option<u64> {
1370 has_now.then_some(now)
1371}
1372
1373unsafe fn write_slot(slot: u8, out_slot: *mut u8) {
1379 if !out_slot.is_null() {
1380 *out_slot = slot;
1381 }
1382}
1383
1384unsafe fn write_verified(verified: &Verified, out_size: *mut u32, out_digest: *mut u8) {
1391 if !out_size.is_null() {
1392 *out_size = verified.size();
1393 }
1394 if !out_digest.is_null() {
1395 let digest = verified.digest();
1396 ptr::copy_nonoverlapping(digest.as_ptr(), out_digest, PAMOJA_UPDATE_DIGEST_LEN);
1397 }
1398}
1399
1400fn open_transfer(staging: &mut Option<Staging>) -> Option<&mut Staging> {
1402 if staging.is_none() {
1403 set_last_error("no transfer is open; call pamoja_updater_begin first".to_owned());
1404 }
1405 staging.as_mut()
1406}
1407
1408fn rust_manifest(manifest: PamojaManifest) -> Result<Manifest, PamojaStatus> {
1410 if manifest.format != PAMOJA_UPDATE_FORMAT_RAW {
1411 return Err(refuse(Refusal::UnsupportedVersion));
1412 }
1413 Ok(Manifest {
1414 structure_version: manifest.structure_version,
1415 sequence: manifest.sequence,
1416 vendor_id: manifest.vendor_id,
1417 class_id: manifest.class_id,
1418 format: PayloadFormat::Raw,
1419 storage: manifest.storage,
1420 digest: manifest.digest,
1421 size: manifest.size,
1422 expires: manifest.expires,
1423 })
1424}
1425
1426fn boundary_manifest(manifest: &Manifest) -> PamojaManifest {
1428 PamojaManifest {
1429 structure_version: manifest.structure_version,
1430 sequence: manifest.sequence,
1431 vendor_id: manifest.vendor_id,
1432 class_id: manifest.class_id,
1433 format: manifest.format as u8,
1434 storage: manifest.storage,
1435 digest: manifest.digest,
1436 size: manifest.size,
1437 expires: manifest.expires,
1438 }
1439}
1440
1441fn boundary_delegation(delegation: &Delegation) -> PamojaDelegation {
1443 PamojaDelegation {
1444 epoch: delegation.epoch,
1445 release_key: delegation.release_key,
1446 expires: delegation.expires,
1447 }
1448}
1449
1450fn boundary_record(record: &SlotRecord) -> PamojaSlotRecord {
1452 PamojaSlotRecord {
1453 state: match record.state {
1454 SlotState::Empty => PamojaSlotState::Empty,
1455 SlotState::Receiving => PamojaSlotState::Receiving,
1456 SlotState::Staged => PamojaSlotState::Staged,
1457 SlotState::Pending => PamojaSlotState::Pending,
1458 SlotState::Confirmed => PamojaSlotState::Confirmed,
1459 SlotState::Failed => PamojaSlotState::Failed,
1460 },
1461 sequence: record.sequence,
1462 size: record.size,
1463 digest: record.digest,
1464 written: record.written,
1465 }
1466}
1467
1468fn boundary_boot(boot: Boot) -> PamojaBoot {
1470 match boot {
1471 Boot::Confirmed(slot) => PamojaBoot {
1472 action: PamojaBootAction::Confirmed,
1473 slot,
1474 fallback: slot,
1475 },
1476 Boot::Trying(slot) => PamojaBoot {
1477 action: PamojaBootAction::Trying,
1478 slot,
1479 fallback: slot,
1480 },
1481 Boot::Reverted { failed, fallback } => PamojaBoot {
1482 action: PamojaBootAction::Reverted,
1483 slot: failed,
1484 fallback,
1485 },
1486 }
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491 use sha2::{Digest, Sha256};
1492
1493 use super::*;
1494 use crate::security::{
1495 pamoja_device_identity_free, pamoja_device_identity_new, pamoja_device_identity_public_key,
1496 };
1497 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
1498
1499 unsafe fn signer(seed: u8) -> (*mut PamojaDeviceIdentity, [u8; PAMOJA_KEY_LEN]) {
1501 let seed = [seed; PAMOJA_KEY_LEN];
1502 let identity = pamoja_device_identity_new(seed.as_ptr(), seed.len());
1503 assert!(!identity.is_null());
1504 let mut public = [0u8; PAMOJA_KEY_LEN];
1505 assert_eq!(
1506 pamoja_device_identity_public_key(identity, public.as_mut_ptr()),
1507 PamojaStatus::Ok
1508 );
1509 (identity, public)
1510 }
1511
1512 unsafe fn take(buffer: *mut PamojaBuffer) -> Vec<u8> {
1514 assert!(!buffer.is_null());
1515 let bytes =
1516 std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
1517 .to_vec();
1518 pamoja_buffer_free(buffer);
1519 bytes
1520 }
1521
1522 fn manifest(image: &[u8], sequence: u64, storage: u8) -> PamojaManifest {
1524 let digest: [u8; PAMOJA_UPDATE_DIGEST_LEN] = Sha256::digest(image).into();
1525 PamojaManifest {
1526 structure_version: PAMOJA_UPDATE_STRUCTURE_VERSION,
1527 sequence,
1528 vendor_id: [1; PAMOJA_UPDATE_ID_LEN],
1529 class_id: [2; PAMOJA_UPDATE_ID_LEN],
1530 format: PAMOJA_UPDATE_FORMAT_RAW,
1531 storage,
1532 digest,
1533 size: image.len() as u32,
1534 expires: 0,
1535 }
1536 }
1537
1538 fn updater(anchor: [u8; PAMOJA_KEY_LEN]) -> *mut PamojaUpdater {
1540 let handle = pamoja_updater_new(
1541 PamojaDevice {
1542 vendor_id: [1; PAMOJA_UPDATE_ID_LEN],
1543 class_id: [2; PAMOJA_UPDATE_ID_LEN],
1544 anchor,
1545 },
1546 2,
1547 4096,
1548 );
1549 assert!(!handle.is_null());
1550 handle
1551 }
1552
1553 #[test]
1554 fn a_signed_release_stages_boots_and_confirms() {
1555 unsafe {
1556 let (author, anchor) = signer(3);
1557 let device = updater(anchor);
1558 assert_eq!(pamoja_updater_slot_count(device), 2);
1559 assert_eq!(pamoja_updater_provision(device, 0, 1), PamojaStatus::Ok);
1560
1561 let image = vec![0xa5u8; 512];
1562 let envelope = take(pamoja_manifest_sign(manifest(&image, 2, 1), author));
1563
1564 let mut slot = 0u8;
1565 assert_eq!(
1566 pamoja_updater_stage(
1567 device,
1568 envelope.as_ptr(),
1569 envelope.len(),
1570 image.as_ptr(),
1571 image.len(),
1572 false,
1573 0,
1574 &mut slot,
1575 ),
1576 PamojaStatus::Ok
1577 );
1578 assert_eq!(slot, 1);
1579
1580 let mut boot = PamojaBoot {
1581 action: PamojaBootAction::Confirmed,
1582 slot: 0,
1583 fallback: 0,
1584 };
1585 assert_eq!(pamoja_updater_on_boot(device, &mut boot), PamojaStatus::Ok);
1586 assert_eq!(boot.action, PamojaBootAction::Trying);
1587 assert_eq!(boot.slot, 1);
1588
1589 let mut confirmed = 0u8;
1590 assert_eq!(
1591 pamoja_updater_confirm(device, &mut confirmed),
1592 PamojaStatus::Ok
1593 );
1594 assert_eq!(confirmed, 1);
1595
1596 let mut record = boundary_record(&SlotRecord::default());
1597 assert_eq!(
1598 pamoja_updater_slot_record(device, 1, &mut record),
1599 PamojaStatus::Ok
1600 );
1601 assert_eq!(record.state, PamojaSlotState::Confirmed);
1602 assert_eq!(record.size, image.len() as u32);
1603
1604 pamoja_updater_free(device);
1605 pamoja_device_identity_free(author);
1606 }
1607 }
1608
1609 #[test]
1610 fn an_image_arriving_in_pieces_reaches_the_same_place() {
1611 unsafe {
1612 let (author, anchor) = signer(4);
1613 let device = updater(anchor);
1614 assert_eq!(pamoja_updater_provision(device, 0, 1), PamojaStatus::Ok);
1615
1616 let image = vec![0x5au8; 300];
1617 let envelope = take(pamoja_manifest_sign(manifest(&image, 2, 1), author));
1618
1619 let mut slot = 0u8;
1620 assert_eq!(
1621 pamoja_updater_begin(
1622 device,
1623 envelope.as_ptr(),
1624 envelope.len(),
1625 false,
1626 0,
1627 &mut slot
1628 ),
1629 PamojaStatus::Ok
1630 );
1631 assert_eq!(slot, 1);
1632
1633 for chunk in image.chunks(64) {
1634 assert_eq!(
1635 pamoja_updater_write(device, chunk.as_ptr(), chunk.len()),
1636 PamojaStatus::Ok
1637 );
1638 }
1639
1640 let (mut written, mut total) = (0u32, 0u32);
1641 assert_eq!(
1642 pamoja_updater_progress(device, &mut written, &mut total),
1643 PamojaStatus::Ok
1644 );
1645 assert_eq!(written, image.len() as u32);
1646 assert_eq!(total, image.len() as u32);
1647
1648 let mut staged = 0u8;
1649 assert_eq!(pamoja_updater_finish(device, &mut staged), PamojaStatus::Ok);
1650 assert_eq!(staged, 1);
1651
1652 pamoja_updater_free(device);
1653 pamoja_device_identity_free(author);
1654 }
1655 }
1656
1657 #[test]
1658 fn a_release_from_an_untrusted_key_is_refused() {
1659 unsafe {
1660 let (_, anchor) = signer(5);
1661 let (impostor, _) = signer(6);
1662 let device = updater(anchor);
1663 assert_eq!(pamoja_updater_provision(device, 0, 1), PamojaStatus::Ok);
1664
1665 let image = vec![0u8; 16];
1666 let envelope = take(pamoja_manifest_sign(manifest(&image, 2, 1), impostor));
1667
1668 assert_eq!(
1669 pamoja_updater_stage(
1670 device,
1671 envelope.as_ptr(),
1672 envelope.len(),
1673 image.as_ptr(),
1674 image.len(),
1675 false,
1676 0,
1677 ptr::null_mut(),
1678 ),
1679 PamojaStatus::Auth
1680 );
1681
1682 pamoja_updater_free(device);
1683 pamoja_device_identity_free(impostor);
1684 }
1685 }
1686
1687 #[test]
1688 fn an_older_release_cannot_roll_a_device_back() {
1689 unsafe {
1690 let (author, anchor) = signer(7);
1691 let device = updater(anchor);
1692 assert_eq!(pamoja_updater_provision(device, 0, 9), PamojaStatus::Ok);
1693
1694 let image = vec![0u8; 16];
1695 let envelope = take(pamoja_manifest_sign(manifest(&image, 4, 1), author));
1696
1697 assert_eq!(
1698 pamoja_updater_stage(
1699 device,
1700 envelope.as_ptr(),
1701 envelope.len(),
1702 image.as_ptr(),
1703 image.len(),
1704 false,
1705 0,
1706 ptr::null_mut(),
1707 ),
1708 PamojaStatus::Auth
1709 );
1710
1711 let mut sequence = 0u64;
1712 assert_eq!(
1713 pamoja_updater_installed_sequence(device, &mut sequence),
1714 PamojaStatus::Ok
1715 );
1716 assert_eq!(sequence, 9);
1717
1718 pamoja_updater_free(device);
1719 pamoja_device_identity_free(author);
1720 }
1721 }
1722
1723 #[test]
1724 fn a_delegated_key_may_sign_releases() {
1725 unsafe {
1726 let (anchor_identity, anchor) = signer(8);
1727 let (release_identity, release_key) = signer(9);
1728 let device = updater(anchor);
1729 assert_eq!(pamoja_updater_provision(device, 0, 1), PamojaStatus::Ok);
1730
1731 let statement = PamojaDelegation {
1732 epoch: 1,
1733 release_key,
1734 expires: 0,
1735 };
1736 let signed = take(pamoja_delegation_sign(statement, anchor_identity));
1737
1738 let mut opened = PamojaDelegation {
1739 epoch: 0,
1740 release_key: [0; PAMOJA_KEY_LEN],
1741 expires: 0,
1742 };
1743 assert_eq!(
1744 pamoja_delegation_open(signed.as_ptr(), signed.len(), anchor.as_ptr(), &mut opened),
1745 PamojaStatus::Ok
1746 );
1747 assert_eq!(opened.release_key, release_key);
1748
1749 assert_eq!(
1750 pamoja_updater_adopt(
1751 device,
1752 signed.as_ptr(),
1753 signed.len(),
1754 false,
1755 0,
1756 ptr::null_mut()
1757 ),
1758 PamojaStatus::Ok
1759 );
1760 assert!(pamoja_updater_delegation(device, ptr::null_mut()));
1761
1762 let image = vec![7u8; 64];
1763 let envelope = take(pamoja_manifest_sign(
1764 manifest(&image, 2, 1),
1765 release_identity,
1766 ));
1767 assert_eq!(
1768 pamoja_updater_stage(
1769 device,
1770 envelope.as_ptr(),
1771 envelope.len(),
1772 image.as_ptr(),
1773 image.len(),
1774 false,
1775 0,
1776 ptr::null_mut(),
1777 ),
1778 PamojaStatus::Ok
1779 );
1780
1781 pamoja_updater_free(device);
1782 pamoja_device_identity_free(release_identity);
1783 pamoja_device_identity_free(anchor_identity);
1784 }
1785 }
1786
1787 #[test]
1788 fn a_manifest_survives_a_round_trip_and_verifies() {
1789 unsafe {
1790 let (author, public) = signer(10);
1791 let image = vec![3u8; 128];
1792 let want = manifest(&image, 5, 1);
1793
1794 let body = take(pamoja_manifest_encode(want));
1795 let mut decoded = want;
1796 assert_eq!(
1797 pamoja_manifest_decode(body.as_ptr(), body.len(), &mut decoded),
1798 PamojaStatus::Ok
1799 );
1800 assert_eq!(decoded, want);
1801
1802 let envelope = take(pamoja_manifest_sign(want, author));
1803 assert_eq!(
1804 take(pamoja_envelope_body(envelope.as_ptr(), envelope.len())),
1805 body
1806 );
1807
1808 let mut verified = want;
1809 assert_eq!(
1810 pamoja_envelope_verify(
1811 envelope.as_ptr(),
1812 envelope.len(),
1813 public.as_ptr(),
1814 &mut verified
1815 ),
1816 PamojaStatus::Ok
1817 );
1818 assert_eq!(verified, want);
1819
1820 pamoja_device_identity_free(author);
1821 }
1822 }
1823
1824 #[test]
1825 fn a_verifier_refuses_an_image_that_is_not_the_one_described() {
1826 unsafe {
1827 let image = vec![1u8; 64];
1828 let want = manifest(&image, 2, 1);
1829
1830 let verifier = pamoja_image_verifier_new(want);
1831 assert_eq!(
1832 pamoja_image_verifier_update(verifier, image.as_ptr(), image.len()),
1833 PamojaStatus::Ok
1834 );
1835 let mut size = 0u32;
1836 let mut digest = [0u8; PAMOJA_UPDATE_DIGEST_LEN];
1837 assert_eq!(
1838 pamoja_image_verifier_finish(verifier, &mut size, digest.as_mut_ptr()),
1839 PamojaStatus::Ok
1840 );
1841 assert_eq!(size, image.len() as u32);
1842 assert_eq!(digest, want.digest);
1843
1844 let mut altered = image.clone();
1845 altered[0] ^= 0xff;
1846 let verifier = pamoja_image_verifier_new(want);
1847 assert_eq!(
1848 pamoja_image_verifier_update(verifier, altered.as_ptr(), altered.len()),
1849 PamojaStatus::Ok
1850 );
1851 assert_eq!(
1852 pamoja_image_verifier_finish(verifier, ptr::null_mut(), ptr::null_mut()),
1853 PamojaStatus::Auth
1854 );
1855 }
1856 }
1857
1858 #[test]
1859 fn writing_without_opening_a_transfer_is_refused() {
1860 unsafe {
1861 let (_, anchor) = signer(11);
1862 let device = updater(anchor);
1863
1864 assert_eq!(
1865 pamoja_updater_write(device, b"x".as_ptr(), 1),
1866 PamojaStatus::InvalidArgument
1867 );
1868
1869 pamoja_updater_free(device);
1870 }
1871 }
1872
1873 #[test]
1874 fn a_null_handle_is_refused_rather_than_dereferenced() {
1875 unsafe {
1876 assert!(pamoja_manifest_sign(manifest(&[], 1, 0), ptr::null()).is_null());
1877 assert_eq!(
1878 pamoja_updater_on_boot(ptr::null_mut(), ptr::null_mut()),
1879 PamojaStatus::InvalidArgument
1880 );
1881 assert_eq!(
1882 pamoja_updater_write(ptr::null_mut(), b"x".as_ptr(), 1),
1883 PamojaStatus::InvalidArgument
1884 );
1885 assert_eq!(
1886 pamoja_updater_progress(ptr::null_mut(), ptr::null_mut(), ptr::null_mut()),
1887 PamojaStatus::InvalidArgument
1888 );
1889 assert_eq!(
1890 pamoja_updater_finish(ptr::null_mut(), ptr::null_mut()),
1891 PamojaStatus::InvalidArgument
1892 );
1893 assert!(!pamoja_updater_delegation(ptr::null(), ptr::null_mut()));
1894 assert_eq!(pamoja_updater_slot_count(ptr::null()), 0);
1895 pamoja_updater_free(ptr::null_mut());
1896 pamoja_image_verifier_free(ptr::null_mut());
1897 }
1898 }
1899}