Skip to content

CI only: TC-887 S1 stack #325+#326+#327 (do not merge) - #328

Closed
samgbafa wants to merge 9 commits into
mainfrom
skgbafa/tc-887-s1-ci
Closed

samgbafa wants to merge 9 commits into
mainfrom
skgbafa/tc-887-s1-ci

Conversation

@samgbafa

@samgbafa samgbafa commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Runs the main-only Rust CI on the full S1 stack (#325 4222883, #326 5bd7656, #327 4872828). Close after CI reports; do not merge.

@samgbafa
samgbafa force-pushed the skgbafa/tc-887-s1-ci branch 2 times, most recently from e92bd43 to 58ff6b9 Compare October 17, 2026 08:47
Adds two workspace crates that the node does not use yet:

- tinycloud-at-rest-escrow: 2-of-3 Shamir split of the at-rest root key
  (vsss-rs 5.4.0, GF(256)), shares sealed with age 0.12.1 to pinned
  age1tag1 (hardware) or age1 recipients, the escrow bundle and its
  fingerprint, the canary, the ARK-MACed verification record and the
  recovery wire format.
- tinycloud-at-rest-cli (tinycloud-at-rest): recipients config, nonce,
  inspect, extract-share, check-share, verify-node, share-wrap and
  verify-record. share-wrap verifies the node's TDX quote with DCAP
  collateral (dcap-qvl 0.7.0) and a pinned trust policy before it
  releases a share; shares are read from stdin only.

Fixtures: real TDX quotes and collateral from dcap-qvl 0.7.0 (MIT).
Lockfile: dcap-qvl raises anyhow, borsh, chrono and typenum within
their semver ranges. docs/at-rest-escrow.md describes the formats.
Workspace isolation:
- tinycloud-at-rest-cli is excluded from the node workspace, with its own
  Cargo.lock and CI job (fmt, clippy, tests incl. stock age v1.3.2 interop,
  sha256-pinned). dcap-qvl no longer enters the node's dependency graph:
  `cargo tree -p tinycloud-node -e features` is identical to main and the
  root lockfile only adds the escrow crate's dependencies.

Escrow crate:
- Accept age1yubikey1 (piv-p256) recipients, the format released
  age-plugin-yubikey 0.5.1 prints and decrypts, sealed exactly as the
  plugin's wrap_file_key; age1tag1 stays supported.
- RecipientPolicy: X25519 software recipients are refused unless
  explicitly allowed (tests and self-host).
- Bundle, record and recovery response are parsed version-first;
  RecoveryInfo gains a version. Clearer share/commitment errors.
- forbid(unsafe_code).

CLI:
- TCB status is a fixed class: UpToDate passes; SWHardeningNeeded /
  ConfigurationNeeded / ConfigurationAndSWHardeningNeeded only if allowed
  and every advisory ID is pinned; OutOfDate*, TDRelaunch*, Revoked and
  unknown never pass. Status, advisories and collateral dates are printed.
- Local checks (stale bundle, nonce, binding) run before any PCCS/DCAP
  work; PCCS fetch has a 60 s timeout.
- --expect-efp and --allow-software on every bundle command; exit codes
  0 OK / 1 REFUSED / 2 usage / 3 ERROR; verify-node shows node state and
  shares held; verify-record renamed record-efp and marked NOT
  AUTHENTICATED.
- New: self-test (ceremony step 0 round trip with the TEE's sealing
  code), recipient-key (compare with a YubiKey attestation), and
  verify-compose (compose_hash + every image pinned by @sha256:).
- Tests: every TCB class from a genuine quote and collateral, identity
  event ordering/duplication, v2 preimage, malformed logs, binary-level
  exit codes and messages, stock age interop (X25519 round trip and a
  Go-produced p256tag vector).
…#325)

- verify-compose parses the compose YAML with yaml-rust2 (=0.13.0, CLI
  workspace only) and accepts a strict subset: one document, no anchors,
  aliases, merge keys, tags or duplicate keys, a services mapping without
  include, and every service with an `image` of exactly
  `<name>[:tag]@sha256:<64 hex>` and no build/extends. It also lists the
  other app-compose settings the hash covers. The four reported bypasses
  (comment, flow mapping, quoted key, build) are tests.
- Checked-in piv-p256 vector produced by the released age-plugin-yubikey
  0.5.1; the test identity now rejects what the plugin's from_stanza
  rejects (argument lengths, uncompressed epk, body length).
- Warn on age1tag1 holders (0.5.1 can't decrypt p256tag).
- Version-first parsing keeps duplicate-key rejection; RecoveryInfo mode
  and state are validated.
- CLI: self-test creates --out; malformed bundle/recovery/record JSON is
  exit 3 (ERROR); a hardening TCB class without advisories is refused.
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.
- [at_rest] config (mode off|encrypt|strict, escrow recipients,
  verified_fp, allow_software, escrow_none), deny_unknown_fields; default
  off sets nothing up.
- AtRestRuntime in two phases: created before block storage opens (for
  the storage seal path), keyring installed after migrations. It exposes
  mode(), encrypting() and provider() for storage.
- Boot gate: encrypt/strict refuse to start when the keys are unlocked
  and the escrow gate fails; a node whose root can't unseal boots locked.
- Key mirror on the block store: S3 (_tc/at-rest/) and filesystem
  ({datadir}/at-rest/).
- Endpoints: GET /at-rest/status, /at-rest/escrow, /at-rest/escrow/record,
  /at-rest/recovery?nonce=; POST /at-rest/recovery/shares (body cap, rate
  limit, no share logging). /info carries an at_rest summary.
- Tests: ceremony and recovery through the HTTP API; compose env parsing.
@samgbafa
samgbafa force-pushed the skgbafa/tc-887-s1-ci branch from 58ff6b9 to 4872828 Compare October 17, 2026 09:11
@samgbafa

Copy link
Copy Markdown
Contributor Author

CI green on the full S1 stack (#325 4222883, #326 5bd7656, #327 4872828); closing.

@samgbafa samgbafa closed this Oct 17, 2026
@samgbafa
samgbafa deleted the skgbafa/tc-887-s1-ci branch October 17, 2026 09:48
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