You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Rust TextEqQuery probes against a column written by cipherstash-client match nothing, silently #1051
EQL (Encrypt Query Language) is the SQL library we install into a customer's PostgreSQL database as the eql_v3 schema. An encrypted text column with equality search uses the eql_v3_text_eq domain: each row stores a ciphertext (c) and an equality term (hm), a keyed hash of the plaintext. A search sends a probe, an eql_v3.query_text_eq value holding the same kind of hash for the value being searched for, and the index matches rows whose hm equals the probe's.
Today those columns are written by the TypeScript stack through cipherstash-client. #971 adds a second writer: eql-bindings with the stack-encrypt feature, a new producer profile whose equality term is Stack Encrypt's native 32-byte term, hex encoded. The two profiles derive their terms differently, so a term from one never equals a term from the other.
Problem
Nothing in the payload says which profile produced an equality term. hm is a plain hex string in both profiles, and both go through the same eql_v3.query_text_eq domain. So a Rust TextEqQuery probe run against a column populated by cipherstash-client is accepted by PostgreSQL, compared against every row, and matches nothing. The query returns zero rows with no error, which reads exactly like "no such user" rather than "wrong profile".
The plan for #971 (docs/plans/2026-09-12-eql-stack-encrypt-derives.md, section 1) states the requirement: new-profile query terms must never silently probe a column indexed by the old producer. #971 documents the rule in the eql-bindings README but does not enforce it. The ciphertext half is already marked (c begins with stack-encrypt:1:); the term half is not.
Proposal
Decide where the profile marker for terms lives. The two candidates are a prefix on hm for the new profile (mirroring c), or a profile field in the EQL envelope that both the stored row and the probe carry.
Make the SQL side refuse a mismatch loudly: the equality extractor or the query_text_eq domain raises when a probe's profile differs from the stored row's, instead of comparing hashes that can never be equal.
Add a PostgreSQL test in eql-encryption-tests that writes a row with one profile and probes with the other, and asserts an error rather than zero rows.
Record the outcome in the interoperability matrix in the eql-bindings README.
A cheap interim step, if the full fix waits: have the Rust probe builder refuse to run against a column whose first stored c lacks the stack-encrypt:1: prefix, which at least catches the common case in application code.
Closing: not needed. The stack-encrypt text profile in #971 is an interim step on EQL v3, used knowingly by a single consumer. The profile split will be handled by incrementing the EQL version to v4, where the version itself separates the two producers, rather than by a term marker inside v3.
Activity
docs(eql): record the interoperability matrix and the open probe gap
coderdan commentedon Oct 4, 2026
Closing: not needed. The stack-encrypt text profile in #971 is an interim step on EQL v3, used knowingly by a single consumer. The profile split will be handled by incrementing the EQL version to v4, where the version itself separates the two producers, rather than by a term marker inside v3.
docs(eql): the profile split comes from EQL v4, not a term marker in v3
docs(eql): record the interoperability matrix and the open probe gap
docs(eql): the profile split comes from EQL v4, not a term marker in v3
docs(eql): record the interoperability matrix and the open probe gap
docs(eql): the profile split comes from EQL v4, not a term marker in v3
docs(eql): record the interoperability matrix and the open probe gap
docs(eql): the profile split comes from EQL v4, not a term marker in v3
docs(plans): Go binding as typed plans with ctx-first methods
docs(plans): Go binding as typed plans with ctx-first methods
docs(plans): Go binding as typed plans with ctx-first methods