Update code and docs to the latest CSA
diff --git a/docs/src/design/orchestrator/orchestrator-machine.md b/docs/src/design/orchestrator/orchestrator-machine.md index bac9f44..fe31f66 100644 --- a/docs/src/design/orchestrator/orchestrator-machine.md +++ b/docs/src/design/orchestrator/orchestrator-machine.md
@@ -2,20 +2,20 @@ This document describes the state machine that lives in `services/orchestrator/sm/src/lib.rs`: its states, shared storage, entry -actions, transition table, and the `Operational` superstate. +actions, transition table, and the `SupervisingPlatform` superstate. ```mermaid stateDiagram-v2 [*] --> PowerOnReset - PowerOnReset --> VerifyingPlatform : PowerGood(Provisioned) + PowerOnReset --> PreSupervision : PowerGood(Provisioned) PowerOnReset --> Locked : PowerGood(Unprovisioned) PowerOnReset --> Locked : PowerGood(SelfVerificationFailed) - VerifyingPlatform --> VerifyingPlatform : VerificationPassed [more, Passive]<br/>/ ReleaseReset · ReadFirmware · VerifyFirmware - VerifyingPlatform --> AwaitingReady : VerificationPassed [more, Active]<br/>/ ReleaseReset · ReadFirmware · VerifyFirmware - VerifyingPlatform --> Ready : VerificationPassed [chain done]<br/>/ ReleaseReset - VerifyingPlatform --> Recovering : VerificationFailed (any policy)<br/>/ RestoreGoldenImage + PreSupervision --> PreSupervision : VerificationPassed [more, Passive]<br/>/ ReleaseReset · ReadFirmware · VerifyFirmware + PreSupervision --> AwaitingReady : VerificationPassed [more, Active]<br/>/ ReleaseReset · ReadFirmware · VerifyFirmware + PreSupervision --> Ready : VerificationPassed [chain done]<br/>/ ReleaseReset + PreSupervision --> Recovering : VerificationFailed (any policy)<br/>/ RestoreGoldenImage AwaitingReady --> AwaitingReady : VerificationPassed [more]<br/>/ ReleaseReset · ReadFirmware · VerifyFirmware AwaitingReady --> Ready : ComponentReady [chain done or cursor past end] @@ -23,7 +23,7 @@ AwaitingReady --> Recovering : VerificationFailed (any policy)<br/>/ RestoreGoldenImage AwaitingReady --> Recovering : Timeout(id) [id == awaiting]<br/>/ RestoreGoldenImage - state Operational { + state SupervisingPlatform { [*] --> Ready Ready --> Updating : UpdateRequest<br/>/ AuthenticateUpdate · StageUpdate Updating --> Ready : UpdateVerified / ActivateUpdate @@ -33,8 +33,8 @@ AwaitingReady --> Recovering : CorruptionDetected<br/>/ RestoreGoldenImage } - Recovering --> VerifyingPlatform : Restored [retry < max_retry]<br/>(re-verify) - Recovering --> VerifyingPlatform : Restored [retry ≥ max_retry, Isolable/Cascading]<br/>/ AssertReset (skip — held) + Recovering --> PreSupervision : Restored [retry < max_retry]<br/>(re-verify) + Recovering --> PreSupervision : Restored [retry ≥ max_retry, Isolable/Cascading]<br/>/ AssertReset (skip — held) Recovering --> Locked : Restored [retry ≥ max_retry, PlatformHalt]<br/>(self-emits RecoveryFailed) / LatchLockdown Locked --> Locked : (terminal — all events ignored) ``` @@ -51,7 +51,7 @@ | Field | Type | Purpose | |---|---|---| | `chain` | `Vec<(ComponentId, ComponentAttrs), N>` | Ordered trust chain, supplied by the shell at construction time. Never mutated after build. | -| `cursor` | `u8` | Index of the component currently under verification. Reset to 0 on every `VerifyingPlatform` entry. Advances on each `VerificationPassed`, and past any component in `held` (skipped without verification), via `Outcome::Handled`. | +| `cursor` | `u8` | Index of the component currently under verification. Reset to 0 on every `PreSupervision` entry. Advances on each `VerificationPassed`, and past any component in `held` (skipped without verification), via `Outcome::Handled`. | | `held` | `Vec<ComponentId, N>` | Components skipped because their recovery was **exhausted**: an `Isolable` component, or a `Cascading` component plus its `depends_on` dependents. Not verified during the walk — held in reset, cursor advances past them. Populated in `Recovering` when `retry_count` reaches `max_retry`; persists across re-walks; cleared on `Ready` entry. | | `failed` | `Option<ComponentId>` | The component whose recovery episode is in progress; `None` while healthy. Set on any `VerificationFailed`, `Timeout`, or `CorruptionDetected` of a managed component. | | `retry_count` | `u8` | Number of consecutive failed restore attempts in the current recovery episode. Cleared to 0 in `Ready`'s entry action — consecutive only (INV7). | @@ -83,14 +83,14 @@ | Event | Guard | Effects | Next state | |---|---|---|---| -| `PowerGood(Provisioned)` | — | — | `VerifyingPlatform` | +| `PowerGood(Provisioned)` | — | — | `PreSupervision` | | `PowerGood(Unprovisioned)` | — | — | `Locked` | | `PowerGood(SelfVerificationFailed)` | — | — | `Locked` | | anything else | — | — | `Outcome::Super` (top level — discarded) | --- -### `VerifyingPlatform` +### `PreSupervision` Walks the trust chain component-by-component. The cursor advances on each `VerificationPassed` (or optional `VerificationFailed`) using `Outcome::Handled` @@ -123,7 +123,7 @@ Reached when an `Active` component passes eRoT authentication. The machine waits here until the component's iRoT signals readiness via `ComponentReady`. The speculative eRoT check for the next component (`ReadFirmware` + `VerifyFirmware`) -was already emitted by the `VerifyingPlatform` handler that triggered this +was already emitted by the `PreSupervision` handler that triggered this transition. **Entry action**: none. @@ -138,7 +138,7 @@ | `Timeout(id)` | `id == awaiting` | — | `Recovering` (failed = Some(id), awaiting = None) | | `Timeout(id)` | `id != awaiting` | — | `Handled` (stale — ignore) | | `VerificationFailed(id)` | — | — | `Recovering` (failed = Some(id), awaiting = None) — recovery attempted first | -| anything else | — | — | `Outcome::Super` → `Operational` | +| anything else | — | — | `Outcome::Super` → `SupervisingPlatform` | `ComponentReady` and `VerificationPassed` are independent and may arrive in either order. Both must be seen before the walk advances. `awaiting` tracks @@ -160,7 +160,7 @@ | Event | Guard | Effects | Next state | |---|---|---|---| | `UpdateRequest` | — | — | `Updating` | -| anything else | — | — | `Outcome::Super` → `Operational` | +| anything else | — | — | `Outcome::Super` → `SupervisingPlatform` | --- @@ -174,7 +174,7 @@ |---|---|---|---| | `UpdateVerified` | — | `ActivateUpdate` | `Ready` | | `UpdateRejected` | — | `DiscardStaged` | `Ready` (rejected update is not corruption — INV4) | -| anything else | — | — | `Outcome::Super` → `Operational` | +| anything else | — | — | `Outcome::Super` → `SupervisingPlatform` | --- @@ -190,12 +190,12 @@ | Event | Guard | Effects | Next state | |---|---|---|---| -| `Restored(_)` | `retry_count + 1 < max_retry` | — | `VerifyingPlatform` (re-verify — the restored image may pass) | -| `Restored(_)` | cap reached, `failed` `Isolable` | `AssertReset(failed)` | `VerifyingPlatform` (recovery exhausted: add `failed` to `held`, clear `failed`; the re-walk skips it) | -| `Restored(_)` | cap reached, `failed` `Cascading` | `AssertReset(failed)` · `AssertReset(dependent…)` | `VerifyingPlatform` (recovery exhausted: add `failed` + `depends_on` dependents to `held`, clear `failed`) | +| `Restored(_)` | `retry_count + 1 < max_retry` | — | `PreSupervision` (re-verify — the restored image may pass) | +| `Restored(_)` | cap reached, `failed` `Isolable` | `AssertReset(failed)` | `PreSupervision` (recovery exhausted: add `failed` to `held`, clear `failed`; the re-walk skips it) | +| `Restored(_)` | cap reached, `failed` `Cascading` | `AssertReset(failed)` · `AssertReset(dependent…)` | `PreSupervision` (recovery exhausted: add `failed` + `depends_on` dependents to `held`, clear `failed`) | | `Restored(_)` | cap reached, `failed` `PlatformHalt` | `Effect::Emit(RecoveryFailed)` | `Handled` (orchestrator queues `RecoveryFailed` next — INV7) | | `RecoveryFailed` | — | — | `Locked` | -| anything else | — | — | `Outcome::Super` → `Operational` | +| anything else | — | — | `Outcome::Super` → `SupervisingPlatform` | ("cap reached" = `retry_count + 1 >= max_retry`.) @@ -241,7 +241,7 @@ and shows up in the same trace. **Why re-walk from `cursor = 0`?** After restoring a component the machine -re-enters `VerifyingPlatform` and re-verifies the entire chain from scratch +re-enters `PreSupervision` and re-verifies the entire chain from scratch rather than resuming at the failed component. This is a deliberate conservative policy: a corruption event may indicate a broader integrity problem, and the CSA architecture's core principle — "no component executes unverified firmware" @@ -264,7 +264,7 @@ --- -## Superstate — `Operational` +## Superstate — `SupervisingPlatform` `Ready`, `Updating`, `Recovering`, and `AwaitingReady` share this superstate. When a leaf state returns `Outcome::Super`, `statig` calls the superstate handler. @@ -278,12 +278,12 @@ --- -## Centralizing the Operational Contract +## Centralizing the Supervision Contract Four states run once the platform is up: `Ready`, `Updating`, `Recovering`, and `AwaitingReady`. Two things must be true in all four, no matter which one the machine is in — an attestation challenge always gets answered, and a corruption -report always starts recovery. Call those two shared rules the **operational +report always starts recovery. Call those two shared rules the **supervision contract**. The question is where to write the contract down. One option is to copy it into @@ -298,8 +298,8 @@ *superstate* — a parent that several states sit under. When a state does not handle an event itself, the event falls through to the parent. So each state handles what is unique to it, and the parent handles what they all share. Our -four operational states sit under one superstate, `Operational`, and that is -where the operational contract lives — written exactly once. +four states sit under one superstate, `SupervisingPlatform`, and that is +where the supervision contract lives — written exactly once. To be fair to the alternative: a careful flat state machine could get the same result by giving every state a default branch that calls one shared function. @@ -311,13 +311,13 @@ The first reason is that there is only one copy to get right. In the flat version each state's default branch is written by hand, so they can quietly diverge — one calls the shared function, another does something slightly -different. With the superstate, each state points at the one `Operational` +different. With the superstate, each state points at the one `SupervisingPlatform` handler and nothing else. What that single copy buys us for auditing and verification is covered in [Invariant Verification](#invariant-verification) below. The second reason is that the superstate also covers the *in-between* states, -which are the easy ones to forget. `Operational` includes not just the settled +which are the easy ones to forget. `SupervisingPlatform` includes not just the settled `Ready` state but also `AwaitingReady` (still booting) and `Recovering` (still restoring). Corruption has to be handled even during those brief windows — and those are exactly the states a developer is tempted to skip as "temporary." @@ -341,7 +341,7 @@ The cost is that reading one state no longer tells the whole story. To know what `Ready` does with an event, you also have to know it falls through to -`Operational` and go read that. This is a trade, not a free win: the design takes +`SupervisingPlatform` and go read that. This is a trade, not a free win: the design takes on more indirection \u2014 and a bit more machinery, since a flat state machine is just\na match on state and event while this adds superstates and the fall-through rule \u2014\nin exchange for removing duplication of the one rule where duplication is most\ndangerous. We accept that because the `statig` library is used here without macros, so the fall-through is plain, visible code rather than hidden generation, and the small amount of library machinery involved is @@ -361,23 +361,23 @@ ## Invariant Verification -The invariants for the operational regime describe the whole regime, not one +The invariants for the `SupervisingPlatform` regime describe the whole regime, not one state at a time. Because the hierarchy stores each rule at that same whole-regime level, checking that the code matches the spec stays simple instead of turning into a state-by-state comparison. -Take INV6: *"an attestation challenge is answered in any operational state -without changing state."* In this design the rule itself is one line of code — -the `AttestationChallenge` row in the `Operational` handler. Reading that row +Take INV6: *"an attestation challenge is answered in any `SupervisingPlatform` +state without changing state."* In this design the rule itself is one line of code — +the `AttestationChallenge` row in the `SupervisingPlatform` handler. Reading that row tells you *what* happens; to know it happens *everywhere it should*, you also -check that each of the four operational states is linked to the superstate (its -`superstate()` points at `Operational`) and does not handle the event itself. +check that each of the four states is linked to the superstate (its +`superstate()` points at `SupervisingPlatform`) and does not handle the event itself. That is one row plus four link checks: | Invariant | Where it lives | To verify | |---|---|---| -| INV6 — attestation answered in any operational state, no transition | `AttestationChallenge → SignAttestation`, `Handled` (one row in `Operational`) | Read one row + confirm four states link to `Operational` | -| INV5 — corruption triggers recovery from any operational state | `CorruptionDetected → Recovering` (one row in `Operational`) | Read one row + confirm four states link to `Operational` | +| INV6 — attestation answered in any `SupervisingPlatform` state, no transition | `AttestationChallenge → SignAttestation`, `Handled` (one row in `SupervisingPlatform`) | Read one row + confirm four states link to `SupervisingPlatform` | +| INV5 — corruption triggers recovery from any `SupervisingPlatform` state | `CorruptionDetected → Recovering` (one row in `SupervisingPlatform`) | Read one row + confirm four states link to `SupervisingPlatform` | This is not free — the four links matter, because a state that forgets to link, or handles the event itself, silently drops out of the rule. But it is far less
diff --git a/docs/src/design/orchestrator/orchestrator-model.md b/docs/src/design/orchestrator/orchestrator-model.md index c56f278..5e9dc5c 100644 --- a/docs/src/design/orchestrator/orchestrator-model.md +++ b/docs/src/design/orchestrator/orchestrator-model.md
@@ -149,7 +149,7 @@ ``` chain: [(C0, {Active, Required}), (C1, {Passive, Required})] -VerifyingPlatform (entry): +PreSupervision (entry): emit ReadFirmware(C0) emit VerifyFirmware(C0) @@ -196,7 +196,7 @@ ``` chain: [(BMC, {Active, Required}), (HOST, {Active, Required}), (NIC, {Passive, Isolable})] -VerifyingPlatform (entry): +PreSupervision (entry): emit ReadFirmware(BMC) emit VerifyFirmware(BMC) ← eRoT reads and checks BMC firmware from SPI flash @@ -291,7 +291,7 @@ (eRoT ROM + measuring bootloader) before this machine runs. The result is delivered as `PowerOnResult` in `Event::PowerGood`. - **Attestation** (`AttestationChallenge` / `SignAttestation`): handled in the - `Operational` superstate, not part of the boot-time verification chain. + `SupervisingPlatform` superstate, not part of the boot-time verification chain. - **Firmware update verification** (`AuthenticateUpdate`): handled in the `Updating` state, distinct from boot-time chain verification. - **Multiple intermediate boot-progress checkpoints per component**: the CSA
diff --git a/docs/src/design/orchestrator/orchestrator-overview.md b/docs/src/design/orchestrator/orchestrator-overview.md index 2aa2105..d0e5453 100644 --- a/docs/src/design/orchestrator/orchestrator-overview.md +++ b/docs/src/design/orchestrator/orchestrator-overview.md
@@ -21,27 +21,31 @@ stateDiagram-v2 [*] --> PowerOnReset - PowerOnReset --> VerifyingPlatform : PowerGood [Provisioned] + PowerOnReset --> PreSupervision : PowerGood [Provisioned] PowerOnReset --> Locked : PowerGood [Unprovisioned] PowerOnReset --> Locked : PowerGood [SelfVerificationFailed] - VerifyingPlatform --> VerifyingPlatform : VerificationPassed [more, Passive] - VerifyingPlatform --> Operational : VerificationPassed [more, Active] - VerifyingPlatform --> Operational : VerificationPassed [chain done] - VerifyingPlatform --> Recovering : VerificationFailed + PreSupervision --> PreSupervision : VerificationPassed [more, Passive] - Operational --> Recovering : VerificationFailed [Required] - Operational --> Recovering : Timeout [id == awaiting] - Operational --> Recovering : CorruptionDetected [required] - Operational --> Operational : ComponentReady - Operational --> Operational : CorruptionDetected [optional] - Operational --> Operational : AttestationChallenge - Operational --> Operational : UpdateRequest - Operational --> Operational : UpdateVerified - Operational --> Operational : UpdateRejected + state SupervisingPlatform { + AwaitingReady --> Ready : ComponentReady [chain done] + AwaitingReady --> Recovering : VerificationFailed [Required] + AwaitingReady --> Recovering : Timeout [id == awaiting] + Ready --> Updating : UpdateRequest + Updating --> Ready : UpdateVerified / UpdateRejected + Ready --> Recovering : CorruptionDetected [required] + Updating --> Recovering : CorruptionDetected [required] + } - Recovering --> VerifyingPlatform : Restored [retry < max_retry] - Recovering --> VerifyingPlatform : Restored [retry >= max_retry, Isolable or Cascading] + SupervisingPlatform --> SupervisingPlatform : AttestationChallenge + SupervisingPlatform --> SupervisingPlatform : CorruptionDetected [optional] + + PreSupervision --> AwaitingReady : VerificationPassed [more, Active] + PreSupervision --> Ready : VerificationPassed [chain done] + PreSupervision --> Recovering : VerificationFailed + + Recovering --> PreSupervision : Restored [retry < max_retry] + Recovering --> PreSupervision : Restored [retry >= max_retry, Isolable or Cascading] Recovering --> Locked : Restored [retry >= max_retry, PlatformHalt] Locked --> [*] @@ -53,7 +57,7 @@ verification model (eRoT gate + optional iRoT gate), the verification boundary, `ComponentAttrs`, and concrete sequencing examples. - [**State Machine**](./orchestrator-machine.md): All states, shared storage, entry - actions, transition table, and the `Operational` superstate. + actions, transition table, and the `SupervisingPlatform` superstate. ## Design Principles @@ -82,7 +86,7 @@ | CSA concept | State machine encoding | |---|---| -| eRoT holds component in reset until firmware verified | `VerifyingPlatform` emits `ReleaseReset` only on `VerificationPassed` | +| eRoT holds component in reset until firmware verified | `PreSupervision` emits `ReleaseReset` only on `VerificationPassed` | | Component with Caliptra iRoT requires two independent checks | `ComponentKind::Active` → `AwaitingReady` until `ComponentReady` | | Passive component (no iRoT): eRoT check only | `ComponentKind::Passive` → advance immediately after `ReleaseReset` | | Isolable component: failure skips, not blocks | `FailurePolicy::Isolable` → skip (held in reset); advance without `Recovering`; no cascade |
diff --git a/docs/src/design/orchestrator/orchestrator-sm-transitions.md b/docs/src/design/orchestrator/orchestrator-sm-transitions.md index 96c68e3..f057c59 100644 --- a/docs/src/design/orchestrator/orchestrator-sm-transitions.md +++ b/docs/src/design/orchestrator/orchestrator-sm-transitions.md
@@ -20,12 +20,12 @@ The machine's initial state. It waits for the platform's first event, which is always `PowerGood`, carrying the result of the eRoT's power-on self-check. -### `PowerGood(Provisioned)` → `VerifyingPlatform` +### `PowerGood(Provisioned)` → `PreSupervision` The eRoT is provisioned and passed its own self-verification, so it is entitled to vouch for the rest of the platform. The machine leaves the gate and begins walking the trust chain. No effects are emitted by the transition itself; the -`VerifyingPlatform` entry action starts the first verification. +`PreSupervision` entry action starts the first verification. ### `PowerGood(Unprovisioned)` → `Locked` @@ -41,25 +41,26 @@ ### anything else → discarded -`PowerOnReset` sits outside the `Operational` superstate, so any event other than +`PowerOnReset` sits outside the `SupervisingPlatform` superstate, so any event other than `PowerGood` falls through to the top level and is discarded. The machine does not answer attestation or act on corruption before it has even begun verifying. --- -## From `VerifyingPlatform` +## From `PreSupervision` Walks the trust chain component by component. The entry action points the cursor at the first component not already in `held` and asks the platform to read and -verify its firmware. The transitions below react to the platform's verdicts. +verify its firmware. The transitions below react to the platform's firmware +verification results. -### `VerificationPassed` [more components, current is `Passive`] → `VerifyingPlatform` (self) +### `VerificationPassed` [more components, current is `Passive`] → `PreSupervision` (self) The current component is a symbiont device with no root of trust of its own, and its single eRoT-side check just passed. The machine releases it (`ReleaseReset`), asks the platform to read and verify the next component (`ReadFirmware` · `VerifyFirmware`), and advances the cursor — all while staying -in `VerifyingPlatform`. This is the self-loop that rolls the walk forward through +in `PreSupervision`. This is the self-loop that rolls the walk forward through symbiont devices. It uses `Outcome::Handled` rather than a real self-transition so the entry action does not re-run and reset the cursor. @@ -96,7 +97,7 @@ ### anything else → discarded -`VerifyingPlatform` also sits outside `Operational`, so unrelated events fall +`PreSupervision` also sits outside `SupervisingPlatform`, so unrelated events fall through to the top level and are discarded. Attestation challenges and corruption reports are not serviced during the initial chain walk. @@ -120,7 +121,7 @@ The awaited component's iRoT has come up. The machine clears `awaiting` to record that the readiness gate is satisfied, but stays in `AwaitingReady` because the -next component's eRoT verdict is still outstanding. +next component's firmware verification result is still outstanding. ### `ComponentReady` [id = `awaiting`, cursor past end] → `Ready` @@ -133,7 +134,7 @@ The speculative eRoT check for the next component passed. The machine releases that component, starts reading and verifying the one after it, and advances the -cursor — mirroring the `VerifyingPlatform` walk — while remaining in +cursor — mirroring the `PreSupervision` walk — while remaining in `AwaitingReady` because it may still be waiting on an iRoT readiness signal. ### `VerificationPassed` [chain done] → `Ready` @@ -161,10 +162,10 @@ clears `awaiting` (abandoning the in-flight readiness wait), and enters `Recovering`. -### anything else → `Operational` +### anything else → `SupervisingPlatform` -`AwaitingReady` is one of the four operational states, so unrelated events fall -through to the `Operational` superstate — which answers attestation challenges +`AwaitingReady` is one of the four `SupervisingPlatform` states, so unrelated events fall +through to the `SupervisingPlatform` superstate — which answers attestation challenges and acts on corruption reports even while the platform is still coming up. --- @@ -180,10 +181,10 @@ The platform has requested a firmware update. The machine transitions to `Updating`, whose entry action begins authenticating and staging the new image. -### anything else → `Operational` +### anything else → `SupervisingPlatform` Everything else `Ready` does — answering attestation, handling corruption — is -inherited from the `Operational` superstate via fall-through. +inherited from the `SupervisingPlatform` superstate via fall-through. --- @@ -204,9 +205,9 @@ A rejected update is deliberately **not** treated as corruption — nothing trusted was damaged, so there is no reason to enter recovery. -### anything else → `Operational` +### anything else → `SupervisingPlatform` -Attestation and corruption handling during an update come from the `Operational` +Attestation and corruption handling during an update come from the `SupervisingPlatform` superstate. --- @@ -219,7 +220,7 @@ transitions below fire on `Restored` and branch on how many attempts remain and, once exhausted, on the component's recovery-failure policy. -### `Restored` [`retry_count + 1 < max_retry`] → `VerifyingPlatform` +### `Restored` [`retry_count + 1 < max_retry`] → `PreSupervision` The restore completed and attempts remain. The machine re-walks the chain from the top to re-verify — the restored image may now pass. Re-verifying end to end @@ -227,7 +228,7 @@ whole platform, which is the conservative reading of the "no component executes unverified firmware" principle. -### `Restored` [cap reached, `failed` is `Isolable`] → `VerifyingPlatform` +### `Restored` [cap reached, `failed` is `Isolable`] → `PreSupervision` Restore attempts are exhausted and the recovery image still fails, and the component's policy is `Isolable`. The machine gives up on this one component @@ -235,7 +236,7 @@ `failed`, and re-walks to continue booting the rest of the platform. The re-walk skips the now-`held` component. -### `Restored` [cap reached, `failed` is `Cascading`] → `VerifyingPlatform` +### `Restored` [cap reached, `failed` is `Cascading`] → `PreSupervision` As with `Isolable`, but the failed component's dependents go down with it. The machine emits `AssertReset` for the component and each component whose @@ -257,10 +258,10 @@ `Locked`. Routing lockdown through this single event means `Locked` is only ever entered one way, no matter where the give-up decision originated. -### anything else → `Operational` +### anything else → `SupervisingPlatform` -`Recovering` is an operational state, so attestation and corruption events fall -through to the `Operational` superstate and are handled even mid-recovery. +`Recovering` is a `SupervisingPlatform` state, so attestation and corruption events fall +through to the `SupervisingPlatform` superstate and are handled even mid-recovery. --- @@ -277,17 +278,17 @@ --- -## From the `Operational` superstate +## From the `SupervisingPlatform` superstate `Ready`, `Updating`, `Recovering`, and `AwaitingReady` share this parent. When one of them returns `Outcome::Super`, these handlers run. Centralizing them here guarantees the two platform-wide behaviors apply identically in all four states. -### `AttestationChallenge` → `Operational` (no transition) +### `AttestationChallenge` → `SupervisingPlatform` (no transition) The machine emits `SignAttestation` to answer the challenge and stays exactly where it is. Answering an attestation challenge never changes state, so it is safe -to service from any operational state — including mid-boot (`AwaitingReady`) and +to service from any `SupervisingPlatform` state — including mid-boot (`AwaitingReady`) and mid-recovery (`Recovering`). ### `CorruptionDetected` [component required] → `Recovering` @@ -296,7 +297,7 @@ it in `failed` and drops into `Recovering`, re-entering the same two-stage recovery flow used at boot. -### `CorruptionDetected` [component not required] → `Operational` (no transition) +### `CorruptionDetected` [component not required] → `SupervisingPlatform` (no transition) The corrupt component is not required for the platform to run. Rather than tear down the platform, the machine emits `AssertReset` to gate the component (put it
diff --git a/docs/src/design/orchestrator/orchestrator-sm-walkthru.md b/docs/src/design/orchestrator/orchestrator-sm-walkthru.md index 0cd6463..fae756e 100644 --- a/docs/src/design/orchestrator/orchestrator-sm-walkthru.md +++ b/docs/src/design/orchestrator/orchestrator-sm-walkthru.md
@@ -13,7 +13,7 @@ encoding of that flow for a discrete eRoT running OpenPRoT. > **How to read this alongside the reference.** Every state named here -> (`PowerOnReset`, `VerifyingPlatform`, …) has a full entry in +> (`PowerOnReset`, `PreSupervision`, …) has a full entry in > [State Machine](./orchestrator-machine.md) with its entry action and transition > table. This document explains *why* the transitions are shaped the way they are > and *which CSA guarantee* each one upholds. When a claim needs the exact guard, @@ -23,39 +23,70 @@ ## The shape of the journey -At the highest level the machine moves through three phases: +At the highest level the machine has two operating regimes, a provisioning gate, +and a terminal exit: ```mermaid stateDiagram-v2 direction LR - [*] --> Boot - state Boot { - PowerOnReset --> VerifyingPlatform - VerifyingPlatform --> AwaitingReady - AwaitingReady --> VerifyingPlatform - } - Boot --> Operate : chain verified - state Operate { + [*] --> PowerOnReset + PowerOnReset --> PreSupervision : provisioned, self-check passed + PreSupervision --> PreSupervision : Passive component verified (self-loop) + state SupervisingPlatform { + AwaitingReady --> Ready : iRoT ready, chain done Ready --> Updating Updating --> Ready + Ready --> Recovering : corruption + Updating --> Recovering : corruption + AwaitingReady --> Recovering : timeout / failure } - Boot --> Recover : verification / readiness failure - Operate --> Recover : corruption - Recover --> Boot : restored (re-verify) - Recover --> Operate : restored (re-verify) - Boot --> Halt : unprovisioned / self-check failed - Recover --> Halt : recovery exhausted (PlatformHalt) - Halt --> [*] + PowerOnReset --> Locked : unprovisioned / self-check failed + PreSupervision --> AwaitingReady : Active component verified + PreSupervision --> Ready : Passive chain verified + PreSupervision --> Recovering : verification failure + Recovering --> PreSupervision : restored (re-verify) + Recovering --> Locked : recovery exhausted (PlatformHalt) + Locked --> [*] ``` -- **Boot** establishes the trust chain: verify each component, release it, and — - for components with their own root of trust — wait for it to come up. -- **Operate** is steady state: answer attestation challenges, apply firmware - updates, and watch for corruption. -- **Recover / Halt** handle failure: restore a component and re-verify, or, when - restoration is hopeless, stop. +**Two operating modes, never simultaneous.** `PreSupervision` and +`SupervisingPlatform` are mutually exclusive. The distinction is not about which +activities are happening — `AwaitingReady` sits inside `SupervisingPlatform` and +continues the chain walk — but about whether the **supervision contract is active**. +In `PreSupervision`, attestation challenges are not answered and corruption +events are not acted on. The moment `PreSupervision` exits, the supervision +contract switches on and stays on: attestation is always answered and corruption +is always acted on, regardless of whether the chain walk is still in progress. -The rest of this document walks each phase. +The threshold is one firmware verification result — passed or failed — not all +components having passed verification. CSA does not define a safe window before +supervision begins. A remote verifier may issue an attestation challenge as soon +as the first component's measurements exist. A corruption can be detected the +moment a component is running. The supervision contract — attestation always +answered, corruption always acted on — must therefore hold continuously from the +first result onward, including during recovery before anything has been released. +Deferring supervision until `Ready` would leave a gap that CSA does not permit. + +During recovery the machine temporarily exits supervision +(`SupervisingPlatform::Recovering` → `PreSupervision`), gating all components +back into reset. `SupervisingPlatform` is re-entered on the first transition +back out of `PreSupervision`. + +- **`PowerOnReset`** is the provisioning gate: the eRoT checks its own integrity + before it vouches for anything else. +- **`PreSupervision`** is regime one — supervision contract off: the eRoT walks + the trust chain, verifying and releasing components without yet answering + attestation challenges or acting on corruption. +- **`SupervisingPlatform`** is regime two — supervision contract on: attestation, + firmware updates, iRoT gating (`AwaitingReady`), and active recovery + (`SupervisingPlatform::Recovering`) all run under one shared superstate. + `PreSupervision` drives into it; `SupervisingPlatform::Recovering` drives + back out to re-walk the chain. +- **`Locked`** is the terminal exit: the platform stops and holds every component + in reset when provisioning is absent, the self-check fails, or recovery is + exhausted. + +The rest of this document walks each state. --- @@ -66,12 +97,11 @@ > **CSA:** *"The eRoT is the first component to execute after standby power is > applied. It is the trust anchor for the entire boot sequence."* -The machine starts in `PowerOnReset` and does nothing until the shell delivers -the first event, `PowerGood`, carrying the result of the eRoT's own power-on -self-check. That single event fans out three ways: +The machine starts in `PowerOnReset` and does nothing until `PowerGood` is +received, carrying the result of the eRoT's own power-on self-check. That single event fans out three ways: - `PowerGood(Provisioned)` — the eRoT is provisioned and self-verified, so it is - entitled to vouch for others. The machine advances to `VerifyingPlatform` and + entitled to vouch for others. The machine advances to `PreSupervision` and begins the chain walk. - `PowerGood(Unprovisioned)` — there is nothing to verify against, so the machine goes straight to `Locked`. @@ -86,7 +116,7 @@ ## Phase 2 — Walking the trust chain -**State: `VerifyingPlatform`.** +**State: `PreSupervision`.** > **CSA:** *"No downstream component boots until the eRoT has verified its > firmware. The eRoT holds each downstream component in reset until verification @@ -96,10 +126,15 @@ > releases it from reset, then waits for that device's boot-progress signal > before proceeding."* -`VerifyingPlatform` walks the shell-supplied trust chain one component at a time. -On entry it points the cursor at the first component not already skipped and asks -the shell to read and verify that component's firmware (`ReadFirmware` + -`VerifyFirmware`). From then on it reacts to the shell's verdicts. +`PreSupervision` is the phase before the supervision contract is active: +attestation challenges are not answered and corruption events are not acted on. +The eRoT processes firmware verification results here — one component at a time, +advancing the cursor and releasing each verified component — but it may exit after +a single result (on the first Active component or the first failure) or only after +walking the entire Passive chain. How much of the chain it covers depends on the +platform topology. On entry it points the cursor at the first component not +already skipped and emits `ReadFirmware` and `VerifyFirmware` for that +component. From then on it reacts to incoming firmware verification results. The machine is **device-agnostic**, exactly as CSA requires: the chain is a list of opaque `ComponentId`s with per-component `ComponentAttrs`. The core never @@ -117,7 +152,7 @@ - **`Passive`** — a *symbiont device* (e.g. a NIC): no root of trust of its own. The eRoT's signature/SVN check is the only gate. On `VerificationPassed` the machine releases it (`ReleaseReset`), immediately starts the next component's - read, advances the cursor, and stays in `VerifyingPlatform`. This is the + read, advances the cursor, and stays in `PreSupervision`. This is the self-loop: the walk rolls forward through symbiont devices. - **`Active`** — a *SoC with an integrated iRoT* (e.g. a BMC or CPU with Caliptra): it must clear **two independent gates**. When it passes the eRoT @@ -171,20 +206,21 @@ finished its local self-verification and the component is operational (e.g. its MCTP channel is up). - The **next eRoT check** — the speculative `VerificationPassed` for the following - component, which the `VerifyingPlatform` handler kicked off on the way in. + component, which the `PreSupervision` handler kicked off on the way in. The `awaiting` field remembers which component's readiness is still outstanding; -the state itself remembers whether the next component's eRoT verdict is still -pending. Both must resolve before the walk moves on, which is why the machine can -loop back into `AwaitingReady` several times: +the state itself remembers whether the next component's firmware verification +result is still pending. Both must resolve before the walk moves on, which is why +the machine can loop back into `AwaitingReady` several times: - `ComponentReady` from the awaited component clears the readiness gate. If there - is still chain left it stays here (now waiting only on the next eRoT verdict); + is still chain left it stays here (now waiting only on the next firmware + verification result); if the chain is already complete it advances to `Ready`. - `ComponentReady` for any *other* id is stale or spurious and is ignored — a guard so a late or duplicated signal cannot push the walk forward incorrectly. - `VerificationPassed` for the next component advances the walk the same way - `VerifyingPlatform` does. + `PreSupervision` does. ### The boot-progress watchdog @@ -203,9 +239,9 @@ --- -## Phase 4 — The operational regime +## Phase 4 — The `SupervisingPlatform` superstate -**States: `Ready`, `Updating`, and the `Operational` superstate.** +**States: `Ready`, `Updating`, `Recovering`, `AwaitingReady`, and the `SupervisingPlatform` superstate.** Once the whole chain is verified and released, the machine settles in `Ready`. On entry it clears the recovery bookkeeping (`retry_count`, `held`, `failed`) — @@ -213,16 +249,16 @@ is over. `Ready` itself does only one state-changing thing: on `UpdateRequest` it moves to -`Updating`, which asks the shell to authenticate and stage the new image, then -waits for a verdict — `UpdateVerified` activates the staged image and returns to +`Updating`, which issues a request to authenticate and stage the new image, then +waits for a result — `UpdateVerified` activates the staged image and returns to `Ready`; `UpdateRejected` discards it and returns to `Ready`. A rejected update is explicitly **not** treated as corruption: the platform simply keeps running the image it already had. -### The operational contract +### The supervision contract `Ready`, `Updating`, `Recovering`, and `AwaitingReady` all sit under one shared -parent, `Operational`, which handles the two events that must behave identically +parent, `SupervisingPlatform`, which handles the two events that must behave identically no matter which of those states is active: - **`AttestationChallenge`** → the machine signs an attestation response and stays @@ -240,9 +276,10 @@ re-enters the recovery flow rather than being ignored. Because these live in the parent, they apply during boot-time waiting -(`AwaitingReady`) and during recovery (`Recovering`) just as much as in `Ready`. -They do **not** apply in `PowerOnReset` or `VerifyingPlatform`, which sit outside -`Operational`: the eRoT does not answer attestation challenges or act on +(`AwaitingReady`) and during recovery (`SupervisingPlatform::Recovering`) just +as much as in `Ready`. +They do **not** apply in `PowerOnReset` or `PreSupervision`, which sit outside +`SupervisingPlatform`: the eRoT does not answer attestation challenges or act on corruption reports while it is still establishing the chain. --- @@ -319,8 +356,8 @@ > **CSA:** *"Platform halt — stop the boot sequence entirely and enter a manual or > out-of-band recovery mode."* -`Locked` is terminal. On entry it instructs the shell to hold every component in -reset permanently (`LatchLockdown`), and from then on every event is ignored. The +`Locked` is terminal. On entry `LatchLockdown` is emitted, holding every +component in reset permanently, and from then on every event is ignored. The machine reaches here from exactly three places, all meaning "no trustworthy state could be established, so refuse to run one": @@ -335,7 +372,7 @@ | CSA principle / policy | Where the machine upholds it | |---|---| | eRoT is the trust anchor, first to execute | `PowerOnReset` + `PowerGood` self-check gate | -| No downstream boots until eRoT verifies it; held in reset until release | `VerifyingPlatform` emits `ReleaseReset` only on `VerificationPassed` | +| No downstream boots until eRoT verifies it; held in reset until release | `PreSupervision` emits `ReleaseReset` only on `VerificationPassed` | | iRoT independently verifies; complementary eRoT/iRoT gates | `Active` → `AwaitingReady` on `ComponentReady`; `Passive` → immediate | | Device-agnostic ordered walk | Opaque `ComponentId` chain with `ComponentAttrs` | | Symbiont devices (NIST SP 800-193 §3.4) | `ComponentKind::Passive` | @@ -344,7 +381,7 @@ | Recover first for every failed device | Any `VerificationFailed` → `Recovering` | | Classify only after recovery fails (Isolable/Cascading/halt) | `Recovering` applies the policy when `retry_count` reaches `max_retry` | | Platform halt on unrecoverable failure | `PlatformHalt` → `RecoveryFailed` → `Locked` | -| Measurements form attestation evidence | `AttestationChallenge` → `SignAttestation` in `Operational` | +| Measurements form attestation evidence | `AttestationChallenge` → `SignAttestation` in `SupervisingPlatform` | ---
diff --git a/docs/src/design/orchestrator/plain.md b/docs/src/design/orchestrator/plain.md new file mode 100644 index 0000000..e356013 --- /dev/null +++ b/docs/src/design/orchestrator/plain.md
@@ -0,0 +1,120 @@ +# The Orchestrator in Plain Language + +This document explains how the orchestrator state machine works using plain +language and analogies. Nothing here is normative — for the precise rules, see +the [State Machine reference](./orchestrator-machine.md). + +--- + +## The job + +The eRoT is the first thing that powers on. Its job is to make sure nothing else +runs unless it has been checked. Think of it as a customs officer at an airport: +every passenger (platform component) must pass through the checkpoint before they +are allowed through. + +--- + +## Two zones, never both at once + +The machine is always in one of two zones. + +**The checkpoint** — `PreSupervision`. The eRoT is standing at the gate, +checking documents one by one. It is focused entirely on the queue. It is not +answering questions from people already through — that is not its job right now. + +**Airside** — `SupervisingPlatform`. At least one decision has been made about +the queue: a passenger was cleared, or one was flagged for a problem. The eRoT +is now managing the airside area: answering challenges from auditors, responding +to incidents. This is where most of the machine's life is spent. + +Once the eRoT steps away from the checkpoint and into the airside role, it does +not go back — unless a recovery incident forces a full evacuation and re-check +(see below). + +--- + +## Walking the queue + +At the checkpoint the eRoT processes the queue one at a time. + +- **Symbiont component** (a NIC, a storage controller — no root of trust of its + own): the eRoT checks its firmware signature, clears it through, and + immediately calls the next person in the queue. The eRoT *stays at the + checkpoint* — this is the self-loop in the diagram. + +- **Active component** (a BMC or CPU with its own security processor): the eRoT + checks the firmware signature and clears it through, but this passenger also + has to clear their own internal check before they are truly settled. The eRoT + moves them to a holding gate (`AwaitingReady`) and waits for the component's + own root of trust to report in. At this point the eRoT has stepped airside — + supervision has started — even though the rest of the queue is still waiting. + +--- + +## Why `Recovering` is airside + +This is the part that surprises people. + +Suppose a passenger is flagged at the checkpoint — their documents fail. The eRoT +moves them to a holding area and starts recovery: trying to restore a known-good +image and re-check. During this time: + +- Auditors are still walking the airside area: *"Prove to me that this platform + is in a known state."* The eRoT must still answer. +- A second component might corrupt while the first is being fixed. The eRoT must + still act on it. + +The supervision contract — *always answer attestation challenges, always act on +corruption* — cannot have a gap. So `Recovering` sits inside `SupervisingPlatform`: +the eRoT is dealing with a problem, but it is still on duty. + +Crucially, this means the eRoT can step airside *before anyone is actually +through the checkpoint*. If the very first passenger in the queue fails, the eRoT +immediately enters recovery — and is therefore in `SupervisingPlatform` — even +though zero components have been released. The supervision contract starts the +moment `PreSupervision` exits, for any reason. + +--- + +## The evacuation + +There is one moment when supervision is explicitly suspended. + +When recovery has done what it can — restored a golden image — the eRoT must +re-verify the whole chain from scratch. To do that it gates *all* components back +into reset: the equivalent of evacuating the building and locking the doors. It +then walks back to the checkpoint and starts the queue from the top. + +During an evacuation the eRoT does not answer auditors. It is busy re-checking +credentials. Once the first component is cleared through again, the eRoT steps +back airside and the supervision contract resumes. + +In state-machine terms: `Recovering → PreSupervision` is the evacuation; +the next `PreSupervision` exit re-enters `SupervisingPlatform`. + +--- + +## Terminal lockdown + +If a component cannot be recovered after all retries, the machine emits +`RecoveryFailed` and transitions to `Locked`. The building is evacuated and +the doors are physically bolted. Every component is held in reset permanently. +No further event has any effect. The only way out is out-of-band intervention. + +The same fate applies if the eRoT itself fails its self-check at power-on: there +is no point running a checkpoint if the officer cannot be trusted. + +--- + +## One-line summary of each state + +| State | Plain meaning | +|---|---| +| `PowerOnReset` | Waiting to find out if the officer passed their own check | +| `PreSupervision` | Standing at the checkpoint, working through the queue | +| `AwaitingReady` | A passenger is through the eRoT gate but still clearing their own internal check | +| `Ready` | Everyone is through; the platform is up | +| `Updating` | A passenger is swapping to a new version of their documents | +| `Recovering` | A passenger failed; attempting to restore their documents before re-checking | +| `Locked` | Building evacuated, doors bolted, no further admittance |
diff --git a/services/orchestrator/sm/src/lib.rs b/services/orchestrator/sm/src/lib.rs index b5277bb..628df58 100644 --- a/services/orchestrator/sm/src/lib.rs +++ b/services/orchestrator/sm/src/lib.rs
@@ -69,48 +69,129 @@ Passive, } +/// Recovery-failure classification: what the machine does once a required +/// component's restore attempts are **exhausted** (`retry_count` reaches +/// `max_retry`). Every verification or corruption failure enters +/// [`State::Recovering`] and is retried first, regardless of this +/// classification — CSA's "recover first" principle. This value is consulted +/// only after retries are exhausted. +/// +/// (The narrative design docs sometimes call the `Required` outcome "platform +/// halt" — same behavior, this is the type-level name.) +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum FailurePolicy { + /// Stop the boot sequence entirely: self-emits [`Event::RecoveryFailed`], + /// which drives the machine to [`State::Locked`]. + Required, + /// Hold this component in reset (added to `Rot.held`) and continue + /// booting the rest of the platform. + Isolable, + /// Hold this component **and** any component whose `depends_on` names it + /// (transitively), then continue booting the rest of the platform. + Cascading, +} + +/// Opaque recovery-region key supplied by the board at chain-build time. +/// Components sharing a `RegionId` are restored together: when any region +/// member enters [`State::Recovering`], the shell resolves and restores the +/// whole region. The core treats this as an equality key only and never +/// inspects membership itself. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct RegionId(u8); + +impl RegionId { + pub const fn new(id: u8) -> Self { + Self(id) + } + pub const fn get(self) -> u8 { + self.0 + } +} + /// Per-component attributes supplied by the board at chain-build time. /// -/// Two orthogonal axes: +/// Three orthogonal axes: /// - [`kind`](ComponentAttrs::kind): controls the iRoT gate (Active vs Passive). -/// - [`required`](ComponentAttrs::required): controls failure policy. -/// * `true` — verification failure triggers recovery and halts the chain walk. -/// * `false` — verification failure holds the component in reset and skips it; -/// the chain walk continues to the next component without recovery. +/// - [`failure_policy`](ComponentAttrs::failure_policy): controls what happens +/// once this component's recovery is exhausted. +/// - [`recovery_region`](ComponentAttrs::recovery_region) / +/// [`depends_on`](ComponentAttrs::depends_on): control restore grouping and +/// cascade-skip on exhaustion. /// -/// A `required: false` component is never released from reset on failure — running -/// untrusted firmware would break the trust invariant regardless of policy. +/// A component that fails verification is never released from reset — +/// running untrusted firmware would break the trust invariant regardless of +/// `failure_policy`. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct ComponentAttrs { pub kind: ComponentKind, - pub required: bool, + pub failure_policy: FailurePolicy, + pub recovery_region: RegionId, + pub depends_on: Option<ComponentId>, } impl ComponentAttrs { pub const fn active_required() -> Self { Self { kind: ComponentKind::Active, - required: true, + failure_policy: FailurePolicy::Required, + recovery_region: RegionId::new(0), + depends_on: None, } } pub const fn passive_required() -> Self { Self { kind: ComponentKind::Passive, - required: true, + failure_policy: FailurePolicy::Required, + recovery_region: RegionId::new(0), + depends_on: None, } } - pub const fn active_optional() -> Self { + pub const fn active_isolable() -> Self { Self { kind: ComponentKind::Active, - required: false, + failure_policy: FailurePolicy::Isolable, + recovery_region: RegionId::new(0), + depends_on: None, } } - pub const fn passive_optional() -> Self { + pub const fn passive_isolable() -> Self { Self { kind: ComponentKind::Passive, - required: false, + failure_policy: FailurePolicy::Isolable, + recovery_region: RegionId::new(0), + depends_on: None, } } + pub const fn active_cascading() -> Self { + Self { + kind: ComponentKind::Active, + failure_policy: FailurePolicy::Cascading, + recovery_region: RegionId::new(0), + depends_on: None, + } + } + pub const fn passive_cascading() -> Self { + Self { + kind: ComponentKind::Passive, + failure_policy: FailurePolicy::Cascading, + recovery_region: RegionId::new(0), + depends_on: None, + } + } + + /// Builder: assign a non-default recovery region (default is region `0`). + pub const fn with_region(mut self, region: RegionId) -> Self { + self.recovery_region = region; + self + } + + /// Builder: mark this component as cascade-held whenever `dependency` is + /// held. Only meaningful in combination with `FailurePolicy::Cascading` + /// on the *dependency*, not on this component. + pub const fn with_depends_on(mut self, dependency: ComponentId) -> Self { + self.depends_on = Some(dependency); + self + } } /// The result of the board's power-on checks, delivered inside [`Event::PowerGood`]. @@ -154,8 +235,9 @@ VerifyFirmware(ComponentId), ReleaseReset(ComponentId), /// Assert reset on a component that is already running — the inverse of - /// [`ReleaseReset`]. Emitted when an optional component is found corrupt at - /// runtime: the component is gated without triggering a recovery cycle. + /// [`ReleaseReset`]. Emitted when an `Isolable`/`Cascading` component is + /// found corrupt at runtime, or is held after recovery is exhausted: the + /// component is gated without triggering (or continuing) a recovery cycle. AssertReset(ComponentId), SignAttestation, AuthenticateUpdate, @@ -174,7 +256,7 @@ #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum State { PowerOnReset, - VerifyingPlatform, + PreSupervision, /// eRoT has released an `Active` component; waiting for its iRoT to finish /// local verification and signal [`Event::ComponentReady`]. AwaitingReady, @@ -184,10 +266,16 @@ Locked, } -/// Group state shared by the operational states. +/// Superstate entered on the eRoT's first component release and held until +/// [`State::Locked`]. Provides two platform-wide guarantees that must hold +/// across all four sub-states ([`State::AwaitingReady`], [`State::Ready`], +/// [`State::Updating`], [`State::Recovering`]): +/// +/// - Attestation challenges are always answered. +/// - Corruption of a required component always triggers recovery. #[derive(Debug)] pub enum Superstate<'sub> { - Operational(PhantomData<&'sub ()>), + SupervisingPlatform(PhantomData<&'sub ()>), } /// The effect buffer handed to every handler (statig's `Context`). @@ -221,6 +309,10 @@ pub struct Rot<const N: usize> { chain: heapless::Vec<(ComponentId, ComponentAttrs), N>, cursor: u8, + /// Components skipped because their recovery was exhausted under + /// `FailurePolicy::Isolable` or `Cascading`. Held in reset; not + /// re-verified on subsequent re-walks. Cleared on `Ready` entry. + held: heapless::Vec<ComponentId, N>, failed: Option<ComponentId>, retry_count: u8, max_retry: u8, @@ -234,12 +326,69 @@ Self { chain, cursor: 0, + held: heapless::Vec::new(), failed: None, retry_count: 0, max_retry, awaiting: None, } } + + /// Look up a component's attributes by id. `None` if the id is not in the + /// chain (should never happen for ids the core itself produced). + fn attrs_of(&self, id: ComponentId) -> Option<ComponentAttrs> { + self.chain.iter().find(|(cid, _)| *cid == id).map(|(_, a)| *a) + } + + fn is_held(&self, id: ComponentId) -> bool { + self.held.iter().any(|h| *h == id) + } + + /// Advance `cursor` from `start_idx` to the first component not in + /// `held`, emitting its `ReadFirmware`/`VerifyFirmware`. Returns `true` if + /// found. If the rest of the chain is exhausted or entirely held, sets + /// `cursor` to a past-the-end sentinel (`chain.len()`) and returns + /// `false` — the caller should treat that as "chain done". + fn advance_to_next_unheld(&mut self, ctx: &mut Sink, start_idx: usize) -> bool { + let mut idx = start_idx; + while let Some(&(id, _)) = self.chain.get(idx) { + if !self.is_held(id) { + // `idx < chain.len() <= N`; chain capacity is board-chosen and + // assumed to fit `u8`, matching the existing `cursor: u8` contract. + self.cursor = idx as u8; + ctx.emit(Effect::ReadFirmware(id)); + ctx.emit(Effect::VerifyFirmware(id)); + return true; + } + idx += 1; + } + self.cursor = self.chain.len() as u8; + false + } + + /// Hold `root` and cascade-hold every component whose `depends_on` + /// (transitively) names it. Emits `AssertReset` for each newly held + /// component, including `root` itself. + fn cascade_hold(&mut self, ctx: &mut Sink, root: ComponentId) { + if !self.is_held(root) { + ctx.emit(Effect::AssertReset(root)); + let _ = self.held.push(root); + } + let mut i = 0; + while let Some(&holder) = self.held.get(i) { + i += 1; + let mut newly_held: heapless::Vec<ComponentId, N> = heapless::Vec::new(); + for &(id, attrs) in self.chain.iter() { + if attrs.depends_on == Some(holder) && !self.is_held(id) { + let _ = newly_held.push(id); + } + } + for id in newly_held { + ctx.emit(Effect::AssertReset(id)); + let _ = self.held.push(id); + } + } + } } impl<const N: usize> IntoStateMachine for Rot<N> { @@ -258,7 +407,7 @@ match self { State::PowerOnReset => match event { Event::PowerGood(PowerOnResult::Provisioned) => { - Outcome::Transition(State::VerifyingPlatform) + Outcome::Transition(State::PreSupervision) } Event::PowerGood(PowerOnResult::Unprovisioned) => { Outcome::Transition(State::Locked) @@ -270,46 +419,29 @@ }, // Cursor walk via Outcome::Handled — a self-transition would reset cursor. - State::VerifyingPlatform => match event { + State::PreSupervision => match event { Event::VerificationPassed(id) => { ctx.emit(Effect::ReleaseReset(*id)); - let current_attrs = rot.chain[rot.cursor as usize].1; - let next_idx = (rot.cursor as usize) + 1; - if next_idx < rot.chain.len() { - let (next_id, _) = rot.chain[next_idx]; - rot.cursor += 1; - // Speculative: start next eRoT check while current Active iRoT boots. - ctx.emit(Effect::ReadFirmware(next_id)); - ctx.emit(Effect::VerifyFirmware(next_id)); - match current_attrs.kind { - ComponentKind::Active => { + let current_kind = rot.chain.get(rot.cursor as usize).map(|(_, a)| a.kind); + let next_idx = (rot.cursor as usize).saturating_add(1); + if rot.advance_to_next_unheld(ctx, next_idx) { + match current_kind { + Some(ComponentKind::Active) => { rot.awaiting = Some(*id); Outcome::Transition(State::AwaitingReady) } - ComponentKind::Passive => Outcome::Handled, + _ => Outcome::Handled, } } else { Outcome::Transition(State::Ready) } } Event::VerificationFailed(id) => { - let attrs = rot.chain[rot.cursor as usize].1; - if attrs.required { - rot.failed = Some(*id); - Outcome::Transition(State::Recovering) - } else { - // Optional: hold in reset, skip, advance walk. - let next_idx = (rot.cursor as usize) + 1; - rot.cursor += 1; - if next_idx < rot.chain.len() { - let (next_id, _) = rot.chain[next_idx]; - ctx.emit(Effect::ReadFirmware(next_id)); - ctx.emit(Effect::VerifyFirmware(next_id)); - Outcome::Handled - } else { - Outcome::Transition(State::Ready) - } - } + // Recovery is attempted first for every failure, regardless + // of the component's recovery-failure policy (CSA: recover + // first, classify only once retries are exhausted). + rot.failed = Some(*id); + Outcome::Transition(State::Recovering) } _ => Outcome::Super, }, @@ -320,8 +452,9 @@ return Outcome::Handled; // spurious / stale (INV9) } rot.awaiting = None; - // If cursor is past the end, the last component was skipped - // (optional failure) — nothing left to verify, we're done. + // If cursor is past the end, the eRoT side of the walk has + // already finished (chain done, or the remainder is held) — + // nothing left to verify, we're done. if (rot.cursor as usize) >= rot.chain.len() { Outcome::Transition(State::Ready) } else { @@ -330,40 +463,19 @@ } Event::VerificationPassed(id) => { ctx.emit(Effect::ReleaseReset(*id)); - let next_idx = (rot.cursor as usize) + 1; - if next_idx < rot.chain.len() { - let (next_id, _) = rot.chain[next_idx]; - rot.cursor += 1; - ctx.emit(Effect::ReadFirmware(next_id)); - ctx.emit(Effect::VerifyFirmware(next_id)); + let next_idx = (rot.cursor as usize).saturating_add(1); + if rot.advance_to_next_unheld(ctx, next_idx) { Outcome::Handled } else { Outcome::Transition(State::Ready) } } Event::VerificationFailed(id) => { - let attrs = rot.chain[rot.cursor as usize].1; - if attrs.required { - rot.failed = Some(*id); - rot.awaiting = None; - Outcome::Transition(State::Recovering) - } else { - // Optional: hold in reset, skip, advance walk. - let next_idx = (rot.cursor as usize) + 1; - rot.cursor += 1; - if next_idx < rot.chain.len() { - let (next_id, _) = rot.chain[next_idx]; - ctx.emit(Effect::ReadFirmware(next_id)); - ctx.emit(Effect::VerifyFirmware(next_id)); - Outcome::Handled - } else if rot.awaiting.is_none() { - // No iRoT gate pending — done. - Outcome::Transition(State::Ready) - } else { - // Still waiting for ComponentReady; it will fire Ready. - Outcome::Handled - } - } + // Recovery is attempted first for every failure, regardless + // of the component's recovery-failure policy. + rot.failed = Some(*id); + rot.awaiting = None; + Outcome::Transition(State::Recovering) } _ => Outcome::Super, }, @@ -388,11 +500,33 @@ State::Recovering => match event { Event::Restored(_) => { rot.retry_count = rot.retry_count.saturating_add(1); - if rot.retry_count >= rot.max_retry { - ctx.emit(Effect::Emit(Event::RecoveryFailed)); - Outcome::Handled + if rot.retry_count < rot.max_retry { + Outcome::Transition(State::PreSupervision) } else { - Outcome::Transition(State::VerifyingPlatform) + // Retries exhausted: consult the recovery-failure policy. + let classification = rot.failed.and_then(|id| { + rot.attrs_of(id).map(|attrs| (id, attrs.failure_policy)) + }); + match classification { + Some((id, FailurePolicy::Isolable)) => { + ctx.emit(Effect::AssertReset(id)); + let _ = rot.held.push(id); + rot.failed = None; + rot.retry_count = 0; + Outcome::Transition(State::PreSupervision) + } + Some((id, FailurePolicy::Cascading)) => { + rot.cascade_hold(ctx, id); + rot.failed = None; + rot.retry_count = 0; + Outcome::Transition(State::PreSupervision) + } + // `Required` (or an unknown/missing id — safe default): halt. + _ => { + ctx.emit(Effect::Emit(Event::RecoveryFailed)); + Outcome::Handled + } + } } } Event::RecoveryFailed => Outcome::Transition(State::Locked), @@ -405,13 +539,9 @@ fn call_entry_action(&mut self, rot: &mut Rot<N>, ctx: &mut Sink) { match self { - State::VerifyingPlatform => { - rot.cursor = 0; + State::PreSupervision => { rot.awaiting = None; - if let Some(&(first_id, _)) = rot.chain.first() { - ctx.emit(Effect::ReadFirmware(first_id)); - ctx.emit(Effect::VerifyFirmware(first_id)); - } + let _ = rot.advance_to_next_unheld(ctx, 0); } State::Updating => { ctx.emit(Effect::AuthenticateUpdate); @@ -427,6 +557,8 @@ } State::Ready => { rot.retry_count = 0; + rot.held.clear(); + rot.failed = None; } _ => {} } @@ -435,7 +567,7 @@ fn superstate(&mut self) -> Option<Superstate<'_>> { match self { State::Ready | State::Updating | State::Recovering | State::AwaitingReady => { - Some(Superstate::Operational(PhantomData)) + Some(Superstate::SupervisingPlatform(PhantomData)) } _ => None, } @@ -445,28 +577,27 @@ impl<const N: usize> StatigSuperstate<Rot<N>> for Superstate<'_> { fn call_handler(&mut self, rot: &mut Rot<N>, event: &Event, ctx: &mut Sink) -> Outcome<State> { match self { - Superstate::Operational(_) => match event { + Superstate::SupervisingPlatform(_) => match event { Event::AttestationChallenge => { ctx.emit(Effect::SignAttestation); Outcome::Handled } Event::CorruptionDetected(id) => { // Respect the per-component policy encoded at chain-build time. - // required: true → recover (halt chain, restore, re-walk) - // required: false → ignore corruption; component stays running - // but is not considered trusted by the core. + // `FailurePolicy::Required` → recover (halt chain, restore, re-walk) + // `Isolable` / `Cascading` → gate the component; it stays running + // but is not considered trusted by the core, and no recovery + // episode is started (it is already known to be skippable). let required = rot - .chain - .iter() - .find(|(cid, _)| cid == id) - .map(|(_, attrs)| attrs.required) + .attrs_of(*id) + .map(|attrs| matches!(attrs.failure_policy, FailurePolicy::Required)) .unwrap_or(true); // unknown id: treat as required (safe default) if required { rot.failed = Some(*id); Outcome::Transition(State::Recovering) } else { - // Optional: gate the component (put it back in reset) but - // do not halt the chain or trigger recovery. + // Isolable/Cascading: gate the component (put it back in + // reset) but do not halt the chain or trigger recovery. ctx.emit(Effect::AssertReset(*id)); Outcome::Handled } @@ -635,7 +766,7 @@ } /// INV11: SelfVerificationFailed latches immediately without entering - /// VerifyingPlatform. + /// PreSupervision. #[test] fn self_verification_failure_latches_immediately() { let (effects, state) = drive( @@ -646,9 +777,9 @@ assert_eq!(state, State::Locked); } - /// INV6: AttestationChallenge is answerable from every Operational state. + /// INV6: AttestationChallenge is answerable from every SupervisingPlatform state. #[test] - fn attestation_shared_across_operational_states() { + fn attestation_shared_across_supervising_platform_states() { let (effects, state) = drive( passive_required(&[C0]), &[ @@ -718,7 +849,7 @@ tail, &[Effect::ReadFirmware(C0), Effect::VerifyFirmware(C0)] ); - assert_eq!(state, State::VerifyingPlatform); + assert_eq!(state, State::PreSupervision); } /// INV7 (feedback-as-data): after MAX_RETRY restores the core self-emits @@ -884,65 +1015,72 @@ assert_eq!(effects.last(), Some(&Effect::SignAttestation)); } - /// Optional component: VerificationFailed skips it (held in reset), chain - /// continues to Ready. + /// Isolable component: every `VerificationFailed` is retried through a full + /// recovery episode first; only once retries are exhausted does the + /// component get held in reset and the walk continue to `Ready`. #[test] - fn optional_component_failure_skips_and_continues() { + fn isolable_component_exhausts_recovery_then_skips() { + let mut script = std::vec![BOOT, Event::VerificationPassed(C0)]; + for _ in 0..MAX_RETRY { + script.push(Event::VerificationFailed(C1)); + script.push(Event::Restored(C1)); + script.push(Event::VerificationPassed(C0)); + } let (effects, state) = drive( chain(&[ (C0, ComponentAttrs::passive_required()), - (C1, ComponentAttrs::passive_optional()), + (C1, ComponentAttrs::passive_isolable()), ]), - &[ - BOOT, - Event::VerificationPassed(C0), - Event::VerificationFailed(C1), - ], + &script, ); assert_eq!(state, State::Ready); // C1 must never be released. assert!(!effects.contains(&Effect::ReleaseReset(C1))); - // No recovery triggered. - assert!(!effects.contains(&Effect::RestoreGoldenImage(C1))); + // Recovery IS attempted before C1 is classified and held. + assert!(effects.contains(&Effect::RestoreGoldenImage(C1))); + assert!(effects.contains(&Effect::AssertReset(C1))); assert!(!effects.contains(&Effect::LatchLockdown)); } - /// Optional Active component failure in AwaitingReady: skipped, held in - /// reset, chain reaches Ready once ComponentReady clears awaiting. + /// Isolable Active component failure in AwaitingReady: retried through a + /// full recovery episode, then held once exhausted; the walk still + /// reaches Ready once the remaining chain (past the held component) drains. #[test] - fn optional_active_failure_in_awaiting_ready_skips() { - // C0 Active required, C1 Active optional. + fn isolable_active_component_exhausted_in_awaiting_ready_skips() { + // C0 Active required, C1 Active isolable. + let mut script = std::vec![BOOT, Event::VerificationPassed(C0)]; // → AwaitingReady; spec ReadFirmware(C1) + for _ in 0..MAX_RETRY { + script.push(Event::VerificationFailed(C1)); + script.push(Event::Restored(C1)); + script.push(Event::VerificationPassed(C0)); // re-walk restarts at C0 each episode + } let (effects, state) = drive( chain(&[ (C0, ComponentAttrs::active_required()), - (C1, ComponentAttrs::active_optional()), + (C1, ComponentAttrs::active_isolable()), ]), - &[ - BOOT, - Event::VerificationPassed(C0), // → AwaitingReady; spec ReadFirmware(C1) - Event::VerificationFailed(C1), // optional → skip C1 - Event::ComponentReady(C0), // iRoT gate clears; cursor past end → Ready - ], + &script, ); assert_eq!(state, State::Ready); assert!(!effects.contains(&Effect::ReleaseReset(C1))); - assert!(!effects.contains(&Effect::RestoreGoldenImage(C1))); + assert!(effects.contains(&Effect::RestoreGoldenImage(C1))); + assert!(effects.contains(&Effect::AssertReset(C1))); } - /// Runtime corruption of a `required: false` component gates the component + /// Runtime corruption of an `Isolable` component gates the component /// (AssertReset) but does not trigger recovery — the machine stays in Ready. #[test] - fn optional_runtime_corruption_is_ignored() { + fn isolable_runtime_corruption_is_ignored() { let (effects, state) = drive( chain(&[ (C0, ComponentAttrs::passive_required()), - (C1, ComponentAttrs::passive_optional()), + (C1, ComponentAttrs::passive_isolable()), ]), &[ BOOT, Event::VerificationPassed(C0), Event::VerificationPassed(C1), - Event::CorruptionDetected(C1), // optional → gate, no recovery + Event::CorruptionDetected(C1), // Isolable → gate, no recovery ], ); assert_eq!(state, State::Ready); @@ -951,14 +1089,14 @@ assert!(!effects.contains(&Effect::LatchLockdown)); } - /// Runtime corruption of a `required: true` component still triggers + /// Runtime corruption of a `Required` component still triggers /// recovery as before. #[test] fn required_runtime_corruption_triggers_recovery() { let (effects, state) = drive( chain(&[ (C0, ComponentAttrs::passive_required()), - (C1, ComponentAttrs::passive_optional()), + (C1, ComponentAttrs::passive_isolable()), ]), &[ BOOT, @@ -1026,7 +1164,7 @@ } /// CorruptionDetected while in AwaitingReady (required component) → - /// Recovering via the Operational superstate handler. + /// Recovering via the SupervisingPlatform superstate handler. #[test] fn corruption_in_awaiting_ready_triggers_recovery() { let (effects, state) = drive( @@ -1045,7 +1183,7 @@ } /// CorruptionDetected while in Updating (required component) → Recovering - /// via the Operational superstate handler. + /// via the SupervisingPlatform superstate handler. #[test] fn corruption_in_updating_triggers_recovery() { let (effects, state) = drive( @@ -1113,25 +1251,30 @@ ); } - /// An optional component at the head of the chain can fail and the walk - /// continues to the remaining required components. + /// An Isolable component at the head of the chain exhausts its recovery + /// retries, gets held, and the walk continues to the remaining required + /// components. #[test] - fn optional_first_component_skipped_walk_continues() { + fn isolable_first_component_exhausts_then_walk_continues() { + let mut script = std::vec![BOOT]; + for _ in 0..MAX_RETRY { + script.push(Event::VerificationFailed(C0)); + script.push(Event::Restored(C0)); + } + script.push(Event::VerificationPassed(C1)); let (effects, state) = drive( chain(&[ - (C0, ComponentAttrs::passive_optional()), + (C0, ComponentAttrs::passive_isolable()), (C1, ComponentAttrs::passive_required()), ]), - &[ - BOOT, - Event::VerificationFailed(C0), // optional → skip C0 - Event::VerificationPassed(C1), - ], + &script, ); assert_eq!(state, State::Ready); assert!(!effects.contains(&Effect::ReleaseReset(C0))); assert!(effects.contains(&Effect::ReleaseReset(C1))); - assert!(!effects.contains(&Effect::RestoreGoldenImage(C0))); + // Recovery IS attempted before C0 is classified and held. + assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + assert!(effects.contains(&Effect::AssertReset(C0))); } /// The speculative read emits ReleaseReset · ReadFirmware · VerifyFirmware @@ -1170,7 +1313,7 @@ /// A chain with a single Active component goes directly to Ready on /// VerificationPassed — no AwaitingReady, no ComponentReady required. - /// This exercises the `chain done` branch of VerifyingPlatform for an + /// This exercises the `chain done` branch of PreSupervision for an /// Active component (distinct from the multi-component Active path which /// transitions to AwaitingReady). #[test]