Skip to content

Seal not bound to round/code-commitment slot — stale-share replay (SGX + launcher) #92

Description

@0xHansLee

Description and context

The kernel seals DKG secret material (dist_key_share.sealed, private_coeffs.sealed) with no binding to its storage slot (round / code commitment), so a sealed blob can be relocated or rolled back into another round's slot and still unseals and is accepted.

  • SGX path (enclave/seal.go): SealWithUniqueKey(data, nil) / Unseal(sealed, nil)nil AAD.
  • store/dkg_store.go: LoadDistKeyShare derives the round purely from the file path ({stateDir}/{round}/{codeCommitment}/dist_key_share.sealed); MarshalDistKeyShare embeds no round. The only binding is the enclave sealing key (binds to TEE identity, not to a round).
  • feat/launcher refactor does not fix this: the new Sealer.Seal(plaintext) interface (enclave/sealer.go) has no AAD/context parameter, and sealdb.Set(key, value) seals value without binding the DB key. The TDX header AAD binds config fields (magic/version/provider_count), not the round slot. SGX still uses nil AAD.

Impact — stale-share replay. A host with write access to the state dir can copy/rollback a sealed share from round N's directory into round N+1's slot; after a cold-cache restart it unseals and is accepted as round N+1's share → the node deals/decrypts under the wrong share → divergence. Reproduced on an SGX devnet during PR #89 review (relocate round N's sealed share into round N+1's slot → restart → N+1 divergence; a restart-only control with no file change stayed clean). Same class as the med3 "store key binding" finding from the PR #75 audit (previously discussed for the launcher/TDX path), now confirmed on the SGX kernel path. Both dist_key_share.sealed and private_coeffs.sealed are affected.

Related: PR #89 (#89), PR #75 audit med3.

Suggested solution

Bind the storage slot into the seal so an unseal at the wrong slot fails:

  • Thread an AAD/context through the seal API: Seal(plaintext, aad) / Unseal(sealed, aad), with aad = round || code_commitment (and the artifact kind).
  • On feat/launcher, extend the Sealer interface accordingly and have sealdb pass the DB key (which already encodes round/code_commitment) as AAD.
  • On the current SGX kernel path, pass the round + code commitment as AAD to SealWithUniqueKey / Unseal.

Because this spans both the SGX kernel path and the feat/launcher seal architecture, coordinate the change across both (single seal-AAD contract used by SGX and TDX).

Secondary hardening (same file): SealToFile uses os.WriteFile with no fsync / temp-file+rename, so a crash right after finalize can lose or truncate the sealed share and drop the next reshare into the divergent recompute fallback — worth an atomic write + fsync.

Definition of done

  • Sealed dist_key_share and private_coeffs blobs bind round + code commitment into AAD; unseal at a mismatched slot fails.
  • A relocated/rolled-back sealed blob is rejected on unseal (regression test reproducing the replay must now fail to unseal).
  • Consistent seal-AAD contract across the SGX kernel path and the feat/launcher (Sealer/sealdb, SGX + TDX) paths.
  • Atomic + fsync write for sealed files.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions