Skip to content

stack-encrypt refactor: separate ciphertext binding from search scope #1487

Closed

Description

@coderdan

Background

Stack Encrypt (packages/stack-encrypt) is the Rust engine that every CipherStash language SDK runs on. Each value it encrypts is sealed under a context. A context is a list of parts, such as a table name, column name, tenant ID, or row ID. The context goes into the data key and into the AAD (additional authenticated data). Encryption checks the AAD but does not hide it.

A value opens only under the same context. Search terms, the values PostgreSQL compares in a query, are also derived under a context.

The Stack Encrypt API plan, merged in pull request #1365, gives every part one of two kinds:

  • A scope is a part a query can supply, such as a table, column, or tenant.
  • A binding ties a value to one record, such as a row ID. A query does not know it.

Problem

Today every part is in both contexts. So a value bound to its row ID also has search terms derived under that row ID. A query for "every user named Ada" cannot produce those terms, so it finds nothing. A developer has to choose between binding a value to its row and searching the column.

Proposal

The plan decided the rule on 7 November 2026.

  1. Search terms derive under the query context. That is the scopes only.
  2. The key and the AAD derive under the stored context. That is every part: the scopes, then the bindings, in that fixed order.
  3. The kind of a part is not in any derivation. Only the values and their order are. The kind decides only which parts are in the query context. So a record with no binding stores exactly the bytes it stores today.
  4. Add the binding kind to the Rust declaration and to the dynamic grammar the Go SDK sends. A binding can be a field stored in the row.
  5. Change the words. The Rust chain's .extend(..) is removed, and the context argument carries scopes and bindings instead. The Go method Extend becomes Scope, with Extend kept as a deprecated alias until 0.1.0. Go adds Bind. The context contract and the conformance README say "scope" where they say "extension".
  6. Keep the frozen vectors. No stored byte names the kind, so they do not change.

Facts about how a row was written, such as who wrote it, are labels, not bindings. The key policy document covers labels. It also covers claims. ZeroKMS, our key service, resolves a claim from the caller's token. Both are separate work.

Sending scopes and bindings to ZeroKMS as key request items is also separate. The plan item "Context parts as key request items" covers it. This issue does not change how the data key is requested.

Acceptance

Each check runs in Rust and in Go:

  • A record with a binding row ID encrypts.
  • A column query finds that record.
  • The record fails to open under another row ID.
  • The stored bytes of a record with no binding are unchanged.
  • A binding passed to a query is refused. Rust refuses it at compile time. The dynamic path refuses it before any key is fetched.
  • Cross-language cases cover a match, a refusal, and a tampered binding.
  • The new search-term form sits beside the rc.3 form. rc.3 rows stay searchable.

Relationship to other work

Plans: the Stack Encrypt API plan and the conformance roadmap.

Activity

  1. self-assigned this
    on Nov 1, 2026
  2. coderdan commented on Nov 9, 2026

    @coderdan
    ContributorAuthor

    The contexts() view and assert_context! from #1568 do not yet cover scenarios 2 to 7 of the context scenarios, because they need the scope(..) and bind(..) attributes this issue adds. #1590 tracks that work; the pull request for this issue should close it, with #1486 covering the field-marker form.

  3. added 11 commits that reference this issue on Nov 9, 2026
    94218fc
    4a26ea2
    6539d37
    2b8e606
    24e872a
    8486bfa
    bf1dd7a
    a1e3118
    6e59605
    6490521
    b3fde17
  4. 6 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions