From 01014210842df7cbdef74dd1fa2f01a394965fd0 Mon Sep 17 00:00:00 2001 From: Christina Quast Date: Mon, 17 Aug 2026 18:36:04 +0200 Subject: [PATCH 1/3] orchestrator: Add the SvnFloor anti-rollback capability trait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One device's durable SVN floor behind the capability seam: floor() and advance(), monotonic and idempotent so a replayed commit is harmless, durable when advance returns. Storage encoding (OTP fuse counters, protected flash) stays behind the trait; PLDM devices that commit internally simply have no eRoT-side floor. Trait only — the OTP-backed adapter and the driver's commit_svn_floor executor follow separately. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast --- .../orchestrator/capabilities/BUILD.bazel | 1 + services/orchestrator/capabilities/src/lib.rs | 5 + .../capabilities/src/svn_floor.rs | 130 ++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 services/orchestrator/capabilities/src/svn_floor.rs diff --git a/services/orchestrator/capabilities/BUILD.bazel b/services/orchestrator/capabilities/BUILD.bazel index 011d2e11c..859f7a9c5 100644 --- a/services/orchestrator/capabilities/BUILD.bazel +++ b/services/orchestrator/capabilities/BUILD.bazel @@ -10,6 +10,7 @@ rust_library( "src/boot_watch.rs", "src/evidence.rs", "src/lib.rs", + "src/svn_floor.rs", ], edition = "2024", visibility = ["//visibility:public"], diff --git a/services/orchestrator/capabilities/src/lib.rs b/services/orchestrator/capabilities/src/lib.rs index 0de7196fe..0a721eac5 100644 --- a/services/orchestrator/capabilities/src/lib.rs +++ b/services/orchestrator/capabilities/src/lib.rs @@ -7,6 +7,9 @@ //! single managed device's reset without knowing which controller line it //! maps to. //! +//! `SvnFloor` is the anti-rollback capability: one device's durable SVN +//! floor, read at verification and advanced only after a confirmed boot. +//! //! `BootStatus` is the shared vocabulary for boot-liveness evidence, and //! `EvidenceReader` resolves a board-defined signal id to it. The schema //! names no signal kinds: each board's device table declares its @@ -29,7 +32,9 @@ mod boot_control; mod boot_watch; mod evidence; +mod svn_floor; pub use boot_control::BootControl; pub use boot_watch::{BootWatch, FailureCause, WalkVerdict}; pub use evidence::{BootStatus, EvidenceReader}; +pub use svn_floor::{Svn, SvnFloor}; diff --git a/services/orchestrator/capabilities/src/svn_floor.rs b/services/orchestrator/capabilities/src/svn_floor.rs new file mode 100644 index 000000000..9f6839a45 --- /dev/null +++ b/services/orchestrator/capabilities/src/svn_floor.rs @@ -0,0 +1,130 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! The [`SvnFloor`] anti-rollback capability contract. + +/// A security version number, as carried in image manifests and compared +/// against a device's anti-rollback floor. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct Svn(pub u32); + +/// Anti-rollback capability: one managed device's durable SVN floor. +/// +/// The floor is the lowest SVN the device may still boot. Storage — OTP +/// fuse counters, a monotonic counter in protected flash, a mock in tests — +/// is the implementor's concern; its encoding (e.g. unary fuse bits) never +/// leaks through this seam. Devices that keep their own floor (a PLDM +/// firmware device commits internally) have no `SvnFloor` on the eRoT side. +/// +/// The orchestrator advances the floor only after an activated image proved +/// itself at runtime (`BootConfirmed`), never on activation alone, so a bad +/// image can still be rolled back. +/// +/// # Contract +/// +/// - **Monotonic.** `advance(to)` with `to` at or below the current floor +/// succeeds as a no-op (a replayed commit is harmless); no call ever +/// lowers the floor. A lower target is not distinguishable from a replay +/// at this seam; callers that need to detect one compare against +/// [`floor`](Self::floor) first. +/// - **Durable on return.** When `advance` returns `Ok`, the new floor +/// survives power loss. A torn write may lose the advance (the caller +/// re-commits) but must never leave the floor below its previous value. +pub trait SvnFloor { + /// The error type of this device's floor storage. + /// + /// Bounded by [`core::error::Error`] so the orchestrator gets `Display` + /// and a `source()` cause chain, not just a `Debug` dump. Error + /// categories are implementation-defined. + type Error: core::error::Error; + + /// The current floor: the lowest SVN this device may still boot. + fn floor(&mut self) -> Result; + + /// Raises the floor to `to`. At or below the current floor: `Ok`, no-op. + fn advance(&mut self, to: Svn) -> Result<(), Self::Error>; +} + +#[cfg(test)] +mod tests { + use super::*; + + // Implements SvnFloor with no HAL dependency — the contract must be + // satisfiable from any stack (mock, IPC proxy, simulator). A HAL-bound + // `Error` type would stop this compiling. + struct MockFloor { + floor: Svn, + fail: bool, + } + + #[derive(Debug, PartialEq, Eq)] + struct MockFault; + + impl core::fmt::Display for MockFault { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("mock floor fault") + } + } + + impl core::error::Error for MockFault {} + + impl SvnFloor for MockFloor { + type Error = MockFault; + + fn floor(&mut self) -> Result { + if self.fail { + Err(MockFault) + } else { + Ok(self.floor) + } + } + + fn advance(&mut self, to: Svn) -> Result<(), MockFault> { + if self.fail { + return Err(MockFault); + } + self.floor = self.floor.max(to); + Ok(()) + } + } + + /// The orchestrator's commit shape: advance, then read the floor back. + fn commit(floor: &mut F, confirmed: Svn) -> Result { + floor.advance(confirmed)?; + floor.floor() + } + + #[test] + fn contract_is_implementable_without_the_hal() { + let mut floor = MockFloor { + floor: Svn(3), + fail: false, + }; + + assert_eq!(commit(&mut floor, Svn(5)), Ok(Svn(5))); + } + + #[test] + fn replayed_commit_is_a_noop() { + let mut floor = MockFloor { + floor: Svn(5), + fail: false, + }; + + assert_eq!(commit(&mut floor, Svn(5)), Ok(Svn(5))); + assert_eq!(commit(&mut floor, Svn(4)), Ok(Svn(5)), "never lowers"); + } + + #[test] + fn errors_surface_through_the_generic_seam() { + let mut floor = MockFloor { + floor: Svn(0), + fail: true, + }; + + let err = commit(&mut floor, Svn(1)).expect_err("expected the floor fault"); + + // Display comes from the core::error::Error bound, not a Debug dump. + assert_eq!(err.to_string(), "mock floor fault"); + } +} From 3d8ee176c127e0b565c8e02d0e2a8727eaf534cb Mon Sep 17 00:00:00 2001 From: Christina Quast Date: Tue, 18 Aug 2026 09:54:22 +0200 Subject: [PATCH 2/3] orchestrator: Take &self in SvnFloor::floor Per review: reading the floor does not require exclusive access. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast --- services/orchestrator/capabilities/src/svn_floor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/orchestrator/capabilities/src/svn_floor.rs b/services/orchestrator/capabilities/src/svn_floor.rs index 9f6839a45..362660b07 100644 --- a/services/orchestrator/capabilities/src/svn_floor.rs +++ b/services/orchestrator/capabilities/src/svn_floor.rs @@ -39,7 +39,7 @@ pub trait SvnFloor { type Error: core::error::Error; /// The current floor: the lowest SVN this device may still boot. - fn floor(&mut self) -> Result; + fn floor(&self) -> Result; /// Raises the floor to `to`. At or below the current floor: `Ok`, no-op. fn advance(&mut self, to: Svn) -> Result<(), Self::Error>; @@ -71,7 +71,7 @@ mod tests { impl SvnFloor for MockFloor { type Error = MockFault; - fn floor(&mut self) -> Result { + fn floor(&self) -> Result { if self.fail { Err(MockFault) } else { From 879f413ea0889c27811463403d020626b7658f92 Mon Sep 17 00:00:00 2001 From: Christina Quast Date: Tue, 18 Aug 2026 11:44:36 +0200 Subject: [PATCH 3/3] orchestrator: Say where the monotonic no-op is enforced The trait cannot enforce it: one-way encodings get it for free, any other storage must check itself. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast --- services/orchestrator/capabilities/src/svn_floor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/orchestrator/capabilities/src/svn_floor.rs b/services/orchestrator/capabilities/src/svn_floor.rs index 362660b07..233d59f73 100644 --- a/services/orchestrator/capabilities/src/svn_floor.rs +++ b/services/orchestrator/capabilities/src/svn_floor.rs @@ -26,7 +26,9 @@ pub struct Svn(pub u32); /// succeeds as a no-op (a replayed commit is harmless); no call ever /// lowers the floor. A lower target is not distinguishable from a replay /// at this seam; callers that need to detect one compare against -/// [`floor`](Self::floor) first. +/// [`floor`](Self::floor) first. The no-op lives in the implementor: an +/// encoding that is naturally one-way (unary fuse counters) gets it for +/// free, any other storage must check `to` against its floor itself. /// - **Durable on return.** When `advance` returns `Ok`, the new floor /// survives power loss. A torn write may lose the advance (the caller /// re-commits) but must never leave the floor below its previous value.