re-walk of the chain explained
diff --git a/docs/src/design/orchestrator/orchestrator-machine.md b/docs/src/design/orchestrator/orchestrator-machine.md index dc679b4..0671828 100644 --- a/docs/src/design/orchestrator/orchestrator-machine.md +++ b/docs/src/design/orchestrator/orchestrator-machine.md
@@ -188,6 +188,18 @@ produces the event internally, the orchestrator intercepts and re-dispatches it before returning, and the decision is visible in the effect trace. +**Why re-walk from `cursor = 0`?** After restoring a component the machine +re-enters `VerifyingPlatform` 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" +(NIST SP 800-193) — requires that trust be re-established end-to-end before the +platform is considered healthy again. The CSA document does not prescribe the +exact recovery sequencing, but the re-walk implements the spirit of that +principle. Optional components that fail during the re-walk are skipped (held in +reset) as during initial boot; they are re-released only if they pass +`VerificationPassed` in the new walk. + --- ### `Locked` @@ -207,8 +219,9 @@ | Event | Effects | Next state | |---|---|---| | `AttestationChallenge` | `SignAttestation` | `Handled` (no transition — INV6) | -| `CorruptionDetected(id)` | — | `Recovering` (failed = Some(id) — INV5) | -| anything else | — | `Outcome::Super` (discarded) | +| `CorruptionDetected(id)` | `attrs.required == true` | — | `Recovering` (failed = Some(id) — INV5) | +| `CorruptionDetected(id)` | `attrs.required == false` | `AssertReset(id)` | `Handled` (component gated; machine stays in current state) | +| anything else | — | — | `Outcome::Super` (discarded) | ---
diff --git a/services/orchestrator/sm/src/lib.rs b/services/orchestrator/sm/src/lib.rs index 9646e98..59388f9 100644 --- a/services/orchestrator/sm/src/lib.rs +++ b/services/orchestrator/sm/src/lib.rs
@@ -153,6 +153,10 @@ ReadFirmware(ComponentId), 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. + AssertReset(ComponentId), SignAttestation, AuthenticateUpdate, StageUpdate, @@ -447,8 +451,25 @@ Outcome::Handled } Event::CorruptionDetected(id) => { - rot.failed = Some(*id); - Outcome::Transition(State::Recovering) + // 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. + let required = rot + .chain + .iter() + .find(|(cid, _)| cid == id) + .map(|(_, attrs)| attrs.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. + ctx.emit(Effect::AssertReset(*id)); + Outcome::Handled + } } _ => Outcome::Super, }, @@ -907,4 +928,46 @@ assert!(!effects.contains(&Effect::ReleaseReset(C1))); assert!(!effects.contains(&Effect::RestoreGoldenImage(C1))); } + + /// Runtime corruption of a `required: false` component gates the component + /// (AssertReset) but does not trigger recovery — the machine stays in Ready. + #[test] + fn optional_runtime_corruption_is_ignored() { + let (effects, state) = drive( + chain(&[ + (C0, ComponentAttrs::passive_required()), + (C1, ComponentAttrs::passive_optional()), + ]), + &[ + BOOT, + Event::VerificationPassed(C0), + Event::VerificationPassed(C1), + Event::CorruptionDetected(C1), // optional → gate, no recovery + ], + ); + assert_eq!(state, State::Ready); + assert!(effects.contains(&Effect::AssertReset(C1))); + assert!(!effects.contains(&Effect::RestoreGoldenImage(C1))); + assert!(!effects.contains(&Effect::LatchLockdown)); + } + + /// Runtime corruption of a `required: true` 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()), + ]), + &[ + BOOT, + Event::VerificationPassed(C0), + Event::VerificationPassed(C1), + Event::CorruptionDetected(C0), // required → Recovering + ], + ); + assert_eq!(state, State::Recovering); + assert!(effects.contains(&Effect::RestoreGoldenImage(C0))); + } }