Stand: abgeleitet aus dem Quellstand toobloader/core + toobloader/stage0 (Phase-5/6-Codebasis inkl. Cloud-Command, DSLC, KDM, Provisioning). Die mitgelieferte Datei registry_production_architecture.md beschreibt die Registry-Cloud-Infrastruktur und ist nicht Teil der Bootloader-Laufzeitarchitektur; sie wird hier nicht behandelt.
Toob-Boot ist als mehrstufige, transaktionale Boot-Kette für MCU-Klasse-Geräte organisiert. Jede Stufe hat ein klar abgegrenztes Vertrauensmodell und eigene Speicherdisziplin:
ROM/Reset
│
▼
Stage 0 (immutabel) stage0_main.c, stage0_*.c
│ DSLC-Gate, Bank-Auswahl A/B, Ed25519-Verify von Stage 1
▼
Stage 1 (updatebar, A/B) boot_main.c → boot_state.c
│ WAL/TMR, Confirm, Cloud-Commands, Update-Pipeline, Rollback
│
├──► Stage 1.5 Serial Rescue boot_panic.c (Fehler-/Lock-Pfad, _Noreturn)
├──► Factory Provisioning boot_provisioning.c (nur DSLC == 0x00)
▼
Feature-OS / Recovery-OS Handoff via .noinit (toob_handoff_t, toob_boot_diag_t)
Stage 0 ist bewusst minimal: kein WAL-Parser, kein Update-Code, nur Pointer-Resolution, Tentative-Auswertung und Signaturprüfung der aktiven Stage-1-Bank. Stage 1 trägt die vollständige Lifecycle-Logik (Journal, Verifikation, Swap, Rollback, Cloud-Control-Plane). Stage 1.5 ist kein eigenes Image, sondern der terminale Rescue-Zustand innerhalb von Stage 1 (boot_panic). Der OS-Übergang erfolgt nie per Funktionsaufruf, sondern über einen versiegelten .noinit-Handoff plus Assembler-Jump im Vendor-Startup.
Diese Muster ziehen sich durch praktisch jedes Modul und sind die eigentliche Architektur-Signatur des Cores.
Es gibt genau einen großen RAM-Block: uint8_t crypto_arena[BOOT_CRYPTO_ARENA_SIZE] (definiert in boot_main.c, 8-Byte aligned). Kein malloc, keine VLAs, kein nennenswertes BSS pro Modul. Die Arena wird phasenexklusiv übernommen: Wer sie nutzt (Delta-VM, Swap, Panic, Provisioning, Merkle, Cloud-Cmd), partitioniert sie intern über Makros oder Offsets, beweist die Partitionierung per _Static_assert und zeroized sie am Single-Exit vollständig. Beispiele für Partitionierungsschemata: boot_panic.c (Challenge/RX/Verify/Chunk mit Compile-Time-Summenbeweis), boot_delta.c (heatshrink-Decoder + zwei DMA-alignte Hälften als Write-Combiner/Read-Buffer), boot_cloud_cmd.c (Envelope-Zone + Work-Zone mit explizitem Aliasing-Verbot).
Vier wiederkehrende Mechanismen gegen Fault-Injection (Voltage/EMFI/Laser):
Double-Check-Shields. Jede sicherheitsentscheidende Bedingung wird zweimal evaluiert, getrennt durch BOOT_GLITCH_DELAY(), in zwei volatile uint32_t-Flags. Beide Flags müssen exakt BOOT_OK ergeben. BOOT_OK == 0x55AA55AA ist per _Static_assert als High-Hamming-Weight-Pattern erzwungen, damit ein einzelner Bit-Flip nie aus „Fehler“ ein „OK“ machen kann.
CFI-Akkumulatoren mit TRNG-Randomisierung. Jede größere Funktion führt einen volatile-XOR-Akkumulator über Tokens, die pro Boot via cfi_derive(seed, slot) (Fibonacci-Hashing, Bit 0 erzwungen ≠ 0) aus einem TRNG-Seed abgeleitet werden. Am Funktionsende muss der Akkumulator dem erwarteten XOR aller Pfad-Tokens entsprechen — ein PC-Glitch, der Blöcke überspringt, fällt damit auf. Negative Pfade (z. B. „PQC legitim übersprungen“ in boot_verify.c, „kein Update pending“ in boot_state.c) bekommen eigene Skip-Tokens, damit die Kaskade auch ohne Aktivität geschlossen wird. In boot_multiimage.c ist der erwartete Wert zusätzlich dynamisch aus den Component-IDs berechnet, damit sich das Überspringen zweier Komponenten nicht per XOR aufheben kann.
WDT-Starvation als Fail-Secure-Halt. Erkennt ein Modul einen aktiven Angriff oder einen Zustand, aus dem es keinen sicheren Software-Pfad gibt (ALU-Glitch in boot_delay, RAM-Wipe-Glitch in boot_main, CFI-Bruch in boot_energy), wird die CPU absichtlich in eine Endlosschleife ohne WDT-Kick gehängt — vorher Clock-Deinit und disable_interrupts(), damit auch RTOS-/ROM-Hintergrund-Interrupts den Watchdog nicht versehentlich füttern. Der Hardware-Watchdog erzwingt den Kaltstart.
Constant-Time-Primitiven. Zentralisiert in boot_ct_utils.h: constant_time_memcmp_glitch_safe (dualer Vorwärts-/Rückwärts-Akkumulator, len == 0 ist explizit ein Fehler), is_fully_erased_constant_time (Timing-Orakel-freie Smart-Erase-Checks), boot_read_monotonic_counter_safe (Doppel-Read mit Shield), boot_random_safe (TRNG-Health-Check gegen All-0x00/All-0xFF).
Konsequent umgesetzt: Loop-Guards mit harten Obergrenzen (Delta-VM, Erase-Schleifen, Rollback), subtraktive Bounds-Checks gegen 32-Bit-Wraparounds vor jeder Adressarithmetik, Pointer-Provenance-Beweise (is_buffer_within mit uintptr_t-Wrap-Proofs), 8-Byte-Alignment-Erzwingung für DMA/Cortex-M0, Single-Exit-Cleanup mit boot_secure_zeroize (eigene Assembler-Implementierung gegen Dead-Code-Elimination, Host-Mock für die Sandbox), Pre-Declaration aller Buffer am Scope-Anfang.
Aller persistente Zustand lebt in genau zwei Strukturen des WAL-Subsystems: dem TMR-Payload (langlebige Werte, per 3-Sektor-Majority-Vote geschützt: SVNs, Failure-Counter, Erase-Counter, aktive Nonce, KDM-State, Stage-1-Bank) und den Append-Entries (transaktionale Intents mit CRC-Trailer). Module schreiben nie direkt rohe Statusbytes in den Flash; jede Zustandsänderung läuft über boot_journal_append/boot_journal_update_tmr, jeder Resume nach Brownout über boot_journal_reconstruct_txn.
| Datei | Funktion |
|---|---|
stage0_main.c |
Entry, DSLC-Gate, Header-Check, Hash+Verify, Jump |
stage0_boot_pointer.c |
O(1)-Majority-Vote über WAL-Header → aktive Stage-1-Bank (A/B) |
stage0_tentative.c |
Trial-Boot-Auswertung, Bank-Rollback bei Crash |
stage0_hash.c |
Zero-Allocation SHA-256 über Flash mit Poisoning bei Read-Fehler |
stage0_verify.c |
Glitch-resistenter Ed25519(ph)-Check (HW/SW/Hash-only per Build-Mode) |
stage0_otp.c |
Key-Index-Rotation aus dem Monotonic-Counter (eFuse-Epoch) |
Ablauf in main():
- Minimal-Init der HAL (Flash zwingend, Clock/Crypto/WDT optional).
- DSLC-Majority-Vote-Gate: bis zu 5 eFuse-Reads mit TRNG-randomisiertem Jitter zwischen den Reads (Anti-Sustained-Glitch); ein Wert gilt erst ab 3 übereinstimmenden Lesungen. Kein stabiler Wert →
dead_halt()(fail-closed). - Bank-Resolution:
stage0_get_active_slotliest alle WAL-Sektor-Header, validiert Magic+CRC glitch-geschirmt und wählt über die höchstesequence_iddie Stage-1-Bank austmr_data.active_stage1_bank— explizit die Bootloader-Bank, nicht das Feature-OS. - Tentative-Auswertung: War der letzte Stage-1-Boot
TOOB_STATE_TENTATIVE(Self-Update-Trial) und die Reset-Ursache ein Crash (WDT/HardFault/Brownout), wird das.noinit-Magic atomar genullt und auf die andere Bank zurückgefallen (Anti-Death-Loop für Bootloader-Self-Updates). - Header + Signatur: Image-Header lesen, Magic doppelt prüfen, Größe gegen
CHIP_APP_SLOT_SIZEbegrenzen. Key-Index = eFuse-Epoch (Monotonic-Counter, gekappt auf 255). Hash läuft über Header + Payload (Entry-Point-Fälschung = ACE wird damit signiert abgedeckt); der SHA-256-Digest liegt in einem genullten 64-Byte-Buffer, weil Monocyphers Ed25519ph 64 Bytes erwartet (OOB-Read-Prävention). Die Signatur liegt hinter dem Payload. - Dev-Bypass existiert nur, wenn
TOOB_ALLOW_DEV_BYPASSexplizit gesetzt ist und DSLC == 0x00; Produktions-Builds erzwingen per#error, dass das Flag bewusst gesetzt wurde. - Jump: bei doppelt geschirmtem Verify-OK Deinit-Kaskade, ICache-Invalidierung (XIP-Stale-Code-Prävention), dann naked
jump_to_payload(active_slot + hdr.entry_point)(ARM: MSP+Reset-Handler aus Vector-Table; RISC-V: direkterjr).
Stage 0 enthält Stubs für boot_panic/Handoff, um den Linker nicht in den Core zu ziehen — Fehlerpfade enden ausnahmslos in Halt-Loops.
boot_main() ist der Lifecycle-Rahmen um die State-Machine. Sechs Blöcke, durch einen 6-Token-CFI-Akkumulator verkettet:
Block 1 — Zero-Trust-Plattformvalidierung. Alle HAL-Pointer (clock, flash, wdt, crypto, confirm; console/soc optional) werden auf NULL geprüft, jede HAL muss TOOB_HAL_ABI_V2 melden, Pflicht-Funktionspointer werden einzeln verifiziert, max_erase_cycles == 0 ist verboten.
Block 2 — Init-Kaskade mit Bitmasken-Rollback. Reihenfolge: Bus-Matrix-Flush + Secondary-Core-Reset (hängende DMAs), Clock → Flash → OTFDEC-Off → WDT → Crypto → Confirm → Console → SoC. Jeder Schritt setzt ein Bit in init_mask; bei Fehlern wird in exakt umgekehrter Reihenfolge nur das Initialisierte deinitalisiert und hart an Stage 0 zurückgegeben (Panic wäre hier noch unsicher). Erst nach crypto->init() werden die CFI-Tokens aus dem TRNG gezogen.
Block 2.5 — Recovery-Pin. Pin-High → 500 ms Debounce (boot_delay_with_wdt) → doppelt geschirmter Re-Check → atomarer Sprung in boot_panic(BOOT_RECOVERY_REQUESTED).
Block 2.6 — Provisioning-Gate. DSLC == 0x00 (unprovisioniert) und Provisioning-HAL vorhanden → boot_provisioning_run() (kein Return). Fabriksicherheit basiert auf physischem Zugang, kein Recovery-Pin nötig.
Block 3 — State-Machine. boot_state_run() (Abschnitt 5), umrahmt von Wrap-around-sicherer Boot-Zeitmessung; das Resultat wird glitch-geschirmt evaluiert.
Block 4 — XIP-Bounds. active_entry_point muss in [CHIP_FLASH_BASE_ADDR, +CHIP_FLASH_TOTAL_SIZE) liegen (subtraktiv, wrap-frei), active_image_size > 0 und innerhalb der Restgröße, Vector-Table 4-Byte-aligned.
Block 5 — Handoff-Versiegelung (TOCTOU-frei). Diag-State wird befüllt (Boot-Zeit, Recovery-Events und Wear-Counter aus dem TMR) und via boot_diag_seal() CRC-versiegelt. Der toob_handoff_t (Magic TENTATIVE/COMMITTED, Struct-Version, Boot-Nonce, Reset-Reason, Partition, Net-Search-Accum, Resume-Offset, DICE-Device-ID, wipe_requested) wird lokal im Stack assembliert, CRC-32-versiegelt, dann atomar per memcpy in den .noinit-Bereich kopiert und sofort per Read-Back-CRC + Doppel-Shield verifiziert.
Block 6 — Deinit + Wipe-Beweis. OTFDEC wieder an, Flash-Deinit, Arena-Zeroize, danach ein stichprobenartiger O(1)-Beweis (erste 32 Bytes ODER-akkumuliert == 0, doppelt geschirmt) — schlägt der Wipe fehl (geglitcht), friert das System ein. Crypto/Confirm/Console/SoC-Deinit, Bus-Matrix-Flush, Clock-Deinit. Finales CFI-Gate über alle 6 Tokens; hier ist boot_panic nicht mehr erlaubt (Hardware bereits down), Fehler enden in WDT-Starvation. Der WDT wird als allerletztes deinitalisiert, unmittelbar vor dem Return an den Vendor-Jump.
Der panic_fallthrough-Pfad füllt einen minimalen Diag-Record (Error-Code, Boot-Dauer, CRC-Trailer) ins .noinit und ruft boot_panic.
boot_state_run() ist der CFI-Orchestrator mit 8 Token-Slots. Sequenz:
Step 1 — Rekonstruktion. boot_journal_init (Sliding-Window + TMR-Quorum), boot_journal_get_tmr, boot_journal_reconstruct_txn liefert dreierlei getrennt: den offenen Haupt-Intent, den Netz-Such-Akkumulator (NET_SEARCH_ACCUM) und den Download-Resume-Offset (DOWNLOAD_CHECKPOINT). Leerer WAL wird auf WAL_INTENT_NONE normalisiert.
Step 2 — Confirmation. Die 64-Bit-Trial-Nonce liegt im TMR (hi/lo, hardware-quorum-geschützt — bewusst nicht in den Append-Entries). Drei Autorisierungswege: confirm->check_ok(nonce) (RTC-basiertes OS-Confirm), WAL_INTENT_CONFIRM_COMMIT mit XOR-exaktem Nonce-Match, oder RECOVERY_RESOLVED plus harmloser Reset-Reason (PIN/POWER_ON). Alles doppelt geschirmt; jede Inkonsistenz wird stumm als NONE verworfen. Bei Erfolg: Failure-Counter → 0 (TMR-Update), Intent → NONE persistieren, danach Datenhygiene: Fire-and-Forget-Erase des kompletten Staging-Slots (Firmware-Leak-Prävention; Fehler hier dürfen den committeten Boot nicht mehr gefährden). Sticky Lock: DEVICE_LOCKED überspringt den gesamten Confirm-Block — ein gelocktes Gerät kann sich nicht durch normale OS-Confirmation entsperren.
Step 2.5 — Cloud-Command-Evaluation (_handle_cloud_cmd, Details Abschnitt 9). Läuft bewusst vor dem Lock-Gate, damit TOOB_CMD_UNLOCK greifen kann. Leerer/invalider Slot ist der Normalfall und ein Skip-Pfad mit eigenem CFI-Token. Nach erfolgreichem Dispatch wird der Command-Slot gelöscht (Fire-and-Forget).
Step 2.7 — Lock-Gate. DEVICE_LOCKED aktiv → doppelt geschirmt → boot_panic(BOOT_ERR_DEVICE_LOCKED); dort wartet Block 3A des Rescue auf ein UART-Unlock-Envelope. Der Nicht-Lock-Pfad XORt sein eigenes Token.
Step 3 — Crash-Kaskade. App-Crash = Reset-Reason ∈ {WDT, HardFault, Brownout} und Intent ∉ {UPDATE_PENDING, TXN_BEGIN} (damit Crashes während der Intent-Verarbeitung selbst nicht doppelt zählen). Counter++ im TMR. _handle_rollback_flow: Crash unmittelbar nach TXN_COMMIT (oder offenes ROLLBACK_PENDING) → boot_rollback_trigger_revert (physischer Restore Staging→App), danach vollständige Heilung (Counter 0, Intent NONE, kein Recovery-OS — die alte Firmware ist wieder Baseline). Sonst → boot_rollback_evaluate_os (Kaskade App/Recovery-OS/Terminal, Abschnitt 8.1).
Step 4 — Update-Pipeline (_handle_update_flow), nur bei UPDATE_PENDING:
- Manifest aus
open_txn->offsetkomplett in die Arena lesen, zcbor-Parse des SUIT-Manifests. - Pointer-Sandboxing: Ed25519-Signatur (exakt 64 B), PQC-Signatur/-Key und Conditions-Bytestrings müssen via
is_buffer_withinnachweislich in der Arena liegen; die Ed25519-Signatur wird auf den Stack kopiert (Anti-Aliasing — sie muss das spätere Überschreiben des Buffers überleben). - Envelope-first
boot_verify_manifest_envelope(Abschnitt 7.1), erst danach SVN-Anti-Rollback (boot_rollback_verify_svn), dann Device-Binding: ist Conditions-Feld 201 gesetzt, muss es 32 Bytes lang sein und constant-time mit dem DSLC matchen (Anti-Clone). SBOM-Digest (Payload-Feld 301, 32 B) und SVN/Key-Index gehen in die Diag-Telemetrie (EU-CRA). - Image-Routing über
images[0]: Raw →boot_merkle_verify_streamdirekt auf dem Staging-Slot (Scratch-Bereich der Arena hinter dem Manifest, 8-Byte-aligned). Delta → erst werden die Chunk-Hashes in einen Stack-Buffer gerettet (Use-After-Overwrite-Defense: die SDVM zerstört die Arena, in der die Hashes liegen;_Static_assertbeweist, dass der 2-KB-Buffer für die maximale Chunk-Anzahl reicht), dannboot_delta_applymit ZielCHIP_SCRATCH_SLOT(Anti-Brick: nie in-place in den App-Slot patchen, die Base wird noch gebraucht), anschließend zwingender Merkle-Verify des SDVM-Outputs gegen die geretteten signierten Hashes (ACE-Prävention bei korrumpierter VM-Ausgabe). Swap-Quelle wird entsprechend Scratch oder Staging. - Swap via
boot_swap_applyin den App-Slot; danach optional Multi-Image fürimages[1..3](NetCore/Recovery-OS/inaktive Stage-1-Bank, Whitelist-gebunden, Abschnitt 7.5), mit lückenlos fortgeschriebenem Staging-Offset. - Commit:
TXN_COMMITatomar appen; erst dann wird die extrahierte SVN an den TMR übergeben (extracted_svn→app_svnnur bei echter Installation). - Smart Error Topology: Verifikationsklasse-Fehler (VERIFY/DOWNGRADE/INVALID_ARG/BOUNDS/STATE/NOT_FOUND) verwerfen das Update kontrolliert (Intent → NONE persistieren, kein Bootloop), nur Hardware-Fehler propagieren in die Panic.
Step 5 — Handoff-Vorbereitung. Header des Ziel-Slots (App oder Recovery) lesen, Magic doppelt prüfen, Entry-Point/Size exportieren. is_tentative_boot = (Intent == TXN_COMMIT, doppelt geschirmt). Bei Tentative: 64-Bit-Nonce via boot_random_safe generieren, in den TMR persistieren, offenen Intent re-appen (Stateful-Slide-Abandonment-Fix gegen Verlust durch TMR-Rotation). Net-Accum und Resume-Offset werden ans OS durchgereicht.
Finales CFI-Gate nur, wenn core_status == BOOT_OK (Hardware-Fehler dürfen nicht in INVALID_STATE umgeschrieben werden). Bei jedem Fehler wird target_out genullt; TMR/WAL-Stack-Residuen werden immer zeroized.
Layout. 4–8 physische Sektoren (TOOB_WAL_SECTORS, Adressen/Größen aus dem generierten Manifest, Sanity-Check Header+1 Entry pro Sektor). Jeder Sektor beginnt mit einem 64-Byte-Header (wal_sector_header_aligned_t): Magic, sequence_id, erase_count, eingebetteter TMR-Payload (48 B), CRC-32. Dahinter Append-Entries in 64-Byte-Slots (wal_entry_aligned_t): Magic, Intent, 64-Bit-Nonce (8-Byte-aligned ohne Padding), Deadline, 256-Bit-Transfer-Bitmap, delta_chunk_id, generisches Offset, CRC-Trailer. ABI-Drift zwischen Bootloader- und libtoob-Typen ist über eine Batterie von _Static_asserts versiegelt.
Sequenzlogik. RFC-1982-Serial-Number-Arithmetik (is_newer_sequence) macht das Sliding Window wrap-sicher über die volle 32-Bit-Lebensdauer.
Init. Scan aller Header (CRC glitch-geschirmt) → höchste Sequenz = aktiver Sektor. Fabrikneu: Sektor 0 initialisieren. Sonst: TMR-Majority-Vote als striktes Whole-Struct-Voting (kein „Frankenstein“-Byte-Mixing) über die Sequenzen N, N-1, N-2 mit Constant-Time-Vergleichen; bei totaler Korruption gewinnt die höchste kryptografisch valide Sequenz. Danach linearer Frontier-Scan: vorwärts bis zum ersten vollständig gelöschten Slot oder Torn-Write (CRC/Magic-Fail stoppt die Front) — ECC-trap-frei, UB-frei.
Append. Vor jedem Write ein Full-Width-Pre-Check der Zielposition: ist sie nicht vollständig erased (Brownout-Fragment), wird sofort rotiert statt drübergeschrieben (HardFault-Prävention auf ECC-Flash). Rotation wählt per Wear-Leveling den am wenigsten abgenutzten Sektor, schützt aber die letzten 4 Sequenzen (N…N-3): die 3-Sektor-TMR-Rotation darf nie einen offenen Intent in Sektor N-3 überrollen (Cross-Sector-Fix). Erase-Budget wird gegen max_erase_cycles geprüft (COUNTER_EXHAUSTED). Nach dem Write: Read-Back + Constant-Time-Vergleich; bei Mismatch wird der Sektor logisch als voll markiert, damit der nächste Append rotiert.
TMR-Update. Zero-Wear-Skip bei bit-identischem Payload (constant-time). Sonst Quorum-Write über 3 frische Sektoren (Sequenzen n+1, n+2, n+3): Stromausfall nach Sektor 1 → die alten zwei Kopien gewinnen den Vote; nach Sektor 2 → die neuen zwei gewinnen. Mathematisch tearing-sicher ohne Doppel-Writes in denselben Sektor.
Rekonstruktion. Rückwärts-Scan ab der Frontier, über Sektorgrenzen hinweg (absteigende Sequenzen, Underflow-Guards für Fabrikgeräte). Intent-Isolation: der erste intakte Kernel-Intent ist die aktive Transaktion; NET_SEARCH_ACCUM und DOWNLOAD_CHECKPOINT werden unabhängig davon eingesammelt; Side-Band-Intents (SLEEP_BACKOFF, DEPRECATED_NONCE, ROLLBACK_PENDING) beeinflussen den Hauptzustand nicht. Early-Exit, sobald alle gesuchten Komponenten gefunden sind.
Intent-Übersicht (Auszug):
| Intent | Bedeutung |
|---|---|
NONE |
Stabiler Idle-Zustand |
UPDATE_PENDING |
Manifest im Staging, Pipeline beim nächsten Boot ausführen |
TXN_COMMIT |
Update geswappt, Trial-Boot steht aus |
CONFIRM_COMMIT / RECOVERY_RESOLVED |
OS-/Rescue-seitige Bestätigung |
TXN_ROLLBACK_PENDING / TXN_ROLLBACK |
Physischer Revert läuft / abgeschlossen |
NET_SEARCH_ACCUM, DOWNLOAD_CHECKPOINT, SLEEP_BACKOFF |
OS-/Edge-Checkpoints |
CLOUD_CMD, DEVICE_LOCKED |
Cloud-Control-Plane (Soft-Lock = sticky) |
Envelope-first nach COSE_Sign1-Philosophie: nichts aus dem Manifest wird interpretiert, bevor die Hülle bewiesen ist. Ablauf (7-Token-CFI):
- TOCTOU-Shield: die komplette
boot_verify_envelope_twird auf den Stack geklont, bevor irgendein Feld geprüft wird (DMA-Manipulation zwischen Check und Use ausgeschlossen). - Bounds: Manifest-Größe gegen den Work-Buffer, Adressraum-Wrap-Proofs.
- Constant-Time-Downgrade-Check: es wird immer der eFuse-Slot
key_index + 1gelesen (branchless Index, kein Timing-Leak). Existiert der nächste Key, ist der angefragte revoziert →BOOT_ERR_DOWNGRADE. Hardware-Fehler beim Read schalten kein Downgrade frei. - Root-Key-Extraktion glitch-gegated, plus Zero-Key-Forgery-Defense (All-0x00/All-0xFF-Keys aus geglitchten Reads oder leeren eFuses werden abgewiesen — der bekannte Ed25519-Trivial-Forge-Vektor).
- Ed25519-Verify über den SRAM-geladenen Manifest-Buffer; optional
TOOB_DOUBLE_VERIFY(zweite vollständige Ausführung gegen algorithmus-interne Faults, dokumentierter DPA-Trade-off). Der Root-Key wird unmittelbar danach geschreddert. - PQC-Hybrid-Enforcement: erzwungen, wenn die Hardware (
is_pqc_enforced) oder das Manifest es verlangt; der Skip-Pfad ist per Negativ-CFI bewiesen (ein Glitch kann PQC nicht stillschweigend überspringen). PQC-Key und -Signatur müssen per Pointer-Beweis innerhalb des Ed25519-signierten Buffers liegen (Anchored-Payload-Defense) und den Zero-Key-Check bestehen. - CFI-Resolution, Single-Exit-Zeroize (Keys, Envelope-Klon).
Funktional eine Flat-Hash-List (nicht binärer Baum): chunk-weises Streaming-SHA-256 mit O(1) RAM. Vorab eine vollständige Bounds-Algebra: Wrap-Checks für addr+size, num_chunks*chunk_size, num_chunks*32, Pointer-Wrap des Hash-Arrays, Chunk-Size gegen Sektor-Maximum und Write-Align, Arena ≥ Chunk-Size, und die Truncation-Defense: num_chunks muss exakt ceil(image_size/chunk_size) sein — abgeschnittene Manifeste fallen mathematisch durch. Pro Chunk: Flash→Arena, Hash, Constant-Time-32-Byte-Vergleich (dualer Fwd/Rev-Akkumulator). Abschließender Coverage-Beweis (remaining_bytes == 0, doppelt geschirmt) gegen erzwungene Loop-Abbrüche.
Eine Turing-unvollständige Streaming-Bytecode-VM, die Patches mit O(1) RAM direkt Flash→Flash assembliert:
- Format: 32-Byte-Header (Magic
TDS1, Ziel-/Base-Größe, 8-Byte-Base-Fingerprint, Literal-Offset, Instruktionszahl, CRC) + 16-Byte-Instruktionen mit isolierter CRC pro Instruktion (SPI-Bit-Rot) + Literal-Block. Der Literal-Offset wird hart gegenheader + instr_count*16erzwungen — kein angreifergewählter Offset, kein Arbitrary-Flash-Read. - Opcodes mit High-Hamming-Encodings:
COPY_BASE,INSERT_LIT(heatshrink-LZSS-dekomprimiert),BZERO,EOF. - Ghost-Base-Verifikation: vor dem ersten Patch-Byte wird die alte Firmware komplett gestream-hasht und die ersten 8 Digest-Bytes gegen den Header-Fingerprint geprüft (Anti-Brick: Patch auf falsche Base unmöglich). Beim Brownout-Resume entfällt das, weil die Base bereits im vorigen Lauf verifiziert und im WAL geloggt wurde.
- Arena-Layout: heatshrink-Decoder-State (alignment-gepaddet) + zwei 8-Byte-alignte Hälften (Write-Combiner / Read-Buffer). Wichtig: der Decoder-State wird bei Checkpoints nicht resettet (das würde den LZSS-Stream zerstören).
- Zip-Bomb-Guards an zwei Stellen: statisch für Raw-Opcodes, dynamisch im
poll()-Loop fürINSERT_LITund explizit auch im EOF-Drain (dort lag historisch die Lücke). - Checkpointing/Resume:
flush_target_bufferschreibt mit Smart-Erase-Pre-Emption (isolierte Stack-Buffer, damit die Arena nicht zerstört wird), Read-Back-Verify pro 64-Byte-Step, und setzt WAL-Checkpoints (delta_chunk_id) exakt anCHIP_FLASH_MAX_SECTOR_SIZE-Grenzen. Nach Brownout: Fast-Forward per Dry-Run — die VM läuft logisch erneut, schreibt aber bis zum Checkpoint nichts; Checkpoint-Sektoralignment wird nachgerechnet. - Abschluss: finaler Flush mit Write-Align-Padding, Anti-Truncation-Beweis (
eof_reached && target == expected_size), CFI-Resolution, Arena-Zeroize.
In-Place-Swap über den Scratch-Slot mit kryptografischer Zustandsdeduktion statt Metadaten in den Zielsektoren:
- Blockgrößen-Solver: pro Iteration das Maximum der Sektorgrößen von Src/Dest/Scratch (Anti-Write-Amplification auf asymmetrischen Flashes), letzte Iteration mit Align-Padding.
- Zero-Wear-Identity-Check: CRC-Gleichheit + Constant-Time-Byte-Vergleich → Block überspringen (spart Erases und WAL-Writes massiv, z. B. bei Re-Flashes).
- WAL-Anker: vor jeder destruktiven Phase werden
crc_src,crc_destund der Marker0xAA55AA55in dietransfer_bitmapder offenen Transaktion geschrieben. - Tearing-Deduktion: nach Brownout werden die physischen CRCs von Src/Dest/Scratch gegen die geankerten CRCs gematcht; daraus wird eindeutig abgeleitet, welche der drei Phasen (A: Dest→Scratch-Backup, B: Src→Dest, C: Scratch→Src) noch laufen müssen. Inkonsistente Kombinationen sind FATAL (
FLASH_HW/INVALID_STATE) — niemals raten. - EOL-Schutz: erreicht der App-Slot-Erase-Counter
max_erase_cycles, geht der Swapper in einen persistenten Read-Only-Zustand (COUNTER_EXHAUSTED). - Telemetrie: physische Erases pro Slot landen am Ende kumuliert im TMR (Wear-Leveling-Reporting fürs OS).
Deployment von bis zu 256 Sub-Images (NetCore, Modems, Recovery-OS, inaktive Stage-1-Bank) aus dem Staging-Archiv:
- Software-MPU: bevor ein Byte angefasst wird, müssen alle Komponenten beweisen, dass
[target, target+size)vollständig in einer Whitelist-Region liegt (subtraktive Wrap-Proofs) — Arbitrary-Write in Bootloader/WAL/App-Slot ist mathematisch ausgeschlossen. - O(1)-Brownout-Resume über die 256-Bit-
transfer_bitmap: bereits verankerte Komponenten werden übersprungen (mit CFI-Verrechnung). - Phantom-Read-Back-Defense: gehasht wird ausschließlich der zurückgelesene Buffer, nicht der geschriebene — der Vendor-Treiber kann keinen RAM-Cache validieren lassen; vor dem Read-Back wird der Buffer genullt (TOCTOU-Beweis der physischen SPI-Transaktion). Finaler SHA-256 gegen den signierten
expected_hash. - Pro Komponente ein atomarer WAL-Checkpoint (Bitmap-Bit + Append); dynamischer Erwartungs-CFI aus
~component_id(siehe 2.2).
SVN-Hybrid-Check (verify_svn): Manifest-SVN muss ≥ persistierter WAL-SVN (App oder Recovery getrennt) und ≥ eFuse-Epoch (Monotonic-Counter) sein — identische Versionen (Reparatur-Re-Flash) sind zulässig, Downgrades nicht. Blank-Device-Toleranz (kein TMR → Baseline 0), eFuse NOT_SUPPORTED ist erlaubt, alles glitch-geschirmt + CFI.
Crash-Kaskade (evaluate_os): dreistufige, doppelt evaluierte CFI-Pfad-Flags (0x111…, 0x222…, 0x444…):
Counter ≤ MAX_RETRIES → App; ≤ MAX_RETRIES + MAX_RECOVERY_RETRIES → Recovery-OS; darüber Terminal:
im Unattended-Edge-Mode wird der Backoff-Level (SLEEP_BACKOFF-Intent mit Wakeup-Zeit) vor dem Schlafen ins WAL geschrieben, dann enter_low_power mit eskalierender Penalty (4 h → 12 h → 24 h Cap, saturierende Arithmetik); kehrt die HAL fälschlich zurück, folgt WDT-Starvation. Attended-Mode → direkt boot_panic(BOOT_RECOVERY_REQUESTED). Pfad-Konfusion (State-Confusion-Attack) → Panic.
Physischer Revert (trigger_revert): Staging-Header lesen (DMA-aligned 32-B-Buffer), Magic/Size glitch-geschirmt prüfen, Bounds-Proofs. Resume über ROLLBACK_PENDING.delta_chunk_id oder neuen Intent ankern. Dann sektorweise Staging→App: Zero-Wear-Identity-Fast-Forward, sonst WAL-Checkpoint vor dem Erase, Erase via boot_swap_erase_safe, chunk-weiser Copy mit Phase-Bound-CRC-Verify inkl. Ghost-Match-Prevention (Arena vor dem Read-Back nullen — beweist, dass wirklich der SPI-Flash und nicht ein Treiber-Cache antwortet). Abschluss: Wear-Counter ins TMR, TXN_ROLLBACK-Intent, CFI-Resolution, Single-Exit-Zeroize.
boot_confirm_evaluate: Nonce-Check via Confirm-HAL (WDT-gerahmt), kombiniert mit einer Reset-Reason-Whitelist — nur POWER_ON, PIN_RESET und BROWNOUT gelten als unschädlich. Der Brownout steht bewusst auf der Whitelist: ein Monate nach dem Update leerlaufender Akku ist kein OS-Crash; passierte der Brownout während des Handoffs, liefert check_ok() ohnehin false. Glitch-Doppel-Check; jeder Fail → Flag clearen → Rollback-Signal (BOOT_ERR_VERIFY), Hardware-Fehler werden durchgeschleift.
Vor jeder Flash-destruktiven Update-Aktion: Fail-Open für Netzgeräte (keine SoC-HAL → bedingungslos OK, aber CFI-bewiesen). Sonst zwei unabhängige Kriterien: PMIC-Veto (can_sustain_update) und Roh-ADC mit branchless 3-Sample-Median (SPA-resistent, kompiliert zu CSEL) inkl. ADC-Failure-Traps (0 mV / 0xFFFFFFFF) und Brownout-Hysterese (+12,5 % auf min_battery_mv, weil Ruhespannung nach Brownout trügerisch hoch ist; overflow-gesichert). Fällt das Gate: im Unattended-Mode 1-h-Deep-Sleep-Penalty zum Laden (bevor das WAL angefasst wird — Brownout-Death-Loop-Prevention), sonst boot_panic(BOOT_ERR_POWER). CFI-Bruch → Starvation-Freeze.
Terminalzustand für Hardware-Fehler, Lock-State und Recovery-Pin. Architektur:
- Arena-Mapping: vier disjunkte Zonen (Challenge 128 / RX 128 / Verify 80 / Chunk = Rest), Summenbeweis per
_Static_assert; Stack bleibt sauber (>1,2 KB gespart), freie Zonen werden phasenweise recycelt (DSLC-Read, Root-Key, Read-Back). - 2FA-Challenge:
[TRNG-Nonce(32) | DSLC(≤64, gepaddet) | Monotonic-Counter(4) | Reason(4)], COBS-geframt an den Techniker. - Auth: exakt 104-Byte-Payload (
slot_id,sequence_id, Nonce-Echo, Ed25519-Signatur über die 72-Byte-Message[Nonce|DSLC|Slot|Seq]mit dem Root-Key). Anti-Replay:sequence_idmuss exaktcounter + 1sein; nach Erfolg wird der Counter gebrannt (Token-OTP-Entwertung). Unaligned-Access-sicher viamemcpy+offsetofdirekt aus dem RX-Buffer. - Anti-Brute-Force über Power-Cycles: der Fehlversuchszähler wird in RTC-Backup-Registern persistiert; ohne RTC ersatzweise 5 s deterministischer Boot-Delay. Exponentielle Penalty
2^min(n,10) · 100 ms. - CFI-Beweis zwischen Auth und Transfer: State-Confusion-Glitches, die direkt in den Flash-Pfad springen, landen im SOS-Loop.
- Block 3A (Device-Locked): sendet
LCK, wartet auf ein vollwertiges Cloud-Command-Envelope; nurTOOB_CMD_UNLOCK(kryptografisch viaboot_cloud_cmd_evaluate_buffer) entsperrt →ULK→ erzwungener Kaltstart. - Block 3B (Firmware-Stream):
RDY/ACK/EOF-Protokoll, Naked-COBS-Chunks in den Staging-Slot mit Align-Padding, glitch-geschirmten Bounds, Smart-Erase und Read-Back-Verify pro Chunk; jeder Fehler erzwingtsession_reset(neue Challenge). NachEOF: Arena-Wipe und bewusster WDT-Timeout als Handoff-Reset, mit NULL-Pointer-Trap als HardFault-Fallback. - Randständiges:
enter_sos_loop(Blink-Halt mit WDT-Kicks) wenn keine Console/Crypto-HAL existiert;toob_ecc_trapals kontextfreier NMI/HardFault-Endpunkt (WDT-Biss abwarten); eigener__stack_chk_fail/__stack_chk_guard, um 120–145 KB libc-Bloat (abort/raise/printf) aus dem Binary zu halten.
Der Bootloader kann ohne OS-Beteiligung kryptografisch signierte Cloud-Befehle ausführen, die das OS zuvor in den CHIP_CLOUD_CMD_SLOT gelegt hat.
Key-Hierarchie (KDM). Der Cloud-Key ist nicht der Root-Key, sondern über ein Key Delegation Manifest delegiert: 128-Byte-Struktur (Sequenz, neuer Cloud-Pubkey, Root-Signatur über die ersten 36 Bytes, Flash-Align-Padding, per _Static_assert fixiert) in zwei A/B-Slots. load_active_cloud_key: beide Slots lesen, Root-Key laden (mit Zero-Key-Glitch-Defense — ein All-Zero-Read würde sonst beliebige KDM-Fakes akzeptieren), beide Signaturen glitch-geschirmt prüfen, bei zwei gültigen Slots gewinnt die höhere Sequenz. KDM-Healing (Reparatur des defekten Slots) ist per Architekturentscheid Aufgabe des OS-Background-Tasks — der Bootloader schreibt im Lesepfad nicht (O(1)- und Watchdog-Garantie). Der Fallback auf eFuse-Slot 1 ist ausschließlich im Provisioning-Zustand (DSLC == 0x00) erlaubt; ein provisioniertes Gerät mit zerstörten KDM-Slots verweigert (Downgrade-by-Destruction-Defense).
Envelope-Pipeline (evaluate_buffer, 5-Token-CFI): CBOR-Decode (zcbor-CDDL) → Device-ID-Match constant-time gegen die DICE-abgeleitete ID (boot_derive_device_id) → Anti-Replay (counter_min > Monotonic-Counter, doppelt geschirmt, sonst DOWNGRADE) → Ed25519 über das Envelope abzüglich der 64-Byte-Signatur mit dem aktiven Cloud-Key (Key wird sofort danach gewiped) → Exhaustion-Defense: der Counter wird erst nach erfolgreicher Signaturprüfung exakt um die Differenz gebrannt (ein Angreifer kann eFuses nicht per ungültigen Envelopes verbrauchen); jeder einzelne Burn ist glitch-geschirmt, ein fehlgeschlagener Burn bricht den Dispatch ab (Replay-Fenster geschlossen). evaluate_flash kapselt das mit Single-Read-TOCTOU-Defense (Slot einmal komplett in die Arena) und Aliasing-Verbot zwischen Envelope- und Work-Zone.
Dispatch (in boot_state.c): KILLSWITCH → DEVICE_LOCKED-Intent (Soft-Lock, sticky); UNLOCK → Intent NONE; FORCE_UPDATE → UPDATE_PENDING; WIPE → wipe_requested im Handoff (Factory-Reset führt das OS aus); REVOKE → DSLC wird auf 0xFF gebrannt und das Gerät geht in den permanenten Dead-Halt (Hard-Lock, irreversibel). ROTATE_KEY/NOP sind im Boot-Dispatch No-Ops (Rotation läuft über die KDM-Slots).
Hinweis aus dem Code: die Slot-Adressen (CHIP_CLOUD_CMD_SLOT_ABS_ADDR, KDM A/B) sind aktuell noch als temporäre Fallback-Makros definiert, solange das Device-Manifest sie nicht liefert.
_Noreturn-UART-Loop, ausschließlich erreichbar bei DSLC == 0x00 (Gate in boot_main). Arena-partitioniert (RX 256 / TX 16 / Key 128, Summen-Assert). Wire-Protokoll: COBS-Frames [CMD(1)][Payload][CRC-32(4)], CRC glitch-geschirmt geprüft, jede Antwort ein 4-Byte-LE-Status. Kommandos: BURN_KEY (Index + Key, über Key-Buffer mit Zeroize davor/danach), SET_DSLC, SET_PROTECTION (32-Bit-Maske), ENABLE_SB, ENABLE_FE, READ_ID (Status-Frame + UID-Frame), REBOOT (Arena-Wipe, dann bewusste WDT-Starvation als Hardware-Reset). Kein Rückpfad in den normalen Bootflow — die Trennung Provisioning↔Boot ist strukturell, nicht nur logisch.
boot_cobs.c/.h — gemeinsame COBS-Transportschicht für Panic und Provisioning (aus boot_panic.c extrahiert, DRY). Sende-Pfad mit WDT-Feeding; In-Place-Decode mit der Invariante write_idx ≤ read_idx, glitch-geschirmtem Bounds-Check pro Block (Anti-RCE/Wraparound) und Zeroize des Trailing-Garbage (Anti-Leakage); blockierender Frame-Empfang mit Overflow-Defense (Buffer vernichten, auf nächsten Sync warten).
boot_crc32.c — bitweises IEEE-802.3-CRC-32 (0xEDB88320), tabellenfrei (Flash-Größe vor Geschwindigkeit). Wird überall für WAL-Header/-Entries, Handoff/Diag-Sealing, TDS-Instruktionen und Swap-Zustandsdeduktion verwendet.
boot_ct_utils.h — siehe 2.2; zusätzlich cfi_derive als Token-Fabrik.
boot_secure_zeroize.S / _host.c — byte-weises Zeroize in nacktem ARM-Thumb-/RISC-V-Assembler, damit kein Compiler den Wipe als Dead Code eliminiert; Host-Variante mit volatile + Memory-Clobber für die Sandbox.
boot_delay.c — glitch-resistentes Warten: Dual-Track-Akkumulation (Hardware-Ticks + Software-Zähler mit invertiertem Zwilling für Bit-Flip-Detektion), Time-Warp-Kappung (Delta > 1000 ms → 0; bestraft Timer-Manipulation und fängt 16-Bit-HAL-Wraps), algebraisch bewiesene Obergrenze gegen max_sw_limit-Overflow, 50-ms-Steps (WDT-sicher). Exit nur, wenn HW und SW das Ziel erreichen; eingefrorener Timer oder übersprungene delay_ms (Software-Zähler explodiert) sowie ALU-Divergenz enden in WDT-Starvation — im Penalty-Sleep ist ein Fail-Open-Return streng verboten.
boot_identity.c — DICE-artige Device-ID: SHA-256(Chip-UID ‖ Root-PubKey ‖ "toob-device-id-v1"), O(1) iterativ, vollständige Capability-Gates und Residuen-Zeroize (inkl. Dummy-Finalize bei Update-Fehlern).
boot_diag.c — Telemetrie-Akkumulator im .noinit: Verify-/Boot-Zeiten, letzter Fehler + Vendor-Code, SVN/Key-Index/SBOM-Digest (EU-CRA-Evidenz), Recovery-Events, Wear-Daten; boot_diag_seal() nullt explizit das Struct-Padding (kein Stack-Leak über Alignment-Löcher) und setzt den CRC-Trailer.
Zwei CRC-versiegelte .noinit-Strukturen (8-Byte-aligned, Section-Attribut compilerabhängig):
toob_handoff_state — Magic (TENTATIVE/COMMITTED; Stage 0 wertet TENTATIVE für den Bank-Rollback aus), Struct-Version, 64-Bit-Boot-Nonce (das OS muss sie bei confirm echoen), übersetzter Reset-Reason (1:1-Mapping per Static-Assert garantiert), gebootete Partition (App/Recovery), Netz-Such-Akkumulator, Download-Resume-Offset, 32-Byte-Device-ID (bei Derivation-Fehler im Dev-Mode genullt), wipe_requested.
toob_diag_state — siehe boot_diag.c; das OS extrahiert daraus das CBOR-Telemetriepaket für das Fleet-Management.
Der Vertrag mit dem OS: ein Tentative-Boot muss vor dem nächsten Reset über die Confirm-HAL mit der exakten Nonce bestätigt werden, sonst greift beim nächsten Boot die Crash-/Rollback-Kaskade. Die Nonce selbst liegt redundant im TMR — ein Angreifer kann sie nicht durch WAL-Append-Manipulation ersetzen.
| Modul | Verantwortung | Schreibt Flash? | Nutzt Arena exklusiv? |
|---|---|---|---|
stage0_* |
Root of Trust, Bank-Auswahl, S1-Verify | nein | nein (eigene Stack-Buffer) |
boot_main |
Init/Deinit-Kaskade, Gates, Handoff-Sealing | nein | definiert sie |
boot_state |
Lifecycle-Orchestrierung, SUIT-Parsing, Routing | via Journal | ja (Manifest/Scratch) |
boot_journal |
WAL, TMR-Quorum, Wear-Leveling, Rekonstruktion | ja (WAL-Sektoren) | nein (Stack-Structs) |
boot_verify |
Envelope-first Ed25519 + PQC-Hybrid | nein | nutzt Work-Buffer (Arena) |
boot_merkle |
Flat-Hash-Streaming-Verify | nein | ja (Chunk-Buffer) |
boot_delta |
TDS1-SDVM, heatshrink, Checkpoints | ja (Scratch-Slot) | ja |
boot_swap |
A/B-Swap, Tearing-Deduktion, EOL | ja (App/Staging/Scratch) | ja |
boot_multiimage |
Peripherie-Deployment, Software-MPU | ja (Whitelist-Regionen) | ja |
boot_rollback |
SVN, Crash-Kaskade, Revert | ja (App-Slot, WAL) | ja |
boot_confirm |
Nonce-/Reset-Reason-Bewertung | via Confirm-HAL | nein |
boot_energy |
Update-Energie-Gate | nein | nein |
boot_cloud_cmd |
KDM, Envelope-Verify, Counter-Burn | eFuse-Counter, Cmd-Slot-Erase | ja |
boot_panic |
Stage 1.5 Rescue, Lock-Pfad | ja (Staging-Slot) | ja |
boot_provisioning |
Factory-Loop (DSLC 0) | via Provisioning-HAL | ja |
boot_cobs/crc32/ct_utils/zeroize/delay/identity/diag |
Querschnitts-Primitiven | nein | nein |
Drei Punkte, die der Code selbst als offen markiert und die für die Architektur-Lesart relevant sind:
- Cloud-Cmd/KDM-Slot-Adressen sind temporäre Fallback-Makros, bis das Device-Manifest (
generated_boot_config.h) sie liefert — bis dahin ist die Slot-Platzierung pro Chip nicht garantiert kollisionsfrei mit User-Daten. - Signatur-Längenermittlung im Cloud-Envelope (
envelope_len − 64statt eines vom zcbor-Generator gelieferten Payload-Offsets) ist als pragmatische Übergangslösung im Kommentar dokumentiert; sie setzt voraus, dass die 64-Byte-Signatur das physisch letzte Feld des Envelopes ist. - KDM-Healing ist bewusst aus dem Bootloader herausgehalten (OS-Background-Task) — der Recovery-Pfad bei dauerhaft einseitig defektem KDM-Slot hängt damit an der OS-Verfügbarkeit.
Diese Punkte ändern nichts am Sicherheitsmodell der dokumentierten Pfade, gehören aber in jede ARCHITECTURE.md-Pflege und ins Pre-Go-Live-Tracking.