orchestrator-sm: factor out model types from lib.rs into model.rs
diff --git a/services/orchestrator/sm/BUILD.bazel b/services/orchestrator/sm/BUILD.bazel index 212638a..b9ea993 100644 --- a/services/orchestrator/sm/BUILD.bazel +++ b/services/orchestrator/sm/BUILD.bazel
@@ -7,6 +7,7 @@ name = "orchestrator_sm", srcs = [ "src/lib.rs", + "src/model.rs", "src/tests.rs", ], crate_name = "openprot_orchestrator_sm",
diff --git a/services/orchestrator/sm/src/lib.rs b/services/orchestrator/sm/src/lib.rs index d4338c7..292d726 100644 --- a/services/orchestrator/sm/src/lib.rs +++ b/services/orchestrator/sm/src/lib.rs
@@ -29,6 +29,9 @@ Superstate as StatigSuperstate, }; +mod model; +pub use model::*; + // Internal capacities — these follow from how the machine works, not from the // deployment. The board owns `N` (chain length), `E` (effect-buffer size) and // max_retry. @@ -36,234 +39,6 @@ /// 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, -} - -/// Recovery-failure classification: what the machine does once a required -/// component's restore attempts are **exhausted** (its per-component 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.gated`) 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. -/// -/// Three orthogonal axes: -/// - [`kind`](ComponentAttrs::kind): controls the iRoT gate (Active vs Passive). -/// - [`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 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 failure_policy: FailurePolicy, - pub recovery_region: RegionId, - pub depends_on: Option<ComponentId>, -} - -impl ComponentAttrs { - pub const fn active_required() -> Self { - Self { - kind: ComponentKind::Active, - failure_policy: FailurePolicy::Required, - recovery_region: RegionId::new(0), - depends_on: None, - } - } - pub const fn passive_required() -> Self { - Self { - kind: ComponentKind::Passive, - failure_policy: FailurePolicy::Required, - recovery_region: RegionId::new(0), - depends_on: None, - } - } - pub const fn active_isolable() -> Self { - Self { - kind: ComponentKind::Active, - failure_policy: FailurePolicy::Isolable, - recovery_region: RegionId::new(0), - depends_on: None, - } - } - pub const fn passive_isolable() -> Self { - Self { - kind: ComponentKind::Passive, - 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`]. -#[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), - /// Assert reset on a component that is already running — the inverse of - /// [`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, - 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, - PreSupervision, - /// eRoT has released an `Active` component; waiting for its iRoT to finish - /// local verification and signal [`Event::ComponentReady`]. - AwaitingReady, - Ready, - Updating, - Recovering, - Locked, -} - /// Superstate entered once the eRoT exits [`State::PreSupervision`] — i.e. on /// release of the first `Active` component, or once the whole chain has /// finished if it is all-`Passive`. Provides two platform-wide guarantees @@ -348,101 +123,6 @@ NotGated, } -/// A validated **chain of trust**: the ordered list of components the eRoT -/// walks, verifies, and supervises, in walk order. -/// -/// Build one with [`TryFrom`]/[`TryInto`] from a `heapless::Vec` of -/// `(ComponentId, ComponentAttrs)` pairs. The conversion is the single place -/// the reducer's structural invariants are enforced, so a malformed chain -/// fails closed at the boundary instead of misbehaving later: -/// -/// - the chain is non-empty, -/// - every [`ComponentId`] is unique, -/// - every `depends_on` names a component that exists and appears *strictly -/// earlier* in the chain (no dangling, forward, or self dependencies — a -/// dependency is always walked before its dependents), -/// - the length fits `u8`, the `cursor` index type. -/// -/// ```ignore -/// let mut v = heapless::Vec::<_, 4>::new(); -/// v.push((ComponentId::new(0), ComponentAttrs::passive_required())).unwrap(); -/// let chain: Chain<4> = v.try_into()?; -/// ``` -#[derive(Clone, Debug)] -pub struct Chain<const N: usize> { - entries: heapless::Vec<(ComponentId, ComponentAttrs), N>, -} - -/// Why a `heapless::Vec` of components is not a valid [`Chain`]. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum ChainError { - /// The chain has no components. - Empty, - /// The chain is longer than `u8::MAX`, so `cursor` could not index it. - TooLong, - /// The same [`ComponentId`] appears more than once. - DuplicateId(ComponentId), - /// A `depends_on` names an id that is not in the chain. - UnknownDependency { - component: ComponentId, - depends_on: ComponentId, - }, - /// A `depends_on` names a component that does not appear strictly earlier - /// in the chain (a forward reference or a self-reference). A dependency - /// must be walked before its dependents. - ForwardDependency { - component: ComponentId, - depends_on: ComponentId, - }, -} - -impl<const N: usize> Chain<N> { - /// Consume the validated chain, yielding its components in walk order. - fn into_entries(self) -> heapless::Vec<(ComponentId, ComponentAttrs), N> { - self.entries - } -} - -impl<const N: usize> TryFrom<heapless::Vec<(ComponentId, ComponentAttrs), N>> for Chain<N> { - type Error = ChainError; - - fn try_from( - entries: heapless::Vec<(ComponentId, ComponentAttrs), N>, - ) -> Result<Self, ChainError> { - if entries.is_empty() { - return Err(ChainError::Empty); - } - if entries.len() > u8::MAX as usize { - return Err(ChainError::TooLong); - } - for (i, (id, _)) in entries.iter().enumerate() { - if entries[..i].iter().any(|(prev, _)| prev == id) { - return Err(ChainError::DuplicateId(*id)); - } - } - for (i, (id, attrs)) in entries.iter().enumerate() { - if let Some(dep) = attrs.depends_on { - match entries.iter().position(|(cid, _)| *cid == dep) { - None => { - return Err(ChainError::UnknownDependency { - component: *id, - depends_on: dep, - }); - } - Some(j) if j >= i => { - return Err(ChainError::ForwardDependency { - component: *id, - depends_on: dep, - }); - } - Some(_) => {} - } - } - } - Ok(Self { entries }) - } -} - /// Shared storage: data that persists across events. `N` is the chain capacity /// and `E` the effect-buffer size — both board choices; the core sets no /// default. `E` must be at least `N + 2` (enforced in [`Rot::new`]).
diff --git a/services/orchestrator/sm/src/model.rs b/services/orchestrator/sm/src/model.rs new file mode 100644 index 0000000..0a0ac4c --- /dev/null +++ b/services/orchestrator/sm/src/model.rs
@@ -0,0 +1,329 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Domain vocabulary for the orchestrator state machine: the component model, +//! events, effects, states, and the validated [`Chain`] of trust. These types +//! carry no reducer behavior; the state machine itself lives in the crate root. + +/// 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, +} + +/// Recovery-failure classification: what the machine does once a required +/// component's restore attempts are **exhausted** (its per-component 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.gated`) 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. +/// +/// Three orthogonal axes: +/// - [`kind`](ComponentAttrs::kind): controls the iRoT gate (Active vs Passive). +/// - [`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 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 failure_policy: FailurePolicy, + pub recovery_region: RegionId, + pub depends_on: Option<ComponentId>, +} + +impl ComponentAttrs { + pub const fn active_required() -> Self { + Self { + kind: ComponentKind::Active, + failure_policy: FailurePolicy::Required, + recovery_region: RegionId::new(0), + depends_on: None, + } + } + pub const fn passive_required() -> Self { + Self { + kind: ComponentKind::Passive, + failure_policy: FailurePolicy::Required, + recovery_region: RegionId::new(0), + depends_on: None, + } + } + pub const fn active_isolable() -> Self { + Self { + kind: ComponentKind::Active, + failure_policy: FailurePolicy::Isolable, + recovery_region: RegionId::new(0), + depends_on: None, + } + } + pub const fn passive_isolable() -> Self { + Self { + kind: ComponentKind::Passive, + 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`]. +#[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), + /// Assert reset on a component that is already running — the inverse of + /// [`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, + StageUpdate, + ActivateUpdate, + DiscardStaged, + RestoreGoldenImage(ComponentId), + LatchLockdown, + /// Internal only — tells the orchestrator to handle this event next. + /// Never forwarded to a [`Platform`](crate::Platform). + Emit(Event), +} + +/// The states the machine can be in. None carry data; all mutable state lives +/// in [`Rot`](crate::Rot) shared storage. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum State { + PowerOnReset, + PreSupervision, + /// eRoT has released an `Active` component; waiting for its iRoT to finish + /// local verification and signal [`Event::ComponentReady`]. + AwaitingReady, + Ready, + Updating, + Recovering, + Locked, +} + +/// A validated **chain of trust**: the ordered list of components the eRoT +/// walks, verifies, and supervises, in walk order. +/// +/// Build one with [`TryFrom`]/[`TryInto`] from a `heapless::Vec` of +/// `(ComponentId, ComponentAttrs)` pairs. The conversion is the single place +/// the reducer's structural invariants are enforced, so a malformed chain +/// fails closed at the boundary instead of misbehaving later: +/// +/// - the chain is non-empty, +/// - every [`ComponentId`] is unique, +/// - every `depends_on` names a component that exists and appears *strictly +/// earlier* in the chain (no dangling, forward, or self dependencies — a +/// dependency is always walked before its dependents), +/// - the length fits `u8`, the `cursor` index type. +/// +/// ```ignore +/// let mut v = heapless::Vec::<_, 4>::new(); +/// v.push((ComponentId::new(0), ComponentAttrs::passive_required())).unwrap(); +/// let chain: Chain<4> = v.try_into()?; +/// ``` +#[derive(Clone, Debug)] +pub struct Chain<const N: usize> { + entries: heapless::Vec<(ComponentId, ComponentAttrs), N>, +} + +/// Why a `heapless::Vec` of components is not a valid [`Chain`]. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ChainError { + /// The chain has no components. + Empty, + /// The chain is longer than `u8::MAX`, so `cursor` could not index it. + TooLong, + /// The same [`ComponentId`] appears more than once. + DuplicateId(ComponentId), + /// A `depends_on` names an id that is not in the chain. + UnknownDependency { + component: ComponentId, + depends_on: ComponentId, + }, + /// A `depends_on` names a component that does not appear strictly earlier + /// in the chain (a forward reference or a self-reference). A dependency + /// must be walked before its dependents. + ForwardDependency { + component: ComponentId, + depends_on: ComponentId, + }, +} + +impl<const N: usize> Chain<N> { + /// Consume the validated chain, yielding its components in walk order. + pub(crate) fn into_entries(self) -> heapless::Vec<(ComponentId, ComponentAttrs), N> { + self.entries + } +} + +impl<const N: usize> TryFrom<heapless::Vec<(ComponentId, ComponentAttrs), N>> for Chain<N> { + type Error = ChainError; + + fn try_from( + entries: heapless::Vec<(ComponentId, ComponentAttrs), N>, + ) -> Result<Self, ChainError> { + if entries.is_empty() { + return Err(ChainError::Empty); + } + if entries.len() > u8::MAX as usize { + return Err(ChainError::TooLong); + } + for (i, (id, _)) in entries.iter().enumerate() { + if entries[..i].iter().any(|(prev, _)| prev == id) { + return Err(ChainError::DuplicateId(*id)); + } + } + for (i, (id, attrs)) in entries.iter().enumerate() { + if let Some(dep) = attrs.depends_on { + match entries.iter().position(|(cid, _)| *cid == dep) { + None => { + return Err(ChainError::UnknownDependency { + component: *id, + depends_on: dep, + }); + } + Some(j) if j >= i => { + return Err(ChainError::ForwardDependency { + component: *id, + depends_on: dep, + }); + } + Some(_) => {} + } + } + } + Ok(Self { entries }) + } +}