Repository navigation
Conversation
A design-level model of how Proxy maps, forwards or refuses each statement, checked with TLC: plaintext must never reach an encrypted column except with UNSAFE_DISABLE_MAPPING. Covers the refuse-by-default route from #484, simple Query batches, Parse and Bind, transactions and concurrent DDL. TLC confirms the safety properties for clients that never disable mapping, and records three findings as expected counterexamples: stale routes under concurrent DDL, statement kinds Proxy never type checks, and a Parse under UNSAFE_DISABLE_MAPPING bound after mapping is enabled again. Three injected faults are each caught. Adds a non-required CI workflow. No production code changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
S3 tracks writes into columns that are encrypted when the write happens; values already stored before DDL makes a column encrypted are out of scope. The README and verification record no longer imply that a change to Proxy's Rust code alone changes the TLC results. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
a2c955f to
dd25d54
Compare
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
Proxy refuses a Bind with PreparedSettingsChanged when the mapping setting differs from the one at Parse, for any statement it type checks. The model left that check out, so it reported a plaintext path (F3) that Proxy built from main does not have. Model the check, drop the candidate-fix instances, and add mutation M4, which removes it and is caught by S1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
cipherstash-agent
left a comment
There was a problem hiding this comment.
Review attestation
Source: Claude Code, /code-review medium plus company-skills:cross-check-review-findings (loaded from cipherstash/skills). Reviewed head 1d3997f.
Result: No critical or blocking issues. 3 minor findings, inline.
Cross-check: The PR had no prior reviews, review threads or comments, so all 3 findings are new.
What I checked
- The model against
requirements.md: properties S1–S4, assumptions A1–A5 and findings F1–F3. - The four mutation patterns in
check.sh. Eachfromstring occurs once inrouting.qnt. - The scenario tests' expectations traced against the actions.
- The workflow and tooling files.
Not done: I did not run Quint or TLC. I did not compare the model with Frontend::route or the other Rust sources it cites (#484 and related). It is a design-level model with no Rust adapter, so that comparison is the open risk, as F3 showed.
The lockfile was skimmed, not audited.
Generated by Claude Code
| atRest.forall(r => r.payload == Cipher or r.unsafe) | ||
|
|
||
| /// S4 (open decision D1): Proxy never forwards plaintext for a column its | ||
| /// schema says is encrypted at the moment it sends the write. |
There was a problem hiding this comment.
Minor (stale comment). This says "S4 (open decision D1)", but requirements.md records D1 as decided on 2026-10-25 (S4 is not a requirement). Suggest "S4 (not required; D1 decided)" so the model and the contract agree.
Source: specs/quint/routing/requirements.md (Decisions, D1; Properties, S4) at head 1d3997f.
Generated by Claude Code
There was a problem hiding this comment.
| - '.github/workflows/quint.yml' | ||
| workflow_dispatch: | ||
|
|
||
| jobs: |
There was a problem hiding this comment.
Minor (hardening). The workflow sets no permissions:, so the job gets the repository's default GITHUB_TOKEN scope. It only checks out and runs checks, so a top-level permissions: contents: read is enough and keeps the token minimal.
Generated by Claude Code
There was a problem hiding this comment.
Fixed in 8791e17: the workflow now sets a top-level permissions: contents: read.
Generated by Claude Code
| echo "$out" >&2 | ||
| fail "$main should satisfy $invariant" | ||
| fi | ||
| grep -E "distinct states found|No violation found" <<<"$out" |
There was a problem hiding this comment.
Minor. If this grep matches nothing (for example Quint changes its output wording), set -e and pipefail end the script here with no FAIL: message, even though TLC passed. Add || true, or fail explicitly with the captured output, so a wording change is not mistaken for a model failure.
Generated by Claude Code
There was a problem hiding this comment.
Fixed in 8791e17: the summary grep now ends with || true, so if Quint changes its wording a passing check still passes.
Generated by Claude Code
- check.sh: the TLC summary grep no longer fails a passing check if Quint changes its output wording. - routing.qnt: S4 comment says D1 is decided. - quint.yml: restrict GITHUB_TOKEN to contents: read. Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
check.sh reported witness counts without enforcing them, so a dead action would pass. Each simulated instance now fails when any witness is reached in 0 traces. Add witnesses for Parse storing a route, an open transaction, a transaction snapshot made stale by concurrent DDL, and a stale prepared route. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add a source-file correspondence table to the routing model's requirements, so a change to one of those functions prompts a model check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address the skill-driven review of the routing model. - Split the unchecked statement kind into CopyFrom, NestedWrite and Opaque, and route them as Frontend::route does since #488: check_copy_into refuses COPY ... FROM into an encrypted or unresolvable column, and a nested write is refused while encrypted columns are configured. The Bind settings check is gated by requires_schema. - A3 and F2 now cover only DO, CALL and SQL EXECUTE. New assumption A6: a nested write takes no parameters. A5 is documented as an over-approximation, which widens F1. Decision D4 keeps Bind and Execute as one step. - Parse is only of an unused name, a refused Parse leaves it unused, and an unknown Bind under UNSAFE_DISABLE_MAPPING is forwarded, as in Proxy. - New witnesses for Bind, Close, transaction end and the mapping setting, so every action has one only it can reach. New mutations M5 to M8. - Two-statement batches hold type-checked statements only, which keeps check.sh all at about 6 minutes. - Fill in the source-file correspondence table and the AGENTS.md pointer. No property changes. TLC: asBuilt 56,994 states, concurrentDdl 445,094, safety holds in both; all expected violations and M1 to M8 reproduce. Signed-off-by: Claude <noreply@anthropic.com>
Requested by Toby
Stacked on #490, which adds the Quint agent skills, the mise tasks and their docs. Merge #490 first.
This is a proof-of-concept formal model, in Quint, of how Proxy decides to map, forward or refuse each statement.
It checks that plaintext never reaches an encrypted column, except when the client uses
UNSAFE_DISABLE_MAPPING.The model is design-level only: it changes no production code, and no Rust code is tested against it yet.
Start with
specs/quint/routing/requirements.md, which covers the scope, assumptions, properties, findings and open decisions, and maps each model element to its Rust code.verification.mdhas the exact results.What it models
Frontend::routefrom refactor(proxy): route each statement through one refuse-by-default decision #484, includingmay_fall_back, the unresolvable-table refusal (fix(proxy): refuse writes to a table Proxy cannot resolve #486), the simple Query batch rule, and theCOPY ... FROMand nested-write refusals from fix(proxy): refuse COPY FROM into encrypted columns, check statements after COPY FROM STDIN #488.PreparedSettingsChanged), gated byrequires_schema.UNSAFE_DISABLE_MAPPING.mapping_errors_enabled.Results (Quint 0.33.0, TLC, exhaustive for each finite instance)
UNSAFE_DISABLE_MAPPINGand concurrent DDL.With a stable schema, Proxy's routing alone keeps them true, even with PostgreSQL's domain check switched off.
main's behaviour before refactor(proxy): route each statement through one refuse-by-default decision #484), no Bind settings check, and noCOPY ... FROMrefusal.Findings to confirm against real Proxy before acting
These are recorded as expected counterexamples, so CI fails if one stops reproducing.
F3(retracted): the first version of the model left out the Bind settings check (PreparedSettingsChanged), so it reported a plaintext path that Proxy does not have. The model now includes the check, and mutation M4 covers it.DO,CALLand SQLEXECUTEare opaque to Proxy, so nothing in routing stops them writing plaintext.COPY ... FROMis no longer part of F2: fix(proxy): refuse COPY FROM into encrypted columns, check statements after COPY FROM STDIN #488 refuses it.PostgreSQL's domain check should reject the value in every case, so none of these is a confirmed leak. Each would need a failing test first.
Requirements changes for review
No property (S1 to S4) changes. These assumptions change:
DO,CALLand SQLEXECUTE;COPY ... FROMand nested writes are modelled.Tooling
specs/quinthas a pinnedpackage-lock.json.check.shcovers typecheck, scenario tests, simulation with every witness required to be reached, TLC and mutations..github/workflows/quint.ymlruns only on changes underspecs/quint. It isn't a required check, and a full run takes about 7 minutes.npm auditflagsadm-zip, which Quint uses only to unpack its own Apalache download, as the README notes.Acknowledgment
By submitting this pull request, I confirm that CipherStash can use, modify, copy, and redistribute this contribution, under the terms of CipherStash's choice.
Generated by Claude Code