Skip to main content

pamoja_ffi/
update.rs

1//! The C ABI for signed firmware updates.
2//!
3//! These functions wrap [`pamoja_update`] for callers that reach the SDK through
4//! the flat C boundary. Two audiences meet here. A build server signs a manifest
5//! and a delegation, which is all value math over
6//! [`PamojaManifest`] and [`PamojaDelegation`]. A device decides what to accept,
7//! which needs the slots it keeps images in, so an updater crosses as an opaque
8//! handle.
9//!
10//! The updater is built over the in-memory slot store. The Rust crate takes any
11//! store through a trait, and a trait cannot cross a C ABI, so a caller wiring
12//! real flash writes that in Rust; what crosses here is the whole of the decision
13//! logic, which is the part that has to be right.
14
15use 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
27/// The length in bytes of a vendor or device-class identifier.
28pub const PAMOJA_UPDATE_ID_LEN: usize = ID_LEN;
29
30/// The length in bytes of an image digest.
31pub const PAMOJA_UPDATE_DIGEST_LEN: usize = DIGEST_LEN;
32
33/// The manifest structure version this build writes.
34pub const PAMOJA_UPDATE_STRUCTURE_VERSION: u8 = STRUCTURE_VERSION;
35
36/// The payload format meaning the payload is the image itself, byte for byte.
37pub const PAMOJA_UPDATE_FORMAT_RAW: u8 = 1;
38
39/// What a release says about itself, and what a device checks it against.
40#[repr(C)]
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct PamojaManifest {
43    /// Which iteration of the manifest format this is.
44    pub structure_version: u8,
45    /// Rises with every release, which is what stops an older image being
46    /// replayed at a device.
47    pub sequence: u64,
48    /// Who built the image.
49    pub vendor_id: [u8; PAMOJA_UPDATE_ID_LEN],
50    /// Which kind of device it is for.
51    pub class_id: [u8; PAMOJA_UPDATE_ID_LEN],
52    /// How the payload is encoded, currently only
53    /// [`PAMOJA_UPDATE_FORMAT_RAW`].
54    pub format: u8,
55    /// Which slot the payload belongs in.
56    pub storage: u8,
57    /// The SHA-256 of the payload, which every other guarantee rests on.
58    pub digest: [u8; PAMOJA_UPDATE_DIGEST_LEN],
59    /// The payload length in bytes, known before a single byte is accepted.
60    pub size: u32,
61    /// When this release stops being offered, in seconds since the Unix epoch,
62    /// or `0` to never expire.
63    pub expires: u64,
64}
65
66/// A statement, signed by the anchor, that a second key may sign releases.
67#[repr(C)]
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub struct PamojaDelegation {
70    /// Rises with every rotation, so a retired key cannot be reinstated by
71    /// replaying the statement that once authorised it.
72    pub epoch: u64,
73    /// The public key that may sign manifests while this delegation stands.
74    pub release_key: [u8; PAMOJA_KEY_LEN],
75    /// When the delegation stops being honoured, in seconds since the Unix
76    /// epoch, or `0` to never expire.
77    pub expires: u64,
78}
79
80/// Who a device is, and whose signature it trusts.
81#[repr(C)]
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub struct PamojaDevice {
84    /// Who built this firmware.
85    pub vendor_id: [u8; PAMOJA_UPDATE_ID_LEN],
86    /// What kind of device this is.
87    pub class_id: [u8; PAMOJA_UPDATE_ID_LEN],
88    /// The [`PAMOJA_KEY_LEN`]-byte key this device anchors its trust in.
89    pub anchor: [u8; PAMOJA_KEY_LEN],
90}
91
92/// What a device believes about one slot.
93#[repr(C)]
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum PamojaSlotState {
96    /// Nothing has been written here.
97    Empty = 0,
98    /// An image is arriving, and `written` says how much of it has.
99    Receiving = 1,
100    /// A complete image that matched its manifest, not yet tried.
101    Staged = 2,
102    /// Being tried for the first time; it reverts unless it confirms.
103    Pending = 3,
104    /// Tried and confirmed working.
105    Confirmed = 4,
106    /// Tried and did not confirm, so it will not be tried again.
107    Failed = 5,
108}
109
110/// The record a device keeps about one slot, durable across a reboot.
111#[repr(C)]
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub struct PamojaSlotRecord {
114    /// The state of the slot.
115    pub state: PamojaSlotState,
116    /// The sequence number of the image in the slot.
117    pub sequence: u64,
118    /// The length of the image in bytes.
119    pub size: u32,
120    /// The digest of the image.
121    pub digest: [u8; PAMOJA_UPDATE_DIGEST_LEN],
122    /// How many bytes have been stored, which is where a resumed transfer picks
123    /// up.
124    pub written: u32,
125}
126
127/// What a bootloader should do with what it found.
128#[repr(C)]
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum PamojaBootAction {
131    /// Nothing new to try; run the confirmed image.
132    Confirmed = 0,
133    /// A staged image is being tried for the first time.
134    Trying = 1,
135    /// A pending image never confirmed, so it was failed.
136    Reverted = 2,
137}
138
139/// The decision a device made at boot, already recorded before it was returned.
140#[repr(C)]
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142pub struct PamojaBoot {
143    /// What the bootloader should do.
144    pub action: PamojaBootAction,
145    /// The image the decision is about, which for
146    /// [`PamojaBootAction::Reverted`] is the one that failed.
147    pub slot: u8,
148    /// The slot to run. It is the same as `slot` for anything but
149    /// [`PamojaBootAction::Reverted`].
150    pub fallback: u8,
151}
152
153/// An opaque handle that hashes an image as it arrives.
154///
155/// Create it with [`pamoja_image_verifier_new`], feed it with
156/// [`pamoja_image_verifier_update`], and settle it with
157/// [`pamoja_image_verifier_finish`], which consumes the handle.
158pub struct PamojaImageVerifier {
159    verifier: ImageVerifier,
160}
161
162/// An opaque handle to a device slots and the rules applied to them.
163///
164/// Create it with [`pamoja_updater_new`] and release it with
165/// [`pamoja_updater_free`].
166pub struct PamojaUpdater {
167    updater: Updater<MemoryStore>,
168    staging: Option<Staging>,
169}
170
171/// The transfer an updater is part-way through, remembered between calls.
172struct Staging {
173    envelope: Vec<u8>,
174    now: Option<u64>,
175}
176
177/// Encodes the body of a manifest, which is the part a signature covers.
178///
179/// # Arguments
180///
181/// * `manifest` - the manifest to encode.
182///
183/// # Returns
184///
185/// A buffer the caller must release with
186/// [`pamoja_buffer_free`](crate::pamoja_buffer_free), or null if the manifest
187/// carries a payload format this build cannot write.
188#[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/// Reads a manifest body back from its bytes.
204///
205/// This reads what a manifest claims; it proves nothing about who wrote it. Use
206/// [`pamoja_envelope_verify`] to read one whose signature has been checked.
207///
208/// # Arguments
209///
210/// * `bytes` - the encoded manifest body.
211/// * `len` - the length of `bytes`.
212/// * `out_manifest` - receives the decoded manifest.
213///
214/// # Returns
215///
216/// [`PamojaStatus::Ok`] on success.
217///
218/// # Safety
219///
220/// `bytes` must point to at least `len` readable bytes, or be null when `len` is
221/// 0, and `out_manifest` must be writable.
222#[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/// Signs a manifest into the envelope that is offered to a device.
246///
247/// # Arguments
248///
249/// * `manifest` - the manifest to sign.
250/// * `author` - the identity signing the release.
251///
252/// # Returns
253///
254/// A buffer the caller must release with
255/// [`pamoja_buffer_free`](crate::pamoja_buffer_free), or null on failure.
256///
257/// # Safety
258///
259/// `author` must be a live handle from
260/// [`pamoja_device_identity_new`](crate::security::pamoja_device_identity_new),
261/// or null.
262#[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/// Verifies an envelope against a key and reads the manifest inside it.
284///
285/// # Arguments
286///
287/// * `bytes` - the signed envelope.
288/// * `len` - the length of `bytes`.
289/// * `public_key` - the [`PAMOJA_KEY_LEN`]-byte key expected to have signed it.
290/// * `out_manifest` - receives the verified manifest.
291///
292/// # Returns
293///
294/// [`PamojaStatus::Ok`] if the signature is from that key, or
295/// [`PamojaStatus::Auth`] if it is not.
296///
297/// # Safety
298///
299/// `bytes` must point to at least `len` readable bytes or be null when `len` is
300/// 0, `public_key` must point to at least [`PAMOJA_KEY_LEN`] readable bytes, and
301/// `out_manifest` must be writable.
302#[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/// Copies out the signed body of an envelope, without checking the signature.
336///
337/// This is what a gateway relays onward unchanged, and what a device hashes when
338/// it checks the signature itself.
339///
340/// # Arguments
341///
342/// * `bytes` - the signed envelope.
343/// * `len` - the length of `bytes`.
344///
345/// # Returns
346///
347/// A buffer the caller must release with
348/// [`pamoja_buffer_free`](crate::pamoja_buffer_free), or null if the envelope is
349/// malformed.
350///
351/// # Safety
352///
353/// `bytes` must point to at least `len` readable bytes, or be null when `len` is
354/// 0.
355#[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/// Signs a delegation, naming a release key the anchor stands behind.
370///
371/// # Arguments
372///
373/// * `delegation` - the statement to sign.
374/// * `anchor` - the anchor identity, which is the root of the trust.
375///
376/// # Returns
377///
378/// A buffer the caller must release with
379/// [`pamoja_buffer_free`](crate::pamoja_buffer_free), or null on failure.
380///
381/// # Safety
382///
383/// `anchor` must be a live identity handle, or null.
384#[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/// Opens a signed delegation against the anchor that should have signed it.
408///
409/// # Arguments
410///
411/// * `bytes` - the signed delegation envelope.
412/// * `len` - the length of `bytes`.
413/// * `anchor_public_key` - the [`PAMOJA_KEY_LEN`]-byte anchor key.
414/// * `out_delegation` - receives the verified delegation.
415///
416/// # Returns
417///
418/// [`PamojaStatus::Ok`] if the delegation is from the anchor, or
419/// [`PamojaStatus::Auth`] if it is not.
420///
421/// # Safety
422///
423/// `bytes` must point to at least `len` readable bytes or be null when `len` is
424/// 0, `anchor_public_key` must point to at least [`PAMOJA_KEY_LEN`] readable
425/// bytes, and `out_delegation` must be writable.
426#[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/// Creates a verifier that hashes an image against what a manifest declares.
455///
456/// # Arguments
457///
458/// * `manifest` - the manifest describing the image.
459///
460/// # Returns
461///
462/// A handle the caller must settle with [`pamoja_image_verifier_finish`] or
463/// abandon with [`pamoja_image_verifier_free`], or null if the manifest carries
464/// Hashes a complete image, for a publisher filling in a manifest.
465///
466/// # Returns
467///
468/// [`PamojaStatus::Ok`] on success, with the 32-byte SHA-256 written to `out_digest`.
469///
470/// # Safety
471///
472/// `image` must point to at least `image_len` readable bytes, or be null when
473/// `image_len` is 0, and `out_digest` must point to at least 32 writable bytes.
474#[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/// a payload format this build cannot apply.
494#[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/// Takes the next piece of the image.
505///
506/// # Arguments
507///
508/// * `verifier` - the verifier.
509/// * `chunk` - the next bytes of the image, in order.
510/// * `len` - the length of `chunk`.
511///
512/// # Returns
513///
514/// [`PamojaStatus::Ok`] once the chunk is hashed, or a failure if more bytes
515/// have arrived than the manifest declared.
516///
517/// # Safety
518///
519/// `verifier` must be a live handle from [`pamoja_image_verifier_new`], and
520/// `chunk` must point to at least `len` readable bytes, or be null when `len` is
521/// 0.
522#[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/// Settles an image against its manifest, consuming the verifier.
543///
544/// The handle is released whether the image matched or not, so it must not be
545/// used again after this call and must not also be passed to
546/// [`pamoja_image_verifier_free`].
547///
548/// # Arguments
549///
550/// * `verifier` - the verifier, consumed by this call.
551/// * `out_size` - receives the length of the image that was hashed.
552/// * `out_digest` - receives [`PAMOJA_UPDATE_DIGEST_LEN`] bytes of digest.
553///
554/// # Returns
555///
556/// [`PamojaStatus::Ok`] if the image is the one the manifest described, or a
557/// failure naming the rule it broke.
558///
559/// # Safety
560///
561/// `verifier` must be a live handle from [`pamoja_image_verifier_new`] that has
562/// not been freed, `out_size` must be writable or null, and `out_digest` must
563/// point to at least [`PAMOJA_UPDATE_DIGEST_LEN`] writable bytes or be null.
564#[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/// Releases a verifier handle that will not be settled.
585///
586/// Passing null is a no-op.
587///
588/// # Safety
589///
590/// `verifier` must be a handle from [`pamoja_image_verifier_new`] that has not
591/// already been freed or passed to [`pamoja_image_verifier_finish`], or null.
592#[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/// Creates an updater over a device slots.
600///
601/// # Arguments
602///
603/// * `device` - who the device is and whose signature it trusts.
604/// * `slot_count` - how many slots the device has.
605/// * `slot_capacity` - how many bytes each slot holds.
606///
607/// # Returns
608///
609/// A handle the caller must release with [`pamoja_updater_free`], or null if the
610/// anchor is not a valid public key.
611#[no_mangle]
612pub extern "C" fn pamoja_updater_new(
613    device: PamojaDevice,
614    slot_count: u8,
615    slot_capacity: u32,
616) -> *mut PamojaUpdater {
617    // Safety: the anchor is a fixed-size array inside a value that crossed by
618    // value, so the pointer is always valid for the key length.
619    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/// Adopts a delegation, so releases signed by the key it names are accepted.
635///
636/// # Arguments
637///
638/// * `updater` - the updater.
639/// * `bytes` - the signed delegation envelope.
640/// * `len` - the length of `bytes`.
641/// * `has_now` - `true` if the device has a clock, `false` if it does not.
642/// * `now` - seconds since the Unix epoch, read only when `has_now` is `true`.
643/// * `out_delegation` - receives the adopted delegation, or may be null.
644///
645/// # Returns
646///
647/// [`PamojaStatus::Ok`] if the delegation was signed by the anchor, is newer
648/// than the one held, and has not expired.
649///
650/// # Safety
651///
652/// `updater` must be a live handle from [`pamoja_updater_new`], `bytes` must
653/// point to at least `len` readable bytes or be null when `len` is 0, and
654/// `out_delegation` must be writable or null.
655#[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/// Reads the delegation an updater currently honours.
684///
685/// # Arguments
686///
687/// * `updater` - the updater.
688/// * `out_delegation` - receives the delegation when there is one.
689///
690/// # Returns
691///
692/// `true` if a delegation is held and was written out, or `false` if releases
693/// must be signed by the anchor itself.
694///
695/// # Safety
696///
697/// `updater` must be a live handle from [`pamoja_updater_new`], and
698/// `out_delegation` must be writable or null.
699#[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/// Reads the highest sequence number the device already holds.
719///
720/// # Arguments
721///
722/// * `updater` - the updater.
723/// * `out_sequence` - receives the sequence number.
724///
725/// # Returns
726///
727/// [`PamojaStatus::Ok`] on success.
728///
729/// # Safety
730///
731/// `updater` must be a live handle from [`pamoja_updater_new`], and
732/// `out_sequence` must be writable.
733#[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/// Reads what a device believes about one slot.
752///
753/// # Arguments
754///
755/// * `updater` - the updater.
756/// * `slot` - the slot to read.
757/// * `out_record` - receives the record.
758///
759/// # Returns
760///
761/// [`PamojaStatus::Ok`] on success, or a failure if the device has no such slot.
762///
763/// # Safety
764///
765/// `updater` must be a live handle from [`pamoja_updater_new`], and `out_record`
766/// must be writable.
767#[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/// Returns how many slots a device has.
787///
788/// # Arguments
789///
790/// * `updater` - the updater.
791///
792/// # Returns
793///
794/// The slot count, or 0 if `updater` is null.
795///
796/// # Safety
797///
798/// `updater` must be a live handle from [`pamoja_updater_new`], or null.
799#[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/// Records that a slot already holds a confirmed image at a sequence number.
808///
809/// This is how a device that shipped with firmware tells the updater what it is
810/// running, so the rollback rule has something to compare against.
811///
812/// # Arguments
813///
814/// * `updater` - the updater.
815/// * `slot` - the slot holding the running image.
816/// * `sequence` - the sequence number of that image.
817///
818/// # Returns
819///
820/// [`PamojaStatus::Ok`] on success.
821///
822/// # Safety
823///
824/// `updater` must be a live handle from [`pamoja_updater_new`].
825#[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/// Checks a manifest and stages an image that is already held whole.
842///
843/// # Arguments
844///
845/// * `updater` - the updater.
846/// * `envelope` - the signed manifest offered to this device.
847/// * `envelope_len` - the length of `envelope`.
848/// * `image` - the whole image.
849/// * `image_len` - the length of `image`.
850/// * `has_now` - `true` if the device has a clock.
851/// * `now` - seconds since the Unix epoch, read only when `has_now` is `true`.
852/// * `out_slot` - receives the slot the image was staged into.
853///
854/// # Returns
855///
856/// [`PamojaStatus::Ok`] on success, or a failure naming the rule that refused
857/// the update.
858///
859/// # Safety
860///
861/// `updater` must be a live handle from [`pamoja_updater_new`], `envelope` and
862/// `image` must point to at least their stated lengths of readable bytes or be
863/// null when those lengths are 0, and `out_slot` must be writable or null.
864#[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/// Checks a manifest and opens the slot it names for a transfer in pieces.
901///
902/// Every check that can be made without the image runs here, so a release that
903/// is not for this device, would roll it back, or does not fit is refused before
904/// a byte of it is accepted.
905///
906/// The envelope is remembered until [`pamoja_updater_finish`], so the calls that
907/// follow do not repeat it. Each of those reopens the transfer from what the
908/// slot records, which is the same path a device takes after a reset, and is
909/// what lets a transfer survive one.
910///
911/// # Arguments
912///
913/// * `updater` - the updater.
914/// * `envelope` - the signed manifest offered to this device.
915/// * `envelope_len` - the length of `envelope`.
916/// * `has_now` - `true` if the device has a clock.
917/// * `now` - seconds since the Unix epoch, read only when `has_now` is `true`.
918/// * `out_slot` - receives the slot the image will be written into.
919///
920/// # Returns
921///
922/// [`PamojaStatus::Ok`] on success.
923///
924/// # Safety
925///
926/// `updater` must be a live handle from [`pamoja_updater_new`], `envelope` must
927/// point to at least `envelope_len` readable bytes or be null when it is 0, and
928/// `out_slot` must be writable or null.
929#[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/// Takes the next piece of an image opened with [`pamoja_updater_begin`].
958///
959/// # Arguments
960///
961/// * `updater` - the updater.
962/// * `chunk` - the next bytes of the image, in order.
963/// * `len` - the length of `chunk`.
964///
965/// # Returns
966///
967/// [`PamojaStatus::Ok`] once the chunk is stored and its progress recorded.
968///
969/// # Safety
970///
971/// `updater` must be a live handle from [`pamoja_updater_new`], and `chunk` must
972/// point to at least `len` readable bytes, or be null when `len` is 0.
973#[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/// Reports how much of an opened image has arrived.
1000///
1001/// # Arguments
1002///
1003/// * `updater` - the updater.
1004/// * `out_written` - receives the bytes stored so far.
1005/// * `out_total` - receives the total the manifest declares.
1006///
1007/// # Returns
1008///
1009/// [`PamojaStatus::Ok`] on success.
1010///
1011/// # Safety
1012///
1013/// `updater` must be a live handle from [`pamoja_updater_new`], and the output
1014/// pointers must be writable or null.
1015#[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/// Finishes an opened image and marks the slot bootable if it matched.
1044///
1045/// # Arguments
1046///
1047/// * `updater` - the updater.
1048/// * `out_slot` - receives the slot now holding a staged image.
1049///
1050/// # Returns
1051///
1052/// [`PamojaStatus::Ok`] on success, or a failure if the image is not the one the
1053/// manifest described, which leaves the slot unbootable.
1054///
1055/// # Safety
1056///
1057/// `updater` must be a live handle from [`pamoja_updater_new`], and `out_slot`
1058/// must be writable or null.
1059#[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/// Decides what to run, and records that decision before returning it.
1086///
1087/// Call this once per boot, before jumping to an image. A staged image becomes
1088/// pending here, so a device that resets before confirming reverts on the next
1089/// call rather than trying a broken image forever.
1090///
1091/// # Arguments
1092///
1093/// * `updater` - the updater.
1094/// * `out_boot` - receives the decision.
1095///
1096/// # Returns
1097///
1098/// [`PamojaStatus::Ok`] on success, or a failure if there is nothing to fall
1099/// back to.
1100///
1101/// # Safety
1102///
1103/// `updater` must be a live handle from [`pamoja_updater_new`], and `out_boot`
1104/// must be writable.
1105#[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/// Confirms the pending image, so it will be run from now on.
1124///
1125/// # Arguments
1126///
1127/// * `updater` - the updater.
1128/// * `out_slot` - receives the slot that is now confirmed.
1129///
1130/// # Returns
1131///
1132/// [`PamojaStatus::Ok`] on success.
1133///
1134/// # Safety
1135///
1136/// `updater` must be a live handle from [`pamoja_updater_new`], and `out_slot`
1137/// must be writable or null.
1138#[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/// Fails the pending image and goes back to the confirmed one.
1157///
1158/// # Arguments
1159///
1160/// * `updater` - the updater.
1161/// * `out_slot` - receives the slot to fall back to.
1162///
1163/// # Returns
1164///
1165/// [`PamojaStatus::Ok`] on success, or a failure if there is nothing to fall
1166/// back to.
1167///
1168/// # Safety
1169///
1170/// `updater` must be a live handle from [`pamoja_updater_new`], and `out_slot`
1171/// must be writable or null.
1172#[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/// Releases an updater handle.
1191///
1192/// Passing null is a no-op.
1193///
1194/// # Safety
1195///
1196/// `updater` must be a handle from [`pamoja_updater_new`] that has not already
1197/// been freed, or null. After this call it must not be used again.
1198#[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
1205/// Records a refusal as the last error and maps it onto a status.
1206fn 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
1213/// Turns the two clock arguments into the optional the crate takes.
1214fn clock(has_now: bool, now: u64) -> Option<u64> {
1215    has_now.then_some(now)
1216}
1217
1218/// Copies out a slot number when the caller asked for one.
1219///
1220/// # Safety
1221///
1222/// `out_slot` must be writable, or null.
1223unsafe fn write_slot(slot: u8, out_slot: *mut u8) {
1224    if !out_slot.is_null() {
1225        *out_slot = slot;
1226    }
1227}
1228
1229/// Copies out what a settled image turned out to be.
1230///
1231/// # Safety
1232///
1233/// `out_size` must be writable or null, and `out_digest` must point to at least
1234/// [`PAMOJA_UPDATE_DIGEST_LEN`] writable bytes, or be null.
1235unsafe 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
1245/// Borrows the envelope an updater is part-way through, if there is one.
1246fn 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
1256/// Rebuilds the Rust manifest from the fields that crossed the boundary.
1257fn 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
1274/// Maps a Rust manifest onto the value that crosses the boundary.
1275fn 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
1289/// Maps a Rust delegation onto the value that crosses the boundary.
1290fn boundary_delegation(delegation: &Delegation) -> PamojaDelegation {
1291    PamojaDelegation {
1292        epoch: delegation.epoch,
1293        release_key: delegation.release_key,
1294        expires: delegation.expires,
1295    }
1296}
1297
1298/// Maps a Rust slot record onto the value that crosses the boundary.
1299fn 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
1316/// Maps a Rust boot decision onto the value that crosses the boundary.
1317fn 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    /// Builds an identity and its public key from a repeated-byte seed.
1348    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    /// Copies a buffer out and releases it.
1361    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    /// Describes a release of `image` at `sequence`.
1371    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    /// Builds an updater that trusts `anchor` and has two slots.
1387    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}