Orchestrator spec and reference impl
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index bf6ff0e..b7c13b5 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md
@@ -32,3 +32,6 @@ * [Design](./design/README.md) * [Pigweed Integration Overview](./design/pigweed-overview.md) * [pw_kernel IPC](./design/pw-kernel-ipc.md) + * [Orchestrator](./design/orchestrator/orchestrator-overview.md) + * [Verification Model](./design/orchestrator/orchestrator-model.md) + * [State Machine](./design/orchestrator/orchestrator-machine.md)
diff --git a/docs/src/design/README.md b/docs/src/design/README.md index f7c767f..5326ea9 100644 --- a/docs/src/design/README.md +++ b/docs/src/design/README.md
@@ -13,3 +13,7 @@ - [**pw_kernel IPC**](./pw-kernel-ipc.md): How to declare and use channel objects to communicate between two `pw_kernel` userspace processes. Worked example lives at `target/veer/ipc/`. +- [**Orchestrator**](./orchestrator/orchestrator-overview.md): The eRoT boot-sequence state + machine (`services/orchestrator/sm`). Covers the two-tier firmware + verification model (`ComponentAttrs`, eRoT gate, iRoT gate), the + verification boundary, and the full state/transition table.
diff --git a/docs/src/design/orchestrator/orchestrator-machine.md b/docs/src/design/orchestrator/orchestrator-machine.md new file mode 100644 index 0000000..dc679b4 --- /dev/null +++ b/docs/src/design/orchestrator/orchestrator-machine.md
@@ -0,0 +1,227 @@ +# State Machine + +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. + +```mermaid +stateDiagram-v2 + [*] --> PowerOnReset + + PowerOnReset --> VerifyingPlatform : PowerGood(Provisioned) + PowerOnReset --> Locked : PowerGood(Unprovisioned) + PowerOnReset --> Locked : PowerGood(SelfVerificationFailed) + + VerifyingPlatform --> VerifyingPlatform : VerificationPassed [more, Passive]\n/ ReleaseReset · ReadFirmware · VerifyFirmware + VerifyingPlatform --> AwaitingReady : VerificationPassed [more, Active]\n/ ReleaseReset · ReadFirmware · VerifyFirmware + VerifyingPlatform --> Ready : VerificationPassed [chain done]\n/ ReleaseReset + VerifyingPlatform --> VerifyingPlatform : VerificationFailed [optional]\n(skip — held in reset) + VerifyingPlatform --> Recovering : VerificationFailed [required]\n/ RestoreGoldenImage + + AwaitingReady --> AwaitingReady : VerificationPassed [more]\n/ ReleaseReset · ReadFirmware · VerifyFirmware + AwaitingReady --> Ready : ComponentReady [chain done or cursor past end] + AwaitingReady --> AwaitingReady : ComponentReady [more] + AwaitingReady --> AwaitingReady : VerificationFailed [optional, iRoT pending] + AwaitingReady --> Ready : VerificationFailed [optional, no iRoT pending, chain done] + AwaitingReady --> Recovering : VerificationFailed [required]\n/ RestoreGoldenImage + + state Operational { + [*] --> Ready + Ready --> Updating : UpdateRequest\n/ AuthenticateUpdate · StageUpdate + Updating --> Ready : UpdateVerified / ActivateUpdate + Updating --> Ready : UpdateRejected / DiscardStaged + Ready --> Recovering : CorruptionDetected\n/ RestoreGoldenImage + Updating --> Recovering : CorruptionDetected\n/ RestoreGoldenImage + AwaitingReady --> Recovering : CorruptionDetected\n/ RestoreGoldenImage + } + + Recovering --> VerifyingPlatform : Restored [retry < max_retry] + Recovering --> Locked : Restored [retry ≥ max_retry]\n(self-emits RecoveryFailed)\n/ LatchLockdown + Locked --> Locked : (terminal — all events ignored) +``` + +--- + +## Shared storage — `Rot<N>` + +Every handler receives a `&mut Rot<N>` alongside the event and the `Sink`. This +struct is `statig`'s *shared storage*: a single allocation that persists across +events and is visible to every state and superstate. States carry no data; all +mutable state lives here. + +| 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 on optional `VerificationFailed`) via `Outcome::Handled`. | +| `failed` | `Option<ComponentId>` | The component that triggered the current recovery episode; `None` while healthy. Set on required `VerificationFailed` or `CorruptionDetected`. | +| `retry_count` | `u8` | Number of consecutive failed restore attempts. Cleared to 0 in `Ready`'s entry action — consecutive only (INV7). | +| `max_retry` | `u8` | Shell-chosen ceiling for `retry_count`. When `retry_count >= max_retry` the machine self-emits `RecoveryFailed` instead of re-walking the chain. | +| `awaiting` | `Option<ComponentId>` | The `Active` component whose iRoT readiness is currently outstanding. `Some` only while in `AwaitingReady`; `None` everywhere else (INV9). | + +The effect buffer is deliberately **absent** from `Rot`. Effects flow through the +`Sink` (the `statig` context), which the orchestrator creates fresh for every +event and drains afterward. + +--- + +## Context — `Sink` + +The only thing a handler can do to the outside world is call `ctx.emit(effect)`. +`Sink` is an append-only `heapless::Vec<Effect, EFFECT_CAP>`. It can push; it +cannot pull, read, or do I/O. The orchestrator owns a fresh `Sink` per dispatch +and reads the effects out after `handle_with_context` returns. + +--- + +## States + +### `PowerOnReset` + +The machine's initial state. The first event is always `PowerGood(PowerOnResult)`. + +**Entry action**: none. + +| Event | Guard | Effects | Next state | +|---|---|---|---| +| `PowerGood(Provisioned)` | — | — | `VerifyingPlatform` | +| `PowerGood(Unprovisioned)` | — | — | `Locked` | +| `PowerGood(SelfVerificationFailed)` | — | — | `Locked` | +| anything else | — | — | `Outcome::Super` (top level — discarded) | + +--- + +### `VerifyingPlatform` + +Walks the trust chain component-by-component. The cursor advances on each +`VerificationPassed` (or optional `VerificationFailed`) using `Outcome::Handled` +rather than a self-transition — a self-transition would re-run the entry action +and reset the cursor. + +**Entry action**: reset `cursor` to 0, `awaiting` to `None`, emit +`ReadFirmware(chain[0])` + `VerifyFirmware(chain[0])`. + +| Event | Guard | Effects | Next state | +|---|---|---|---| +| `VerificationPassed(id)` | more, current `Passive` | `ReleaseReset` · `ReadFirmware(next)` · `VerifyFirmware(next)` | `Handled` (cursor ++) | +| `VerificationPassed(id)` | more, current `Active` | `ReleaseReset` · `ReadFirmware(next)` · `VerifyFirmware(next)` | `AwaitingReady` (awaiting = Some(id)) | +| `VerificationPassed(id)` | chain done | `ReleaseReset(id)` | `Ready` | +| `VerificationFailed(id)` | `attrs.required` | — | `Recovering` (failed = Some(id)) | +| `VerificationFailed(id)` | `!attrs.required` | — | `Handled` (skip; cursor ++; if chain done → `Ready`) | +| anything else | — | — | `Outcome::Super` → `Operational` | + +--- + +### `AwaitingReady` + +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 +transition. + +**Entry action**: none. + +| Event | Guard | Effects | Next state | +|---|---|---|---| +| `ComponentReady(id)` | `id != awaiting` | — | `Handled` (stale/spurious — ignore, INV9) | +| `ComponentReady(id)` | `id == awaiting`, cursor in bounds | — | `Handled` (clear awaiting) | +| `ComponentReady(id)` | `id == awaiting`, cursor past end | — | `Ready` | +| `VerificationPassed(id)` | more | `ReleaseReset` · `ReadFirmware(next)` · `VerifyFirmware(next)` | `Handled` (cursor ++) | +| `VerificationPassed(id)` | chain done | `ReleaseReset(id)` | `Ready` | +| `VerificationFailed(id)` | `attrs.required` | — | `Recovering` (failed = Some(id), awaiting = None) | +| `VerificationFailed(id)` | `!attrs.required`, iRoT pending | — | `Handled` (skip; cursor ++) | +| `VerificationFailed(id)` | `!attrs.required`, no iRoT pending, chain done | — | `Ready` | +| anything else | — | — | `Outcome::Super` → `Operational` | + +`ComponentReady` and `VerificationPassed` are independent and may arrive in +either order. Both must be seen before the walk advances. `awaiting` tracks +whether `ComponentReady` is still outstanding; the state itself tracks whether +`VerificationPassed` is still outstanding. + +--- + +### `Ready` + +Normal operational state: the full chain has been verified, all required +components are released, and the machine handles attestation, update requests, +and corruption events. + +**Entry action**: reset `retry_count` to 0 (makes the cap count *consecutive* +failures — INV7). + +| Event | Guard | Effects | Next state | +|---|---|---|---| +| `UpdateRequest` | — | — | `Updating` | +| anything else | — | — | `Outcome::Super` → `Operational` | + +--- + +### `Updating` + +An update is in progress. + +**Entry action**: emit `AuthenticateUpdate` + `StageUpdate`. + +| Event | Guard | Effects | Next state | +|---|---|---|---| +| `UpdateVerified` | — | `ActivateUpdate` | `Ready` | +| `UpdateRejected` | — | `DiscardStaged` | `Ready` (rejected update is not corruption — INV4) | +| anything else | — | — | `Outcome::Super` → `Operational` | + +--- + +### `Recovering` + +The machine is attempting to restore a corrupted or rejected component. + +**Entry action**: emit `RestoreGoldenImage(rot.failed)` — exactly the named +component, not the whole chain (INV5). + +| Event | Guard | Effects | Next state | +|---|---|---|---| +| `Restored(_)` | `retry_count + 1 < max_retry` | — | `VerifyingPlatform` (re-walk from top) | +| `Restored(_)` | `retry_count + 1 >= max_retry` | `Effect::Emit(RecoveryFailed)` | `Handled` (orchestrator queues `RecoveryFailed` next — INV7) | +| `RecoveryFailed` | — | — | `Locked` | +| anything else | — | — | `Outcome::Super` → `Operational` | + +`Effect::Emit(RecoveryFailed)` is the *feedback-as-data* mechanism: the core +produces the event internally, the orchestrator intercepts and re-dispatches it +before returning, and the decision is visible in the effect trace. + +--- + +### `Locked` + +Terminal state. All events are discarded. + +**Entry action**: emit `LatchLockdown` — instruct the shell to hold all +components in reset permanently. + +--- + +## Superstate — `Operational` + +`Ready`, `Updating`, `Recovering`, and `AwaitingReady` share this superstate. +When a leaf state returns `Outcome::Super`, `statig` calls the superstate handler. + +| Event | Effects | Next state | +|---|---|---| +| `AttestationChallenge` | `SignAttestation` | `Handled` (no transition — INV6) | +| `CorruptionDetected(id)` | — | `Recovering` (failed = Some(id) — INV5) | +| anything else | — | `Outcome::Super` (discarded) | + +--- + +## `statig` integration + +The machine uses `statig` 0.4.1 with hand-written trait impls — no proc-macros. + +| Trait | Implemented by | Role | +|---|---|---| +| `IntoStateMachine` | `Rot<N>` | Declares associated types and `initial() -> State`. | +| `StatigState<Rot<N>>` | `State` | `call_handler`, `call_entry_action`, `superstate`. | +| `StatigSuperstate<Rot<N>>` | `Superstate<'_>` | `call_handler` for events that fell through from a leaf state. | + +`initial()` is a `fn() -> State` with no `self`, so the machine always starts +in `PowerOnReset`. The shell-supplied `PowerGood(PowerOnResult)` event is the +first real branching point.
diff --git a/docs/src/design/orchestrator/orchestrator-model.md b/docs/src/design/orchestrator/orchestrator-model.md new file mode 100644 index 0000000..03b3be5 --- /dev/null +++ b/docs/src/design/orchestrator/orchestrator-model.md
@@ -0,0 +1,244 @@ +# Verification Model + +This document describes how platform firmware verification is modelled in the +orchestrator state machine (`services/orchestrator/sm`): the problem it solves, +the types that carry the domain, the states that sequence the work, and the +boundary between the pure core and the platform shell that executes it. + +--- + +## 1. The Problem + +The eRoT (external Root of Trust — the discrete RoT device, e.g. on a DC-SCM) +must verify every platform component's firmware before releasing it from reset. +Two independent mechanisms do this: + +1. **eRoT-side**: the eRoT reads the component's firmware image from the SPI + flash it controls, verifies the signature and SVN against a Reference + Integrity Manifest (RIM/PFM), and only then releases the component from reset. + +2. **iRoT-side**: components with an integrated Root of Trust (e.g. a BMC SoC + or CPU with Caliptra) perform their own independent local self-verification + after reset. The eRoT must wait for this local check to complete before + treating the component as trusted and advancing to the next one in the chain. + +Components that have no integrated iRoT (e.g. a NIC) rely solely on the +eRoT-side check. The eRoT can advance immediately after releasing them. + +This two-tier model — eRoT gate + optional iRoT gate — is the core problem the +verification states solve. It is grounded directly in the CSA architecture boot +sequence: "The eRoT and the iRoT provide complementary guarantees: the eRoT +controls whether a component is released from reset; the iRoT controls whether +the component's own firmware executes." + +The **verification boundary** is the interface between the platform shell and the +pure state-machine core. Only verdicts cross it: the shell performs all +cryptographic work (reading flash, checking signatures and SVN) and then signals +the outcome via an event. The core never sees raw firmware data or hash values — +it only acts on the resulting `VerificationPassed` or `VerificationFailed`. This +keeps the core free of I/O and testable without hardware. + +--- + +## 2. Domain Types + +### `ComponentKind` + +Classifies the iRoT gate for a component. Supplied by the shell at chain-build +time; the core never derives it. + +``` +Active — has an integrated iRoT (e.g. Caliptra); both eRoT and iRoT checks apply +Passive — no integrated iRoT; only the eRoT check applies +``` + +### `ComponentAttrs` + +Per-component attributes that combine two orthogonal axes: + +```rust +pub struct ComponentAttrs { + pub kind: ComponentKind, // iRoT gate: Active | Passive + pub required: bool, // failure policy: true = recover, false = skip +} +``` + +| `kind` | `required` | `VerificationFailed` behaviour | +|---|---|---| +| Active / Passive | `true` | → `Recovering`; component held in reset; chain walk halts | +| Active / Passive | `false` | component held in reset; cursor advances; chain walk continues | + +A `required: false` component that fails verification is **never** released from +reset — releasing a component whose firmware failed verification would mean +running untrusted code, which breaks the trust invariant regardless of the +recovery policy. + +Convenience constructors: `ComponentAttrs::active_required()`, +`passive_required()`, `active_optional()`, `passive_optional()`. + +### `ComponentId` + +An opaque `u8` the core carries and equality-compares but never inspects. The +shell decides which id maps to which physical device. + +### Events that cross the verification boundary + +| Event | Direction | Meaning | +|---|---|---| +| `VerificationPassed(ComponentId)` | shell → core | The eRoT-side check passed: signature and SVN valid. | +| `VerificationFailed(ComponentId)` | shell → core | The eRoT-side check failed: image rejected. | +| `ComponentReady(ComponentId)` | shell → core | An `Active` component's integrated iRoT has finished its local verification and the component is operational (e.g. MCTP channel established). | + +### Effects the core emits for verification work + +| Effect | Meaning | +|---|---| +| `ReadFirmware(ComponentId)` | Ask the shell to read the component's firmware image from eRoT-controlled flash. | +| `VerifyFirmware(ComponentId)` | Ask the shell to verify the image against the RIM/PFM. The shell responds with `VerificationPassed` or `VerificationFailed`. | +| `ReleaseReset(ComponentId)` | Release the named component from reset. Emitted only after `VerificationPassed`. | + +These are descriptions, not actions. The shell's `Platform::execute` carries +them out; the core never touches hardware. + +--- + +## 3. Sequencing by `ComponentAttrs` + +### Active → Passive (happy path) + +``` +chain: [(C0, {Active, required}), (C1, {Passive, required})] + +VerifyingPlatform (entry): + emit ReadFirmware(C0) + emit VerifyFirmware(C0) + +VerificationPassed(C0): ← eRoT check done + emit ReleaseReset(C0) + emit ReadFirmware(C1) ← speculative eRoT check of next + emit VerifyFirmware(C1) + cursor = 1, awaiting = Some(C0) + → AwaitingReady + +ComponentReady(C0): ← C0's iRoT done + awaiting = None + Handled (stay in AwaitingReady, wait for VerificationPassed(C1)) + +VerificationPassed(C1): ← speculative eRoT check resolved + emit ReleaseReset(C1) + chain done → Ready +``` + +### Optional component failure (skip, continue) + +``` +chain: [(BMC, {Active, required}), (NIC, {Passive, optional})] + +VerificationPassed(BMC): + emit ReleaseReset(BMC) + emit ReadFirmware(NIC) + emit VerifyFirmware(NIC) + awaiting = Some(BMC) → AwaitingReady + +VerificationFailed(NIC): ← NIC firmware rejected; optional → skip + NIC stays held in reset + cursor advances past end + awaiting is still Some(BMC) → stay in AwaitingReady + +ComponentReady(BMC): ← BMC iRoT done; cursor past end → Ready + awaiting = None → Ready +``` + +### Concrete example: BMC (Active, required) → HOST (Active, required) → NIC (Passive, optional) + +This matches the CSA single-node boot sequence. + +``` +chain: [(BMC, {Active, required}), (HOST, {Active, required}), (NIC, {Passive, optional})] + +VerifyingPlatform (entry): + emit ReadFirmware(BMC) + emit VerifyFirmware(BMC) ← eRoT reads and checks BMC firmware from SPI flash + +VerificationPassed(BMC): ← eRoT: BMC firmware signature + SVN valid + emit ReleaseReset(BMC) ← eRoT releases BMC from reset; Caliptra iRoT runs + emit ReadFirmware(HOST) ← speculative: eRoT starts HOST firmware check + emit VerifyFirmware(HOST) while BMC's Caliptra iRoT is still booting + cursor = 1, awaiting = Some(BMC) + → AwaitingReady + +ComponentReady(BMC): ← BMC Caliptra iRoT done; MCTP channel up + awaiting = None + Handled (still in AwaitingReady, waiting for VerificationPassed(HOST)) + +VerificationPassed(HOST): ← eRoT: HOST firmware signature + SVN valid + emit ReleaseReset(HOST) ← eRoT releases HOST from reset; Caliptra iRoT runs + emit ReadFirmware(NIC) ← speculative: eRoT starts NIC firmware check + emit VerifyFirmware(NIC) while HOST's Caliptra iRoT is still booting + cursor = 2 + Handled (stay in AwaitingReady — still waiting on ComponentReady(HOST) and/or NIC result) + +ComponentReady(HOST): ← HOST Caliptra iRoT done; BIOS/UEFI executing + awaiting = None + Handled + +VerificationPassed(NIC): ← eRoT: NIC firmware valid (Passive — no iRoT gate) + emit ReleaseReset(NIC) + chain done → Ready +``` + +If NIC fails verification instead: +``` +VerificationFailed(NIC): ← NIC optional → skip; NIC stays held in reset + cursor = 3 (past end) + awaiting = None (already cleared by ComponentReady(HOST)) + → Ready +``` + +--- + +## 4. The Speculative Read Pattern + +When an `Active` component passes eRoT verification the core does three things +in the same handler, before transitioning to `AwaitingReady`: + +``` +emit ReleaseReset(current) +emit ReadFirmware(next) ← speculative: next eRoT check starts immediately +emit VerifyFirmware(next) ← while current's iRoT is still booting +cursor += 1 +awaiting = Some(current) +→ Transition(AwaitingReady) +``` + +This overlaps the integrated iRoT boot time of the current component with the +eRoT firmware read of the next. The two checks are independent (different +hardware paths), so the overlap is safe. + +--- + +## 5. The Platform Boundary + +The core never reads flash, never checks signatures, never observes reset lines. +It only emits descriptions. The complete split: + +| Responsibility | Core (`sm/src/lib.rs`) | Shell (`Platform` impl) | +|---|---|---| +| Chain order and `ComponentAttrs` | reads from `Rot.chain`, set by shell at startup | decides and provides | +| Read firmware image | emits `ReadFirmware(id)` | executes: eRoT reads via SPI interposition, I3C, or other transport | +| Verify signature / SVN | emits `VerifyFirmware(id)` | executes: eRoT checks against RIM/PFM; responds with `VerificationPassed` or `VerificationFailed` | +| Release from reset | emits `ReleaseReset(id)` | executes: eRoT drives reset GPIO or equivalent | +| Detect iRoT readiness | waits for `ComponentReady(id)` event | observes: integrated iRoT signals readiness (MCTP channel-up, GPIO, etc.); calls `dispatch` | +| Required vs optional failure policy | checks `attrs.required` in handler | none — policy is encoded in the chain at startup | + +--- + +## 6. What This Model Does Not Cover + +- **Self-verification of the eRoT firmware itself**: happens one boot layer down + (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. +- **Firmware update verification** (`AuthenticateUpdate`): handled in the + `Updating` state, distinct from boot-time chain verification.
diff --git a/docs/src/design/orchestrator/orchestrator-overview.md b/docs/src/design/orchestrator/orchestrator-overview.md new file mode 100644 index 0000000..c782f50 --- /dev/null +++ b/docs/src/design/orchestrator/orchestrator-overview.md
@@ -0,0 +1,50 @@ +# Orchestrator State Machine + +The orchestrator is the eRoT's boot-sequence controller. It walks the platform +trust chain — verifying each component's firmware and releasing it from reset in +order — and then governs the operational lifecycle (attestation, firmware update, +corruption recovery). + +It lives in `services/orchestrator/sm` as a pure state machine: it never touches +hardware directly. Every action is described as an [`Effect`] value that the +surrounding shell carries out; every piece of outside information arrives as an +[`Event`]. This keeps the core testable without hardware and free of I/O. + +## Documents + +- [**Verification Model**](./orchestrator-model.md): The two-tier firmware + 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. + +## Design Principles + +**Effects, not actions.** Handlers call `ctx.emit(Effect::…)` to describe what +should happen. The shell's `Platform::execute` carries it out. The core never +reads flash, drives a GPIO, or opens a channel. + +**Reads as events.** The core never reads OTP, UFM, or any provisioning store. +Outside information (power-on result, verification verdicts, iRoT readiness +signals) arrives in event payloads. + +**Feedback as data.** Internal follow-up signals (e.g. the retry-cap lockdown +`RecoveryFailed`) are emitted as `Effect::Emit(event)`. The orchestrator queues +and handles them immediately, making them visible in the effect trace rather than +hiding them as implicit state changes. + +**Board-supplied policy.** The core hard-codes no deployment-specific values. +The shell supplies the trust chain (component ids, kinds, and required/optional +policy) and the recovery-retry cap at startup. + +## Relationship to CSA Architecture + +The state machine is a direct implementation of the boot sequence described in +the CSA architecture document: + +| CSA concept | State machine encoding | +|---|---| +| eRoT holds component in reset until firmware verified | `VerifyingPlatform` 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` | +| Optional component: failure skips, not blocks | `ComponentAttrs::required = false` → advance without `Recovering` |
diff --git a/services/orchestrator/sm/BUILD.bazel b/services/orchestrator/sm/BUILD.bazel new file mode 100644 index 0000000..444bde8 --- /dev/null +++ b/services/orchestrator/sm/BUILD.bazel
@@ -0,0 +1,22 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "orchestrator_sm", + srcs = ["src/lib.rs"], + crate_name = "openprot_orchestrator_sm", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "@rust_crates//:heapless", + "@rust_crates//:statig", + ], +) + +rust_test( + name = "orchestrator_sm_test", + crate = ":orchestrator_sm", + edition = "2024", +)
diff --git a/services/orchestrator/sm/README.md b/services/orchestrator/sm/README.md new file mode 100644 index 0000000..c801643 --- /dev/null +++ b/services/orchestrator/sm/README.md
@@ -0,0 +1,55 @@ +<!-- Licensed under the Apache-2.0 license --> +<!-- SPDX-License-Identifier: Apache-2.0 --> + +# orchestrator state machine (`openprot_orchestrator_sm`) + +Pure-reducer eRoT boot-sequence state machine. Walks the platform trust chain +— verifying each component's firmware and releasing it from reset in order — +then governs the operational lifecycle (attestation, firmware update, corruption +recovery). + +**No I/O, no hardware.** Every action is an [`Effect`] the surrounding shell +carries out. Every piece of outside information arrives as an [`Event`]. + +## Key types + +| Type | Role | +|---|---| +| `ComponentId` | Opaque `u8` — the shell maps it to hardware; the core never inspects it. | +| `ComponentKind` | `Active` (eRoT + iRoT gates) or `Passive` (eRoT gate only). | +| `ComponentAttrs` | `kind` + `required`: if `false`, a failed component is skipped (held in reset) rather than triggering recovery. | +| `Orchestrator<N>` | Public handle for the caller's event loop. Call `dispatch` or `dispatch_with` once per event. | +| `Platform` | Implement this to carry out effects (drives reset GPIOs, reads flash, etc.). | + +## Usage + +```rust +use openprot_orchestrator_sm::{ + ComponentAttrs, ComponentId, Orchestrator, Event, PowerOnResult, State, +}; + +const CAPACITY: usize = 3; +const BMC: ComponentId = ComponentId::new(0); +const HOST: ComponentId = ComponentId::new(1); +const NIC: ComponentId = ComponentId::new(2); + +let mut chain = heapless::Vec::<_, CAPACITY>::new(); +let _ = chain.push((BMC, ComponentAttrs::active_required())); +let _ = chain.push((HOST, ComponentAttrs::active_required())); +let _ = chain.push((NIC, ComponentAttrs::passive_optional())); + +let mut orch = Orchestrator::new(chain, /*max_retry=*/ 3); +let mut board = MyBoard; + +orch.dispatch(&mut board, Event::PowerGood(PowerOnResult::Provisioned)); +// ...deliver VerificationPassed / ComponentReady events as they arrive... +assert_eq!(orch.state(), State::Ready); +``` + +## Design docs + +Full domain model, verification boundary, and state transition tables are in the +OpenPRoT book: + +- `docs/src/design/orchestrator/verification-model.md` +- `docs/src/design/orchestrator/state-machine.md`
diff --git a/services/orchestrator/sm/src/lib.rs b/services/orchestrator/sm/src/lib.rs new file mode 100644 index 0000000..4d9fe9e --- /dev/null +++ b/services/orchestrator/sm/src/lib.rs
@@ -0,0 +1,867 @@ +//! `openprot_orchestrator_sm` — the eRoT boot-sequence state machine. +//! +//! This is the pure-reducer core ported from `rot_reducer`. It describes side +//! effects as [`Effect`] values rather than performing them; the surrounding +//! OpenPRoT shell carries them out via a [`Platform`] impl. No concrete hardware +//! appears here — the machine is generic over an opaque [`ComponentId`]. +//! +//! See `docs/verification-model.md` and `docs/state-machine.md` in the +//! `rot_reducer` workspace for the full domain context and design rationale. +//! +//! Three invariants define the boundary: +//! 1. **Effects flow through [`Sink`]** — fresh per event, drained afterward. +//! 2. **Feedback as data ([`Effect::Emit`])** — follow-up events are effects, +//! visible in the trace; used for the retry cap (INV7). +//! 3. **Reads as events** — outside information arrives in [`Event`] payloads; +//! the core never reads anything directly. + +#![no_std] +#![forbid(unsafe_code)] + +use core::marker::PhantomData; + +use statig::blocking::{ + IntoStateMachine, IntoStateMachineExt as _, State as StatigState, StateMachine, + Superstate as StatigSuperstate, +}; +use statig::Outcome; + +// Internal capacities — these follow from how the machine works, not from the +// deployment. The board owns CAPACITY (chain length) and max_retry. + +/// Max effects one event can emit. The busiest handler emits 3; 8 is plenty. +const EFFECT_CAP: usize = 8; + +/// Max pending events while settling one outside event (original + Emit follow-ups). +const PENDING_CAP: usize = 8; + +/// An opaque identifier for one platform component. The core never inspects it; +/// the board layer decides which real hardware each id refers to. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct ComponentId(u8); + +impl ComponentId { + pub const fn new(id: u8) -> Self { + Self(id) + } + + pub const fn get(self) -> u8 { + self.0 + } +} + +/// How a component in the trust chain is classified. The board supplies one +/// [`ComponentKind`] per [`ComponentId`] when building the chain. +/// +/// Corresponds directly to the two-tier model in the CSA architecture document: +/// `Active` = eRoT gate + iRoT gate; `Passive` = eRoT gate only. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ComponentKind { + /// Has an integrated iRoT (e.g. Caliptra). Both eRoT-side (signature + SVN) + /// and iRoT-side (local self-verification) checks apply. The machine waits in + /// [`State::AwaitingReady`] for [`Event::ComponentReady`] before advancing. + Active, + /// No integrated iRoT. The eRoT's signature + SVN check is the only gate. + /// The chain walk advances immediately after `ReleaseReset`. + Passive, +} + +/// Per-component attributes supplied by the board at chain-build time. +/// +/// Two 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. +/// +/// A `required: false` component is never released from reset on failure — running +/// untrusted firmware would break the trust invariant regardless of policy. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct ComponentAttrs { + pub kind: ComponentKind, + pub required: bool, +} + +impl ComponentAttrs { + pub const fn active_required() -> Self { + Self { kind: ComponentKind::Active, required: true } + } + pub const fn passive_required() -> Self { + Self { kind: ComponentKind::Passive, required: true } + } + pub const fn active_optional() -> Self { + Self { kind: ComponentKind::Active, required: false } + } + pub const fn passive_optional() -> Self { + Self { kind: ComponentKind::Passive, required: false } + } +} + +/// The result of the board's power-on checks, delivered inside [`Event::PowerGood`]. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum PowerOnResult { + /// Self-verified and provisioned. + Provisioned, + /// Self-verified but not provisioned — cannot act as a RoT. + Unprovisioned, + /// Self-verification failed — latches immediately to [`State::Locked`]. + SelfVerificationFailed, +} + +/// Everything the outside world can tell the state machine. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Event { + /// Power-on, carrying the shell's self-verification and provisioning result. + PowerGood(PowerOnResult), + VerificationPassed(ComponentId), + VerificationFailed(ComponentId), + /// An `Active` component's iRoT has finished local verification and is ready + /// (e.g. MCTP channel established). + ComponentReady(ComponentId), + AttestationChallenge, + UpdateRequest, + UpdateVerified, + UpdateRejected, + CorruptionDetected(ComponentId), + Restored(ComponentId), + RecoveryFailed, +} + +/// Everything the state machine can ask the outside world to do. +/// +/// [`Effect::Emit`] is the sole internal effect: the orchestrator catches it and +/// queues the carried event for immediate handling, making follow-up events +/// visible in the effect trace instead of hidden state changes. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Effect { + ReadFirmware(ComponentId), + VerifyFirmware(ComponentId), + ReleaseReset(ComponentId), + SignAttestation, + AuthenticateUpdate, + StageUpdate, + ActivateUpdate, + DiscardStaged, + RestoreGoldenImage(ComponentId), + LatchLockdown, + /// Internal only — tells the orchestrator to handle this event next. + /// Never forwarded to a [`Platform`]. + Emit(Event), +} + +/// The states the machine can be in. None carry data; all mutable state lives +/// in [`Rot`] shared storage. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum State { + PowerOnReset, + VerifyingPlatform, + /// eRoT has released an `Active` component; waiting for its iRoT to finish + /// local verification and signal [`Event::ComponentReady`]. + AwaitingReady, + Ready, + Updating, + Recovering, + Locked, +} + +/// Group state shared by the operational states. +#[derive(Debug)] +pub enum Superstate<'sub> { + Operational(PhantomData<&'sub ()>), +} + +/// The effect buffer handed to every handler (statig's `Context`). +/// +/// The only thing a handler can do to the outside world is call `emit`. The +/// orchestrator gives each event a fresh `Sink` and drains it afterward. +pub struct Sink { + effects: heapless::Vec<Effect, EFFECT_CAP>, +} + +impl Sink { + fn new() -> Self { + Self { + effects: heapless::Vec::new(), + } + } + + /// Append one effect. Overflow is silently dropped rather than panicking + /// (`no_std` safety); overflow means a logic bug. + pub fn emit(&mut self, effect: Effect) { + let _ = self.effects.push(effect); + } + + pub fn effects(&self) -> &[Effect] { + &self.effects + } +} + +/// Shared storage: data that persists across events. `N` is the chain capacity +/// — a board choice; the core sets no default. +pub struct Rot<const N: usize> { + chain: heapless::Vec<(ComponentId, ComponentAttrs), N>, + cursor: u8, + failed: Option<ComponentId>, + retry_count: u8, + max_retry: u8, + /// The `Active` component whose iRoT readiness is outstanding. `Some` only + /// while in `AwaitingReady` (INV9). + awaiting: Option<ComponentId>, +} + +impl<const N: usize> Rot<N> { + pub fn new(chain: heapless::Vec<(ComponentId, ComponentAttrs), N>, max_retry: u8) -> Self { + Self { + chain, + cursor: 0, + failed: None, + retry_count: 0, + max_retry, + awaiting: None, + } + } +} + +impl<const N: usize> IntoStateMachine for Rot<N> { + type Event<'evt> = Event; + type Context<'ctx> = Sink; + type State = State; + type Superstate<'sub> = Superstate<'sub>; + + fn initial() -> State { + State::PowerOnReset + } +} + +impl<const N: usize> StatigState<Rot<N>> for State { + fn call_handler(&mut self, rot: &mut Rot<N>, event: &Event, ctx: &mut Sink) -> Outcome<State> { + match self { + State::PowerOnReset => match event { + Event::PowerGood(PowerOnResult::Provisioned) => { + Outcome::Transition(State::VerifyingPlatform) + } + Event::PowerGood(PowerOnResult::Unprovisioned) => { + Outcome::Transition(State::Locked) + } + Event::PowerGood(PowerOnResult::SelfVerificationFailed) => { + Outcome::Transition(State::Locked) + } + _ => Outcome::Super, + }, + + // Cursor walk via Outcome::Handled — a self-transition would reset cursor. + State::VerifyingPlatform => 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 => { + rot.awaiting = Some(*id); + Outcome::Transition(State::AwaitingReady) + } + ComponentKind::Passive => 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) + } + } + } + _ => Outcome::Super, + }, + + State::AwaitingReady => match event { + Event::ComponentReady(id) => { + if rot.awaiting != Some(*id) { + 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 (rot.cursor as usize) >= rot.chain.len() { + Outcome::Transition(State::Ready) + } else { + Outcome::Handled + } + } + 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)); + 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 + } + } + } + _ => Outcome::Super, + }, + + State::Ready => match event { + Event::UpdateRequest => Outcome::Transition(State::Updating), + _ => Outcome::Super, + }, + + State::Updating => match event { + Event::UpdateVerified => { + ctx.emit(Effect::ActivateUpdate); + Outcome::Transition(State::Ready) + } + Event::UpdateRejected => { + ctx.emit(Effect::DiscardStaged); + Outcome::Transition(State::Ready) + } + _ => Outcome::Super, + }, + + 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 + } else { + Outcome::Transition(State::VerifyingPlatform) + } + } + Event::RecoveryFailed => Outcome::Transition(State::Locked), + _ => Outcome::Super, + }, + + State::Locked => Outcome::Super, + } + } + + fn call_entry_action(&mut self, rot: &mut Rot<N>, ctx: &mut Sink) { + match self { + State::VerifyingPlatform => { + rot.cursor = 0; + rot.awaiting = None; + if let Some(&(first_id, _)) = rot.chain.first() { + ctx.emit(Effect::ReadFirmware(first_id)); + ctx.emit(Effect::VerifyFirmware(first_id)); + } + } + State::Updating => { + ctx.emit(Effect::AuthenticateUpdate); + ctx.emit(Effect::StageUpdate); + } + State::Recovering => { + if let Some(failed) = rot.failed { + ctx.emit(Effect::RestoreGoldenImage(failed)); + } + } + State::Locked => { + ctx.emit(Effect::LatchLockdown); + } + State::Ready => { + rot.retry_count = 0; + } + _ => {} + } + } + + fn superstate(&mut self) -> Option<Superstate<'_>> { + match self { + State::Ready | State::Updating | State::Recovering | State::AwaitingReady => { + Some(Superstate::Operational(PhantomData)) + } + _ => None, + } + } +} + +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 { + Event::AttestationChallenge => { + ctx.emit(Effect::SignAttestation); + Outcome::Handled + } + Event::CorruptionDetected(id) => { + rot.failed = Some(*id); + Outcome::Transition(State::Recovering) + } + _ => Outcome::Super, + }, + } + } +} + +/// Outward connection to the platform. Carry out one effect. Never called with +/// [`Effect::Emit`] — the orchestrator consumes those internally. +pub trait Platform { + fn execute(&mut self, effect: Effect); +} + +/// A handle for a caller's own event loop. Wraps the statig machine so callers +/// only depend on this crate, never on statig types directly. +pub struct Orchestrator<const N: usize> { + machine: StateMachine<Rot<N>>, +} + +impl<const N: usize> Orchestrator<N> { + pub fn new(chain: heapless::Vec<(ComponentId, ComponentAttrs), N>, max_retry: u8) -> Self { + Self { + machine: Rot::new(chain, max_retry).state_machine(), + } + } + + pub fn state(&self) -> State { + *self.machine.state() + } + + /// Handle one event all the way through — including any [`Effect::Emit`] + /// follow-ups — calling `on_effect` for each external effect in order. + pub fn dispatch_with(&mut self, event: Event, mut on_effect: impl FnMut(Effect)) { + let mut pending: heapless::Vec<Event, PENDING_CAP> = heapless::Vec::new(); + let _ = pending.push(event); + + let mut i = 0; + while i < pending.len() { + let ev = pending[i]; + i += 1; + + let mut buf = Sink::new(); + self.machine.handle_with_context(&ev, &mut buf); + + for &effect in buf.effects() { + match effect { + Effect::Emit(internal) => { + let _ = pending.push(internal); + } + external => on_effect(external), + } + } + } + } + + /// Same as [`dispatch_with`] but routes effects to a [`Platform`]. + pub fn dispatch(&mut self, platform: &mut impl Platform, event: Event) { + self.dispatch_with(event, |effect| platform.execute(effect)); + } +} + +#[cfg(test)] +mod tests { + extern crate std; + + use super::*; + use std::vec::Vec; + + const C0: ComponentId = ComponentId::new(0); + const C1: ComponentId = ComponentId::new(1); + const C2: ComponentId = ComponentId::new(2); + + const BOOT: Event = Event::PowerGood(PowerOnResult::Provisioned); + + const CAPACITY: usize = 8; + const MAX_RETRY: u8 = 3; + + fn chain( + ids: &[(ComponentId, ComponentAttrs)], + ) -> heapless::Vec<(ComponentId, ComponentAttrs), CAPACITY> { + let mut c = heapless::Vec::new(); + for &entry in ids { + c.push(entry).expect("chain within CAPACITY"); + } + c + } + + fn passive_required( + ids: &[ComponentId], + ) -> heapless::Vec<(ComponentId, ComponentAttrs), CAPACITY> { + chain( + &ids.iter() + .map(|&id| (id, ComponentAttrs::passive_required())) + .collect::<std::vec::Vec<_>>(), + ) + } + + struct Recorder { + recorded: Vec<Effect>, + } + + impl Recorder { + fn new() -> Self { + Self { recorded: Vec::new() } + } + } + + impl Platform for Recorder { + fn execute(&mut self, effect: Effect) { + self.recorded.push(effect); + } + } + + fn drive( + chain: heapless::Vec<(ComponentId, ComponentAttrs), CAPACITY>, + script: &[Event], + ) -> (Vec<Effect>, State) { + let mut orch = Orchestrator::new(chain, MAX_RETRY); + let mut platform = Recorder::new(); + for &event in script { + orch.dispatch(&mut platform, event); + } + (platform.recorded, orch.state()) + } + + /// INV1/INV2/INV3: provisioned power-on walks the chain in order; no + /// component is released before its eRoT-side verification passes. + #[test] + fn cold_boot_walks_chain_in_order() { + let (effects, state) = drive( + passive_required(&[C0, C1]), + &[BOOT, Event::VerificationPassed(C0), Event::VerificationPassed(C1)], + ); + assert_eq!( + effects, + std::vec![ + Effect::ReadFirmware(C0), + Effect::VerifyFirmware(C0), + Effect::ReleaseReset(C0), + Effect::ReadFirmware(C1), + Effect::VerifyFirmware(C1), + Effect::ReleaseReset(C1), + ], + ); + assert_eq!(state, State::Ready); + } + + /// Unprovisioned power-on latches immediately. + #[test] + fn unprovisioned_boot_locks_down() { + let (effects, state) = drive( + passive_required(&[C0]), + &[Event::PowerGood(PowerOnResult::Unprovisioned)], + ); + assert_eq!(effects, std::vec![Effect::LatchLockdown]); + assert_eq!(state, State::Locked); + } + + /// INV11: SelfVerificationFailed latches immediately without entering + /// VerifyingPlatform. + #[test] + fn self_verification_failure_latches_immediately() { + let (effects, state) = drive( + passive_required(&[C0]), + &[Event::PowerGood(PowerOnResult::SelfVerificationFailed)], + ); + assert_eq!(effects, std::vec![Effect::LatchLockdown]); + assert_eq!(state, State::Locked); + } + + /// INV6: AttestationChallenge is answerable from every Operational state. + #[test] + fn attestation_shared_across_operational_states() { + let (effects, state) = drive( + passive_required(&[C0]), + &[BOOT, Event::VerificationPassed(C0), Event::AttestationChallenge], + ); + assert_eq!(effects.last(), Some(&Effect::SignAttestation)); + assert_eq!(state, State::Ready); + + let (effects, state) = drive( + passive_required(&[C0]), + &[ + BOOT, + Event::VerificationPassed(C0), + Event::UpdateRequest, + Event::AttestationChallenge, + ], + ); + assert_eq!(effects.last(), Some(&Effect::SignAttestation)); + assert_eq!(state, State::Updating); + } + + /// INV4: a rejected update rolls back via DiscardStaged and never enters + /// Recovering. + #[test] + fn update_rollback_is_not_recovery() { + let (effects, state) = drive( + passive_required(&[C0]), + &[ + BOOT, + Event::VerificationPassed(C0), + Event::UpdateRequest, + Event::UpdateRejected, + ], + ); + let tail = &effects[effects.len() - 3..]; + assert_eq!( + tail, + &[Effect::AuthenticateUpdate, Effect::StageUpdate, Effect::DiscardStaged], + ); + assert_eq!(state, State::Ready); + assert!(!effects.contains(&Effect::LatchLockdown)); + } + + /// INV5: runtime corruption targets the named component and re-walks from + /// the top after restore. + #[test] + fn runtime_corruption_targets_component_and_rewalks() { + let (effects, state) = drive( + passive_required(&[C0, C1]), + &[ + BOOT, + Event::VerificationPassed(C0), + Event::VerificationPassed(C1), + Event::CorruptionDetected(C1), + Event::Restored(C1), + ], + ); + let tail = &effects[effects.len() - 2..]; + assert_eq!(tail, &[Effect::ReadFirmware(C0), Effect::VerifyFirmware(C0)]); + assert_eq!(state, State::VerifyingPlatform); + } + + /// INV7 (feedback-as-data): after MAX_RETRY restores the core self-emits + /// RecoveryFailed and latches to Locked without any external RecoveryFailed + /// in the script. + #[test] + fn retry_cap_self_latches_via_emit() { + let mut script = std::vec![BOOT, Event::VerificationPassed(C0)]; + script.push(Event::CorruptionDetected(C0)); + for _ in 0..(MAX_RETRY - 1) { + script.push(Event::Restored(C0)); + script.push(Event::VerificationFailed(C0)); + } + script.push(Event::Restored(C0)); + + let (effects, state) = drive(passive_required(&[C0]), &script); + + assert!(!script.contains(&Event::RecoveryFailed)); + assert_eq!(state, State::Locked); + assert_eq!(effects.last(), Some(&Effect::LatchLockdown)); + } + + /// INV7: retry count resets after a successful recovery so a later episode + /// starts from zero. + #[test] + fn retry_count_resets_after_successful_recovery() { + let mut c = heapless::Vec::<(ComponentId, ComponentAttrs), CAPACITY>::new(); + c.push((C0, ComponentAttrs::passive_required())).expect("fits"); + let mut orch = Orchestrator::new(c, 2); + let mut effects = Vec::new(); + + for ev in [ + BOOT, + Event::VerificationPassed(C0), + Event::CorruptionDetected(C0), + Event::Restored(C0), + Event::VerificationPassed(C0), + ] { + orch.dispatch_with(ev, |e| effects.push(e)); + } + assert_eq!(orch.state(), State::Ready); + + let start = effects.len(); + for ev in [ + Event::CorruptionDetected(C0), + Event::Restored(C0), + Event::VerificationPassed(C0), + ] { + orch.dispatch_with(ev, |e| effects.push(e)); + } + assert_eq!(orch.state(), State::Ready); + assert!(!effects[start..].contains(&Effect::LatchLockdown)); + } + + /// Board-supplied retry cap: max_retry = 1 latches on the first failed + /// restore. + #[test] + fn custom_retry_cap_latches_sooner() { + let mut c = heapless::Vec::<(ComponentId, ComponentAttrs), CAPACITY>::new(); + c.push((C0, ComponentAttrs::passive_required())).expect("fits"); + let mut orch = Orchestrator::new(c, 1); + let mut effects = Vec::new(); + for ev in [ + BOOT, + Event::VerificationPassed(C0), + Event::CorruptionDetected(C0), + Event::Restored(C0), + ] { + orch.dispatch_with(ev, |e| effects.push(e)); + } + assert_eq!(orch.state(), State::Locked); + assert_eq!(effects.last(), Some(&Effect::LatchLockdown)); + } + + /// Three-component chain uses N=3; walks all three to Ready. + #[test] + fn custom_capacity_walks_full_chain() { + let mut c = heapless::Vec::<(ComponentId, ComponentAttrs), 3>::new(); + for &id in &[C0, C1, C2] { + c.push((id, ComponentAttrs::passive_required())).expect("3 fits"); + } + let mut orch = Orchestrator::new(c, MAX_RETRY); + let mut effects = Vec::new(); + for ev in [ + BOOT, + Event::VerificationPassed(C0), + Event::VerificationPassed(C1), + Event::VerificationPassed(C2), + ] { + orch.dispatch_with(ev, |e| effects.push(e)); + } + assert_eq!(orch.state(), State::Ready); + assert_eq!(effects.last(), Some(&Effect::ReleaseReset(C2))); + } + + /// INV10: Active component gates the chain walk — cursor does not advance + /// until ComponentReady arrives. + #[test] + fn active_component_gates_on_component_ready() { + let (effects, state) = drive( + chain(&[ + (C0, ComponentAttrs::active_required()), + (C1, ComponentAttrs::passive_required()), + ]), + &[BOOT, Event::VerificationPassed(C0)], + ); + assert_eq!(state, State::AwaitingReady); + assert!(effects.contains(&Effect::ReleaseReset(C0))); + assert!(effects.contains(&Effect::ReadFirmware(C1))); + + let (effects2, state2) = drive( + chain(&[ + (C0, ComponentAttrs::active_required()), + (C1, ComponentAttrs::passive_required()), + ]), + &[ + BOOT, + Event::VerificationPassed(C0), + Event::ComponentReady(C0), + Event::VerificationPassed(C1), + ], + ); + assert_eq!(state2, State::Ready); + assert!(effects2.contains(&Effect::ReleaseReset(C1))); + } + + /// INV9: a ComponentReady for the wrong id is silently ignored. + #[test] + fn spurious_component_ready_is_ignored() { + let (effects, state) = drive( + chain(&[ + (C0, ComponentAttrs::active_required()), + (C1, ComponentAttrs::passive_required()), + ]), + &[ + BOOT, + Event::VerificationPassed(C0), + Event::ComponentReady(C1), // wrong id + ], + ); + assert_eq!(state, State::AwaitingReady); + assert!(!effects.contains(&Effect::ReleaseReset(C1))); + } + + /// INV12: AttestationChallenge is handled in AwaitingReady. + #[test] + fn attestation_in_awaiting_ready() { + let (effects, state) = drive( + chain(&[ + (C0, ComponentAttrs::active_required()), + (C1, ComponentAttrs::passive_required()), + ]), + &[BOOT, Event::VerificationPassed(C0), Event::AttestationChallenge], + ); + assert_eq!(state, State::AwaitingReady); + assert_eq!(effects.last(), Some(&Effect::SignAttestation)); + } + + /// Optional component: VerificationFailed skips it (held in reset), chain + /// continues to Ready. + #[test] + fn optional_component_failure_skips_and_continues() { + let (effects, state) = drive( + chain(&[ + (C0, ComponentAttrs::passive_required()), + (C1, ComponentAttrs::passive_optional()), + ]), + &[BOOT, Event::VerificationPassed(C0), Event::VerificationFailed(C1)], + ); + 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))); + assert!(!effects.contains(&Effect::LatchLockdown)); + } + + /// Optional Active component failure in AwaitingReady: skipped, held in + /// reset, chain reaches Ready once ComponentReady clears awaiting. + #[test] + fn optional_active_failure_in_awaiting_ready_skips() { + // C0 Active required, C1 Active optional. + let (effects, state) = drive( + chain(&[ + (C0, ComponentAttrs::active_required()), + (C1, ComponentAttrs::active_optional()), + ]), + &[ + BOOT, + Event::VerificationPassed(C0), // → AwaitingReady; spec ReadFirmware(C1) + Event::VerificationFailed(C1), // optional → skip C1 + Event::ComponentReady(C0), // iRoT gate clears; cursor past end → Ready + ], + ); + assert_eq!(state, State::Ready); + assert!(!effects.contains(&Effect::ReleaseReset(C1))); + assert!(!effects.contains(&Effect::RestoreGoldenImage(C1))); + } +}
diff --git a/third_party/crates_io/Cargo.lock b/third_party/crates_io/Cargo.lock index 3af825c..f99ec31 100644 --- a/third_party/crates_io/Cargo.lock +++ b/third_party/crates_io/Cargo.lock
@@ -158,14 +158,14 @@ dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "bitflags" -version = "2.12.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block-buffer" @@ -184,9 +184,9 @@ [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cfg-if" @@ -206,9 +206,9 @@ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" dependencies = [ "clap_builder", "clap_derive", @@ -216,9 +216,9 @@ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -236,7 +236,7 @@ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -293,7 +293,7 @@ dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -603,7 +603,7 @@ dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -822,24 +822,24 @@ [[package]] name = "log" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "mctp" version = "0.2.0" -source = "git+https://github.com/CodeConstruct/mctp-rs.git?branch=main#b134e145f93d634dff7eb9f2a01559273c687365" +source = "git+https://github.com/CodeConstruct/mctp-rs.git?branch=main#574e3a9889fe09954f9e08b10c72e7ea9e156dd0" [[package]] name = "mctp" version = "0.2.0" -source = "git+https://github.com/CodeConstruct/mctp-rs.git#b134e145f93d634dff7eb9f2a01559273c687365" +source = "git+https://github.com/CodeConstruct/mctp-rs.git#574e3a9889fe09954f9e08b10c72e7ea9e156dd0" [[package]] name = "mctp-estack" version = "0.1.0" -source = "git+https://github.com/CodeConstruct/mctp-rs.git?branch=main#b134e145f93d634dff7eb9f2a01559273c687365" +source = "git+https://github.com/CodeConstruct/mctp-rs.git?branch=main#574e3a9889fe09954f9e08b10c72e7ea9e156dd0" dependencies = [ "crc", "embedded-io", @@ -862,9 +862,9 @@ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memo-map" @@ -883,9 +883,9 @@ [[package]] name = "minijinja" -version = "2.20.0" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2929e494b2280e1e18959bb2e121da03347ae896896fdfaceaab43c88a02803f" +checksum = "cb3d648e68cea56d9858d535ee28f9538404e2dd8cb08ed0bd05dca379477f39" dependencies = [ "memo-map", "serde", @@ -909,9 +909,9 @@ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", @@ -1042,9 +1042,9 @@ [[package]] name = "pest" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" dependencies = [ "memchr", "ucd-trie", @@ -1052,9 +1052,9 @@ [[package]] name = "pest_derive" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" dependencies = [ "pest", "pest_generator", @@ -1062,25 +1062,24 @@ [[package]] name = "pest_generator" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "pest_meta" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" dependencies = [ "pest", - "sha2", ] [[package]] @@ -1139,14 +1138,14 @@ "itertools", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -1226,7 +1225,7 @@ dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1237,7 +1236,7 @@ dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1264,7 +1263,7 @@ dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1340,9 +1339,10 @@ "sha3", "smlang", "spdm-lib", + "statig", "subtle", "syn 1.0.109", - "syn 2.0.117", + "syn 2.0.119", "thiserror", "tock-registers", "tokio", @@ -1354,9 +1354,9 @@ [[package]] name = "rustc-demangle" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" [[package]] name = "rustc_version" @@ -1450,7 +1450,7 @@ dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1507,9 +1507,9 @@ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "slab" @@ -1519,9 +1519,9 @@ [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smbus-pec" @@ -1555,9 +1555,9 @@ [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys", @@ -1579,6 +1579,12 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] +name = "statig" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c04b4a9f2d66294d63bdd8df834caad9f8e181997c3cf766b6b4f6d12d4fbc" + +[[package]] name = "string_morph" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1609,9 +1615,9 @@ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -1645,7 +1651,7 @@ dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1655,9 +1661,9 @@ [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" dependencies = [ "bytes", "libc", @@ -1672,13 +1678,13 @@ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1736,9 +1742,9 @@ [[package]] name = "uuid" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" [[package]] name = "vcell" @@ -1790,40 +1796,40 @@ [[package]] name = "zerocopy" -version = "0.8.50" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.50" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ]
diff --git a/third_party/crates_io/Cargo.toml b/third_party/crates_io/Cargo.toml index 7833db3..20f3b80 100644 --- a/third_party/crates_io/Cargo.toml +++ b/third_party/crates_io/Cargo.toml
@@ -32,6 +32,7 @@ serde = { version = "1.0.219", default-features = false, features = ["derive"] } serde_derive = "1.0.228" smlang = { version = "0.8.0", default-features = false } +statig = { version = "0.4.1", default-features = false } syn = { version = "2.0.104", features = ["full", "extra-traits"] } syn1 = { package = "syn", version = "1.0.109", features = ["full", "extra-traits"] } tock-registers = "0.9.0"