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, Transfer, Updater, DELEGATION_MAX, DIGEST_LEN, ENVELOPE_MAX, ID_LEN,
20    MANIFEST_MAX, STRUCTURE_VERSION,
21};
22use pamoja_update::{ImageVerifier, Verified};
23
24use crate::security::{identity_handle, read_public, PamojaDeviceIdentity, PAMOJA_KEY_LEN};
25use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
26
27/// The length in bytes of a vendor or device-class identifier.
28pub const PAMOJA_UPDATE_ID_LEN: usize = 16;
29
30/// The length in bytes of an image digest.
31pub const PAMOJA_UPDATE_DIGEST_LEN: usize = 32;
32
33/// The manifest structure version this build writes.
34pub const PAMOJA_UPDATE_STRUCTURE_VERSION: u8 = 1;
35
36// The header carries these as literals, because a generated macro cannot name a Rust
37// constant. These hold them to what the crate says.
38const _: () = assert!(PAMOJA_UPDATE_ID_LEN == ID_LEN);
39const _: () = assert!(PAMOJA_UPDATE_DIGEST_LEN == DIGEST_LEN);
40const _: () = assert!(PAMOJA_UPDATE_STRUCTURE_VERSION == STRUCTURE_VERSION);
41
42/// The payload format meaning the payload is the image itself, byte for byte.
43pub const PAMOJA_UPDATE_FORMAT_RAW: u8 = 1;
44
45/// What a release says about itself, and what a device checks it against.
46#[repr(C)]
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct PamojaManifest {
49    /// Which iteration of the manifest format this is.
50    pub structure_version: u8,
51    /// Rises with every release, which is what stops an older image being
52    /// replayed at a device.
53    pub sequence: u64,
54    /// Who built the image.
55    pub vendor_id: [u8; PAMOJA_UPDATE_ID_LEN],
56    /// Which kind of device it is for.
57    pub class_id: [u8; PAMOJA_UPDATE_ID_LEN],
58    /// How the payload is encoded, currently only
59    /// [`PAMOJA_UPDATE_FORMAT_RAW`].
60    pub format: u8,
61    /// Which slot the payload belongs in.
62    pub storage: u8,
63    /// The SHA-256 of the payload, which every other guarantee rests on.
64    pub digest: [u8; PAMOJA_UPDATE_DIGEST_LEN],
65    /// The payload length in bytes, known before a single byte is accepted.
66    pub size: u32,
67    /// When this release stops being offered, in seconds since the Unix epoch,
68    /// or `0` to never expire.
69    pub expires: u64,
70}
71
72/// A statement, signed by the anchor, that a second key may sign releases.
73#[repr(C)]
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct PamojaDelegation {
76    /// Rises with every rotation, so a retired key cannot be reinstated by
77    /// replaying the statement that once authorized it.
78    pub epoch: u64,
79    /// The public key that may sign manifests while this delegation stands.
80    pub release_key: [u8; PAMOJA_KEY_LEN],
81    /// When the delegation stops being honored, in seconds since the Unix
82    /// epoch, or `0` to never expire.
83    pub expires: u64,
84}
85
86/// Who a device is, and whose signature it trusts.
87#[repr(C)]
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub struct PamojaDevice {
90    /// Who built this firmware.
91    pub vendor_id: [u8; PAMOJA_UPDATE_ID_LEN],
92    /// What kind of device this is.
93    pub class_id: [u8; PAMOJA_UPDATE_ID_LEN],
94    /// The [`PAMOJA_KEY_LEN`]-byte key this device anchors its trust in.
95    pub anchor: [u8; PAMOJA_KEY_LEN],
96}
97
98/// What a device believes about one slot.
99#[repr(C)]
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub enum PamojaSlotState {
102    /// Nothing has been written here.
103    Empty = 0,
104    /// An image is arriving, and `written` says how much of it has.
105    Receiving = 1,
106    /// A complete image that matched its manifest, not yet tried.
107    Staged = 2,
108    /// Being tried for the first time; it reverts unless it confirms.
109    Pending = 3,
110    /// Tried and confirmed working.
111    Confirmed = 4,
112    /// Tried and did not confirm, so it will not be tried again.
113    Failed = 5,
114}
115
116/// The record a device keeps about one slot, durable across a reboot.
117#[repr(C)]
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct PamojaSlotRecord {
120    /// The state of the slot.
121    pub state: PamojaSlotState,
122    /// The sequence number of the image in the slot.
123    pub sequence: u64,
124    /// The length of the image in bytes.
125    pub size: u32,
126    /// The digest of the image.
127    pub digest: [u8; PAMOJA_UPDATE_DIGEST_LEN],
128    /// How many bytes have been stored, which is where a resumed transfer picks
129    /// up.
130    pub written: u32,
131}
132
133/// What a bootloader should do with what it found.
134#[repr(C)]
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub enum PamojaBootAction {
137    /// Nothing new to try; run the confirmed image.
138    Confirmed = 0,
139    /// A staged image is being tried for the first time.
140    Trying = 1,
141    /// A pending image never confirmed, so it was failed.
142    Reverted = 2,
143}
144
145/// The decision a device made at boot, already recorded before it was returned.
146#[repr(C)]
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub struct PamojaBoot {
149    /// What the bootloader should do.
150    pub action: PamojaBootAction,
151    /// The image the decision is about, which for
152    /// [`PamojaBootAction::Reverted`] is the one that failed.
153    pub slot: u8,
154    /// The slot to run. It is the same as `slot` for anything but
155    /// [`PamojaBootAction::Reverted`].
156    pub fallback: u8,
157}
158
159/// An opaque handle that hashes an image as it arrives.
160///
161/// Create it with [`pamoja_image_verifier_new`], feed it with
162/// [`pamoja_image_verifier_update`], and settle it with
163/// [`pamoja_image_verifier_finish`], which consumes the handle.
164pub struct PamojaImageVerifier {
165    verifier: ImageVerifier,
166}
167
168/// An opaque handle to a device's slots and the rules applied to them.
169///
170/// Create it with [`pamoja_updater_new`] and release it with
171/// [`pamoja_updater_free`].
172pub struct PamojaUpdater {
173    updater: Updater<MemoryStore>,
174    staging: Option<Staging>,
175}
176
177/// The transfer an updater is part-way through, remembered between calls.
178///
179/// The detached transfer carries the hash of what has arrived, so each piece costs
180/// only its own bytes. It is dropped when a piece is refused, and the next call reads
181/// back what the slot holds instead.
182struct Staging {
183    envelope: Vec<u8>,
184    now: Option<u64>,
185    transfer: Option<Transfer>,
186}
187
188/// Takes up an open transfer, carrying its hash when the slot still holds it.
189fn resume<'a>(
190    updater: &'a mut Updater<MemoryStore>,
191    open: &mut Staging,
192) -> Result<pamoja_update::Staging<'a, MemoryStore>, Refusal> {
193    match open.transfer.take() {
194        Some(transfer) => updater.resume_from(&open.envelope, open.now, transfer),
195        None => updater.resume_at(&open.envelope, open.now),
196    }
197}
198
199/// Encodes the body of a manifest, which is the part a signature covers.
200///
201/// # Arguments
202///
203/// * `manifest` - the manifest to encode.
204///
205/// # Returns
206///
207/// A buffer the caller must release with
208/// [`pamoja_buffer_free`](crate::pamoja_buffer_free), or null if the manifest
209/// carries a payload format this build cannot write.
210#[no_mangle]
211pub extern "C" fn pamoja_manifest_encode(manifest: PamojaManifest) -> *mut PamojaBuffer {
212    let Ok(manifest) = rust_manifest(manifest) else {
213        return ptr::null_mut();
214    };
215    let mut buf = [0u8; MANIFEST_MAX];
216    match manifest.encode(&mut buf) {
217        Ok(written) => PamojaBuffer::into_raw(buf[..written].to_vec()),
218        Err(refusal) => {
219            refuse(refusal);
220            ptr::null_mut()
221        }
222    }
223}
224
225/// Reads a manifest body back from its bytes.
226///
227/// This reads what a manifest claims; it proves nothing about who wrote it. Use
228/// [`pamoja_envelope_verify`] to read one whose signature has been checked.
229///
230/// # Arguments
231///
232/// * `bytes` - the encoded manifest body.
233/// * `len` - the length of `bytes`.
234/// * `out_manifest` - receives the decoded manifest.
235///
236/// # Returns
237///
238/// [`PamojaStatus::Ok`] on success.
239///
240/// # Safety
241///
242/// `bytes` must point to at least `len` readable bytes, or be null when `len` is
243/// 0, and `out_manifest` must be writable.
244#[no_mangle]
245pub unsafe extern "C" fn pamoja_manifest_decode(
246    bytes: *const u8,
247    len: usize,
248    out_manifest: *mut PamojaManifest,
249) -> PamojaStatus {
250    let bytes = match read_bytes(bytes, len) {
251        Ok(bytes) => bytes,
252        Err(status) => return status,
253    };
254    if out_manifest.is_null() {
255        set_last_error("out_manifest must not be null".to_owned());
256        return PamojaStatus::InvalidArgument;
257    }
258    match Manifest::decode(&bytes) {
259        Ok(manifest) => {
260            *out_manifest = boundary_manifest(&manifest);
261            PamojaStatus::Ok
262        }
263        Err(refusal) => refuse(refusal),
264    }
265}
266
267/// Signs a manifest into the envelope that is offered to a device.
268///
269/// # Arguments
270///
271/// * `manifest` - the manifest to sign.
272/// * `author` - the identity signing the release.
273///
274/// # Returns
275///
276/// A buffer the caller must release with
277/// [`pamoja_buffer_free`](crate::pamoja_buffer_free), or null on failure.
278///
279/// # Safety
280///
281/// `author` must be a live handle from
282/// [`pamoja_device_identity_new`](crate::security::pamoja_device_identity_new),
283/// or null.
284#[no_mangle]
285pub unsafe extern "C" fn pamoja_manifest_sign(
286    manifest: PamojaManifest,
287    author: *const PamojaDeviceIdentity,
288) -> *mut PamojaBuffer {
289    let Ok(manifest) = rust_manifest(manifest) else {
290        return ptr::null_mut();
291    };
292    let Some(author) = identity_handle(author) else {
293        return ptr::null_mut();
294    };
295    let mut buf = [0u8; ENVELOPE_MAX];
296    match manifest.sign(&author.inner, &mut buf) {
297        Ok(written) => PamojaBuffer::into_raw(buf[..written].to_vec()),
298        Err(refusal) => {
299            refuse(refusal);
300            ptr::null_mut()
301        }
302    }
303}
304
305/// Verifies an envelope against a key and reads the manifest inside it.
306///
307/// # Arguments
308///
309/// * `bytes` - the signed envelope.
310/// * `len` - the length of `bytes`.
311/// * `public_key` - the [`PAMOJA_KEY_LEN`]-byte key expected to have signed it.
312/// * `out_manifest` - receives the verified manifest.
313///
314/// # Returns
315///
316/// [`PamojaStatus::Ok`] if the signature is from that key, or
317/// [`PamojaStatus::Auth`] if it is not.
318///
319/// # Safety
320///
321/// `bytes` must point to at least `len` readable bytes or be null when `len` is
322/// 0, `public_key` must point to at least [`PAMOJA_KEY_LEN`] readable bytes, and
323/// `out_manifest` must be writable.
324#[no_mangle]
325pub unsafe extern "C" fn pamoja_envelope_verify(
326    bytes: *const u8,
327    len: usize,
328    public_key: *const u8,
329    out_manifest: *mut PamojaManifest,
330) -> PamojaStatus {
331    let bytes = match read_bytes(bytes, len) {
332        Ok(bytes) => bytes,
333        Err(status) => return status,
334    };
335    let public = match read_public(public_key) {
336        Ok(public) => public,
337        Err(status) => return status,
338    };
339    if out_manifest.is_null() {
340        set_last_error("out_manifest must not be null".to_owned());
341        return PamojaStatus::InvalidArgument;
342    }
343
344    let envelope = match Envelope::decode(&bytes) {
345        Ok(envelope) => envelope,
346        Err(refusal) => return refuse(refusal),
347    };
348    match envelope.verify(&public) {
349        Ok(manifest) => {
350            *out_manifest = boundary_manifest(&manifest);
351            PamojaStatus::Ok
352        }
353        Err(refusal) => refuse(refusal),
354    }
355}
356
357/// Copies out the signed body of an envelope, without checking the signature.
358///
359/// This is what a gateway relays onward unchanged, and what a device hashes when
360/// it checks the signature itself.
361///
362/// # Arguments
363///
364/// * `bytes` - the signed envelope.
365/// * `len` - the length of `bytes`.
366///
367/// # Returns
368///
369/// A buffer the caller must release with
370/// [`pamoja_buffer_free`](crate::pamoja_buffer_free), or null if the envelope is
371/// malformed.
372///
373/// # Safety
374///
375/// `bytes` must point to at least `len` readable bytes, or be null when `len` is
376/// 0.
377#[no_mangle]
378pub unsafe extern "C" fn pamoja_envelope_body(bytes: *const u8, len: usize) -> *mut PamojaBuffer {
379    let Ok(bytes) = read_bytes(bytes, len) else {
380        return ptr::null_mut();
381    };
382    match Envelope::decode(&bytes) {
383        Ok(envelope) => PamojaBuffer::into_raw(envelope.body().to_vec()),
384        Err(refusal) => {
385            refuse(refusal);
386            ptr::null_mut()
387        }
388    }
389}
390
391/// Signs a delegation, naming a release key the anchor stands behind.
392///
393/// # Arguments
394///
395/// * `delegation` - the statement to sign.
396/// * `anchor` - the anchor identity, which is the root of the trust.
397///
398/// # Returns
399///
400/// A buffer the caller must release with
401/// [`pamoja_buffer_free`](crate::pamoja_buffer_free), or null on failure.
402///
403/// # Safety
404///
405/// `anchor` must be a live identity handle, or null.
406#[no_mangle]
407pub unsafe extern "C" fn pamoja_delegation_sign(
408    delegation: PamojaDelegation,
409    anchor: *const PamojaDeviceIdentity,
410) -> *mut PamojaBuffer {
411    let Some(anchor) = identity_handle(anchor) else {
412        return ptr::null_mut();
413    };
414    let delegation = Delegation {
415        epoch: delegation.epoch,
416        release_key: delegation.release_key,
417        expires: delegation.expires,
418    };
419    let mut buf = [0u8; DELEGATION_MAX];
420    match delegation.sign(&anchor.inner, &mut buf) {
421        Ok(written) => PamojaBuffer::into_raw(buf[..written].to_vec()),
422        Err(refusal) => {
423            refuse(refusal);
424            ptr::null_mut()
425        }
426    }
427}
428
429/// Opens a signed delegation against the anchor that should have signed it.
430///
431/// # Arguments
432///
433/// * `bytes` - the signed delegation envelope.
434/// * `len` - the length of `bytes`.
435/// * `anchor_public_key` - the [`PAMOJA_KEY_LEN`]-byte anchor key.
436/// * `out_delegation` - receives the verified delegation.
437///
438/// # Returns
439///
440/// [`PamojaStatus::Ok`] if the delegation is from the anchor, or
441/// [`PamojaStatus::Auth`] if it is not.
442///
443/// # Safety
444///
445/// `bytes` must point to at least `len` readable bytes or be null when `len` is
446/// 0, `anchor_public_key` must point to at least [`PAMOJA_KEY_LEN`] readable
447/// bytes, and `out_delegation` must be writable.
448#[no_mangle]
449pub unsafe extern "C" fn pamoja_delegation_open(
450    bytes: *const u8,
451    len: usize,
452    anchor_public_key: *const u8,
453    out_delegation: *mut PamojaDelegation,
454) -> PamojaStatus {
455    let bytes = match read_bytes(bytes, len) {
456        Ok(bytes) => bytes,
457        Err(status) => return status,
458    };
459    let anchor = match read_public(anchor_public_key) {
460        Ok(anchor) => anchor,
461        Err(status) => return status,
462    };
463    if out_delegation.is_null() {
464        set_last_error("out_delegation must not be null".to_owned());
465        return PamojaStatus::InvalidArgument;
466    }
467    match Delegation::open(&bytes, &anchor) {
468        Ok(delegation) => {
469            *out_delegation = boundary_delegation(&delegation);
470            PamojaStatus::Ok
471        }
472        Err(refusal) => refuse(refusal),
473    }
474}
475
476/// Creates a verifier that hashes an image against what a manifest declares.
477///
478/// # Arguments
479///
480/// * `manifest` - the manifest describing the image.
481///
482/// # Returns
483///
484/// A handle the caller must settle with [`pamoja_image_verifier_finish`] or
485/// abandon with [`pamoja_image_verifier_free`], or null if the manifest carries
486/// Hashes a complete image, for a publisher filling in a manifest.
487///
488/// # Returns
489///
490/// [`PamojaStatus::Ok`] on success, with the 32-byte SHA-256 written to `out_digest`.
491///
492/// # Safety
493///
494/// `image` must point to at least `image_len` readable bytes, or be null when
495/// `image_len` is 0, and `out_digest` must point to at least 32 writable bytes.
496#[no_mangle]
497pub unsafe extern "C" fn pamoja_image_digest(
498    image: *const u8,
499    image_len: usize,
500    out_digest: *mut u8,
501) -> PamojaStatus {
502    if out_digest.is_null() {
503        set_last_error("out_digest must not be null".to_owned());
504        return PamojaStatus::InvalidArgument;
505    }
506    let image = match read_bytes(image, image_len) {
507        Ok(image) => image,
508        Err(status) => return status,
509    };
510    let digest = pamoja_update::image_digest(&image);
511    core::ptr::copy_nonoverlapping(digest.as_ptr(), out_digest, digest.len());
512    PamojaStatus::Ok
513}
514
515/// a payload format this build cannot apply.
516#[no_mangle]
517pub extern "C" fn pamoja_image_verifier_new(manifest: PamojaManifest) -> *mut PamojaImageVerifier {
518    let Ok(manifest) = rust_manifest(manifest) else {
519        return ptr::null_mut();
520    };
521    Box::into_raw(Box::new(PamojaImageVerifier {
522        verifier: ImageVerifier::new(&manifest),
523    }))
524}
525
526/// Takes the next piece of the image.
527///
528/// # Arguments
529///
530/// * `verifier` - the verifier.
531/// * `chunk` - the next bytes of the image, in order.
532/// * `len` - the length of `chunk`.
533///
534/// # Returns
535///
536/// [`PamojaStatus::Ok`] once the chunk is hashed, or a failure if more bytes
537/// have arrived than the manifest declared.
538///
539/// # Safety
540///
541/// `verifier` must be a live handle from [`pamoja_image_verifier_new`], and
542/// `chunk` must point to at least `len` readable bytes, or be null when `len` is
543/// 0.
544#[no_mangle]
545pub unsafe extern "C" fn pamoja_image_verifier_update(
546    verifier: *mut PamojaImageVerifier,
547    chunk: *const u8,
548    len: usize,
549) -> PamojaStatus {
550    if verifier.is_null() {
551        set_last_error("verifier must not be null".to_owned());
552        return PamojaStatus::InvalidArgument;
553    }
554    let chunk = match read_bytes(chunk, len) {
555        Ok(chunk) => chunk,
556        Err(status) => return status,
557    };
558    match (*verifier).verifier.update(&chunk) {
559        Ok(()) => PamojaStatus::Ok,
560        Err(refusal) => refuse(refusal),
561    }
562}
563
564/// Settles an image against its manifest, consuming the verifier.
565///
566/// The handle is released whether the image matched or not, so it must not be
567/// used again after this call and must not also be passed to
568/// [`pamoja_image_verifier_free`].
569///
570/// # Arguments
571///
572/// * `verifier` - the verifier, consumed by this call.
573/// * `out_size` - receives the length of the image that was hashed.
574/// * `out_digest` - receives [`PAMOJA_UPDATE_DIGEST_LEN`] bytes of digest.
575///
576/// # Returns
577///
578/// [`PamojaStatus::Ok`] if the image is the one the manifest described, or a
579/// failure naming the rule it broke.
580///
581/// # Safety
582///
583/// `verifier` must be a live handle from [`pamoja_image_verifier_new`] that has
584/// not been freed, `out_size` must be writable or null, and `out_digest` must
585/// point to at least [`PAMOJA_UPDATE_DIGEST_LEN`] writable bytes or be null.
586#[no_mangle]
587pub unsafe extern "C" fn pamoja_image_verifier_finish(
588    verifier: *mut PamojaImageVerifier,
589    out_size: *mut u32,
590    out_digest: *mut u8,
591) -> PamojaStatus {
592    if verifier.is_null() {
593        set_last_error("verifier must not be null".to_owned());
594        return PamojaStatus::InvalidArgument;
595    }
596    let owned = Box::from_raw(verifier);
597    match owned.verifier.finish() {
598        Ok(verified) => {
599            write_verified(&verified, out_size, out_digest);
600            PamojaStatus::Ok
601        }
602        Err(refusal) => refuse(refusal),
603    }
604}
605
606/// Releases a verifier handle that will not be settled.
607///
608/// Passing null is a no-op.
609///
610/// # Safety
611///
612/// `verifier` must be a handle from [`pamoja_image_verifier_new`] that has not
613/// already been freed or passed to [`pamoja_image_verifier_finish`], or null.
614#[no_mangle]
615pub unsafe extern "C" fn pamoja_image_verifier_free(verifier: *mut PamojaImageVerifier) {
616    if !verifier.is_null() {
617        drop(Box::from_raw(verifier));
618    }
619}
620
621/// Creates an updater over a device's slots.
622///
623/// # Arguments
624///
625/// * `device` - who the device is and whose signature it trusts.
626/// * `slot_count` - how many slots the device has.
627/// * `slot_capacity` - how many bytes each slot holds.
628///
629/// # Returns
630///
631/// A handle the caller must release with [`pamoja_updater_free`], or null if the
632/// anchor is not a valid public key.
633#[no_mangle]
634pub extern "C" fn pamoja_updater_new(
635    device: PamojaDevice,
636    slot_count: u8,
637    slot_capacity: u32,
638) -> *mut PamojaUpdater {
639    // Safety: the anchor is a fixed-size array inside a value that crossed by
640    // value, so the pointer is always valid for the key length.
641    let anchor = unsafe { read_public(device.anchor.as_ptr()) };
642    let Ok(anchor) = anchor else {
643        return ptr::null_mut();
644    };
645    let device = Device {
646        vendor_id: device.vendor_id,
647        class_id: device.class_id,
648        anchor,
649    };
650    Box::into_raw(Box::new(PamojaUpdater {
651        updater: Updater::new(device, MemoryStore::new(slot_count, slot_capacity)),
652        staging: None,
653    }))
654}
655
656/// Adopts a delegation, so releases signed by the key it names are accepted.
657///
658/// # Arguments
659///
660/// * `updater` - the updater.
661/// * `bytes` - the signed delegation envelope.
662/// * `len` - the length of `bytes`.
663/// * `has_now` - `true` if the device has a clock, `false` if it does not.
664/// * `now` - seconds since the Unix epoch, read only when `has_now` is `true`.
665/// * `out_delegation` - receives the adopted delegation, or may be null.
666///
667/// # Returns
668///
669/// [`PamojaStatus::Ok`] if the delegation was signed by the anchor, is newer
670/// than the one held, and has not expired.
671///
672/// # Safety
673///
674/// `updater` must be a live handle from [`pamoja_updater_new`], `bytes` must
675/// point to at least `len` readable bytes or be null when `len` is 0, and
676/// `out_delegation` must be writable or null.
677#[no_mangle]
678pub unsafe extern "C" fn pamoja_updater_adopt(
679    updater: *mut PamojaUpdater,
680    bytes: *const u8,
681    len: usize,
682    has_now: bool,
683    now: u64,
684    out_delegation: *mut PamojaDelegation,
685) -> PamojaStatus {
686    if updater.is_null() {
687        set_last_error("updater must not be null".to_owned());
688        return PamojaStatus::InvalidArgument;
689    }
690    let bytes = match read_bytes(bytes, len) {
691        Ok(bytes) => bytes,
692        Err(status) => return status,
693    };
694    match (*updater).updater.adopt(&bytes, clock(has_now, now)) {
695        Ok(delegation) => {
696            if !out_delegation.is_null() {
697                *out_delegation = boundary_delegation(&delegation);
698            }
699            PamojaStatus::Ok
700        }
701        Err(refusal) => refuse(refusal),
702    }
703}
704
705/// Reads the delegation an updater currently honors.
706///
707/// # Arguments
708///
709/// * `updater` - the updater.
710/// * `out_delegation` - receives the delegation when there is one.
711///
712/// # Returns
713///
714/// `true` if a delegation is held and was written out, or `false` if releases
715/// must be signed by the anchor itself.
716///
717/// # Safety
718///
719/// `updater` must be a live handle from [`pamoja_updater_new`], and
720/// `out_delegation` must be writable or null.
721#[no_mangle]
722pub unsafe extern "C" fn pamoja_updater_delegation(
723    updater: *const PamojaUpdater,
724    out_delegation: *mut PamojaDelegation,
725) -> bool {
726    if updater.is_null() {
727        return false;
728    }
729    match (*updater).updater.delegation() {
730        Some(delegation) => {
731            if !out_delegation.is_null() {
732                *out_delegation = boundary_delegation(&delegation);
733            }
734            true
735        }
736        None => false,
737    }
738}
739
740/// Reads the highest sequence number the device already holds.
741///
742/// # Arguments
743///
744/// * `updater` - the updater.
745/// * `out_sequence` - receives the sequence number.
746///
747/// # Returns
748///
749/// [`PamojaStatus::Ok`] on success.
750///
751/// # Safety
752///
753/// `updater` must be a live handle from [`pamoja_updater_new`], and
754/// `out_sequence` must be writable.
755#[no_mangle]
756pub unsafe extern "C" fn pamoja_updater_installed_sequence(
757    updater: *const PamojaUpdater,
758    out_sequence: *mut u64,
759) -> PamojaStatus {
760    if updater.is_null() || out_sequence.is_null() {
761        set_last_error("updater and out_sequence must not be null".to_owned());
762        return PamojaStatus::InvalidArgument;
763    }
764    match (*updater).updater.installed_sequence() {
765        Ok(sequence) => {
766            *out_sequence = sequence;
767            PamojaStatus::Ok
768        }
769        Err(refusal) => refuse(refusal),
770    }
771}
772
773/// Reads what a device believes about one slot.
774///
775/// # Arguments
776///
777/// * `updater` - the updater.
778/// * `slot` - the slot to read.
779/// * `out_record` - receives the record.
780///
781/// # Returns
782///
783/// [`PamojaStatus::Ok`] on success, or a failure if the device has no such slot.
784///
785/// # Safety
786///
787/// `updater` must be a live handle from [`pamoja_updater_new`], and `out_record`
788/// must be writable.
789#[no_mangle]
790pub unsafe extern "C" fn pamoja_updater_slot_record(
791    updater: *const PamojaUpdater,
792    slot: u8,
793    out_record: *mut PamojaSlotRecord,
794) -> PamojaStatus {
795    if updater.is_null() || out_record.is_null() {
796        set_last_error("updater and out_record must not be null".to_owned());
797        return PamojaStatus::InvalidArgument;
798    }
799    match (*updater).updater.store().record(slot) {
800        Ok(record) => {
801            *out_record = boundary_record(&record);
802            PamojaStatus::Ok
803        }
804        Err(refusal) => refuse(refusal),
805    }
806}
807
808/// Returns how many slots a device has.
809///
810/// # Arguments
811///
812/// * `updater` - the updater.
813///
814/// # Returns
815///
816/// The slot count, or 0 if `updater` is null.
817///
818/// # Safety
819///
820/// `updater` must be a live handle from [`pamoja_updater_new`], or null.
821#[no_mangle]
822pub unsafe extern "C" fn pamoja_updater_slot_count(updater: *const PamojaUpdater) -> u8 {
823    if updater.is_null() {
824        return 0;
825    }
826    (*updater).updater.store().slot_count()
827}
828
829/// Records that a slot already holds a confirmed image at a sequence number.
830///
831/// This is how a device that shipped with firmware tells the updater what it is
832/// running, so the rollback rule has something to compare against.
833///
834/// # Arguments
835///
836/// * `updater` - the updater.
837/// * `slot` - the slot holding the running image.
838/// * `sequence` - the sequence number of that image.
839///
840/// # Returns
841///
842/// [`PamojaStatus::Ok`] on success.
843///
844/// # Safety
845///
846/// `updater` must be a live handle from [`pamoja_updater_new`].
847#[no_mangle]
848pub unsafe extern "C" fn pamoja_updater_provision(
849    updater: *mut PamojaUpdater,
850    slot: u8,
851    sequence: u64,
852) -> PamojaStatus {
853    if updater.is_null() {
854        set_last_error("updater must not be null".to_owned());
855        return PamojaStatus::InvalidArgument;
856    }
857    match (*updater).updater.provision(slot, sequence) {
858        Ok(()) => PamojaStatus::Ok,
859        Err(refusal) => refuse(refusal),
860    }
861}
862
863/// Checks a manifest and stages an image that is already held whole.
864///
865/// # Arguments
866///
867/// * `updater` - the updater.
868/// * `envelope` - the signed manifest offered to this device.
869/// * `envelope_len` - the length of `envelope`.
870/// * `image` - the whole image.
871/// * `image_len` - the length of `image`.
872/// * `has_now` - `true` if the device has a clock.
873/// * `now` - seconds since the Unix epoch, read only when `has_now` is `true`.
874/// * `out_slot` - receives the slot the image was staged into.
875///
876/// # Returns
877///
878/// [`PamojaStatus::Ok`] on success, or a failure naming the rule that refused
879/// the update.
880///
881/// # Safety
882///
883/// `updater` must be a live handle from [`pamoja_updater_new`], `envelope` and
884/// `image` must point to at least their stated lengths of readable bytes or be
885/// null when those lengths are 0, and `out_slot` must be writable or null.
886#[allow(clippy::too_many_arguments)]
887#[no_mangle]
888pub unsafe extern "C" fn pamoja_updater_stage(
889    updater: *mut PamojaUpdater,
890    envelope: *const u8,
891    envelope_len: usize,
892    image: *const u8,
893    image_len: usize,
894    has_now: bool,
895    now: u64,
896    out_slot: *mut u8,
897) -> PamojaStatus {
898    if updater.is_null() {
899        set_last_error("updater must not be null".to_owned());
900        return PamojaStatus::InvalidArgument;
901    }
902    let envelope = match read_bytes(envelope, envelope_len) {
903        Ok(envelope) => envelope,
904        Err(status) => return status,
905    };
906    let image = match read_bytes(image, image_len) {
907        Ok(image) => image,
908        Err(status) => return status,
909    };
910    match (*updater)
911        .updater
912        .stage_at(&envelope, &image, clock(has_now, now))
913    {
914        Ok(slot) => {
915            write_slot(slot, out_slot);
916            PamojaStatus::Ok
917        }
918        Err(refusal) => refuse(refusal),
919    }
920}
921
922/// Checks a manifest and opens the slot it names for a transfer in pieces.
923///
924/// Every check that can be made without the image runs here, so a release that
925/// is not for this device, would roll it back, or does not fit is refused before
926/// a byte of it is accepted.
927///
928/// The envelope is remembered until [`pamoja_updater_finish`], so the calls that
929/// follow do not repeat it. Each of those checks it again, so a release overtaken
930/// or expired while it arrives stops arriving, and each carries the hash of what
931/// has arrived to the next, so an image taken in many small pieces costs no more
932/// than one taken whole.
933///
934/// # Arguments
935///
936/// * `updater` - the updater.
937/// * `envelope` - the signed manifest offered to this device.
938/// * `envelope_len` - the length of `envelope`.
939/// * `has_now` - `true` if the device has a clock.
940/// * `now` - seconds since the Unix epoch, read only when `has_now` is `true`.
941/// * `out_slot` - receives the slot the image will be written into.
942///
943/// # Returns
944///
945/// [`PamojaStatus::Ok`] on success.
946///
947/// # Safety
948///
949/// `updater` must be a live handle from [`pamoja_updater_new`], `envelope` must
950/// point to at least `envelope_len` readable bytes or be null when it is 0, and
951/// `out_slot` must be writable or null.
952#[no_mangle]
953pub unsafe extern "C" fn pamoja_updater_begin(
954    updater: *mut PamojaUpdater,
955    envelope: *const u8,
956    envelope_len: usize,
957    has_now: bool,
958    now: u64,
959    out_slot: *mut u8,
960) -> PamojaStatus {
961    if updater.is_null() {
962        set_last_error("updater must not be null".to_owned());
963        return PamojaStatus::InvalidArgument;
964    }
965    let envelope = match read_bytes(envelope, envelope_len) {
966        Ok(envelope) => envelope,
967        Err(status) => return status,
968    };
969    let now = clock(has_now, now);
970
971    let handle = &mut *updater;
972    let staging = match handle.updater.begin_at(&envelope, now) {
973        Ok(staging) => staging,
974        Err(refusal) => return refuse(refusal),
975    };
976    let slot = staging.manifest().storage;
977    let transfer = Some(staging.detach());
978    handle.staging = Some(Staging {
979        envelope,
980        now,
981        transfer,
982    });
983    write_slot(slot, out_slot);
984    PamojaStatus::Ok
985}
986
987/// Takes the next piece of an image opened with [`pamoja_updater_begin`].
988///
989/// # Arguments
990///
991/// * `updater` - the updater.
992/// * `chunk` - the next bytes of the image, in order.
993/// * `len` - the length of `chunk`.
994///
995/// # Returns
996///
997/// [`PamojaStatus::Ok`] once the chunk is stored and its progress recorded.
998///
999/// # Safety
1000///
1001/// `updater` must be a live handle from [`pamoja_updater_new`], and `chunk` must
1002/// point to at least `len` readable bytes, or be null when `len` is 0.
1003#[no_mangle]
1004pub unsafe extern "C" fn pamoja_updater_write(
1005    updater: *mut PamojaUpdater,
1006    chunk: *const u8,
1007    len: usize,
1008) -> PamojaStatus {
1009    if updater.is_null() {
1010        set_last_error("updater must not be null".to_owned());
1011        return PamojaStatus::InvalidArgument;
1012    }
1013    let chunk = match read_bytes(chunk, len) {
1014        Ok(chunk) => chunk,
1015        Err(status) => return status,
1016    };
1017    let handle = &mut *updater;
1018    let Some(open) = open_transfer(&mut handle.staging) else {
1019        return PamojaStatus::InvalidArgument;
1020    };
1021    let mut staging = match resume(&mut handle.updater, open) {
1022        Ok(staging) => staging,
1023        Err(refusal) => return refuse(refusal),
1024    };
1025    if let Err(refusal) = staging.write(&chunk) {
1026        return refuse(refusal);
1027    }
1028    open.transfer = Some(staging.detach());
1029    PamojaStatus::Ok
1030}
1031
1032/// Reports how much of an opened image has arrived.
1033///
1034/// # Arguments
1035///
1036/// * `updater` - the updater.
1037/// * `out_written` - receives the bytes stored so far.
1038/// * `out_total` - receives the total the manifest declares.
1039///
1040/// # Returns
1041///
1042/// [`PamojaStatus::Ok`] on success.
1043///
1044/// # Safety
1045///
1046/// `updater` must be a live handle from [`pamoja_updater_new`], and the output
1047/// pointers must be writable or null.
1048#[no_mangle]
1049pub unsafe extern "C" fn pamoja_updater_progress(
1050    updater: *mut PamojaUpdater,
1051    out_written: *mut u32,
1052    out_total: *mut u32,
1053) -> PamojaStatus {
1054    if updater.is_null() {
1055        set_last_error("updater must not be null".to_owned());
1056        return PamojaStatus::InvalidArgument;
1057    }
1058    let handle = &mut *updater;
1059    let Some(open) = open_transfer(&mut handle.staging) else {
1060        return PamojaStatus::InvalidArgument;
1061    };
1062    let staging = match resume(&mut handle.updater, open) {
1063        Ok(staging) => staging,
1064        Err(refusal) => return refuse(refusal),
1065    };
1066    let (written, total) = staging.progress();
1067    open.transfer = Some(staging.detach());
1068    if !out_written.is_null() {
1069        *out_written = written;
1070    }
1071    if !out_total.is_null() {
1072        *out_total = total;
1073    }
1074    PamojaStatus::Ok
1075}
1076
1077/// Finishes an opened image and marks the slot bootable if it matched.
1078///
1079/// # Arguments
1080///
1081/// * `updater` - the updater.
1082/// * `out_slot` - receives the slot now holding a staged image.
1083///
1084/// # Returns
1085///
1086/// [`PamojaStatus::Ok`] on success, or a failure if the image is not the one the
1087/// manifest described, which leaves the slot unbootable.
1088///
1089/// # Safety
1090///
1091/// `updater` must be a live handle from [`pamoja_updater_new`], and `out_slot`
1092/// must be writable or null.
1093#[no_mangle]
1094pub unsafe extern "C" fn pamoja_updater_finish(
1095    updater: *mut PamojaUpdater,
1096    out_slot: *mut u8,
1097) -> PamojaStatus {
1098    if updater.is_null() {
1099        set_last_error("updater must not be null".to_owned());
1100        return PamojaStatus::InvalidArgument;
1101    }
1102    let handle = &mut *updater;
1103    let Some(open) = open_transfer(&mut handle.staging) else {
1104        return PamojaStatus::InvalidArgument;
1105    };
1106    let outcome = resume(&mut handle.updater, open).and_then(|staging| staging.finish());
1107    match outcome {
1108        Ok(slot) => {
1109            handle.staging = None;
1110            write_slot(slot, out_slot);
1111            PamojaStatus::Ok
1112        }
1113        Err(refusal) => refuse(refusal),
1114    }
1115}
1116
1117/// Decides what to run, and records that decision before returning it.
1118///
1119/// Call this once per boot, before jumping to an image. A staged image becomes
1120/// pending here, so a device that resets before confirming reverts on the next
1121/// call rather than trying a broken image forever.
1122///
1123/// # Arguments
1124///
1125/// * `updater` - the updater.
1126/// * `out_boot` - receives the decision.
1127///
1128/// # Returns
1129///
1130/// [`PamojaStatus::Ok`] on success, or a failure if there is nothing to fall
1131/// back to.
1132///
1133/// # Safety
1134///
1135/// `updater` must be a live handle from [`pamoja_updater_new`], and `out_boot`
1136/// must be writable.
1137#[no_mangle]
1138pub unsafe extern "C" fn pamoja_updater_on_boot(
1139    updater: *mut PamojaUpdater,
1140    out_boot: *mut PamojaBoot,
1141) -> PamojaStatus {
1142    if updater.is_null() || out_boot.is_null() {
1143        set_last_error("updater and out_boot must not be null".to_owned());
1144        return PamojaStatus::InvalidArgument;
1145    }
1146    match (*updater).updater.on_boot() {
1147        Ok(boot) => {
1148            *out_boot = boundary_boot(boot);
1149            PamojaStatus::Ok
1150        }
1151        Err(refusal) => refuse(refusal),
1152    }
1153}
1154
1155/// Confirms the pending image, so it will be run from now on.
1156///
1157/// # Arguments
1158///
1159/// * `updater` - the updater.
1160/// * `out_slot` - receives the slot that is now confirmed.
1161///
1162/// # Returns
1163///
1164/// [`PamojaStatus::Ok`] on success.
1165///
1166/// # Safety
1167///
1168/// `updater` must be a live handle from [`pamoja_updater_new`], and `out_slot`
1169/// must be writable or null.
1170#[no_mangle]
1171pub unsafe extern "C" fn pamoja_updater_confirm(
1172    updater: *mut PamojaUpdater,
1173    out_slot: *mut u8,
1174) -> PamojaStatus {
1175    if updater.is_null() {
1176        set_last_error("updater must not be null".to_owned());
1177        return PamojaStatus::InvalidArgument;
1178    }
1179    match (*updater).updater.confirm() {
1180        Ok(slot) => {
1181            write_slot(slot, out_slot);
1182            PamojaStatus::Ok
1183        }
1184        Err(refusal) => refuse(refusal),
1185    }
1186}
1187
1188/// Fails the pending image and goes back to the confirmed one.
1189///
1190/// # Arguments
1191///
1192/// * `updater` - the updater.
1193/// * `out_slot` - receives the slot to fall back to.
1194///
1195/// # Returns
1196///
1197/// [`PamojaStatus::Ok`] on success, or a failure if there is nothing to fall
1198/// back to.
1199///
1200/// # Safety
1201///
1202/// `updater` must be a live handle from [`pamoja_updater_new`], and `out_slot`
1203/// must be writable or null.
1204#[no_mangle]
1205pub unsafe extern "C" fn pamoja_updater_revert(
1206    updater: *mut PamojaUpdater,
1207    out_slot: *mut u8,
1208) -> PamojaStatus {
1209    if updater.is_null() {
1210        set_last_error("updater must not be null".to_owned());
1211        return PamojaStatus::InvalidArgument;
1212    }
1213    match (*updater).updater.revert() {
1214        Ok(slot) => {
1215            write_slot(slot, out_slot);
1216            PamojaStatus::Ok
1217        }
1218        Err(refusal) => refuse(refusal),
1219    }
1220}
1221
1222/// Releases an updater handle.
1223///
1224/// Passing null is a no-op.
1225///
1226/// # Safety
1227///
1228/// `updater` must be a handle from [`pamoja_updater_new`] that has not already
1229/// been freed, or null. After this call it must not be used again.
1230#[no_mangle]
1231pub unsafe extern "C" fn pamoja_updater_free(updater: *mut PamojaUpdater) {
1232    if !updater.is_null() {
1233        drop(Box::from_raw(updater));
1234    }
1235}
1236
1237/// Records a refusal as the last error and maps it onto a status.
1238fn refuse(refusal: Refusal) -> PamojaStatus {
1239    let error = pamoja_core::Error::from(refusal);
1240    let status = PamojaStatus::from_error(&error);
1241    set_last_error(refusal.reason().to_owned());
1242    status
1243}
1244
1245/// The bytes a block carrying a signed update puts in front of it.
1246pub const PAMOJA_UPDATE_BLOCK_HEADER_LEN: usize = 6;
1247
1248// The header carries this as a literal, because cbindgen drops a constant whose value names
1249// another crate's. This holds it to what that crate says.
1250const _: () = assert!(PAMOJA_UPDATE_BLOCK_HEADER_LEN == pamoja_update::block::HEADER_LEN);
1251
1252/// Writes a signed update into one block, for a transport that moves blocks.
1253///
1254/// The block is the signed manifest and the image behind a header that says where each
1255/// begins. Nothing in the header is trusted: every rule that decides whether the image runs is
1256/// still the manifest's.
1257///
1258/// # Arguments
1259///
1260/// * `envelope` - the signed manifest.
1261/// * `envelope_len` - how many bytes it holds.
1262/// * `image` - the image it describes.
1263/// * `image_len` - how many bytes it holds.
1264/// * `out_block` - receives the block.
1265/// * `capacity` - how many bytes that buffer holds.
1266/// * `out_len` - receives how many were written.
1267///
1268/// # Returns
1269///
1270/// [`PamojaStatus::Ok`] on success.
1271///
1272/// # Errors
1273///
1274/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, and
1275/// [`PamojaStatus::Codec`] when the envelope is longer than a header can describe or the
1276/// buffer is too small.
1277///
1278/// # Safety
1279///
1280/// Every pointer must be non-null, the inputs readable for their lengths, `out_block`
1281/// writable for `capacity` bytes, and `out_len` writable.
1282#[no_mangle]
1283pub unsafe extern "C" fn pamoja_update_block_frame(
1284    envelope: *const u8,
1285    envelope_len: usize,
1286    image: *const u8,
1287    image_len: usize,
1288    out_block: *mut u8,
1289    capacity: usize,
1290    out_len: *mut usize,
1291) -> PamojaStatus {
1292    let (Ok(envelope), Ok(image), false, false) = (
1293        read_bytes(envelope, envelope_len),
1294        read_bytes(image, image_len),
1295        out_block.is_null(),
1296        out_len.is_null(),
1297    ) else {
1298        set_last_error("a required argument was null".to_owned());
1299        return PamojaStatus::InvalidArgument;
1300    };
1301    let out = core::slice::from_raw_parts_mut(out_block, capacity);
1302    match pamoja_update::block::frame(&envelope, &image, out) {
1303        Ok(written) => {
1304            *out_len = written;
1305            PamojaStatus::Ok
1306        }
1307        Err(refusal) => refuse(refusal),
1308    }
1309}
1310
1311/// Reads a block back into the signed manifest and the image.
1312///
1313/// # Arguments
1314///
1315/// * `block` - the block as it arrived.
1316/// * `block_len` - how many bytes it holds.
1317/// * `out_envelope_at` - receives where the signed manifest starts inside the block.
1318/// * `out_envelope_len` - receives how long it is.
1319/// * `out_image_at` - receives where the image starts.
1320/// * `out_image_len` - receives how long it is.
1321///
1322/// # Returns
1323///
1324/// [`PamojaStatus::Ok`] on success. The two spans point into `block` itself, so nothing is
1325/// copied and the caller keeps it alive.
1326///
1327/// # Errors
1328///
1329/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, and
1330/// [`PamojaStatus::Codec`] when the block does not carry this convention's header.
1331///
1332/// # Safety
1333///
1334/// `block` must be readable for `block_len` bytes and every non-null out pointer writable.
1335#[no_mangle]
1336pub unsafe extern "C" fn pamoja_update_block_split(
1337    block: *const u8,
1338    block_len: usize,
1339    out_envelope_at: *mut usize,
1340    out_envelope_len: *mut usize,
1341    out_image_at: *mut usize,
1342    out_image_len: *mut usize,
1343) -> PamojaStatus {
1344    let Ok(bytes) = read_bytes(block, block_len) else {
1345        set_last_error("a required argument was null".to_owned());
1346        return PamojaStatus::InvalidArgument;
1347    };
1348    match pamoja_update::block::split(&bytes) {
1349        Ok((envelope, image)) => {
1350            if !out_envelope_at.is_null() {
1351                *out_envelope_at = PAMOJA_UPDATE_BLOCK_HEADER_LEN;
1352            }
1353            if !out_envelope_len.is_null() {
1354                *out_envelope_len = envelope.len();
1355            }
1356            if !out_image_at.is_null() {
1357                *out_image_at = PAMOJA_UPDATE_BLOCK_HEADER_LEN + envelope.len();
1358            }
1359            if !out_image_len.is_null() {
1360                *out_image_len = image.len();
1361            }
1362            PamojaStatus::Ok
1363        }
1364        Err(refusal) => refuse(refusal),
1365    }
1366}
1367
1368/// Turns the two clock arguments into the optional the crate takes.
1369fn clock(has_now: bool, now: u64) -> Option<u64> {
1370    has_now.then_some(now)
1371}
1372
1373/// Copies out a slot number when the caller asked for one.
1374///
1375/// # Safety
1376///
1377/// `out_slot` must be writable, or null.
1378unsafe fn write_slot(slot: u8, out_slot: *mut u8) {
1379    if !out_slot.is_null() {
1380        *out_slot = slot;
1381    }
1382}
1383
1384/// Copies out what a settled image turned out to be.
1385///
1386/// # Safety
1387///
1388/// `out_size` must be writable or null, and `out_digest` must point to at least
1389/// [`PAMOJA_UPDATE_DIGEST_LEN`] writable bytes, or be null.
1390unsafe fn write_verified(verified: &Verified, out_size: *mut u32, out_digest: *mut u8) {
1391    if !out_size.is_null() {
1392        *out_size = verified.size();
1393    }
1394    if !out_digest.is_null() {
1395        let digest = verified.digest();
1396        ptr::copy_nonoverlapping(digest.as_ptr(), out_digest, PAMOJA_UPDATE_DIGEST_LEN);
1397    }
1398}
1399
1400/// Borrows the transfer an updater is part-way through, if there is one.
1401fn open_transfer(staging: &mut Option<Staging>) -> Option<&mut Staging> {
1402    if staging.is_none() {
1403        set_last_error("no transfer is open; call pamoja_updater_begin first".to_owned());
1404    }
1405    staging.as_mut()
1406}
1407
1408/// Rebuilds the Rust manifest from the fields that crossed the boundary.
1409fn rust_manifest(manifest: PamojaManifest) -> Result<Manifest, PamojaStatus> {
1410    if manifest.format != PAMOJA_UPDATE_FORMAT_RAW {
1411        return Err(refuse(Refusal::UnsupportedVersion));
1412    }
1413    Ok(Manifest {
1414        structure_version: manifest.structure_version,
1415        sequence: manifest.sequence,
1416        vendor_id: manifest.vendor_id,
1417        class_id: manifest.class_id,
1418        format: PayloadFormat::Raw,
1419        storage: manifest.storage,
1420        digest: manifest.digest,
1421        size: manifest.size,
1422        expires: manifest.expires,
1423    })
1424}
1425
1426/// Maps a Rust manifest onto the value that crosses the boundary.
1427fn boundary_manifest(manifest: &Manifest) -> PamojaManifest {
1428    PamojaManifest {
1429        structure_version: manifest.structure_version,
1430        sequence: manifest.sequence,
1431        vendor_id: manifest.vendor_id,
1432        class_id: manifest.class_id,
1433        format: manifest.format as u8,
1434        storage: manifest.storage,
1435        digest: manifest.digest,
1436        size: manifest.size,
1437        expires: manifest.expires,
1438    }
1439}
1440
1441/// Maps a Rust delegation onto the value that crosses the boundary.
1442fn boundary_delegation(delegation: &Delegation) -> PamojaDelegation {
1443    PamojaDelegation {
1444        epoch: delegation.epoch,
1445        release_key: delegation.release_key,
1446        expires: delegation.expires,
1447    }
1448}
1449
1450/// Maps a Rust slot record onto the value that crosses the boundary.
1451fn boundary_record(record: &SlotRecord) -> PamojaSlotRecord {
1452    PamojaSlotRecord {
1453        state: match record.state {
1454            SlotState::Empty => PamojaSlotState::Empty,
1455            SlotState::Receiving => PamojaSlotState::Receiving,
1456            SlotState::Staged => PamojaSlotState::Staged,
1457            SlotState::Pending => PamojaSlotState::Pending,
1458            SlotState::Confirmed => PamojaSlotState::Confirmed,
1459            SlotState::Failed => PamojaSlotState::Failed,
1460        },
1461        sequence: record.sequence,
1462        size: record.size,
1463        digest: record.digest,
1464        written: record.written,
1465    }
1466}
1467
1468/// Maps a Rust boot decision onto the value that crosses the boundary.
1469fn boundary_boot(boot: Boot) -> PamojaBoot {
1470    match boot {
1471        Boot::Confirmed(slot) => PamojaBoot {
1472            action: PamojaBootAction::Confirmed,
1473            slot,
1474            fallback: slot,
1475        },
1476        Boot::Trying(slot) => PamojaBoot {
1477            action: PamojaBootAction::Trying,
1478            slot,
1479            fallback: slot,
1480        },
1481        Boot::Reverted { failed, fallback } => PamojaBoot {
1482            action: PamojaBootAction::Reverted,
1483            slot: failed,
1484            fallback,
1485        },
1486    }
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491    use sha2::{Digest, Sha256};
1492
1493    use super::*;
1494    use crate::security::{
1495        pamoja_device_identity_free, pamoja_device_identity_new, pamoja_device_identity_public_key,
1496    };
1497    use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
1498
1499    /// Builds an identity and its public key from a repeated-byte seed.
1500    unsafe fn signer(seed: u8) -> (*mut PamojaDeviceIdentity, [u8; PAMOJA_KEY_LEN]) {
1501        let seed = [seed; PAMOJA_KEY_LEN];
1502        let identity = pamoja_device_identity_new(seed.as_ptr(), seed.len());
1503        assert!(!identity.is_null());
1504        let mut public = [0u8; PAMOJA_KEY_LEN];
1505        assert_eq!(
1506            pamoja_device_identity_public_key(identity, public.as_mut_ptr()),
1507            PamojaStatus::Ok
1508        );
1509        (identity, public)
1510    }
1511
1512    /// Copies a buffer out and releases it.
1513    unsafe fn take(buffer: *mut PamojaBuffer) -> Vec<u8> {
1514        assert!(!buffer.is_null());
1515        let bytes =
1516            std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
1517                .to_vec();
1518        pamoja_buffer_free(buffer);
1519        bytes
1520    }
1521
1522    /// Describes a release of `image` at `sequence`.
1523    fn manifest(image: &[u8], sequence: u64, storage: u8) -> PamojaManifest {
1524        let digest: [u8; PAMOJA_UPDATE_DIGEST_LEN] = Sha256::digest(image).into();
1525        PamojaManifest {
1526            structure_version: PAMOJA_UPDATE_STRUCTURE_VERSION,
1527            sequence,
1528            vendor_id: [1; PAMOJA_UPDATE_ID_LEN],
1529            class_id: [2; PAMOJA_UPDATE_ID_LEN],
1530            format: PAMOJA_UPDATE_FORMAT_RAW,
1531            storage,
1532            digest,
1533            size: image.len() as u32,
1534            expires: 0,
1535        }
1536    }
1537
1538    /// Builds an updater that trusts `anchor` and has two slots.
1539    fn updater(anchor: [u8; PAMOJA_KEY_LEN]) -> *mut PamojaUpdater {
1540        let handle = pamoja_updater_new(
1541            PamojaDevice {
1542                vendor_id: [1; PAMOJA_UPDATE_ID_LEN],
1543                class_id: [2; PAMOJA_UPDATE_ID_LEN],
1544                anchor,
1545            },
1546            2,
1547            4096,
1548        );
1549        assert!(!handle.is_null());
1550        handle
1551    }
1552
1553    #[test]
1554    fn a_signed_release_stages_boots_and_confirms() {
1555        unsafe {
1556            let (author, anchor) = signer(3);
1557            let device = updater(anchor);
1558            assert_eq!(pamoja_updater_slot_count(device), 2);
1559            assert_eq!(pamoja_updater_provision(device, 0, 1), PamojaStatus::Ok);
1560
1561            let image = vec![0xa5u8; 512];
1562            let envelope = take(pamoja_manifest_sign(manifest(&image, 2, 1), author));
1563
1564            let mut slot = 0u8;
1565            assert_eq!(
1566                pamoja_updater_stage(
1567                    device,
1568                    envelope.as_ptr(),
1569                    envelope.len(),
1570                    image.as_ptr(),
1571                    image.len(),
1572                    false,
1573                    0,
1574                    &mut slot,
1575                ),
1576                PamojaStatus::Ok
1577            );
1578            assert_eq!(slot, 1);
1579
1580            let mut boot = PamojaBoot {
1581                action: PamojaBootAction::Confirmed,
1582                slot: 0,
1583                fallback: 0,
1584            };
1585            assert_eq!(pamoja_updater_on_boot(device, &mut boot), PamojaStatus::Ok);
1586            assert_eq!(boot.action, PamojaBootAction::Trying);
1587            assert_eq!(boot.slot, 1);
1588
1589            let mut confirmed = 0u8;
1590            assert_eq!(
1591                pamoja_updater_confirm(device, &mut confirmed),
1592                PamojaStatus::Ok
1593            );
1594            assert_eq!(confirmed, 1);
1595
1596            let mut record = boundary_record(&SlotRecord::default());
1597            assert_eq!(
1598                pamoja_updater_slot_record(device, 1, &mut record),
1599                PamojaStatus::Ok
1600            );
1601            assert_eq!(record.state, PamojaSlotState::Confirmed);
1602            assert_eq!(record.size, image.len() as u32);
1603
1604            pamoja_updater_free(device);
1605            pamoja_device_identity_free(author);
1606        }
1607    }
1608
1609    #[test]
1610    fn an_image_arriving_in_pieces_reaches_the_same_place() {
1611        unsafe {
1612            let (author, anchor) = signer(4);
1613            let device = updater(anchor);
1614            assert_eq!(pamoja_updater_provision(device, 0, 1), PamojaStatus::Ok);
1615
1616            let image = vec![0x5au8; 300];
1617            let envelope = take(pamoja_manifest_sign(manifest(&image, 2, 1), author));
1618
1619            let mut slot = 0u8;
1620            assert_eq!(
1621                pamoja_updater_begin(
1622                    device,
1623                    envelope.as_ptr(),
1624                    envelope.len(),
1625                    false,
1626                    0,
1627                    &mut slot
1628                ),
1629                PamojaStatus::Ok
1630            );
1631            assert_eq!(slot, 1);
1632
1633            for chunk in image.chunks(64) {
1634                assert_eq!(
1635                    pamoja_updater_write(device, chunk.as_ptr(), chunk.len()),
1636                    PamojaStatus::Ok
1637                );
1638            }
1639
1640            let (mut written, mut total) = (0u32, 0u32);
1641            assert_eq!(
1642                pamoja_updater_progress(device, &mut written, &mut total),
1643                PamojaStatus::Ok
1644            );
1645            assert_eq!(written, image.len() as u32);
1646            assert_eq!(total, image.len() as u32);
1647
1648            let mut staged = 0u8;
1649            assert_eq!(pamoja_updater_finish(device, &mut staged), PamojaStatus::Ok);
1650            assert_eq!(staged, 1);
1651
1652            pamoja_updater_free(device);
1653            pamoja_device_identity_free(author);
1654        }
1655    }
1656
1657    #[test]
1658    fn a_release_from_an_untrusted_key_is_refused() {
1659        unsafe {
1660            let (_, anchor) = signer(5);
1661            let (impostor, _) = signer(6);
1662            let device = updater(anchor);
1663            assert_eq!(pamoja_updater_provision(device, 0, 1), PamojaStatus::Ok);
1664
1665            let image = vec![0u8; 16];
1666            let envelope = take(pamoja_manifest_sign(manifest(&image, 2, 1), impostor));
1667
1668            assert_eq!(
1669                pamoja_updater_stage(
1670                    device,
1671                    envelope.as_ptr(),
1672                    envelope.len(),
1673                    image.as_ptr(),
1674                    image.len(),
1675                    false,
1676                    0,
1677                    ptr::null_mut(),
1678                ),
1679                PamojaStatus::Auth
1680            );
1681
1682            pamoja_updater_free(device);
1683            pamoja_device_identity_free(impostor);
1684        }
1685    }
1686
1687    #[test]
1688    fn an_older_release_cannot_roll_a_device_back() {
1689        unsafe {
1690            let (author, anchor) = signer(7);
1691            let device = updater(anchor);
1692            assert_eq!(pamoja_updater_provision(device, 0, 9), PamojaStatus::Ok);
1693
1694            let image = vec![0u8; 16];
1695            let envelope = take(pamoja_manifest_sign(manifest(&image, 4, 1), author));
1696
1697            assert_eq!(
1698                pamoja_updater_stage(
1699                    device,
1700                    envelope.as_ptr(),
1701                    envelope.len(),
1702                    image.as_ptr(),
1703                    image.len(),
1704                    false,
1705                    0,
1706                    ptr::null_mut(),
1707                ),
1708                PamojaStatus::Auth
1709            );
1710
1711            let mut sequence = 0u64;
1712            assert_eq!(
1713                pamoja_updater_installed_sequence(device, &mut sequence),
1714                PamojaStatus::Ok
1715            );
1716            assert_eq!(sequence, 9);
1717
1718            pamoja_updater_free(device);
1719            pamoja_device_identity_free(author);
1720        }
1721    }
1722
1723    #[test]
1724    fn a_delegated_key_may_sign_releases() {
1725        unsafe {
1726            let (anchor_identity, anchor) = signer(8);
1727            let (release_identity, release_key) = signer(9);
1728            let device = updater(anchor);
1729            assert_eq!(pamoja_updater_provision(device, 0, 1), PamojaStatus::Ok);
1730
1731            let statement = PamojaDelegation {
1732                epoch: 1,
1733                release_key,
1734                expires: 0,
1735            };
1736            let signed = take(pamoja_delegation_sign(statement, anchor_identity));
1737
1738            let mut opened = PamojaDelegation {
1739                epoch: 0,
1740                release_key: [0; PAMOJA_KEY_LEN],
1741                expires: 0,
1742            };
1743            assert_eq!(
1744                pamoja_delegation_open(signed.as_ptr(), signed.len(), anchor.as_ptr(), &mut opened),
1745                PamojaStatus::Ok
1746            );
1747            assert_eq!(opened.release_key, release_key);
1748
1749            assert_eq!(
1750                pamoja_updater_adopt(
1751                    device,
1752                    signed.as_ptr(),
1753                    signed.len(),
1754                    false,
1755                    0,
1756                    ptr::null_mut()
1757                ),
1758                PamojaStatus::Ok
1759            );
1760            assert!(pamoja_updater_delegation(device, ptr::null_mut()));
1761
1762            let image = vec![7u8; 64];
1763            let envelope = take(pamoja_manifest_sign(
1764                manifest(&image, 2, 1),
1765                release_identity,
1766            ));
1767            assert_eq!(
1768                pamoja_updater_stage(
1769                    device,
1770                    envelope.as_ptr(),
1771                    envelope.len(),
1772                    image.as_ptr(),
1773                    image.len(),
1774                    false,
1775                    0,
1776                    ptr::null_mut(),
1777                ),
1778                PamojaStatus::Ok
1779            );
1780
1781            pamoja_updater_free(device);
1782            pamoja_device_identity_free(release_identity);
1783            pamoja_device_identity_free(anchor_identity);
1784        }
1785    }
1786
1787    #[test]
1788    fn a_manifest_survives_a_round_trip_and_verifies() {
1789        unsafe {
1790            let (author, public) = signer(10);
1791            let image = vec![3u8; 128];
1792            let want = manifest(&image, 5, 1);
1793
1794            let body = take(pamoja_manifest_encode(want));
1795            let mut decoded = want;
1796            assert_eq!(
1797                pamoja_manifest_decode(body.as_ptr(), body.len(), &mut decoded),
1798                PamojaStatus::Ok
1799            );
1800            assert_eq!(decoded, want);
1801
1802            let envelope = take(pamoja_manifest_sign(want, author));
1803            assert_eq!(
1804                take(pamoja_envelope_body(envelope.as_ptr(), envelope.len())),
1805                body
1806            );
1807
1808            let mut verified = want;
1809            assert_eq!(
1810                pamoja_envelope_verify(
1811                    envelope.as_ptr(),
1812                    envelope.len(),
1813                    public.as_ptr(),
1814                    &mut verified
1815                ),
1816                PamojaStatus::Ok
1817            );
1818            assert_eq!(verified, want);
1819
1820            pamoja_device_identity_free(author);
1821        }
1822    }
1823
1824    #[test]
1825    fn a_verifier_refuses_an_image_that_is_not_the_one_described() {
1826        unsafe {
1827            let image = vec![1u8; 64];
1828            let want = manifest(&image, 2, 1);
1829
1830            let verifier = pamoja_image_verifier_new(want);
1831            assert_eq!(
1832                pamoja_image_verifier_update(verifier, image.as_ptr(), image.len()),
1833                PamojaStatus::Ok
1834            );
1835            let mut size = 0u32;
1836            let mut digest = [0u8; PAMOJA_UPDATE_DIGEST_LEN];
1837            assert_eq!(
1838                pamoja_image_verifier_finish(verifier, &mut size, digest.as_mut_ptr()),
1839                PamojaStatus::Ok
1840            );
1841            assert_eq!(size, image.len() as u32);
1842            assert_eq!(digest, want.digest);
1843
1844            let mut altered = image.clone();
1845            altered[0] ^= 0xff;
1846            let verifier = pamoja_image_verifier_new(want);
1847            assert_eq!(
1848                pamoja_image_verifier_update(verifier, altered.as_ptr(), altered.len()),
1849                PamojaStatus::Ok
1850            );
1851            assert_eq!(
1852                pamoja_image_verifier_finish(verifier, ptr::null_mut(), ptr::null_mut()),
1853                PamojaStatus::Auth
1854            );
1855        }
1856    }
1857
1858    #[test]
1859    fn writing_without_opening_a_transfer_is_refused() {
1860        unsafe {
1861            let (_, anchor) = signer(11);
1862            let device = updater(anchor);
1863
1864            assert_eq!(
1865                pamoja_updater_write(device, b"x".as_ptr(), 1),
1866                PamojaStatus::InvalidArgument
1867            );
1868
1869            pamoja_updater_free(device);
1870        }
1871    }
1872
1873    #[test]
1874    fn a_null_handle_is_refused_rather_than_dereferenced() {
1875        unsafe {
1876            assert!(pamoja_manifest_sign(manifest(&[], 1, 0), ptr::null()).is_null());
1877            assert_eq!(
1878                pamoja_updater_on_boot(ptr::null_mut(), ptr::null_mut()),
1879                PamojaStatus::InvalidArgument
1880            );
1881            assert_eq!(
1882                pamoja_updater_write(ptr::null_mut(), b"x".as_ptr(), 1),
1883                PamojaStatus::InvalidArgument
1884            );
1885            assert_eq!(
1886                pamoja_updater_progress(ptr::null_mut(), ptr::null_mut(), ptr::null_mut()),
1887                PamojaStatus::InvalidArgument
1888            );
1889            assert_eq!(
1890                pamoja_updater_finish(ptr::null_mut(), ptr::null_mut()),
1891                PamojaStatus::InvalidArgument
1892            );
1893            assert!(!pamoja_updater_delegation(ptr::null(), ptr::null_mut()));
1894            assert_eq!(pamoja_updater_slot_count(ptr::null()), 0);
1895            pamoja_updater_free(ptr::null_mut());
1896            pamoja_image_verifier_free(ptr::null_mut());
1897        }
1898    }
1899}