Add the re-bootstrap and no-prior-generation trust-generation entry points (#51) - #53
Conversation
The runtime accept path judges a delivered generation against the active one's key set, and two host states have no such key set to offer. A host offline across more rotations than the control plane retains cannot be chained forward at all — the intermediate keys are pruned — and a host the installer never touched carries nothing to chain from. Both are admitted under the anchors the delivered document itself carries, which is the installer's seed reached over a different channel. Re-bootstrap needs an epoch floor between verification and activation, so the admission sequence is split: a verification-only core that writes nothing, a non-activating pin comparison, and an admit-with-pin that is the ordinary order of the two plus the installer. Re-bootstrap composes the same pieces with its floor in the slot the activating form would have closed. Both empty-tree doors run one gate and one sequence, with the package source deferred so the gate stays first on the path that has to read a file to get its bytes. The pin marker and the join material are read through root-based helpers joining the basenames layout declares, since a function handed a resolved tree root cannot reconstruct a namespace to ask a Layout with. Closes #51
The marker tests are a matrix with one cell missing: every case pairing the marker with a supplied pin was untested, so nothing held that the gate asks only whether a pin was supplied. A wrong pin on a marked host must still be the mismatch refusal rather than the no-pin one, and the right pin must admit — the configuration the marker exists for. Part of #51
|
[Reviewer Round 1] No blocking findings. This is the right shape for #51: |
|
[Review Verdict Round 1: APPROVED] |
Suggested squash commitTitle Body |
Summary
The runtime accept path judges a delivered generation against the active generation's key set, and two host states have no such key set to offer: a host offline across more rotations than the control plane retains (the intermediate keys are pruned) and a host the installer never touched (there is nothing to chain from). This adds the two entry points that serve them, both admitting under the anchors the delivered document itself carries — the installer's seed reached over different channels — relaxing the signature-chain check and only that.
The factoring. Re-bootstrap needs its epoch floor to run between verification and activation, so the private
admitsequence is split into a verification-only core that writes nothing into the tree and returns the verified document, its verified epoch and the member bytes; a non-activating pin-comparison helper; and anadmit_with_pinthat is core → comparison →install_generation.admitisadmit_with_pinwith no pin. Both empty-tree doors run one gate and one sequence through a seed-admission helper whose package source is deferred (aFnOncereturningCow), so the tree-state gate stays first on the path that has to read a file for its bytes, andadmit_seed_generationstill reachesextract_memberwith a borrow of its caller's slice.admit_seed_generationandreplace_generationare behaviourally unchanged; their existing tests pass unchanged.rebootstrap_generation(root, package, authorization, pin)runs, in order, every refusal before the single write: the authorization value is demanded by the signature; an empty tree is refused asNoActiveGeneration; the asserted last-confirmed epoch must equal the recorded one; the host-side pin marker is read and an unpinned call refused before the container is opened; then the verification-only core; then the pin compared against the member digest through the shared helper; then the floor against the verified epoch asStaleRebootstrap; theninstall_generation. It does not calladmit_with_pin, which would install before the floor ran.RebootstrapAuthorizationholds the asserted epoch in a private field behind the single constructorasserting_last_confirmed_epoch, derives exactlyDebug, Clone, Copy, and its rustdoc says plainly that it establishes no provenance and is not a capability — only the floor and the out-of-band pin resist a compromised control plane.The pin marker.
require-trust-pinat the tree root is read withsymlink_metadata, so any entry there — a dangling symlink included — reads as set and contents are never touched.NotFoundand onlyNotFoundmeans unset; every other I/O failure is fail-closed throughReleaseTrustError::Io. That classification lives in one private pure helper taking thestd::io::Resultand the resolved path, unit-tested over synthesized errors because no unprivileged filesystem arrangement can reach the branch through the entry point.bootstrap_from_join_material(root, pin)accepts no caller-supplied generation bytes: it hands the join-material location and the pin to the seed-admission helper, so the gate and the sequence areadmit_seed_generation's, not re-derived.layout::JOIN_GENERATION_FILE(join-generation.pkg) andLayout::join_generation_path()are added beside the pin marker's pair, and both files this PR reads are resolved through root-based helpers in the release-trust module. The two doc comments that claimed the runtime resolves the marker through the namespace-based accessor are reworded to name the constant as the single declaration and the two accessors as its resolutions.Four new
ReleaseTrustErrorvariants carry the refusals:RebootstrapEpochMismatch,FingerprintPinRequired,FingerprintPinMismatch,StaleRebootstrap. No dependency added.Closes #51
Test plan
26 new tests in
src/release_trust.rsandsrc/layout.rs, all minting generations in-test from ephemeral keys overtempfile::tempdir()trees, none requiring root.Re-bootstrap:
StaleRebootstrapNoActiveGenerationwith the tree untouched, asserted with the marker present and bytes that are not a well-formed container, so the test fails if either the marker read or the container open runs firstRebootstrapEpochMismatchand the tree untouchedaccept_generationfails on the same inputs — a generation signed by a key absent from the stale active set — proving the chain check is what was relaxedepochand manifest artifactversiondisagreement is refused by the verifier's target check, with no epoch-agreement comparison of this module's owninstall_generationFingerprintPinMismatch; a matching one succeedsCore and factoring:
admit_seed_generationandreplace_generationtests pass unchangedPin marker:
Ok→ set,NotFound→ unset, andPermissionDeniedplus one other kind →Ionaming the marker pathreplace_generationsucceeds with the marker present and no pinBootstrap:
active, no epoch recordIoerror naming that pathActiveGenerationPresent, notIoadmit_seed_generation's own errorepochbootstraps onto an empty tree, proving no floor is synthesizedLayout:
CI:
cargo fmt -- --check --config group_imports=StdExternalCratecargo clippy --all-targets -- -D warningscargo clippy --all-targets --features test-support -- -D warningscargo test(422 passed)cargo test --features test-support(422 passed)