1use std::ptr;
16
17use pamoja_update::{
18 Boot, Delegation, Device, Envelope, Manifest, MemoryStore, PayloadFormat, Refusal, SlotRecord,
19 SlotState, SlotStore, Updater, DELEGATION_MAX, DIGEST_LEN, ENVELOPE_MAX, ID_LEN, MANIFEST_MAX,
20 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 = ID_LEN;
29
30pub const PAMOJA_UPDATE_DIGEST_LEN: usize = DIGEST_LEN;
32
33pub const PAMOJA_UPDATE_STRUCTURE_VERSION: u8 = STRUCTURE_VERSION;
35
36pub const PAMOJA_UPDATE_FORMAT_RAW: u8 = 1;
38
39#[repr(C)]
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct PamojaManifest {
43 pub structure_version: u8,
45 pub sequence: u64,
48 pub vendor_id: [u8; PAMOJA_UPDATE_ID_LEN],
50 pub class_id: [u8; PAMOJA_UPDATE_ID_LEN],
52 pub format: u8,
55 pub storage: u8,
57 pub digest: [u8; PAMOJA_UPDATE_DIGEST_LEN],
59 pub size: u32,
61 pub expires: u64,
64}
65
66#[repr(C)]
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub struct PamojaDelegation {
70 pub epoch: u64,
73 pub release_key: [u8; PAMOJA_KEY_LEN],
75 pub expires: u64,
78}
79
80#[repr(C)]
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub struct PamojaDevice {
84 pub vendor_id: [u8; PAMOJA_UPDATE_ID_LEN],
86 pub class_id: [u8; PAMOJA_UPDATE_ID_LEN],
88 pub anchor: [u8; PAMOJA_KEY_LEN],
90}
91
92#[repr(C)]
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum PamojaSlotState {
96 Empty = 0,
98 Receiving = 1,
100 Staged = 2,
102 Pending = 3,
104 Confirmed = 4,
106 Failed = 5,
108}
109
110#[repr(C)]
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub struct PamojaSlotRecord {
114 pub state: PamojaSlotState,
116 pub sequence: u64,
118 pub size: u32,
120 pub digest: [u8; PAMOJA_UPDATE_DIGEST_LEN],
122 pub written: u32,
125}
126
127#[repr(C)]
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum PamojaBootAction {
131 Confirmed = 0,
133 Trying = 1,
135 Reverted = 2,
137}
138
139#[repr(C)]
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142pub struct PamojaBoot {
143 pub action: PamojaBootAction,
145 pub slot: u8,
148 pub fallback: u8,
151}
152
153pub struct PamojaImageVerifier {
159 verifier: ImageVerifier,
160}
161
162pub struct PamojaUpdater {
167 updater: Updater<MemoryStore>,
168 staging: Option<Staging>,
169}
170
171struct Staging {
173 envelope: Vec<u8>,
174 now: Option<u64>,
175}
176
177#[no_mangle]
189pub extern "C" fn pamoja_manifest_encode(manifest: PamojaManifest) -> *mut PamojaBuffer {
190 let Ok(manifest) = rust_manifest(manifest) else {
191 return ptr::null_mut();
192 };
193 let mut buf = [0u8; MANIFEST_MAX];
194 match manifest.encode(&mut buf) {
195 Ok(written) => PamojaBuffer::into_raw(buf[..written].to_vec()),
196 Err(refusal) => {
197 refuse(refusal);
198 ptr::null_mut()
199 }
200 }
201}
202
203#[no_mangle]
223pub unsafe extern "C" fn pamoja_manifest_decode(
224 bytes: *const u8,
225 len: usize,
226 out_manifest: *mut PamojaManifest,
227) -> PamojaStatus {
228 let bytes = match read_bytes(bytes, len) {
229 Ok(bytes) => bytes,
230 Err(status) => return status,
231 };
232 if out_manifest.is_null() {
233 set_last_error("out_manifest must not be null".to_owned());
234 return PamojaStatus::InvalidArgument;
235 }
236 match Manifest::decode(&bytes) {
237 Ok(manifest) => {
238 *out_manifest = boundary_manifest(&manifest);
239 PamojaStatus::Ok
240 }
241 Err(refusal) => refuse(refusal),
242 }
243}
244
245#[no_mangle]
263pub unsafe extern "C" fn pamoja_manifest_sign(
264 manifest: PamojaManifest,
265 author: *const PamojaDeviceIdentity,
266) -> *mut PamojaBuffer {
267 let Ok(manifest) = rust_manifest(manifest) else {
268 return ptr::null_mut();
269 };
270 let Some(author) = identity_handle(author) else {
271 return ptr::null_mut();
272 };
273 let mut buf = [0u8; ENVELOPE_MAX];
274 match manifest.sign(&author.inner, &mut buf) {
275 Ok(written) => PamojaBuffer::into_raw(buf[..written].to_vec()),
276 Err(refusal) => {
277 refuse(refusal);
278 ptr::null_mut()
279 }
280 }
281}
282
283#[no_mangle]
303pub unsafe extern "C" fn pamoja_envelope_verify(
304 bytes: *const u8,
305 len: usize,
306 public_key: *const u8,
307 out_manifest: *mut PamojaManifest,
308) -> PamojaStatus {
309 let bytes = match read_bytes(bytes, len) {
310 Ok(bytes) => bytes,
311 Err(status) => return status,
312 };
313 let public = match read_public(public_key) {
314 Ok(public) => public,
315 Err(status) => return status,
316 };
317 if out_manifest.is_null() {
318 set_last_error("out_manifest must not be null".to_owned());
319 return PamojaStatus::InvalidArgument;
320 }
321
322 let envelope = match Envelope::decode(&bytes) {
323 Ok(envelope) => envelope,
324 Err(refusal) => return refuse(refusal),
325 };
326 match envelope.verify(&public) {
327 Ok(manifest) => {
328 *out_manifest = boundary_manifest(&manifest);
329 PamojaStatus::Ok
330 }
331 Err(refusal) => refuse(refusal),
332 }
333}
334
335#[no_mangle]
356pub unsafe extern "C" fn pamoja_envelope_body(bytes: *const u8, len: usize) -> *mut PamojaBuffer {
357 let Ok(bytes) = read_bytes(bytes, len) else {
358 return ptr::null_mut();
359 };
360 match Envelope::decode(&bytes) {
361 Ok(envelope) => PamojaBuffer::into_raw(envelope.body().to_vec()),
362 Err(refusal) => {
363 refuse(refusal);
364 ptr::null_mut()
365 }
366 }
367}
368
369#[no_mangle]
385pub unsafe extern "C" fn pamoja_delegation_sign(
386 delegation: PamojaDelegation,
387 anchor: *const PamojaDeviceIdentity,
388) -> *mut PamojaBuffer {
389 let Some(anchor) = identity_handle(anchor) else {
390 return ptr::null_mut();
391 };
392 let delegation = Delegation {
393 epoch: delegation.epoch,
394 release_key: delegation.release_key,
395 expires: delegation.expires,
396 };
397 let mut buf = [0u8; DELEGATION_MAX];
398 match delegation.sign(&anchor.inner, &mut buf) {
399 Ok(written) => PamojaBuffer::into_raw(buf[..written].to_vec()),
400 Err(refusal) => {
401 refuse(refusal);
402 ptr::null_mut()
403 }
404 }
405}
406
407#[no_mangle]
427pub unsafe extern "C" fn pamoja_delegation_open(
428 bytes: *const u8,
429 len: usize,
430 anchor_public_key: *const u8,
431 out_delegation: *mut PamojaDelegation,
432) -> PamojaStatus {
433 let bytes = match read_bytes(bytes, len) {
434 Ok(bytes) => bytes,
435 Err(status) => return status,
436 };
437 let anchor = match read_public(anchor_public_key) {
438 Ok(anchor) => anchor,
439 Err(status) => return status,
440 };
441 if out_delegation.is_null() {
442 set_last_error("out_delegation must not be null".to_owned());
443 return PamojaStatus::InvalidArgument;
444 }
445 match Delegation::open(&bytes, &anchor) {
446 Ok(delegation) => {
447 *out_delegation = boundary_delegation(&delegation);
448 PamojaStatus::Ok
449 }
450 Err(refusal) => refuse(refusal),
451 }
452}
453
454#[no_mangle]
475pub unsafe extern "C" fn pamoja_image_digest(
476 image: *const u8,
477 image_len: usize,
478 out_digest: *mut u8,
479) -> PamojaStatus {
480 if out_digest.is_null() {
481 set_last_error("out_digest must not be null".to_owned());
482 return PamojaStatus::InvalidArgument;
483 }
484 let image = match read_bytes(image, image_len) {
485 Ok(image) => image,
486 Err(status) => return status,
487 };
488 let digest = pamoja_update::image_digest(&image);
489 core::ptr::copy_nonoverlapping(digest.as_ptr(), out_digest, digest.len());
490 PamojaStatus::Ok
491}
492
493#[no_mangle]
495pub extern "C" fn pamoja_image_verifier_new(manifest: PamojaManifest) -> *mut PamojaImageVerifier {
496 let Ok(manifest) = rust_manifest(manifest) else {
497 return ptr::null_mut();
498 };
499 Box::into_raw(Box::new(PamojaImageVerifier {
500 verifier: ImageVerifier::new(&manifest),
501 }))
502}
503
504#[no_mangle]
523pub unsafe extern "C" fn pamoja_image_verifier_update(
524 verifier: *mut PamojaImageVerifier,
525 chunk: *const u8,
526 len: usize,
527) -> PamojaStatus {
528 if verifier.is_null() {
529 set_last_error("verifier must not be null".to_owned());
530 return PamojaStatus::InvalidArgument;
531 }
532 let chunk = match read_bytes(chunk, len) {
533 Ok(chunk) => chunk,
534 Err(status) => return status,
535 };
536 match (*verifier).verifier.update(&chunk) {
537 Ok(()) => PamojaStatus::Ok,
538 Err(refusal) => refuse(refusal),
539 }
540}
541
542#[no_mangle]
565pub unsafe extern "C" fn pamoja_image_verifier_finish(
566 verifier: *mut PamojaImageVerifier,
567 out_size: *mut u32,
568 out_digest: *mut u8,
569) -> PamojaStatus {
570 if verifier.is_null() {
571 set_last_error("verifier must not be null".to_owned());
572 return PamojaStatus::InvalidArgument;
573 }
574 let owned = Box::from_raw(verifier);
575 match owned.verifier.finish() {
576 Ok(verified) => {
577 write_verified(&verified, out_size, out_digest);
578 PamojaStatus::Ok
579 }
580 Err(refusal) => refuse(refusal),
581 }
582}
583
584#[no_mangle]
593pub unsafe extern "C" fn pamoja_image_verifier_free(verifier: *mut PamojaImageVerifier) {
594 if !verifier.is_null() {
595 drop(Box::from_raw(verifier));
596 }
597}
598
599#[no_mangle]
612pub extern "C" fn pamoja_updater_new(
613 device: PamojaDevice,
614 slot_count: u8,
615 slot_capacity: u32,
616) -> *mut PamojaUpdater {
617 let anchor = unsafe { read_public(device.anchor.as_ptr()) };
620 let Ok(anchor) = anchor else {
621 return ptr::null_mut();
622 };
623 let device = Device {
624 vendor_id: device.vendor_id,
625 class_id: device.class_id,
626 anchor,
627 };
628 Box::into_raw(Box::new(PamojaUpdater {
629 updater: Updater::new(device, MemoryStore::new(slot_count, slot_capacity)),
630 staging: None,
631 }))
632}
633
634#[no_mangle]
656pub unsafe extern "C" fn pamoja_updater_adopt(
657 updater: *mut PamojaUpdater,
658 bytes: *const u8,
659 len: usize,
660 has_now: bool,
661 now: u64,
662 out_delegation: *mut PamojaDelegation,
663) -> PamojaStatus {
664 if updater.is_null() {
665 set_last_error("updater must not be null".to_owned());
666 return PamojaStatus::InvalidArgument;
667 }
668 let bytes = match read_bytes(bytes, len) {
669 Ok(bytes) => bytes,
670 Err(status) => return status,
671 };
672 match (*updater).updater.adopt(&bytes, clock(has_now, now)) {
673 Ok(delegation) => {
674 if !out_delegation.is_null() {
675 *out_delegation = boundary_delegation(&delegation);
676 }
677 PamojaStatus::Ok
678 }
679 Err(refusal) => refuse(refusal),
680 }
681}
682
683#[no_mangle]
700pub unsafe extern "C" fn pamoja_updater_delegation(
701 updater: *const PamojaUpdater,
702 out_delegation: *mut PamojaDelegation,
703) -> bool {
704 if updater.is_null() {
705 return false;
706 }
707 match (*updater).updater.delegation() {
708 Some(delegation) => {
709 if !out_delegation.is_null() {
710 *out_delegation = boundary_delegation(&delegation);
711 }
712 true
713 }
714 None => false,
715 }
716}
717
718#[no_mangle]
734pub unsafe extern "C" fn pamoja_updater_installed_sequence(
735 updater: *const PamojaUpdater,
736 out_sequence: *mut u64,
737) -> PamojaStatus {
738 if updater.is_null() || out_sequence.is_null() {
739 set_last_error("updater and out_sequence must not be null".to_owned());
740 return PamojaStatus::InvalidArgument;
741 }
742 match (*updater).updater.installed_sequence() {
743 Ok(sequence) => {
744 *out_sequence = sequence;
745 PamojaStatus::Ok
746 }
747 Err(refusal) => refuse(refusal),
748 }
749}
750
751#[no_mangle]
768pub unsafe extern "C" fn pamoja_updater_slot_record(
769 updater: *const PamojaUpdater,
770 slot: u8,
771 out_record: *mut PamojaSlotRecord,
772) -> PamojaStatus {
773 if updater.is_null() || out_record.is_null() {
774 set_last_error("updater and out_record must not be null".to_owned());
775 return PamojaStatus::InvalidArgument;
776 }
777 match (*updater).updater.store().record(slot) {
778 Ok(record) => {
779 *out_record = boundary_record(&record);
780 PamojaStatus::Ok
781 }
782 Err(refusal) => refuse(refusal),
783 }
784}
785
786#[no_mangle]
800pub unsafe extern "C" fn pamoja_updater_slot_count(updater: *const PamojaUpdater) -> u8 {
801 if updater.is_null() {
802 return 0;
803 }
804 (*updater).updater.store().slot_count()
805}
806
807#[no_mangle]
826pub unsafe extern "C" fn pamoja_updater_provision(
827 updater: *mut PamojaUpdater,
828 slot: u8,
829 sequence: u64,
830) -> PamojaStatus {
831 if updater.is_null() {
832 set_last_error("updater must not be null".to_owned());
833 return PamojaStatus::InvalidArgument;
834 }
835 match (*updater).updater.provision(slot, sequence) {
836 Ok(()) => PamojaStatus::Ok,
837 Err(refusal) => refuse(refusal),
838 }
839}
840
841#[allow(clippy::too_many_arguments)]
865#[no_mangle]
866pub unsafe extern "C" fn pamoja_updater_stage(
867 updater: *mut PamojaUpdater,
868 envelope: *const u8,
869 envelope_len: usize,
870 image: *const u8,
871 image_len: usize,
872 has_now: bool,
873 now: u64,
874 out_slot: *mut u8,
875) -> PamojaStatus {
876 if updater.is_null() {
877 set_last_error("updater must not be null".to_owned());
878 return PamojaStatus::InvalidArgument;
879 }
880 let envelope = match read_bytes(envelope, envelope_len) {
881 Ok(envelope) => envelope,
882 Err(status) => return status,
883 };
884 let image = match read_bytes(image, image_len) {
885 Ok(image) => image,
886 Err(status) => return status,
887 };
888 match (*updater)
889 .updater
890 .stage_at(&envelope, &image, clock(has_now, now))
891 {
892 Ok(slot) => {
893 write_slot(slot, out_slot);
894 PamojaStatus::Ok
895 }
896 Err(refusal) => refuse(refusal),
897 }
898}
899
900#[no_mangle]
930pub unsafe extern "C" fn pamoja_updater_begin(
931 updater: *mut PamojaUpdater,
932 envelope: *const u8,
933 envelope_len: usize,
934 has_now: bool,
935 now: u64,
936 out_slot: *mut u8,
937) -> PamojaStatus {
938 if updater.is_null() {
939 set_last_error("updater must not be null".to_owned());
940 return PamojaStatus::InvalidArgument;
941 }
942 let envelope = match read_bytes(envelope, envelope_len) {
943 Ok(envelope) => envelope,
944 Err(status) => return status,
945 };
946 let now = clock(has_now, now);
947
948 let slot = match (*updater).updater.begin_at(&envelope, now) {
949 Ok(staging) => staging.manifest().storage,
950 Err(refusal) => return refuse(refusal),
951 };
952 (*updater).staging = Some(Staging { envelope, now });
953 write_slot(slot, out_slot);
954 PamojaStatus::Ok
955}
956
957#[no_mangle]
974pub unsafe extern "C" fn pamoja_updater_write(
975 updater: *mut PamojaUpdater,
976 chunk: *const u8,
977 len: usize,
978) -> PamojaStatus {
979 if updater.is_null() {
980 set_last_error("updater must not be null".to_owned());
981 return PamojaStatus::InvalidArgument;
982 }
983 let chunk = match read_bytes(chunk, len) {
984 Ok(chunk) => chunk,
985 Err(status) => return status,
986 };
987 let Some((envelope, now)) = open_transfer(&*updater) else {
988 return PamojaStatus::InvalidArgument;
989 };
990 match (*updater).updater.resume_at(&envelope, now) {
991 Ok(mut staging) => match staging.write(&chunk) {
992 Ok(()) => PamojaStatus::Ok,
993 Err(refusal) => refuse(refusal),
994 },
995 Err(refusal) => refuse(refusal),
996 }
997}
998
999#[no_mangle]
1016pub unsafe extern "C" fn pamoja_updater_progress(
1017 updater: *mut PamojaUpdater,
1018 out_written: *mut u32,
1019 out_total: *mut u32,
1020) -> PamojaStatus {
1021 if updater.is_null() {
1022 set_last_error("updater must not be null".to_owned());
1023 return PamojaStatus::InvalidArgument;
1024 }
1025 let Some((envelope, now)) = open_transfer(&*updater) else {
1026 return PamojaStatus::InvalidArgument;
1027 };
1028 match (*updater).updater.resume_at(&envelope, now) {
1029 Ok(staging) => {
1030 let (written, total) = staging.progress();
1031 if !out_written.is_null() {
1032 *out_written = written;
1033 }
1034 if !out_total.is_null() {
1035 *out_total = total;
1036 }
1037 PamojaStatus::Ok
1038 }
1039 Err(refusal) => refuse(refusal),
1040 }
1041}
1042
1043#[no_mangle]
1060pub unsafe extern "C" fn pamoja_updater_finish(
1061 updater: *mut PamojaUpdater,
1062 out_slot: *mut u8,
1063) -> PamojaStatus {
1064 if updater.is_null() {
1065 set_last_error("updater must not be null".to_owned());
1066 return PamojaStatus::InvalidArgument;
1067 }
1068 let Some((envelope, now)) = open_transfer(&*updater) else {
1069 return PamojaStatus::InvalidArgument;
1070 };
1071 let outcome = match (*updater).updater.resume_at(&envelope, now) {
1072 Ok(staging) => staging.finish(),
1073 Err(refusal) => Err(refusal),
1074 };
1075 match outcome {
1076 Ok(slot) => {
1077 (*updater).staging = None;
1078 write_slot(slot, out_slot);
1079 PamojaStatus::Ok
1080 }
1081 Err(refusal) => refuse(refusal),
1082 }
1083}
1084
1085#[no_mangle]
1106pub unsafe extern "C" fn pamoja_updater_on_boot(
1107 updater: *mut PamojaUpdater,
1108 out_boot: *mut PamojaBoot,
1109) -> PamojaStatus {
1110 if updater.is_null() || out_boot.is_null() {
1111 set_last_error("updater and out_boot must not be null".to_owned());
1112 return PamojaStatus::InvalidArgument;
1113 }
1114 match (*updater).updater.on_boot() {
1115 Ok(boot) => {
1116 *out_boot = boundary_boot(boot);
1117 PamojaStatus::Ok
1118 }
1119 Err(refusal) => refuse(refusal),
1120 }
1121}
1122
1123#[no_mangle]
1139pub unsafe extern "C" fn pamoja_updater_confirm(
1140 updater: *mut PamojaUpdater,
1141 out_slot: *mut u8,
1142) -> PamojaStatus {
1143 if updater.is_null() {
1144 set_last_error("updater must not be null".to_owned());
1145 return PamojaStatus::InvalidArgument;
1146 }
1147 match (*updater).updater.confirm() {
1148 Ok(slot) => {
1149 write_slot(slot, out_slot);
1150 PamojaStatus::Ok
1151 }
1152 Err(refusal) => refuse(refusal),
1153 }
1154}
1155
1156#[no_mangle]
1173pub unsafe extern "C" fn pamoja_updater_revert(
1174 updater: *mut PamojaUpdater,
1175 out_slot: *mut u8,
1176) -> PamojaStatus {
1177 if updater.is_null() {
1178 set_last_error("updater must not be null".to_owned());
1179 return PamojaStatus::InvalidArgument;
1180 }
1181 match (*updater).updater.revert() {
1182 Ok(slot) => {
1183 write_slot(slot, out_slot);
1184 PamojaStatus::Ok
1185 }
1186 Err(refusal) => refuse(refusal),
1187 }
1188}
1189
1190#[no_mangle]
1199pub unsafe extern "C" fn pamoja_updater_free(updater: *mut PamojaUpdater) {
1200 if !updater.is_null() {
1201 drop(Box::from_raw(updater));
1202 }
1203}
1204
1205fn refuse(refusal: Refusal) -> PamojaStatus {
1207 let error = pamoja_core::Error::from(refusal);
1208 let status = PamojaStatus::from_error(&error);
1209 set_last_error(refusal.reason().to_owned());
1210 status
1211}
1212
1213fn clock(has_now: bool, now: u64) -> Option<u64> {
1215 has_now.then_some(now)
1216}
1217
1218unsafe fn write_slot(slot: u8, out_slot: *mut u8) {
1224 if !out_slot.is_null() {
1225 *out_slot = slot;
1226 }
1227}
1228
1229unsafe fn write_verified(verified: &Verified, out_size: *mut u32, out_digest: *mut u8) {
1236 if !out_size.is_null() {
1237 *out_size = verified.size();
1238 }
1239 if !out_digest.is_null() {
1240 let digest = verified.digest();
1241 ptr::copy_nonoverlapping(digest.as_ptr(), out_digest, PAMOJA_UPDATE_DIGEST_LEN);
1242 }
1243}
1244
1245fn open_transfer(updater: &PamojaUpdater) -> Option<(Vec<u8>, Option<u64>)> {
1247 match &updater.staging {
1248 Some(staging) => Some((staging.envelope.clone(), staging.now)),
1249 None => {
1250 set_last_error("no transfer is open; call pamoja_updater_begin first".to_owned());
1251 None
1252 }
1253 }
1254}
1255
1256fn rust_manifest(manifest: PamojaManifest) -> Result<Manifest, PamojaStatus> {
1258 if manifest.format != PAMOJA_UPDATE_FORMAT_RAW {
1259 return Err(refuse(Refusal::UnsupportedVersion));
1260 }
1261 Ok(Manifest {
1262 structure_version: manifest.structure_version,
1263 sequence: manifest.sequence,
1264 vendor_id: manifest.vendor_id,
1265 class_id: manifest.class_id,
1266 format: PayloadFormat::Raw,
1267 storage: manifest.storage,
1268 digest: manifest.digest,
1269 size: manifest.size,
1270 expires: manifest.expires,
1271 })
1272}
1273
1274fn boundary_manifest(manifest: &Manifest) -> PamojaManifest {
1276 PamojaManifest {
1277 structure_version: manifest.structure_version,
1278 sequence: manifest.sequence,
1279 vendor_id: manifest.vendor_id,
1280 class_id: manifest.class_id,
1281 format: manifest.format as u8,
1282 storage: manifest.storage,
1283 digest: manifest.digest,
1284 size: manifest.size,
1285 expires: manifest.expires,
1286 }
1287}
1288
1289fn boundary_delegation(delegation: &Delegation) -> PamojaDelegation {
1291 PamojaDelegation {
1292 epoch: delegation.epoch,
1293 release_key: delegation.release_key,
1294 expires: delegation.expires,
1295 }
1296}
1297
1298fn boundary_record(record: &SlotRecord) -> PamojaSlotRecord {
1300 PamojaSlotRecord {
1301 state: match record.state {
1302 SlotState::Empty => PamojaSlotState::Empty,
1303 SlotState::Receiving => PamojaSlotState::Receiving,
1304 SlotState::Staged => PamojaSlotState::Staged,
1305 SlotState::Pending => PamojaSlotState::Pending,
1306 SlotState::Confirmed => PamojaSlotState::Confirmed,
1307 SlotState::Failed => PamojaSlotState::Failed,
1308 },
1309 sequence: record.sequence,
1310 size: record.size,
1311 digest: record.digest,
1312 written: record.written,
1313 }
1314}
1315
1316fn boundary_boot(boot: Boot) -> PamojaBoot {
1318 match boot {
1319 Boot::Confirmed(slot) => PamojaBoot {
1320 action: PamojaBootAction::Confirmed,
1321 slot,
1322 fallback: slot,
1323 },
1324 Boot::Trying(slot) => PamojaBoot {
1325 action: PamojaBootAction::Trying,
1326 slot,
1327 fallback: slot,
1328 },
1329 Boot::Reverted { failed, fallback } => PamojaBoot {
1330 action: PamojaBootAction::Reverted,
1331 slot: failed,
1332 fallback,
1333 },
1334 }
1335}
1336
1337#[cfg(test)]
1338mod tests {
1339 use sha2::{Digest, Sha256};
1340
1341 use super::*;
1342 use crate::security::{
1343 pamoja_device_identity_free, pamoja_device_identity_new, pamoja_device_identity_public_key,
1344 };
1345 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
1346
1347 unsafe fn signer(seed: u8) -> (*mut PamojaDeviceIdentity, [u8; PAMOJA_KEY_LEN]) {
1349 let seed = [seed; PAMOJA_KEY_LEN];
1350 let identity = pamoja_device_identity_new(seed.as_ptr(), seed.len());
1351 assert!(!identity.is_null());
1352 let mut public = [0u8; PAMOJA_KEY_LEN];
1353 assert_eq!(
1354 pamoja_device_identity_public_key(identity, public.as_mut_ptr()),
1355 PamojaStatus::Ok
1356 );
1357 (identity, public)
1358 }
1359
1360 unsafe fn take(buffer: *mut PamojaBuffer) -> Vec<u8> {
1362 assert!(!buffer.is_null());
1363 let bytes =
1364 std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
1365 .to_vec();
1366 pamoja_buffer_free(buffer);
1367 bytes
1368 }
1369
1370 fn manifest(image: &[u8], sequence: u64, storage: u8) -> PamojaManifest {
1372 let digest: [u8; PAMOJA_UPDATE_DIGEST_LEN] = Sha256::digest(image).into();
1373 PamojaManifest {
1374 structure_version: PAMOJA_UPDATE_STRUCTURE_VERSION,
1375 sequence,
1376 vendor_id: [1; PAMOJA_UPDATE_ID_LEN],
1377 class_id: [2; PAMOJA_UPDATE_ID_LEN],
1378 format: PAMOJA_UPDATE_FORMAT_RAW,
1379 storage,
1380 digest,
1381 size: image.len() as u32,
1382 expires: 0,
1383 }
1384 }
1385
1386 fn updater(anchor: [u8; PAMOJA_KEY_LEN]) -> *mut PamojaUpdater {
1388 let handle = pamoja_updater_new(
1389 PamojaDevice {
1390 vendor_id: [1; PAMOJA_UPDATE_ID_LEN],
1391 class_id: [2; PAMOJA_UPDATE_ID_LEN],
1392 anchor,
1393 },
1394 2,
1395 4096,
1396 );
1397 assert!(!handle.is_null());
1398 handle
1399 }
1400
1401 #[test]
1402 fn a_signed_release_stages_boots_and_confirms() {
1403 unsafe {
1404 let (author, anchor) = signer(3);
1405 let device = updater(anchor);
1406 assert_eq!(pamoja_updater_slot_count(device), 2);
1407 assert_eq!(pamoja_updater_provision(device, 0, 1), PamojaStatus::Ok);
1408
1409 let image = vec![0xa5u8; 512];
1410 let envelope = take(pamoja_manifest_sign(manifest(&image, 2, 1), author));
1411
1412 let mut slot = 0u8;
1413 assert_eq!(
1414 pamoja_updater_stage(
1415 device,
1416 envelope.as_ptr(),
1417 envelope.len(),
1418 image.as_ptr(),
1419 image.len(),
1420 false,
1421 0,
1422 &mut slot,
1423 ),
1424 PamojaStatus::Ok
1425 );
1426 assert_eq!(slot, 1);
1427
1428 let mut boot = PamojaBoot {
1429 action: PamojaBootAction::Confirmed,
1430 slot: 0,
1431 fallback: 0,
1432 };
1433 assert_eq!(pamoja_updater_on_boot(device, &mut boot), PamojaStatus::Ok);
1434 assert_eq!(boot.action, PamojaBootAction::Trying);
1435 assert_eq!(boot.slot, 1);
1436
1437 let mut confirmed = 0u8;
1438 assert_eq!(
1439 pamoja_updater_confirm(device, &mut confirmed),
1440 PamojaStatus::Ok
1441 );
1442 assert_eq!(confirmed, 1);
1443
1444 let mut record = boundary_record(&SlotRecord::default());
1445 assert_eq!(
1446 pamoja_updater_slot_record(device, 1, &mut record),
1447 PamojaStatus::Ok
1448 );
1449 assert_eq!(record.state, PamojaSlotState::Confirmed);
1450 assert_eq!(record.size, image.len() as u32);
1451
1452 pamoja_updater_free(device);
1453 pamoja_device_identity_free(author);
1454 }
1455 }
1456
1457 #[test]
1458 fn an_image_arriving_in_pieces_reaches_the_same_place() {
1459 unsafe {
1460 let (author, anchor) = signer(4);
1461 let device = updater(anchor);
1462 assert_eq!(pamoja_updater_provision(device, 0, 1), PamojaStatus::Ok);
1463
1464 let image = vec![0x5au8; 300];
1465 let envelope = take(pamoja_manifest_sign(manifest(&image, 2, 1), author));
1466
1467 let mut slot = 0u8;
1468 assert_eq!(
1469 pamoja_updater_begin(
1470 device,
1471 envelope.as_ptr(),
1472 envelope.len(),
1473 false,
1474 0,
1475 &mut slot
1476 ),
1477 PamojaStatus::Ok
1478 );
1479 assert_eq!(slot, 1);
1480
1481 for chunk in image.chunks(64) {
1482 assert_eq!(
1483 pamoja_updater_write(device, chunk.as_ptr(), chunk.len()),
1484 PamojaStatus::Ok
1485 );
1486 }
1487
1488 let (mut written, mut total) = (0u32, 0u32);
1489 assert_eq!(
1490 pamoja_updater_progress(device, &mut written, &mut total),
1491 PamojaStatus::Ok
1492 );
1493 assert_eq!(written, image.len() as u32);
1494 assert_eq!(total, image.len() as u32);
1495
1496 let mut staged = 0u8;
1497 assert_eq!(pamoja_updater_finish(device, &mut staged), PamojaStatus::Ok);
1498 assert_eq!(staged, 1);
1499
1500 pamoja_updater_free(device);
1501 pamoja_device_identity_free(author);
1502 }
1503 }
1504
1505 #[test]
1506 fn a_release_from_an_untrusted_key_is_refused() {
1507 unsafe {
1508 let (_, anchor) = signer(5);
1509 let (impostor, _) = signer(6);
1510 let device = updater(anchor);
1511 assert_eq!(pamoja_updater_provision(device, 0, 1), PamojaStatus::Ok);
1512
1513 let image = vec![0u8; 16];
1514 let envelope = take(pamoja_manifest_sign(manifest(&image, 2, 1), impostor));
1515
1516 assert_eq!(
1517 pamoja_updater_stage(
1518 device,
1519 envelope.as_ptr(),
1520 envelope.len(),
1521 image.as_ptr(),
1522 image.len(),
1523 false,
1524 0,
1525 ptr::null_mut(),
1526 ),
1527 PamojaStatus::Auth
1528 );
1529
1530 pamoja_updater_free(device);
1531 pamoja_device_identity_free(impostor);
1532 }
1533 }
1534
1535 #[test]
1536 fn an_older_release_cannot_roll_a_device_back() {
1537 unsafe {
1538 let (author, anchor) = signer(7);
1539 let device = updater(anchor);
1540 assert_eq!(pamoja_updater_provision(device, 0, 9), PamojaStatus::Ok);
1541
1542 let image = vec![0u8; 16];
1543 let envelope = take(pamoja_manifest_sign(manifest(&image, 4, 1), author));
1544
1545 assert_eq!(
1546 pamoja_updater_stage(
1547 device,
1548 envelope.as_ptr(),
1549 envelope.len(),
1550 image.as_ptr(),
1551 image.len(),
1552 false,
1553 0,
1554 ptr::null_mut(),
1555 ),
1556 PamojaStatus::Auth
1557 );
1558
1559 let mut sequence = 0u64;
1560 assert_eq!(
1561 pamoja_updater_installed_sequence(device, &mut sequence),
1562 PamojaStatus::Ok
1563 );
1564 assert_eq!(sequence, 9);
1565
1566 pamoja_updater_free(device);
1567 pamoja_device_identity_free(author);
1568 }
1569 }
1570
1571 #[test]
1572 fn a_delegated_key_may_sign_releases() {
1573 unsafe {
1574 let (anchor_identity, anchor) = signer(8);
1575 let (release_identity, release_key) = signer(9);
1576 let device = updater(anchor);
1577 assert_eq!(pamoja_updater_provision(device, 0, 1), PamojaStatus::Ok);
1578
1579 let statement = PamojaDelegation {
1580 epoch: 1,
1581 release_key,
1582 expires: 0,
1583 };
1584 let signed = take(pamoja_delegation_sign(statement, anchor_identity));
1585
1586 let mut opened = PamojaDelegation {
1587 epoch: 0,
1588 release_key: [0; PAMOJA_KEY_LEN],
1589 expires: 0,
1590 };
1591 assert_eq!(
1592 pamoja_delegation_open(signed.as_ptr(), signed.len(), anchor.as_ptr(), &mut opened),
1593 PamojaStatus::Ok
1594 );
1595 assert_eq!(opened.release_key, release_key);
1596
1597 assert_eq!(
1598 pamoja_updater_adopt(
1599 device,
1600 signed.as_ptr(),
1601 signed.len(),
1602 false,
1603 0,
1604 ptr::null_mut()
1605 ),
1606 PamojaStatus::Ok
1607 );
1608 assert!(pamoja_updater_delegation(device, ptr::null_mut()));
1609
1610 let image = vec![7u8; 64];
1611 let envelope = take(pamoja_manifest_sign(
1612 manifest(&image, 2, 1),
1613 release_identity,
1614 ));
1615 assert_eq!(
1616 pamoja_updater_stage(
1617 device,
1618 envelope.as_ptr(),
1619 envelope.len(),
1620 image.as_ptr(),
1621 image.len(),
1622 false,
1623 0,
1624 ptr::null_mut(),
1625 ),
1626 PamojaStatus::Ok
1627 );
1628
1629 pamoja_updater_free(device);
1630 pamoja_device_identity_free(release_identity);
1631 pamoja_device_identity_free(anchor_identity);
1632 }
1633 }
1634
1635 #[test]
1636 fn a_manifest_survives_a_round_trip_and_verifies() {
1637 unsafe {
1638 let (author, public) = signer(10);
1639 let image = vec![3u8; 128];
1640 let want = manifest(&image, 5, 1);
1641
1642 let body = take(pamoja_manifest_encode(want));
1643 let mut decoded = want;
1644 assert_eq!(
1645 pamoja_manifest_decode(body.as_ptr(), body.len(), &mut decoded),
1646 PamojaStatus::Ok
1647 );
1648 assert_eq!(decoded, want);
1649
1650 let envelope = take(pamoja_manifest_sign(want, author));
1651 assert_eq!(
1652 take(pamoja_envelope_body(envelope.as_ptr(), envelope.len())),
1653 body
1654 );
1655
1656 let mut verified = want;
1657 assert_eq!(
1658 pamoja_envelope_verify(
1659 envelope.as_ptr(),
1660 envelope.len(),
1661 public.as_ptr(),
1662 &mut verified
1663 ),
1664 PamojaStatus::Ok
1665 );
1666 assert_eq!(verified, want);
1667
1668 pamoja_device_identity_free(author);
1669 }
1670 }
1671
1672 #[test]
1673 fn a_verifier_refuses_an_image_that_is_not_the_one_described() {
1674 unsafe {
1675 let image = vec![1u8; 64];
1676 let want = manifest(&image, 2, 1);
1677
1678 let verifier = pamoja_image_verifier_new(want);
1679 assert_eq!(
1680 pamoja_image_verifier_update(verifier, image.as_ptr(), image.len()),
1681 PamojaStatus::Ok
1682 );
1683 let mut size = 0u32;
1684 let mut digest = [0u8; PAMOJA_UPDATE_DIGEST_LEN];
1685 assert_eq!(
1686 pamoja_image_verifier_finish(verifier, &mut size, digest.as_mut_ptr()),
1687 PamojaStatus::Ok
1688 );
1689 assert_eq!(size, image.len() as u32);
1690 assert_eq!(digest, want.digest);
1691
1692 let mut altered = image.clone();
1693 altered[0] ^= 0xff;
1694 let verifier = pamoja_image_verifier_new(want);
1695 assert_eq!(
1696 pamoja_image_verifier_update(verifier, altered.as_ptr(), altered.len()),
1697 PamojaStatus::Ok
1698 );
1699 assert_eq!(
1700 pamoja_image_verifier_finish(verifier, ptr::null_mut(), ptr::null_mut()),
1701 PamojaStatus::Auth
1702 );
1703 }
1704 }
1705
1706 #[test]
1707 fn writing_without_opening_a_transfer_is_refused() {
1708 unsafe {
1709 let (_, anchor) = signer(11);
1710 let device = updater(anchor);
1711
1712 assert_eq!(
1713 pamoja_updater_write(device, b"x".as_ptr(), 1),
1714 PamojaStatus::InvalidArgument
1715 );
1716
1717 pamoja_updater_free(device);
1718 }
1719 }
1720
1721 #[test]
1722 fn a_null_handle_is_refused_rather_than_dereferenced() {
1723 unsafe {
1724 assert!(pamoja_manifest_sign(manifest(&[], 1, 0), ptr::null()).is_null());
1725 assert_eq!(
1726 pamoja_updater_on_boot(ptr::null_mut(), ptr::null_mut()),
1727 PamojaStatus::InvalidArgument
1728 );
1729 assert_eq!(
1730 pamoja_updater_write(ptr::null_mut(), b"x".as_ptr(), 1),
1731 PamojaStatus::InvalidArgument
1732 );
1733 assert_eq!(
1734 pamoja_updater_progress(ptr::null_mut(), ptr::null_mut(), ptr::null_mut()),
1735 PamojaStatus::InvalidArgument
1736 );
1737 assert_eq!(
1738 pamoja_updater_finish(ptr::null_mut(), ptr::null_mut()),
1739 PamojaStatus::InvalidArgument
1740 );
1741 assert!(!pamoja_updater_delegation(ptr::null(), ptr::null_mut()));
1742 assert_eq!(pamoja_updater_slot_count(ptr::null()), 0);
1743 pamoja_updater_free(ptr::null_mut());
1744 pamoja_image_verifier_free(ptr::null_mut());
1745 }
1746 }
1747}