Skip to content

test(TC-887): harness follow-ups 3: secrets in blocks, blocks tied to their DEK, ARK sanity cap - #340

Merged
samgbafa merged 2 commits into
mainfrom
skgbafa/tc-887-harness-followups3
Oct 17, 2026
Merged

samgbafa merged 2 commits into
mainfrom
skgbafa/tc-887-harness-followups3

Conversation

@samgbafa

@samgbafa samgbafa commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

TC-887 harness follow-ups from the #338 approval (review 159550aa). No product code changes.

What changed (tests/at_rest_harness.rs, scripts/at-rest-harness/{harness.sh,README.md})

  1. Blocks are scanned for derived secrets. In both snapshots and in every mode, each block is scanned for each secret the harness derives: root, W, each ARK, KEK, chain, canary and MAC key, NK, tag key, and every unwrapped DEK.

    • The block name is checked as hex (any case) and as base64 or base64url at any alignment, and its last segment is also decoded.
    • The body is checked as raw bytes, then as text.
    • A raw DEK in a TCE1 header salt, or b64url(DEK) as a name, is now RED.
  2. ARK generation cap. ARK_GENERATION_SANITY_CAP = 1000 is a harness-only bound. ARK generations themselves are unbounded (rotate_ark makes g+1), and 1..=9 is the TCE format generation, a different field. The code comment and the README say so. Absurd names still fail fast.

  3. Every block is tied to its mirrored DEK. In encrypt and strict modes, every block must:

    • be named v1/<22>/<43>, with an NK tag that maps to a harness space;
    • parse as TCE1 (main's Header::parse), with a header dek_id that is one of that space's authenticated DEKs;
    • have a name equal to main's sealed_object_name for a logged put (kv_put appends to puts.log; harness.sh exports TC887_PUTS_LOG);
    • open with main's at_rest::open under that DEK, with the store's KvBlock{space, content_hash} AAD, to bytes whose blake3 equals the logged hash.

    Every block is opened, not only one per space.

  4. Free-field bypasses. Now caught:

    • secrets inside a bech32 recipient's data;
    • hex or base64 text inside the armored payload;
    • holder-name splits in any order, with -/_ separators or shifted by 0–7 characters;
    • base32, or text behind a short label: prefix.

    The rest are listed in the README, one line each, as out of scope for a regression harness: unopenable commitments and shares, XOR splits, other separators and encodings, and secrets encrypted under an unknown key.

How it's tested

  • cargo test -p tinycloud-node --test at_rest_harness: 9 self-tests pass, 4 live checks ignored. New or extended self-tests:
    • selftest_blocks_carry_no_derived_secret, run in encrypt and off modes;
    • selftest_blocks_open_under_their_mirrored_dek, with 8 RED cases: wrong dek_id, the wrong DEK, a constant key, a cross-space swap, a bit flip, a non-TCE1 body, an unlogged name, an empty name;
    • the generation-cap cases in selftest_key_mirror_bounds_absurd_generations;
    • 5 new cases in selftest_key_mirror_refuses_escrow_contents_and_derived_keys.
  • cargo clippy … --test at_rest_harness -- -D warnings, cargo fmt --check and bash -n harness.sh are clean.
  • Live, a node built from the S3 sealing stack (feat(TC-887): sealed KV block helpers and plaintext sizes by space tag #329/feat(TC-887): seal KV blocks in the FileSystem store; prepare writes before the invoke lock #333/feat(TC-887): seal KV blocks in the S3 store (S3c, stacked on #333) #336 on S1 round 3), encrypt mode, escrow none: KV GREEN, SQL RED. The check covered 5 control objects and 8 blocks against 8 logged puts, and every block opened to exactly the content the harness wrote.
  • Live tampers:
    • (a) a block's header salt replaced with its space's raw DEK: RED, "raw bytes carry the harness-derived DEK 1", and the block no longer opens;
    • (b) a header dek_id set to 7: RED, "names data key 7, but its space's authenticated DEK mirror(s) are [1]".
  • The same binary with mode off, and main's node, stay RED (20 KV, 16 SQL findings each).

Review fixes (5bf91292), commit 1fd4faf

  • Partial or split secrets in the salt: in every block's TCE1 salt (body[20..52], per Header::build), any 16-byte window of any derived secret is RED. Each salt is checked on its own, so halves split across two blocks' salts flag both blocks. New self-test selftest_salts_hold_no_piece_of_a_secret, run in encrypt and off modes:
    • DEK[0..31] plus one random byte gives exactly 1 finding;
    • DEK halves split across two salts give exactly 2;
    • 64 freshly sealed blocks give none.
  • The README matches the code. The README wasn't narrowed; the code now does what it claims:
    • the byte_views decodings (hex, base64, base64url, base32, bech32 data, armor; prefixes stripped; shifts) apply to the body as text, to the whole name and to every name segment;
    • every run of 32 or more alphabet characters is decoded too;
    • 3 new cases in selftest_blocks_carry_no_derived_secret;
    • the README lists exactly these checks, plus the out-of-scope line: pieces shorter than 32 bytes are only checked in salts.
  • Tested: cargo fmt --check and cargo clippy --test at_rest_harness -- -D warnings are clean, and 10 self-tests pass (4 ignored). The live run against the S3 stack node gives KV GREEN and SQL RED in encrypt mode; the new decodings raise no false RED on real ciphertext. In mode off, KV and SQL are both RED.

… their DEK, ARK sanity cap

Review 159550aa on #338 (Naomi's round-6 rulings):
- Every block's name and bytes are scanned for the harness-derived
  secrets (root, W, ARK_g, KEK_g, chain/canary/MAC keys, NK, tag key,
  every unwrapped DEK): raw bytes, and hex, base64 or base64url text at
  any alignment. A DEK in the TCE1 header salt or b64url(DEK/NK) as the
  object name is RED.
- Encrypt/strict mode ties each block to its mirrored DEK: name
  v1/<22>/<43>, NK tag of a harness space, TCE1 header dek_id among that
  space's authenticated DEKs, name equal to the sealed name of a logged
  put (puts.log), and the block opens under that DEK (main's
  at_rest::open, AAD space + content hash) to the put's content.
- ARK generations get a harness-only sanity cap of 1000 (they are
  unbounded in the node; 1..=9 is the TCE format generation); absurd
  names still fail fast, a chain of 10 passes.
- Cheap free-field bypasses closed: bech32 recipient data, hex/base64
  text inside the armor payload, holder splits with -/_ separators,
  shifts up to 7 chars and any order. The rest is listed as out of
  scope in the README.
Self-tests for each rule.
@samgbafa

Copy link
Copy Markdown
Contributor Author

REQUEST_CHANGES: see TC-887 (head 200a499)

@samgbafa

Copy link
Copy Markdown
Contributor Author

APPROVE: see TC-887 (head 1fd4faf)

@samgbafa
samgbafa merged commit 0592419 into main Oct 17, 2026
30 of 31 checks passed
samgbafa added a commit that referenced this pull request Oct 17, 2026
…ces as out of scope (#344)

Approval nits on #340:
- Encoded runs are now taken per alphabet (all decoders together, and
  base32, hex and base64url each on their own), in block bodies and
  names and in mirror strings, so a base32-encoded secret followed by a
  character outside base32 (9, 1) is still decoded at every alignment.
  Self-test covers a block body and a mirror string, and fails with the
  previous single-alphabet runs.
- README: the out-of-scope list names a secret split into 3 or more
  pieces across salts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant