Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
48c177e
refactor(eql): adopt the cipherstash-client 0.42.0 representation
freshtonic Jul 20, 2026
c572ebb
docs: handoff for the EQL v3 type checker design
freshtonic Jul 20, 2026
8447215
docs(eql-mapper): record v3 type-checker design (glossary + ADRs)
freshtonic Jul 21, 2026
8967fb2
fix(eql-mapper): correct EqlTraits set difference and bound-error ope…
freshtonic Jul 21, 2026
60dbd65
feat(eql-mapper): carry inert v3 domain identity in the type carriers
freshtonic Jul 21, 2026
95173f3
feat(proxy): load EQL v3 per-column capability from the domain name
freshtonic Jul 21, 2026
6a608ba
refactor(eql): make v3 domain identity non-optional; JSON = JsonLike …
freshtonic Jul 22, 2026
adc132d
docs(eql-mapper): warn that DomainIdentity::canonical may collide wit…
freshtonic Jul 23, 2026
c0e902f
feat(proxy): make the v2-column plaintext warning ops-visible
freshtonic Jul 23, 2026
5395a3b
fix(proxy): exclude eql_v3_json_entry from the column-domain catalog
freshtonic Jul 23, 2026
a7b1f09
docs(eql-mapper): correct Contain and cross-check in the v3 ADRs
freshtonic Jul 22, 2026
fc0c40a
feat(eql-mapper): ADR-0003 v3 rewrite pipeline + term-selection found…
freshtonic Jul 22, 2026
74b790e
feat(eql-mapper): rewrite scalar comparisons to the v3 functional-ind…
freshtonic Jul 22, 2026
2f93ce3
feat(eql-mapper): retarget aggregate and jsonb functions to eql_v3
freshtonic Jul 22, 2026
47c1524
feat(eql-mapper): retarget JSON containment to eql_v3
freshtonic Jul 22, 2026
b37e941
feat(eql-mapper): functionalise JSON -> / ->> field access to eql_v3
freshtonic Jul 22, 2026
c208058
fix(eql-mapper): route LIKE/ILIKE through TokenMatch capability checking
freshtonic Jul 22, 2026
87b1e33
feat(eql-mapper): rewrite LIKE/ILIKE to the v3 match_term form
freshtonic Jul 22, 2026
faffed4
test(eql-mapper): cover NOT LIKE/ILIKE, UPDATE SET cast, native LIKE,…
freshtonic Jul 23, 2026
464a4fd
feat(eql-mapper): support @@ fuzzy match and rewrite to match_term
freshtonic Jul 22, 2026
a28fa78
test(eql-mapper): cover ord_ore rewrite via the explicit domain form
freshtonic Jul 22, 2026
68c5a17
docs(changelog): record the EQL v3 migration
freshtonic Jul 23, 2026
7918cd2
fix(eql-mapper): map JSON domains to the eql_v3.query_json operand twin
freshtonic Jul 24, 2026
b84aff3
fix(eql-mapper): emit JSON selectors as text, not a jsonb query cast
freshtonic Jul 24, 2026
073ddaf
fix(proxy): bind JSON selectors as bare text, not JSON-quoted
freshtonic Jul 24, 2026
e6d9096
fix(proxy): decrypt JSON field access results and bind selectors as text
freshtonic Jul 24, 2026
d2ec92e
feat(proxy): support encrypted JSON field range comparisons
freshtonic Jul 25, 2026
2b5faa8
docs(eql-mapper): cleanup after the v3 rewrite — stale strings and notes
freshtonic Jul 22, 2026
6775e22
refactor(showcase): convert the healthcare showcase to EQL v3
freshtonic Jul 22, 2026
ff5a867
feat(proxy): infer encrypt config from the EQL v3 schema
freshtonic Jul 22, 2026
fa757ab
test(integration): convert the shared test schema to EQL v3 domains
freshtonic Jul 22, 2026
cb3b7d6
test(integration): make the integration tests v3-shaped
freshtonic Jul 22, 2026
0842fe7
docs: sweep remaining EQL v2 references to v3
freshtonic Jul 22, 2026
68f56d3
feat(proxy): index encrypted JSON arrays for search (ArrayIndexMode::…
freshtonic Jul 25, 2026
5264706
feat(mapper): fuse JSON field equality into a value-selector needle
freshtonic Jul 25, 2026
474af85
feat(proxy): compose and encrypt the JSON equality value selector
freshtonic Jul 25, 2026
15066cc
docs: changelog entry for encrypted JSON field equality
freshtonic Jul 25, 2026
b7c7a77
refactor: model output params as derived from N input params
freshtonic Jul 26, 2026
51435b0
refactor(mapper): each rewrite rule casts its own operands
freshtonic Jul 26, 2026
c1cc172
fix(mapper): ORDER BY an encrypted column must order by its ordering …
freshtonic Jul 26, 2026
7259d6b
fix(proxy): project encrypted query operands with into_query_operand
freshtonic Jul 26, 2026
477729e
fix(mapper): GROUP BY an encrypted column must group by its equality …
freshtonic Jul 26, 2026
b579e8e
fix(mapper): project grouped columns through eql_v3.grouped_value
freshtonic Jul 27, 2026
6c6ead8
chore(deps): upgrade EQL and eql-bindings to 3.0.3
freshtonic Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This document describes the internal architecture of CipherStash Proxy. It's int

## Overview

CipherStash Proxy sits between an application and PostgreSQL. It intercepts SQL statements over the PostgreSQL wire protocol, determines which columns are encrypted, rewrites queries to use [EQL v2](https://github.com/cipherstash/encrypt-query-language) operations, encrypts literals and parameters, forwards the transformed query to PostgreSQL, and decrypts results before returning them to the application.
CipherStash Proxy sits between an application and PostgreSQL. It intercepts SQL statements over the PostgreSQL wire protocol, determines which columns are encrypted, rewrites queries to use [EQL v3](https://github.com/cipherstash/encrypt-query-language) operations, encrypts literals and parameters, forwards the transformed query to PostgreSQL, and decrypts results before returning them to the application.

The two most interesting pieces of the system are:

Expand Down Expand Up @@ -116,10 +116,12 @@ The current rules:

| Rule | What it does |
|---|---|
| `CastLiteralsAsEncrypted` | Replaces plaintext literals with `eql_v2.cast_as_encrypted(ciphertext)` |
| `CastParamsAsEncrypted` | Wraps parameter placeholders (`$1`, `$2`, ...) with encrypted casts |
| `RewriteContainmentOps` | Transforms `col @> val` to `eql_v2.jsonb_contains(col, val)` |
| `RewriteStandardSqlFnsOnEqlTypes` | Rewrites `min()`, `max()`, `jsonb_path_query()` etc. to `eql_v2.*` equivalents |
| `RewriteEqlComparisonOps` | Rewrites scalar comparisons: `col <op> x` → `eql_v3.<term>(col) <op> eql_v3.<term>(x)` (term chosen from the column's domain: `eq_term`/`ord_term`/`ord_term_ore`) |
| `RewriteEqlMatchOps` | Rewrites `LIKE`/`ILIKE`/`@@` to `eql_v3.match_term(a) @> eql_v3.match_term(b)` |
| `RewriteContainmentOps` | Rewrites JSON `@>`/`<@` to `eql_v3.jsonb_contains`/`jsonb_contained_by`, and `->`/`->>` to `eql_v3."->"`/`"->>"` |
| `RewriteStandardSqlFnsOnEqlTypes` | Rewrites `min()`, `max()`, `jsonb_path_query()` etc. to their `eql_v3.*` counterparts (`count()` stays native) |
| `CastLiteralsAsEncrypted` | Replaces plaintext literals with the ciphertext cast to the column's v3 domain (`::public.eql_v3_*`) or, for a query operand, its query twin (`::eql_v3.query_*`) |
| `CastParamsAsEncrypted` | Wraps parameter placeholders (`$1`, `$2`, ...) with the same v3 domain casts |
| `PreserveEffectiveAliases` | Maintains column aliases through transformations |
| `FailOnPlaceholderChange` | Postcondition check that prepared statement placeholders weren't corrupted |

Expand Down Expand Up @@ -191,7 +193,7 @@ When encrypting values for a statement, many columns may be `NULL` or non-encryp

## Schema Management

The proxy discovers the database schema at startup and reloads it periodically. Schema loading queries PostgreSQL's `information_schema` to discover tables and columns, then checks `eql_v2_configuration` to determine which columns are encrypted and what index types they support.
The proxy discovers the database schema at startup and reloads it periodically. Schema loading queries PostgreSQL's `information_schema` to discover tables and columns, including each column's domain type. EQL v3 columns are self-configuring domain types (e.g. `eql_v3_text_search`), so both the type-checker's capability view and the encrypt config are inferred from that single schema load — the column's domain name determines which columns are encrypted, their token type, and their searchable capabilities. There is no `eql_v2_configuration` table.

Schema state is stored behind an `ArcSwap`, which provides lock-free reads with atomic updates. This means query processing never blocks on a schema reload — readers always get a consistent snapshot.

Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Changed

- **EQL v3 (searchable encryption)**: Proxy now targets EQL v3. Encrypted columns are declared with self-configuring, typed `jsonb` domains (for example `eql_v3_text_search`, `eql_v3_integer_ord`, `eql_v3_json_search`) that encode both the scalar type and the column's searchable capabilities in the column type itself, replacing EQL v2's opaque `eql_v2_encrypted` composite type and its separate `eql_v2_configuration` table. The bundled `cipherstash-client` is upgraded to 0.42.0 and EQL to 3.0.3. Existing v2-encrypted data and schemas must be migrated to v3.

### Added

- **Encrypted full-text match with `@@`**: The `@@` operator is now supported on encrypted text columns whose domain carries a match (bloom-filter) term, rewritten to the EQL v3 `eql_v3.match_term` form.

- **Equality on encrypted JSON fields**: `WHERE col -> 'field' = 'value'` now works on encrypted JSON columns, in both the simple and extended query protocols, and in the `->>` and `jsonb_path_query_first(col, path) = value` spellings. `<>` is supported as the negation. The field and the value are combined into a single encrypted value-selector needle and matched by containment, so a query never reveals the field and value separately. Matching is exact and case-sensitive; the value must be a JSON scalar (comparing a whole object or array to a field is rejected — use containment with `@>` instead).

### Fixed

- **`LIKE`/`ILIKE` capability checking**: `LIKE` and `ILIKE` on an encrypted column are now gated by the column's token-match capability. Previously these predicates bypassed capability checking and were silently accepted on columns that do not support fuzzy match; they are now rejected with a capability error.

## [2.2.4] - 2026-06-18

### Fixed
Expand Down
19 changes: 12 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ CipherStash Proxy is a PostgreSQL proxy that provides **transparent, searchable

Key capabilities:
- Zero-change SQL queries - applications connect to Proxy instead of directly to PostgreSQL
- EQL v2 (Encrypt Query Language) for searchable encryption using CipherStash ZeroKMS
- EQL v3 (Encrypt Query Language) for searchable encryption using CipherStash ZeroKMS
- Support for encrypted equality, comparison, ordering, and grouping operations
- Written in Rust for performance with strongly-typed SQL statement mapping

Expand All @@ -34,14 +34,14 @@ Key capabilities:
- Language-specific integration tests (Python, Go)

**Showcase (`packages/showcase/`):**
- Healthcare data model demonstrating EQL v2 encryption
- Healthcare data model demonstrating EQL v3 encryption
- Example of realistic encrypted application with foreign keys and relationships

### Request Flow

1. Application connects to Proxy (port 6432) using standard PostgreSQL protocol
2. Proxy intercepts SQL statements and uses EQL Mapper to analyze query structure
3. For encrypted columns, Proxy transforms SQL using EQL v2 operations
3. For encrypted columns, Proxy transforms SQL using EQL v3 operations
4. Encrypted queries are sent to actual PostgreSQL database
5. Results are decrypted before returning to application

Expand Down Expand Up @@ -166,12 +166,17 @@ Available targets: `DEVELOPMENT`, `AUTHENTICATION`, `CONFIG`, `CONTEXT`, `ENCODI

## EQL Integration

CipherStash Proxy uses EQL v2 for searchable encryption. Key concepts:
CipherStash Proxy uses EQL v3 for searchable encryption. Key concepts:

- **Plaintext columns** - standard PostgreSQL data types
- **Encrypted columns** - use `eql_v2_encrypted` type in schema
- **Searchable operations** - equality, comparison, ordering work on encrypted data
- **Index support** - ORE (Order Revealing Encryption) and Match indexes for performance
- **Encrypted columns** - use a self-configuring EQL v3 domain type in the schema
(e.g. `eql_v3_text_search`, `eql_v3_integer_ord`, `eql_v3_json_search`); the
domain encodes the token type and searchable capabilities, so there is no
separate `add_search_config` call
- **Searchable operations** - equality, comparison, ordering, text match, and JSON
traversal work on encrypted data, gated by the column's domain capability
- **Index support** - functional indexes over the term-extraction functions
(e.g. `CREATE INDEX ON t (eql_v3.ord_term(col))`)

EQL is automatically downloaded and installed during setup. Use `CS_EQL_PATH` to point to local EQL development version.

Expand Down
29 changes: 17 additions & 12 deletions CONTEXT-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ resolved — a missing one is expected, not a gap to fill upfront.
| Context | Path | Domain |
|---|---|---|
| Proxy | [`packages/cipherstash-proxy/`](./packages/cipherstash-proxy/CONTEXT.md) | PostgreSQL wire protocol, connection and message handling, client authentication, TLS, ZeroKMS key management, encrypt/decrypt of column values |
| EQL Mapper | [`packages/eql-mapper/`](./packages/eql-mapper/CONTEXT.md) | SQL parsing, type inference over statements, schema analysis, transformation rules that rewrite plaintext SQL into EQL v2 operations |
| EQL Mapper | [`packages/eql-mapper/`](./packages/eql-mapper/CONTEXT.md) | SQL parsing, type inference over statements, schema analysis, transformation rules that rewrite plaintext SQL into EQL v3 operations |
| Integration | `packages/cipherstash-proxy-integration/` | End-to-end test harness — container fixtures, encrypted-scenario coverage across the proxy and mapper together |
| Showcase | `packages/showcase/` | Healthcare example data model demonstrating EQL v2 encryption with realistic relationships |
| Showcase | `packages/showcase/` | Healthcare example data model demonstrating EQL v3 encryption with realistic relationships |

`packages/eql-mapper-macros/` is proc-macro support for EQL Mapper, not a context of its
own — treat it as part of the EQL Mapper context.
Expand All @@ -26,22 +26,27 @@ own — treat it as part of the EQL Mapper context.
- **Identity across the seam**: EQL Mapper's `TableColumn` and Proxy's `Identifier` are
the same `table.column` pair under two names. That pair is the only key joining a typed
AST node to its encryption config.
- **Capability across the seam — currently broken.** Proxy marks every encrypted column
with *all* `EqlTrait`s (`packages/cipherstash-proxy/src/proxy/schema/manager.rs:146`)
because it derives them from the PostgreSQL column type alone. The column encrypt
config, which knows the SEM terms actually configured, is loaded by a separate manager
that never meets the schema loader. EQL Mapper's bound checking is therefore
unreachable in production: a query needing `Ord` on a column with no ordering SEM term
type-checks cleanly and fails later. Read `EqlTraits` on a column as *intended*
capability, not observed.
- **Capability across the seam.** Under EQL v3 each encrypted column is a self-configuring
domain type (e.g. `eql_v3_text_search`) whose typname encodes both the token type and the
searchable capabilities. The schema loader resolves that domain to a `DomainIdentity` and
the exact `EqlTraits` it supports (`packages/cipherstash-proxy/src/proxy/schema/manager.rs`,
via `proxy/schema/eql_domains.rs`), so the traits handed to EQL Mapper are *observed*, not
a blanket grant. EQL Mapper's bound checking is therefore effective in production: a query
needing `Ord` on a column whose domain has no ordering capability is rejected at type-check
time. The encrypt config is derived from the same domain type
(`proxy/encrypt_config/from_domain.rs`), so schema view and encrypt config no longer
disagree.

## Shared vocabulary

Terms defined once for the whole system live here rather than in any one context.

- **EQL v2** — Encrypt Query Language; the SQL-level encoding that makes encrypted
- **EQL v3** — Encrypt Query Language; the SQL-level encoding that makes encrypted
values searchable.
- **`eql_v2_encrypted`** — the PostgreSQL column type holding an encrypted value.
- **EQL v3 domain types** — encrypted columns are self-configuring PostgreSQL DOMAINs over
`jsonb` (e.g. `eql_v3_text_search`, `eql_v3_int8_ord`, `eql_v3_json_search`). The domain's
typname encodes the token type and the searchable capabilities, replacing EQL v2's opaque
`eql_v2_encrypted` type plus a separate `eql_v2_configuration` table.
- **ZeroKMS** — CipherStash's key management service, which the proxy calls to encrypt
and decrypt.
- **Keyset** — the ZeroKMS key collection a workspace encrypts against.
Expand Down
130 changes: 124 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ sqltk = { version = "0.10.0" }
cipherstash-client = { version = "=0.42.0" }
cipherstash-config = { version = "=0.42.0" }
cts-common = { version = "=0.42.0" }
# EQL v3 domain catalog: maps Postgres domain names to (token type, SEM terms).
# The authoritative source for a column's domain identity (ADR-0002); only the
# SchemaManager depends on it, keeping eql-mapper wire-format-agnostic.
eql-bindings = { version = "=3.0.3" }

thiserror = "2.0.9"
tokio = { version = "1.44.2", features = ["full"] }
Expand Down
Loading
Loading