Repository navigation
Transport v3, phase 2a: host credential store - #210
Merged
Merged
Conversation
Adds the credstore crate (eternal-credstore), the host's v3 credential store, not yet used by the host: - Pending and active records with the per-slot and host-wide caps, permanent tombstones, and Finalize, Remove and Expire. - Keys wrapped under a per-generation wrapping key held in OS-protected storage (macOS data-protection Keychain, Windows DPAPI), with one writer, the invalidation procedure and startup recovery. - An in-memory snapshot implementing HostCredentials. - Store, restore and crash tests that stop every operation at every write, two fuzz targets, and a persistence bench. The remembered message 1 table in proto is now sized from the credential cap and per-device budgets for answered and unanswered message 1s.
… fixes - Each generation has its own database file, kept until an open sees the keyring committed past it, so a restart lands exactly before or after an operation whichever keyring blob survives. Step 2 starts only after the keyring store succeeded; a failed store is read back, and an unknown outcome fails closed until reopen. - A keyring blob that can't be read is never replaced; the store reports it instead. - Windows opens the keyring value for set and query so the flush runs, and reports a failed flush. macOS adds the Keychain item, or updates only its data. - Keys are boxed in the store and snapshot, and the keyring blob and key wraps are encoded into buffers of their final size. - One generation bound for both formats; rotations past it are refused before any change, and a fresh keyring is seeded with room to rotate. - The database parser enforces every store invariant, including the per-slot pending cap and the replace rules. - Tests: a keyring model that separates visible from durable, in the crash matrix (flushing and not) and the ops fuzzer; fail-closed read-back, damage during a staged rotation, unreadable keyrings, generation bounds, and parser invariants.
- The keyring carries a base generation next to the committed one: the newest generation known to be durable. The database is always written under the base key, which every keyring blob that may survive a crash holds, so every write that returned Ok survives a restart. - A rotation commits a new key, and the database moves to it once it is durable: after the flushed commit on Windows, and at the first open in a later boot on macOS, from a boot-id marker each open records first. A reopen in the same boot changes nothing. - The database header records the newest generation its contents need; an open whose surviving keyring is older rotates again, and a staged intent the database already reflects isn't replayed. - An open syncs the directory before reading. At most two database files exist, and an open reads at most two. - The Keychain update keeps the protection class and borrows the blob without copying it; new records hand over a boxed key; an unreadable read-back after a failed store is unknown. - Tests: unsettled sequences, same-boot and power-loss restarts with every keyring outcome in the crash matrix and the ops fuzzer, the boot marker, promotion, and the file bound.
…anup - A database file whose head is older than the committed generation is a copy from before a rotation: it is never used, and with no current file the open discards and lists it, in the same boot or after a reboot. - Each keyring blob records the id of the boot that stored it, atomically with the commit; an open confirms durability only in a later boot than that (or after a flush on Windows). The separate boot marker is gone. An unreadable boot id confirms nothing, and the store still opens. - A promotion removes every file but the base's before writing the next, and a startup discard removes every other file, so at most two files exist however often cleanup is interrupted. - Tests: restores before promotion, older keyring blobs restored alone, unreadable boot ids, repeated interrupted promotions, and two restore modes in the ops fuzzer; test opens are seeded from the state they open.
- The database header records a fingerprint of its head generation's key next to the head. An open never uses a file that names the committed or staged generation with another key, so equal generation numbers from different keyring histories are told apart. - A rotation goes past any staged generation the keyring holds, as well as the committed one and every generation the database names, so no generation number names two keys. - Promotion happens only when the keyring reports a durable flush; any other result, including an unreadable boot id, waits instead of failing the operation or the open. - Tests: a database from another keyring history, a discard while a rotation is staged, and every procedure with an unreadable boot id. The ops fuzzer forgives a retired record only for a landed re-add, and forgets retirements only at steps that roll the keyring back.
- A keyring blob stored while the boot id couldn't be read is stored again tagged with the boot at the next open that can read it, so the boot after that confirms and promotes it as usual. - The fingerprint is described as what it checks: a file from another history whose head is the keyring's committed or staged generation is never used; a rotation goes past every generation this keyring or its database names. - The ops fuzzer keeps an independent model of the tombstones the operations leave and checks the store against it after every operation; at every open it allows a retired record back only from a file the keyring's history vouches for, after a keyring rollback below its retirement. Two checked-in regression inputs run first in CI.
…ks Remove and expire The fuzz oracle works out from the keyring and files alone whether an open loads, is empty or discards, using the store's candidate rule exactly. A discard's tombstones and retirements are modelled from the files it reads. Retirements are dated by step rather than generation, and an Ok Remove or expire is checked against what it promises. Adds regressions for a discard's tombstones and a two-id Remove, unit tests for a reused generation that is only staged and for a two-id Remove, and words the staged-floor comment precisely.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 2a of transport v3: the host credential store (design §4.3), as a new crate
credstore/(eternal-credstore). It implementsHostCredentialsand isn't wired into the host or the iPad app; shipped v2 behaviour doesn't change.What it does
MAX_CREDENTIALS), enforced when a pairing adds its pending record. A pairing over a cap getsTooMany, and no existing record changes.Retired
dev_ids become tombstones, the last 8 per slot. A rotation past the generation bound is refused before any change.W, using ChaCha20-Poly1305 with(dev_id, g)as associated data. The file is authenticated with HMAC-SHA-256 under a key derived fromWby HKDF; everything comes from ring. The keyring is one OS-protected item:HKCUvalue, flushed before the store returns.CredentialStoreis the only writer.Zeroizingvalues, built when the store opens and after each operation.key()and the otherHostCredentialsmethods never touch the disk. Keys in the store and snapshot are boxed, and the keyring blob and key wraps are encoded into buffers of their final size.HostConfig::max_credentialsand two per-device budgets: answered (stale_per_second) and unanswered (unanswered_per_second, new). Eachdev_idadds a bounded number of entries per second. A table filled by moredev_ids than the cap refuses new answers and never evicts an unexpired entry.Tests
Store behaviour. Caps, duplicate and retired ids, replace rules, every Finalize outcome, Expire boundaries, timestamps that only grow, the snapshot and
HostCredentials, the tombstone limits, and name truncation.Restore. These cases are covered:
Crashes.
tests/crash.rskills every operation, alone and in unsettled sequences of rotations and plain writes on one open store, at every disk and keyring step:Each operation and sequence also runs to completion. Then the process restarts in the same boot, or the power fails: unsynced renames are kept or lost, interrupted writes leave half a file, and the keyring keeps any blob that was visible but not durable (a failed store may have happened, and without a flush any store since the last confirmed one may be lost). Everything runs with a keyring that flushes and one that doesn't, and with a same-boot reopen in the middle of a sequence. After every restart, every operation that returned Ok is there, and a killed one is exactly before or after. In memory, a failed operation leaves exactly before, or exactly after (with writes refused until reopen) once its intent is visible, including when the keyring can't be read back or reads back unparseable. Each recovery is then killed at every step, under every outcome, and the next restart stays within the same states. A staged rotation whose files are all damaged is discarded the same way.
Fuzzing. Two new cargo-fuzz targets:
credstore_formats: the database and keyring parsers, with round trips, MAC checks, and an independent check of every store invariant on each accepted file;credstore_ops: sessions with one store open across many operations, crashes, restarts in the same boot or after a power loss with a chosen keyring outcome (settling only when the input says so), both keyring kinds, damage at rest or mid-operation, and restores of an earlier authentic copy of the database files or of an older keyring blob on their own. It checks memory after each failed operation, after each restart that every acknowledged write is there, and that an Ok Remove leaves neither id held and an Ok expire no pending record 7 days old. It works out from the keyring and files alone what each open must do (load, empty, or discard), and checks that the store's tombstones match an independent model of the operations and of each discard (the last 8 per slot), that a tombstoneddev_idis never re-added, that no retired record is served again unless a keyring rollback or keyring stores lost to a power cut allow it (a keyring from before its retirement, or after a rollback a file whose head is above the keyring's generation), and that what a reopen after a restore serves survives a further power cut.The CI fuzz job lists them, runs the checked-in regression inputs (
proto/fuzz/regressions/) first, and also runs whencredstore/changes.Keyrings. The in-memory keyring separates visible from durable and models boots. On Windows, a native round trip, a test that a failed flush is reported, and the open-time flush. On macOS, the boot id, and an add-then-update round trip that checks the protection class, ignored without a signed build that has a keychain access group.
Proto. The remembered-message table fits every credential at full rate, with every answer budget honoured, and a table filled past the cap refuses without evicting.
Bench.
cargo run --release -p eternal-credstore --features test-hooks --example credstore_bench -- <dir>times persisting a committed timestamp with 64 records.