Recover from failed firmware updates with A/B slots
Node.js 22 or newer. No dependencies, network access or hardware.

Reference solution: not included here, so the exercise does not hand you the answer.
Attempt the repair first. Then download firmware-ab-recovery-solution.zip from the project card
on https://softwaresecurity.com/labs/#project-firmware-ab-recovery and extract it into this
folder; it adds reference.mjs without changing your files, and the reference commands below
need it.

Outcome
Repair the update path of a simulated device with two firmware slots so that no failure leaves
it running the wrong code or nothing at all. The tests inject the failures (a bad signature, a
signed old release, an image for another model, changed bytes, power loss at every flash write,
an image that never confirms, a device whose slots both fail) and write an evidence file that
records each failure and what the device booted next, including every fallback. Your
deliverable is that evidence file with every scenario passed, and a short note of which rule
each scenario proves.

Prerequisites and time
Know SHA-256, public and private keys, JSON and Node.js imports. Allow 60 to 90 minutes.

Files
  device.mjs      the simulated device: flash with slots A and B, a boot control block kept in
                  two CRC-checked copies, a rollback floor that can only count up, power-loss
                  injection, and powerOn(), which boots, runs the firmware's health check and
                  confirms a healthy image. Shared; do not edit.
  manifest.mjs    signed manifests (Ed25519 with Node.js crypto) and firmware images. Shared.
  exercise.mjs    what you repair: checkManifest, installUpdate, boot (the bootloader's slot
                  choice), confirm and recoveryInstall.
  exercise.test.mjs  12 failure scenarios plus one check of the device model.
  demo.mjs        prints the device console for a good update and a failed one.

The manifest
Each release has a manifest signed with the vendor's Ed25519 key, carrying the elements of the
IETF SUIT information model (RFC 9124 section 3): manifest structure version, monotonic
sequence number, vendor ID and class ID (UUID v5, as RFC 9124 recommends), image SHA-256 digest
and size. It is JSON, not SUIT's CBOR and COSE, so you can read it. The device trusts one
public key, provisioned at manufacture; nothing in a download can supply or select a key. The
sequence number, not the version string, is what anti-rollback compares: RFC 9124 notes that a
vendor rolls back on purpose by signing the old firmware again under a new sequence number.

The device
Factory state: release 1.12.0 (sequence 12) confirmed in slot A, slot B empty, rollback floor
12. Each slot is "empty", "trial" (with tries left), "confirmed" or "unbootable". The control
block names the active slot; a write to it is atomic, because it goes to the older of two
copies and a read takes the newest copy whose CRC-32 matches. The rollback floor stands for a
monotonic counter in protected storage (fuses, a replay-protected memory block or a secure
element): it only moves up, so it cannot be undone once raised.

1. Watch the starter
   node demo.mjs
   A good release installs and runs. A release that never passes its health check also
   "installs and runs", forever: there is no trial, and nothing falls back.

2. Run the failure scenarios
   node --test exercise.test.mjs
   Expect 5 passing and 8 failing in the unmodified starter. The signature checks, the
   inactive-slot write and the atomic switch already work, so power loss during an install
   already leaves a bootable device. The rollback, model, digest, size, trial, confirmation,
   busy and recovery scenarios fail.
   Each run writes results/update-evidence-exercise.json: for every scenario, what was
   injected, what was expected, what was observed (boots, slots, floor, flash writes), any
   fallback, the result and the device console. Read the records of the failing scenarios.

3. Repair exercise.mjs
   - checkManifest: refuse an image for another device class (incompatible), a size that does
     not fit a slot (size), and anything malformed (format).
   - installUpdate: compare the integer sequence number with the running image's and with the
     floor, never the version string (rollback); check the image bytes against the signed size
     and digest before the first write (digest); refuse while the running slot is still on trial
     (busy); mark the target slot empty before erasing it; after writing, read it back and
     verify it as the bootloader will (readback); switch as a trial with MAX_TRIAL_BOOTS tries;
     do not touch the floor.
   - boot: verify the signature, class, floor and digest of a slot before running it, and mark
     it unbootable if it fails; count a trial attempt in the control block before running the
     image; give up on a trial with no tries left and fall back to the other slot; when no slot
     can run, return {mode: 'recovery'}.
   - confirm: mark the running slot confirmed first, then raise the floor to its sequence
     number. If power fails between the two, the next boot of a confirmed image above the
     floor raises it (boot does this).
   - recoveryInstall: the same signature, compatibility, floor and digest checks as an update;
     then write slot A and switch to it as a trial.
   Repeat node --test exercise.test.mjs until all 13 tests pass.

