Repository navigation
stashgen refuses TextOrdOre and TextSearchOre — EQL stores block ORE terms and the engine derives CLLW ORE #1132
Description
Background
EQL (Encrypt Query Language) is the SQL library we install into a customer's PostgreSQL database. It stores encrypted values and lets the database search them without decrypting. Each EQL type (for example TextEq, IntegerOrd, TextMatch) fixes what a column stores: the ciphertext, plus the search terms that type needs. A search term is a small encrypted value the database can compare, such as an HMAC (a keyed hash, used for equality) or an ORE term (order-revealing encryption, used for < and >).
Stack Encrypt (packages/stack-encrypt) is our Rust encryption engine. Since #1095, a plan field can name an EQL type as its target: the engine then writes the exact value that EQL type stores. The Go SDK uses this through its WASI guest (a WebAssembly build of the engine) and the stashgen code generator.
A target type is producible when the engine can write it. The engine runs the type's own Rust plan, which is the EncryptFrom derive on the type's generated struct. eql-codegen adds that derive only to the types listed in ENCRYPTION_DOMAINS (packages/eql/crates/eql-codegen/src/bindings.rs:316), and today that list is only ("text", "eq"). So of the 51 EQL types, only TextEq is producible. The generated table packages/eql/crates/eql-bindings/src/v3/targets.rs records a reason for each of the other 50, and stashgen refuses them with that reason. #1062 scoped this on purpose to TextEq only. This issue tracks one group of the remaining types.
Only EQL v4 types derive Stack Encrypt's traits. ADR-0002 (#1139) installs the EQL SQL twice: eql_v3 for payloads written by cipherstash-client (the engine behind the TypeScript SDK), and eql_v4 for payloads written by Stack Encrypt. Stack Encrypt's output is not byte-for-byte compatible with cipherstash-client's, so deriving EncryptFrom or DecryptInto on an eql_v3 struct would claim a compatibility that doesn't exist. Every type in this issue becomes producible as its eql_v4_* struct, and no eql_v3_* struct derives either trait. Today's v3 TextEq and TextEqQuery derives move to v4 in #1142. File paths above name the current v3 modules; the v4 ones are generated in #1142.
Problem
Two text types cannot be written because the engine and EQL use different ORE schemes:
| EQL type | Terms EQL stores |
|---|---|
TextOrdOre |
HMAC + ORE |
TextSearchOre |
HMAC + ORE + bloom filter |
EQL's ore term is block ORE: the SQL compares it with the ore_block functions (for example packages/eql/src/v3/common.sql and the *_ord_ore_functions.sql files). The engine derives CLLW ORE terms, a different scheme. The two are not interchangeable: a CLLW term cannot be compared with block-ORE functions. The generated table refuses both types with: "EQL stores a block ORE term and the engine derives a CLLW ORE term; the two are different algorithms". The engine's test target resolver says the same (packages/stack-encrypt/src/dynamic/target.rs:454).
The same mismatch blocks every *OrdOre type in the numeric and date families (for example IntegerOrdOre, DateOrdOre), once their plaintext encoding exists.
Impact: a developer who needs range queries on text with ORE (for example because their column already uses TextOrdOre) cannot write it from Go or a Rust data plan. Today they can only use the OPE types, once those are producible.
Proposal
Decide which side moves. The options:
- The engine derives block-ORE terms. Add a block-ORE index to Stack Encrypt whose terms EQL's
ore_blockfunctions can compare, then addTextOrdOreandTextSearchOretoENCRYPTION_DOMAINSfor the v4 structs. - EQL accepts CLLW ORE. Add CLLW ORE comparison to EQL (new SQL functions, or a new ORE domain), keeping block ORE for existing data.
- Do not produce ORE types from the engine. Mark them permanently unproducible and point users at the OPE types (
TextOrdOpe,TextSearch), which do not have this problem.
Whatever is chosen, prove it end to end in PostgreSQL with a fixture that pins the term bytes, as TextEq was.
Relationship to other work
- Go and Rust plans cannot target EQL domains, so Go writes rows EQL cannot query — make domains plan field targets #1062: made EQL types plan targets, with
TextEqonly. - Superuser-only ORE SQL coverage suite for the deferred
*_ord_oredomains #629: an ORE SQL coverage suite for the*_ord_oredomains. It tests EQL's SQL; it does not make ORE producible. - stashgen refuses TextMatch, TextSearch, TextOrd and TextOrdOpe — the engine derives their terms, but no EQL type is built from them #1131: the match and OPE text types, whose terms already exist in the engine.
- stashgen refuses every number, date and boolean EQL type — produce them in EQL v4 using ADR-0002's encodings #1130: the non-text families, whose
*OrdOretypes also need this decided.
Activity
coderdan commentedon Oct 8, 2026
Decided in ADR-0002 (#1139): option 1. The engine derives block-ORE terms,
through a new vitaminc crate,
vitaminc-ore. Stack Encrypt's terms areproduced only into EQL v4 domains (
eql_v4_*), which are separate Postgrestypes from the
eql_v3_*domains cipherstash-client writes, so the enginedoesn't need to match cipherstash-client's block-ORE bytes. Block ORE uses
each kind's natural width, without cipherstash-client's 8-byte padding.
Text under block ORE waits for ore.rs's chained, variable-length block ORE
(cipherstash/ore.rs#95). Every other kind gets block ORE in vitaminc 0.6.0.
Work: cipherstash/vitaminc#374, then #1140 and #1142 here.