You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implement the runtime trust-generation accept path with the epoch floor
Summary
Keys rotate, keys get revoked and builds get withdrawn long after the one-shot installer is gone. A new release-trust generation therefore has to be deliverable at runtime, over the same package channel every other package rides, targeting the reserved trust package-id. This effort adds the host side of that: the entry points a root daemon calls to accept a delivered generation, the monotonic epoch floor that stops an old but validly signed generation from being replayed to restore a revoked key or drop a withdrawn build, and the two recovery doors for hosts the ordinary path cannot serve.
The work splits along the line the trust basis itself draws. One half admits a generation against the active generation's key set — the ordinary runtime channel, where the floor, the byte-identical no-op and the chain rules live. The other half admits a generation under the anchors the delivered document itself carries — self-admission, the same cryptographic act the installer's seed performs, reached over different channels for a host that has fallen past the control plane's retention floor and for a host that has no generation at all. The two halves share a tree, a module and an error type but almost no logic; each declares the refusals its own path raises.
Both land in this crate rather than in the installer's repository. The caller these paths are built for is a root daemon that will link this crate directly and can construct no installer type, and there must be exactly one implementation of the floor. That daemon has not linked the crate yet — this API is part of what it will link for — so nothing consumes these entry points on the day they merge, and their shape is settled here rather than negotiated later. Neither half has a counterpart in the installer's repository, and neither requires a pin bump anywhere else.
The first child is the critical path. It establishes the runtime channel's floor semantics and the canonical-generation precondition every runtime entry point runs first, and it supplies the accept path that the second child's central assertion — that re-bootstrap succeeds where ordinary accept fails on the same inputs — is written against, without which that test does not compile. Both children also edit the same module heavily and add variants to the same public error enum, so sequencing them forfeits no parallelism that running them together would have won.
Schedule risk is concentrated in the second child, whose surface is the larger of the two: a factoring of the existing self-admission sequence with a byte-identical regression bar over two callers that already ship, a new authorization type with a deliberately hostile constructibility contract, a host-state gate with a fail-closed read, and a new declared file location with two resolutions. Landing it after the first child keeps that risk separate from the floor's.
Shared background
origin/main already carries the release-trust tree and everything the container format needs; this effort rebuilds none of it.
src/release_trust.rs owns the tree at the directory Layout::release_trust_dir() resolves — an active symlink, gen-<n>/ generation directories, and inside each generation three files: the delivered container as generation.pkg, the trust-set.json member, and an epoch record. src/generation.rs is the generalized activation engine underneath it, which stages a material set, repoints active atomically and prunes the superseded generation. Its failure contract has three outcomes rather than two — nothing published, though a gen-<n>.tmp may survive an I/O fault mid-staging; finalised but not live, leaving a complete gen-<n>/ that nothing points at; and live-but-unpruned after the swap — so what readers resolve is unchanged in the first two and changed in the third. Neither child alters that contract, and neither asserts an exact tree state over a failure inside it. src/verify.rs is the shared verifier and already implements the whole taxonomy — signature, target, manifest-format floor, revocation, withdrawal and the strictly-greater epoch comparison — against a caller-injected TrustSet. src/trust_set.rs holds the document, its refusing reader, and the provisional permissive decode a self-admission needs before anything has authenticated the bytes.
Every entry point in the release-trust module takes the already-resolved tree root as root: &Path, and paths into that tree are resolved from the root by helpers that each join one declared basename. Nothing in this effort takes a Layout: the caller is a daemon that holds a root, not a namespace, and a root cannot be walked back up to the namespace it came from.
Refusals on these paths are ReleaseTrustError variants in src/release_trust.rs. roxyd_trust::TrustError belongs to the mTLS material module — a different concern on a different tree — and gains nothing here.
The effort ends at the API. The wire handler, the delivery channel and the transport that carries a generation to a host are elsewhere, as are the control plane's fan-out, its ledger and its retention window, and as is minting or signing a generation at all.
Execution order
Issues in the same wave have no unmet dependencies among these children and can run in parallel.
Implement the runtime trust-generation accept path with the
epochfloorSummary
Keys rotate, keys get revoked and builds get withdrawn long after the one-shot installer is gone. A new release-trust generation therefore has to be deliverable at runtime, over the same package channel every other package rides, targeting the reserved
trustpackage-id. This effort adds the host side of that: the entry points a root daemon calls to accept a delivered generation, the monotonicepochfloor that stops an old but validly signed generation from being replayed to restore a revoked key or drop a withdrawn build, and the two recovery doors for hosts the ordinary path cannot serve.The work splits along the line the trust basis itself draws. One half admits a generation against the active generation's key set — the ordinary runtime channel, where the floor, the byte-identical no-op and the chain rules live. The other half admits a generation under the anchors the delivered document itself carries — self-admission, the same cryptographic act the installer's seed performs, reached over different channels for a host that has fallen past the control plane's retention floor and for a host that has no generation at all. The two halves share a tree, a module and an error type but almost no logic; each declares the refusals its own path raises.
Both land in this crate rather than in the installer's repository. The caller these paths are built for is a root daemon that will link this crate directly and can construct no installer type, and there must be exactly one implementation of the floor. That daemon has not linked the crate yet — this API is part of what it will link for — so nothing consumes these entry points on the day they merge, and their shape is settled here rather than negotiated later. Neither half has a counterpart in the installer's repository, and neither requires a pin bump anywhere else.
Children
epochfloor #50The first child is the critical path. It establishes the runtime channel's floor semantics and the canonical-generation precondition every runtime entry point runs first, and it supplies the accept path that the second child's central assertion — that re-bootstrap succeeds where ordinary accept fails on the same inputs — is written against, without which that test does not compile. Both children also edit the same module heavily and add variants to the same public error enum, so sequencing them forfeits no parallelism that running them together would have won.
Schedule risk is concentrated in the second child, whose surface is the larger of the two: a factoring of the existing self-admission sequence with a byte-identical regression bar over two callers that already ship, a new authorization type with a deliberately hostile constructibility contract, a host-state gate with a fail-closed read, and a new declared file location with two resolutions. Landing it after the first child keeps that risk separate from the floor's.
Shared background
origin/mainalready carries the release-trust tree and everything the container format needs; this effort rebuilds none of it.src/release_trust.rsowns the tree at the directoryLayout::release_trust_dir()resolves — anactivesymlink,gen-<n>/generation directories, and inside each generation three files: the delivered container asgeneration.pkg, thetrust-set.jsonmember, and anepochrecord.src/generation.rsis the generalized activation engine underneath it, which stages a material set, repointsactiveatomically and prunes the superseded generation. Its failure contract has three outcomes rather than two — nothing published, though agen-<n>.tmpmay survive an I/O fault mid-staging; finalised but not live, leaving a completegen-<n>/that nothing points at; and live-but-unpruned after the swap — so what readers resolve is unchanged in the first two and changed in the third. Neither child alters that contract, and neither asserts an exact tree state over a failure inside it.src/verify.rsis the shared verifier and already implements the whole taxonomy — signature, target, manifest-format floor, revocation, withdrawal and the strictly-greater epoch comparison — against a caller-injectedTrustSet.src/trust_set.rsholds the document, its refusing reader, and the provisional permissive decode a self-admission needs before anything has authenticated the bytes.Every entry point in the release-trust module takes the already-resolved tree root as
root: &Path, and paths into that tree are resolved from the root by helpers that each join one declared basename. Nothing in this effort takes aLayout: the caller is a daemon that holds a root, not a namespace, and a root cannot be walked back up to the namespace it came from.Refusals on these paths are
ReleaseTrustErrorvariants insrc/release_trust.rs.roxyd_trust::TrustErrorbelongs to the mTLS material module — a different concern on a different tree — and gains nothing here.The effort ends at the API. The wire handler, the delivery channel and the transport that carries a generation to a host are elsewhere, as are the control plane's fan-out, its ledger and its retention window, and as is minting or signing a generation at all.
Execution order
Issues in the same wave have no unmet dependencies among these children and can run in parallel.
epochfloor #50