4. Compare with the reference
   macOS/Linux: EXERCISE_MODULE=reference node --test exercise.test.mjs
   PowerShell: $env:EXERCISE_MODULE='reference'; node --test exercise.test.mjs
   Clear that environment variable before testing your own code.
   node demo.mjs --reference
   Expected: 13 of 13, and results/update-evidence-reference.json with 12 scenarios passed.
   In the never-confirms scenario its journal shows three trial boots of B (tries left 2, 1, 0),
   then trial-abandoned, fallback from B to A, a normal boot of 1.12.0, and a floor still at 12.

Worked reasoning
The signed 1.9.0 has a valid signature and the right model; only the sequence number shows it
is old. The starter compared version strings, and "1.9.0" sorts after "1.12.0". The changed
image byte fails because the signature covers the digest, not the bytes; the device must hash
what it received and, before booting, what landed in flash. The floor moves after confirmation,
never at install: raised at install, a failed 1.13.0 trial would leave 1.12.0 below the floor
and the device with nothing to run. Recovery is an installation path, so it gets the same trust
anchor and floor: an unauthenticated recovery mode is a way around every other check, and a
factory image below the floor is no recovery at all.

Completion criteria
All 13 tests pass and results/update-evidence-exercise.json reports 12 of 12 scenarios passed.
For each scenario, name the rule it proves and the line of exercise.mjs that enforces it.

Limits
A model, not firmware. Flash, power loss, the protected floor and the health check are
simulated in memory; a real device needs the same tests on its own bootloader, flash
controller and storage, where page sizes, erase behavior and write ordering differ. The key is
generated per test run and kept in memory; a real release key belongs offline or in a hardware
security module, and key rotation, repository metadata (TUF, Uptane), delta updates, encrypted
payloads and fleet rollout are out of scope. Physical attacks on the device are out of scope.

Primary references
NIST SP 800-193, Platform Firmware Resiliency Guidelines (May 2018): section 3.1 (protection,
detection and recovery); 4.2.1.1 (verify the signature before the update completes, "by loading
the update into a region of flash, verifying it, and then selecting that region of flash as the
active region"); 4.2.1.3 (non-bypassability, and preventing updates to an earlier version with a
security weakness); 4.3.1 (verify firmware before it runs); 4.4.1 (recovery images go through
the authenticated update mechanism, and recovery should not restore an earlier vulnerable
version)
  https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-193.pdf
RFC 9019, A Firmware Update Architecture for Internet of Things (April 2021): section 3
("Devices must not fail when a disruption, such as a power failure or network interruption,
occurs during the update process") and section 4.1 (keep the replaced image as a backup; the
bootloader must select different, valid firmware or obtain new, valid firmware)
  https://www.rfc-editor.org/rfc/rfc9019.html
RFC 9124, A Manifest Information Model for Firmware Updates in Internet of Things (IoT)
Devices (January 2022):
sections 3.1 to 3.4, 3.13 to 3.15, 4.2.1 (THREAT.IMG.EXPIRED), 4.3.1 (REQ.SEC.SEQUENCE) and
4.3.2 (REQ.SEC.COMPATIBLE)
  https://www.rfc-editor.org/rfc/rfc9124.html
The SUIT manifest format itself, draft-ietf-suit-manifest, was in the RFC Editor queue and not
yet an RFC on 11 October 2026:
  https://datatracker.ietf.org/doc/draft-ietf-suit-manifest/
Android A/B (seamless) system updates: slots, bootable, successful and active
  https://source.android.com/docs/core/ota/ab
Android Verified Boot: updating stored rollback indexes only from slots marked successful
  https://android.googlesource.com/platform/external/avb/+/main/README.md#updating-stored-rollback-indexes
U-Boot boot count limit: bootcount, bootlimit, altbootcmd and upgrade_available
  https://docs.u-boot.org/en/latest/api/bootcount.html
Node.js crypto.sign and crypto.verify with Ed25519 (algorithm null)
  https://nodejs.org/api/crypto.html#cryptosignalgorithm-data-key-callback

Dev container
.devcontainer/devcontainer.json opens this folder in VS Code Dev Containers or GitHub
Codespaces using the Dev Containers Node.js 24 image, pinned by digest. Run the commands above
in its terminal.
