Repository navigation
Conversation
Contributor
Author
Contributor
Author
samgbafa
force-pushed
the
skgbafa/tc-887-s1-escrow
branch
from
October 17, 2026 07:58
05c5200 to
65d3f4c
Compare
samgbafa
added a commit
that referenced
this pull request
Oct 17, 2026
… round 2 (#326) - Rotation writes generation n+1 and generation n's chain in one transaction, with the chain only on a NULL chain; a node that lost the race writes nothing, reloads, and gets AtRestError::Conflict. - Every older generation must open by its chain. At boot under the same root a missing or foreign chain is rebuilt from W and logged; under a new root (recovery) it is an error. Mirrored generations or chains the database lacks are restored at boot. unlock_with checks 1..n (no panic). - One active escrow bundle per generation: activate_escrow locks the generation's key row (FOR UPDATE), plus a partial unique index on PG and SQLite; active_escrow orders deterministically. - DEK rows are mirrored (named by a keyed tag, not the space) before a key is served, restored on a database miss, and a different mirrored DEK with the same id is refused rather than overwritten. - DataKey/ObjectNameKey::from_zeroizing (pub(crate)); the hierarchy no longer copies DEK/NK bytes out of zeroizing buffers. - GateRefusal::Unavailable for store/mirror/parse errors; stored bundles and records go through the versioned parsers; mirror objects carry a version. - Provisioning is refused over escrow rows or any _tc/at-rest/ object; the provisioned log names only the winning ark_id. - Migration down refuses while key rows exist. - Deterministic store-conflict tests (SQLite and PG), multi-thread PG test with concurrent re-splits; excluded test lockfiles refreshed.
samgbafa
force-pushed
the
skgbafa/tc-887-s1-keyring
branch
from
October 17, 2026 07:58
c338a42 to
0ae6fe7
Compare
This was referenced Oct 17, 2026
samgbafa
force-pushed
the
skgbafa/tc-887-s1-escrow
branch
from
October 17, 2026 08:47
65d3f4c to
91c5411
Compare
samgbafa
added a commit
that referenced
this pull request
Oct 17, 2026
… round 2 (#326) - Rotation writes generation n+1 and generation n's chain in one transaction, with the chain only on a NULL chain; a node that lost the race writes nothing, reloads, and gets AtRestError::Conflict. - Every older generation must open by its chain. At boot under the same root a missing or foreign chain is rebuilt from W and logged; under a new root (recovery) it is an error. Mirrored generations or chains the database lacks are restored at boot. unlock_with checks 1..n (no panic). - One active escrow bundle per generation: activate_escrow locks the generation's key row (FOR UPDATE), plus a partial unique index on PG and SQLite; active_escrow orders deterministically. - DEK rows are mirrored (named by a keyed tag, not the space) before a key is served, restored on a database miss, and a different mirrored DEK with the same id is refused rather than overwritten. - DataKey/ObjectNameKey::from_zeroizing (pub(crate)); the hierarchy no longer copies DEK/NK bytes out of zeroizing buffers. - GateRefusal::Unavailable for store/mirror/parse errors; stored bundles and records go through the versioned parsers; mirror objects carry a version. - Provisioning is refused over escrow rows or any _tc/at-rest/ object; the provisioned log names only the winning ark_id. - Migration down refuses while key rows exist. - Deterministic store-conflict tests (SQLite and PG), multi-thread PG test with concurrent re-splits; excluded test lockfiles refreshed.
samgbafa
force-pushed
the
skgbafa/tc-887-s1-keyring
branch
from
October 17, 2026 08:47
0ae6fe7 to
1a3e685
Compare
Adds the S1 key hierarchy to tinycloud_core::at_rest. Nothing in the node constructs it yet. - hierarchy: W = HKDF(root), ARK sealed under W with AAD (ark_id, generation, W label), generation chaining (newest ARK opens older ones), KEK/NK derivation, per-space DEK wrap with length-prefixed AAD (space, dek_id, generation, ark_id). Key bytes only live in zeroizing buffers. - store: at_rest_key, at_rest_escrow and space_data_key rows (insert-or-ignore + re-read), and the KeyMirror trait for sealed-ARK and escrow copies next to the blocks (in-memory mirror for tests). - keyring: AtRestKeyring implements DataKeyProvider (current_key creates and converges, key never creates, object_name_key from ARK gen 1). Boot opens or provisions the ARK, restores rows from the mirror, never provisions over a store/mirror error or orphaned data keys, and boots locked when the root can't unseal. Escrow is kept to the configured holders (re-split on change, old bundles kept), the encrypt gate checks recipients, policy, pinned EFP and an ARK-MACed verification record, and the per-boot share intake runs the ceremony test-unlock (all three pairs) or locked-node recovery (two shares, reseal under the new root). Intake is commitment-checked, slot-capped and rate limited per boot. - migration m20261017_000000_at_rest_keys.
… round 2 (#326) - Rotation writes generation n+1 and generation n's chain in one transaction, with the chain only on a NULL chain; a node that lost the race writes nothing, reloads, and gets AtRestError::Conflict. - Every older generation must open by its chain. At boot under the same root a missing or foreign chain is rebuilt from W and logged; under a new root (recovery) it is an error. Mirrored generations or chains the database lacks are restored at boot. unlock_with checks 1..n (no panic). - One active escrow bundle per generation: activate_escrow locks the generation's key row (FOR UPDATE), plus a partial unique index on PG and SQLite; active_escrow orders deterministically. - DEK rows are mirrored (named by a keyed tag, not the space) before a key is served, restored on a database miss, and a different mirrored DEK with the same id is refused rather than overwritten. - DataKey/ObjectNameKey::from_zeroizing (pub(crate)); the hierarchy no longer copies DEK/NK bytes out of zeroizing buffers. - GateRefusal::Unavailable for store/mirror/parse errors; stored bundles and records go through the versioned parsers; mirror objects carry a version. - Provisioning is refused over escrow rows or any _tc/at-rest/ object; the provisioned log names only the winning ark_id. - Migration down refuses while key rows exist. - Deterministic store-conflict tests (SQLite and PG), multi-thread PG test with concurrent re-splits; excluded test lockfiles refreshed.
…on by name The test rolled back one migration assuming it was the newest; with m20261017_000000_at_rest_keys after it, locate it by name and roll back it and every later migration.
samgbafa
force-pushed
the
skgbafa/tc-887-s1-escrow
branch
from
October 17, 2026 09:11
91c5411 to
4222883
Compare
samgbafa
force-pushed
the
skgbafa/tc-887-s1-keyring
branch
from
October 17, 2026 09:11
1a3e685 to
5bd7656
Compare
Contributor
Author
|
REQUEST_CHANGES: see TC-887 (security review round 2, head |
…t; free junk shares (#326) - Boot collects ARK candidates per generation (database row first, then mirror rows) and unlocks from the newest that opens under W with a chain to generation 1. Only rows on that path are stored, resealed under W; forged, junk, gapped or oversized mirror objects are skipped with a warning. A locked node waits for the pinned bundle's generation and stores mirror rows only once the recovered ARK opens them. - restore_dek stores a mirrored DEK only if its wrap authenticates for the space and id; mirror_dek refuses only a genuine different DEK and replaces objects that don't authenticate. - Mirror reads are capped (MAX_MIRROR_OBJECT, MirrorObjectTooLarge); an oversized object counts as absent. - With verified_fp pinned the keyring never re-splits: it uses the pinned bundle (restored from the mirror if the database lost it) or fails with EscrowPinned. - A share spends the per-boot budget only after it decrypts and matches a commitment. - Tests: forged generation, gap and junk on a healthy database; empty database under the same and a new root; a copied DEK object; pinned re-split; junk never spends the budget.
Contributor
Author
|
REQUEST_CHANGES: see TC-887 (head 5bd7656) |
…only (#326) - establish_escrow runs at boot, after a rotation and after a recovery. An active bundle for other holders refuses the boot (EscrowHoldersChanged): the node never re-splits on its own. With no active bundle, a pinned bundle is re-activated from the database or mirror (the pin authenticates it); unpinned, a mirrored bundle whose mirrored record verifies under the ARK is restored; otherwise a new one is made. A missing record is restored from the mirror when its MAC verifies. A database restore no longer forces a new ceremony. - escrow_view, verification_record, gate and share intake only read the active bundle; no request writes escrow rows or mirror objects. - The newest generation never keeps a chain: boot clears a stale one, and rotate_ark answers a stale chain with an explicit Inconsistent ("retry"), not a permanent Conflict. - AtRestError::is_transient and reason(): stable, non-secret reason codes for API answers. - Tests: two nodes with different holders (no flip, reads write nothing), restore before the record and before the escrow (pinned and unpinned), a forged mirrored record, a stale newest chain.
# Conflicts: # test/w5-policy-runtime-node-e2e/Cargo.lock
…326) - A generation picked from the mirror for its chain keeps the database's own seal (same ARK id), or gets a fresh seal under W if the database lacks it; mirror seal bytes are never stored. Test fails before. - GateRefusal::Unavailable carries AtRestError::reason() (a stable, key-free code), never raw store or mirror error text; the detail is logged server-side.
samgbafa
added a commit
that referenced
this pull request
Oct 17, 2026
- With any at-rest setting, a dstack socket that is present but whose Info can't be read refuses the boot instead of booting as non-TEE, which would skip the measured-compose check and the escrow_none / allow_software refusals (refuse_unknown_tee; test). - Merges #326's non-secret gate reasons, so ShareAnswer.gate and the boot errors never carry raw store or mirror text.
Contributor
Author
|
APPROVE: see TC-887 (security review round 3, head |
#326) - Mirrored ARK objects that can't be read are skipped with a warning whenever the database has rows (still fatal with an empty database, where the mirror is all there is). Record and PITR-bundle restores skip unreadable objects too. - KeyringConfig::require_mirror: boot and recovery mirror writes are fatal only when keys may seal data; otherwise they are logged. - MemoryMirror: plant_unreadable and set_read_only for tests. - Tests: odd names, a directory, a symlink and an unreadable record on a healthy database (pinned, unpinned, locked recovery); fatal with an empty database; a read-only mirror is fatal only with require_mirror.
samgbafa
added a commit
that referenced
this pull request
Oct 17, 2026
…ure map (#327) - S3 and FS mirror listings return only names that get accepts; the FS mirror lists regular files only. FS get never follows symlinks (O_NOFOLLOW), treats non-files as absent and bounds the read itself. - require_mirror = mode != off: with mode off, mirror contents never stop the node (with #326's skip-on-unreadable). - The per-client share-failure map is an LRU of 4096 clients, O(1) per call. - Tests: planted names, a directory and a /dev/zero symlink in the real app's datadir with mode off; FS mirror listing, symlink and size cap; 100k distinct clients stay bounded and fast.
Contributor
Author
|
REQUEST_CHANGES: see TC-887 (correctness round 3, head |
# Conflicts: # tinycloud-core/src/migrations/m20261015_000000_delegation_cursor_index.rs # tinycloud-core/src/migrations/mod.rs
…lit (#326) - D1: a mirrored data key that doesn't authenticate is never ignored and never overwritten. It may be the genuine key under a generation that didn't load (a restore plus a failed boot read). restore_dek fails closed for that space (AtRestError::DekUnavailable, transient, reason dek_unavailable) instead of letting create_dek and mirror_dek replace it; mirror_dek never overwrites an existing object with other bytes; an oversized DEK object fails closed too. Test reproduces the review's D1 chain and checks the original key returns once the object reads. - KeyringConfig::allow_resplit_before_verify: a holder change may re-split at boot only with the flag, no pin, an unverified active escrow and no data key ever made; otherwise EscrowHoldersChanged. - Merges main (#341's m20261017_000000_epoch_order_space_index and its migrated_through m20261015 test).
samgbafa
added a commit
that referenced
this pull request
Oct 17, 2026
…ig (#327) - FS mirror get opens with O_NOFOLLOW | O_NONBLOCK and requires a regular file (fstat on the handle) before reading, so a FIFO under an escrow or record name can't hang the boot (test). - at_rest.escrow.allow_resplit_before_verify passes through to the keyring's guarded pre-ceremony re-split. - Merges #326 (D1 fail-closed data keys, main with #341). - Docs: the re-split flag, dek_unavailable, the never-overwrite DEK rule.
Contributor
Author
|
REQUEST_CHANGES: see TC-887 (security review round 3 delta, head |
…e-lock guards (#326) - allow_resplit_before_verify no longer re-splits the same ARK: the node makes a fresh ARK (generation 1), splits it for the configured holders and retires the old one, so old shares rebuild only a key that never protected anything. - A W-authenticated tombstone per old ARK goes to the mirror first; load() then drops that ARK's key and escrow rows (also from a database restored to before the retirement) and ignores and deletes its ARK, escrow and record objects. Forged or junk tombstones are skipped; a database mixing retired and live rows is Inconsistent. - The database swaps keys in one transaction (replace_all_keys). - may_resplit also reads the mirror: any record- or dek- object, or any stored record, refuses the shortcut. - KeyMirror::delete (MemoryMirror implements it). - Takes S3's #333 keyring changes (efa5bbb): write_locked / in_write_lock, DataKeyError::NotPrepared, the guards on restore_dek, mirror_dek and create_dek, DataKeyProvider::prepare_read and its test. D1 still holds: prepare_read's restore fails closed on a mirrored DEK that doesn't authenticate.
samgbafa
added a commit
that referenced
this pull request
Oct 17, 2026
…K holder fix (#327) - KeyMirror::delete for the S3 (DeleteObject) and filesystem (remove_file, absent is fine) mirrors, used only to retire an ARK that never protected anything. - Merges #326 (fresh-ARK re-split, retirement markers, S3's write-lock guards and prepare_read). - Docs: correcting a holder before the ceremony replaces the root key and makes old shares worthless; the dek_unavailable runbook; mirror write failures stop the boot with encrypt or strict.
…ders (#326) When no active bundle exists and none can be restored, establish_escrow used to split the ARK again for whatever holders were configured. Now any evidence of an earlier bundle for this ARK with other holders refuses the boot (EscrowHoldersChanged), strongest first: - a stored escrow row for this ARK; - a mirrored ceremony record whose MAC verifies under the ARK against its mirrored bundle (a record that doesn't verify proves nothing); - a mirrored bundle object naming this ARK: unauthenticated, so it can only refuse, never be used. A planted one fails closed (like D1); the log names the object, the runbook is to verify it and remove it. The operator route is the same as for an active bundle: allow_resplit_before_verify (a fresh ARK) before the ceremony with nothing sealed, an ARK rotation (TC-1345) after it. may_resplit no longer takes the active bundle (any stored or mirrored record refuses). Tests: rows lost + holders changed refuses without the flag and gives a fresh ARK with it; after the ceremony the verified record refuses even with the flag; a planted bundle only refuses and removing it boots.
Contributor
Author
|
APPROVE: see TC-887 (security review round 4, head |
This branch has not been deployed
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.
TC-887 (S1, PR 2 of 3). Adds the at-rest key hierarchy and the
DataKeyProviderimplementation totinycloud_core::at_rest, plus its migration. Nothing in the node constructs the keyring yet (that is #327), so the only runtime effect is three new empty tables. Merge only after the release cut, and after #325.Stack: based on #325 (
skgbafa/tc-887-s1-escrow), which includes current main (#320, #322 and later). Updates are merged in, not rebased. The one-line explicit key type inenvelope.rs((&*k_obj).into()) is needed here because the escrow crate brings generic-array 1.x into tinycloud-core's graph. Rust CI is dispatched on this branch's head.What's added
at_rest/hierarchy.rs:DataKey/ObjectNameKey::from_zeroizing).at_rest/store.rs:KeyMirrortrait, with versioned mirror objects (sealed ARK rows, escrow bundles, ceremony records, wrapped DEKs) and a 64 KiB read cap;MemoryMirrorfor tests.at_rest/keyring.rs:AtRestKeyring.EscrowHoldersChanged);verified_fp, only the pinned bundle is used (re-activated from the database or mirror), otherwiseEscrowPinned;DataKeyProvider:current_keycreates a DEK, converges with concurrent callers, and mirrors the DEK before serving it;keynever creates one, and restores a missing DEK from the mirror only if its wrap authenticates for that space;object_name_keycomes from ARK generation 1;Locked.gate, read-only):Unavailable.submit_share):rotate_ark: a library function.Conflict.Inconsistent.AtRestError::is_transient()andreason()give stable, non-secret reason codes.m20261017_010000_at_rest_keys(ordered after feat(TC-1274): add epoch_order (space, parent, child) index migration #341'sm20261017_000000_epoch_order_space_index) createsat_rest_key,at_rest_escrow(one active bundle per generation) andspace_data_key.downrefuses while the tables have rows.hkdf,lruandparking_lot(already in the lock), andtinycloud-at-rest-escrow. Dev-only:age. The separate test workspaces' lockfiles are refreshed.Tests (
cargo test -p tinycloud-core --lib at_rest: 68 pass with S2's envelope tests and S3's write-lock test, 1 of them on PostgreSQL; clippy-Dwarningsclean):UnknownKey;require_mirroroff (mode off), a mirror that can't be written never stops the node.allow_resplit_before_verify) only before the ceremony and before any data key (database or mirror). The fix makes a fresh ARK and retires the old one, so old shares rebuild only the retired key, and a database restored to before the fix never loads it;NotPrepared), andprepare_readrestores a lost DEK without creating one.Unavailable), changed recipients, software holders without the opt-in, self-host withoutescrow_none.downrefuses with rows.