Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added

- **Application-generated EQL values in statements**: SQL literals and bound parameters may now carry EQL v3 storage payloads or query-only SEM operands produced by an application. Proxy authenticates every stored ciphertext, requires its authenticated descriptor to name the inferred destination column, and independently re-derives every SEM term before forwarding it without double encryption. Query-only operands contain no ciphertext to authenticate, so Proxy instead validates their version, identifier, term shape, column capabilities, and syntactic query role; they are rejected in storage positions. This includes bare SteVec selector hashes matching `^[0-9a-f]{32}$`: in JSON selector query positions a match is treated as already hashed, while a non-match remains plaintext and is encrypted normally. A matching plaintext selector is inherently ambiguous and is intentionally treated as already hashed. Invalid payloads fail closed with one generic, transaction-aborting error so validation details cannot be used as an oracle.

Compatibility note: on every encrypted column type, Proxy reserves three JSON object shapes for application-generated EQL: storage payloads with top-level `v` and `i` plus at least one of `c`, `h`, or `sv`; scalar query payloads with `v` and `i` plus at least one of `hm`, `bf`, `ob`, or `op`; and SteVec query payloads whose only top-level key is `sv`. An object matching one of these shapes is validated as EQL rather than encrypted as plaintext, and there is no opt-out. Query-only shapes are rejected in storage positions. Before upgrading, audit plaintext application writes for these key combinations; [Invalid encrypted value](docs/errors.md#encrypt-invalid-inbound-eql-payload) includes a `jsonb` scan predicate.

### Changed

- **Upstream TLS verification for client traffic**: Connections with `with_tls_verification` enabled now use a cached snapshot of the system root certificates, loaded once when Proxy starts. Unlike Proxy's background database connections, pg-proto client-traffic connections do not apply operating-system revocation checks or enterprise verification policy. Restart Proxy after changing the system trust store.

### Fixed

- **Configured default keyset selection**: when a connection has not selected a keyset explicitly, Proxy now scopes encryption and decryption to `CS_DEFAULT_KEYSET_ID`. Previously it passed no keyset to ZeroKMS and could silently use the account default instead, deriving different searchable-encryption terms when the two defaults differed. Before upgrading, verify that the configured and account defaults are intentional; values written by an affected version under the unintended account default must be decrypted with that old keyset and re-encrypted under the configured default.

- **PostgreSQL protocol error handling after the pg-proto migration**: Proxy now rejects `require_tls` configurations that omit a certificate, preserves PostgreSQL transaction state when statement mapping fails, returns decryption failures as PostgreSQL errors without dropping the connection, and reloads changed schemas only after PostgreSQL confirms the transaction boundary. Prepared-statement replacement also preserves existing portals and overlapping statement metrics.

## [3.0.1] - 2026-08-05
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

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

67 changes: 67 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [Internal Error](#mapping-internal-error)

- Encrypt errors:
- [Invalid encrypted value](#encrypt-invalid-inbound-eql-payload)
- [Column could not be encrypted](#encrypt-column-could-not-be-encrypted)
- [Could not decrypt data for keyset](#encrypt-could-not-decrypt-data-for-keyset)
- [KeysetId could not be parsed](#encrypt-keyset-id-could-not-be-parsed)
Expand Down Expand Up @@ -345,6 +346,72 @@ If the error persists, please contact CipherStash [support](https://cipherstash.
# Encrypt errors


## Invalid encrypted value <a id='encrypt-invalid-inbound-eql-payload'></a>

CipherStash Proxy rejected an application-generated EQL storage payload or
query operand because it was malformed, unauthentic, intended for another
column, carried unexpected searchable encrypted metadata (SEM), or appeared in
an invalid statement position. The response deliberately does not identify
which validation failed.

### Error message

```
Invalid encrypted value. For help visit https://github.com/cipherstash/proxy/blob/main/docs/errors.md#encrypt-invalid-inbound-eql-payload
```

### How to fix

Regenerate the payload using the same column configuration, keyset, and
credentials as Proxy. Storage payloads must target the inferred destination
column and carry ciphertext plus exactly its configured SEM terms. Query-only
payloads must be used only in query positions.

### Plaintext compatibility

On every encrypted column type, Proxy reserves three JSON object shapes for
application-generated EQL:

- a storage payload with top-level `v` and `i` keys plus at least one of `c`,
`h`, or `sv`;
- a scalar query payload with top-level `v` and `i` keys plus at least one of
`hm`, `bf`, `ob`, or `op`; or
- a SteVec query payload whose only top-level key is `sv`.

An object matching one of these patterns is validated as EQL rather than
encrypted as plaintext. Invalid EQL is rejected, and query-only payloads are
rejected in storage positions. There is no opt-out for this fail-closed check.

Before upgrading, audit JSON values supplied as plaintext to encrypted columns.
For a `jsonb` source column named `value`, this predicate identifies all three
reserved shapes:

```sql
WHERE CASE
WHEN jsonb_typeof(value) <> 'object' THEN false
ELSE (
value ? 'v'
AND value ? 'i'
AND value ?| ARRAY['c', 'h', 'sv', 'hm', 'bf', 'ob', 'op']
) OR (
value ? 'sv'
AND NOT EXISTS (
SELECT 1
FROM jsonb_object_keys(value) AS keys(candidate_key)
WHERE candidate_key <> 'sv'
)
)
END
```

For text sources, first restrict the scan to values that your application knows
are valid JSON, then apply the same predicate after casting them to `jsonb`.
Rename one of these top-level keys or generate the value as an EQL payload
before sending it through Proxy.

<!-- ---------------------------------------------------------------------------------------------------- -->


## Column could not be encrypted <a id='encrypt-column-could-not-be-encrypted'></a>

The column could not be encrypted.
Expand Down
Loading
Loading