Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
25 changes: 24 additions & 1 deletion conformance/stack-encrypt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ native Rust against the ciphertexts Go has just produced. It never regenerates
expectations. CI also runs the Go cases on its other supported host platforms.
The same task runs the shared declaration acceptance/rejection corpus in both
languages, with native observations of normalization. It also runs the shared
runtime, read and context cases, and enrolls the existing value-kind, context-field and query-operator
runtime, read, context, value-boundary, query-refusal and raw-frame cases, and
enrolls the existing value-kind, context-field and query-operator
fixtures described below. Set no fixture-update variables when checking conformance.

The Rust runner is `packages/stack-encrypt/tests/conformance.rs`; the Go runner
Expand Down Expand Up @@ -72,6 +73,28 @@ This is representative fixture coverage, not an exhaustive type/operator
matrix. Wider fresh artifact exchange and shared scalar query refusals remain
separate work. The EQL/JSON profiles are described below.

## Value boundaries, query refusals and raw frames

Three profiles close the gaps the representative profiles left. The
[value contract](values-contract.md) defines `record-values-v1` in
`v1/values.json`: every kind at its edges, sealed and SQL nulls for the
fixed-width kinds, write refusals between kinds, frozen typed frames, and
the Go-only host conversions the host refuses before the engine. The
[query-refusal contract](query-refusals-contract.md) defines
`record-query-refusals-v1` in `v1/query-refusals.json`: one plan, every
scalar operator against every field capability that does not answer it,
the range shapes, other-kind values and nulls the contract lists, each refusal's
class and diagnostic, no key I/O, and the stage at which the typed Rust
chain refuses the same query. The [raw-frame contract](raw-frames-contract.md)
defines `record-raw-frames-v1` in `v1/raw-frames.json`: stored batches as
transport bytes, so repeated keys and malformed leaves reach both decoders,
and the table of error classes (transport, leaf, shape, authenticated,
invalid declaration, unsupported host conversion). The runners are
`value_conformance`, `query_refusal_conformance` and `raw_frame_conformance`
in the Rust crate and `TestValueConformance`, `TestQueryRefusalConformance`
and `TestRawFrameConformance` in the Go module; `test:conformance` runs all
six.

## `record-reads-v1` contract

The [read contract](read-contract.md) defines cases in `v1/reads.json` for
Expand Down
35 changes: 31 additions & 4 deletions conformance/stack-encrypt/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ Paths are relative to the repository root. New profile cases run from `v1/`.

| Requirement | Existing evidence | Remaining work |
|---|---|---|
| Scalar value widths and lossless transport | Initial scalar cases and [value-kind requirements](fixture-contract.md#value-kind-requirements), executed by the existing Rust and generated Go runners | Complete per-kind boundary matrices, bytes and broader floats; Python/TS mappings |
| Scalar value widths and lossless transport | Initial scalar cases, [value-kind requirements](fixture-contract.md#value-kind-requirements) and the [value boundary profile](values-contract.md) (`v1/values.json`: every kind's edges, float specials, bytes, decimal and timestamp precision, sealed/SQL nulls per kind, cross-kind write refusals, Go host conversions) | Fresh exchange of every kind; Python/TS mappings; dates before year 1 and the timestamp range ends |
| Owned opaque ciphertext and storage envelope | `packages/stack-encrypt/tests/scalar.rs`, `frozen_bytes.rs`; initial cross-opening cases. Whole-value blobs: `packages/stack-encrypt/tests/opaque_blob.rs` (round trips, the bytes leaf shared with the structural writer, a pinned storage blob and a pinned sealed ciphertext, refusals by class after authentication, source wipe) and `languages/golang/encrypt/gensupport/gensupport_internal_test.go::TestOpaqueReadsARustWrittenBlob`; the storage profile itself is pinned upstream by Vitamin C's golden corpus | Encoded targets; supported version matrix; a fresh sealed exchange of a blob with Go (Go seals, Rust opens, and back) through the conformance task |
| Record declaration and lowering | [Declaration grammar](declaration-grammar.md); `v1/declarations.json` shared by native Rust and Go; native normalization observations; existing record lowering suites | EQL-resolver profile; SDK-to-declaration mappings; runtime source/read shapes |
| Context binding and composition | `packages/stack-encrypt/tests/target.rs`, `descriptor.rs`, `record_lowering.rs`; `languages/golang/encrypt/context_field_test.go`; initial wrong-context cases | Shared composition vectors; expected-context and required-input matrix |
| Query terms and diagnostics | [Query-operator requirements](fixture-contract.md#query-operator-requirements), existing Rust/Go runners and initial equality/query-error cases | Common rejection corpus for every operator, capability and input shape; wider kind/index combinations |
| Query terms and diagnostics | [Query-operator requirements](fixture-contract.md#query-operator-requirements), existing Rust/Go runners, initial equality/query-error cases and the [query-refusal profile](query-refusals-contract.md) (`v1/query-refusals.json`: every scalar operator against every field capability that does not answer it, the listed range shapes, other-kind values and nulls, by class, with the typed chain's refusal stage) | JSON operator and EQL target refusals; moving the typed chain's value refusals before the keyset |
| Missing, SQL NULL and sealed null | `packages/stack-encrypt/src/dynamic/record.rs` null tests; `languages/golang/encrypt/null_test.go` | Explicit language mappings and common missing/null fixtures |
| Fresh cross-language storage | `.github/workflows/tests-golang.yml` exports fresh Go EQL rows for Rust; initial scalar artifact exchange; fixed storage-blob vectors read by both Rust (`opaque_blob.rs`) and Go (`TestOpaqueReadsARustWrittenBlob`) | Every producer/consumer/backend pair and supported released versions; fresh sealed blob exchange |
| JSON queries, extraction and tampering | `packages/stack-encrypt/tests/json_tree_vectors.rs`; `packages/eql/tests/encryption/tests/json_tree.rs`; `languages/golang/encrypt/json_tree_test.go`, `json_postgres_test.go` | Formal operation profile; JSON vectors consumed by all runners |
Expand Down Expand Up @@ -54,8 +54,9 @@ data-key counters and Go's uninitialised guest verify preflight boundaries.
| Native and guest context-source validation | `static-context-with-expectation` | Other source combinations and empty-batch behavior |

These are overlap mappings, not grounds for deleting whole mixed-purpose tests.
Repeated-key trees cannot be emitted by Go's map-based tree encoder; a future
raw-byte profile must cover that boundary explicitly.
Repeated-key trees cannot be emitted by Go's map-based tree encoder; the
[raw-frame profile](raw-frames-contract.md) covers that boundary with
transport bytes both runners decode.

## Consolidation policy

Expand Down Expand Up @@ -123,6 +124,32 @@ it does not replace adapter tests or claim whole-database authentication.
Fresh JSON artifact exchange, released-version matrices and Python/TypeScript
runners remain open.

## Value, refusal and raw-frame checkpoint

Three shared corpora close the boundary gaps the representative profiles
left: `v1/values.json` (the [value contract](values-contract.md)),
`v1/query-refusals.json` (the [query-refusal contract](query-refusals-contract.md))
and `v1/raw-frames.json` (the [raw-frame contract](raw-frames-contract.md)).
Each runs in native Rust and Go WASI under `test:conformance`.

| Existing evidence | Shared case IDs | Retained assertions |
|---|---|---|
| `value_kinds.json` and its runners | `i8-*`, `u8-*`, `i16-*`, `u16-*`, `u32-*`, `i128-*`, `u128-*`, `f32-*`, `f64-*`, `bytes-*`, `decimal-*`, `date-*`, `timestamp-*`, `sealed-*`, `sql-*`, `*-given-*` | Typed Rust term comparisons and generated Go methods |
| Native `dynamic/query.rs` refusal tests, Go `TestAQueryTheFieldDoesNotAnswerIsRefused` | Every `record-query-refusals-v1` case | Tree-notation inputs, the hand-built match options, generated query methods |
| Native typed refusal tests in `tests/op.rs` | The `typed` entries | Case-sensitivity mismatch, batch and `via` forms |
| Native `a_stored_record_that_does_not_fit`, the read profile | `shape-*`, `authenticated-*`, `success-*` | Mutation vocabulary over real ciphertext |
| Guest `test_frame` and `last_error` tests | `transport-*`, `leaf-*` | Plan-frame boundary cases, ABI status mapping |

Unsupported combinations the corpora record rather than skip: the order
index on `bytes` stores a term no operator reads; the typed chain refuses
equality, order and block ORE value refusals only after its keyset loads,
where the binding refuses before any; a repeated key in a stored record is
a transport refusal in both decoders, and the engine's own `field_repeated`
and `repeated_key` guards are reached natively alone; the Go host refuses
the zero `Date`, a decimal scale above 28, a timestamp outside the
decoder's range and non-value Go types before the engine. `NaN` and
negative-zero decimals are excluded as host-dependent.

## Lifecycle checkpoint

The [lifecycle contract](lifecycle-contract.md) enrolls native pending/KMS tests,
Expand Down
117 changes: 117 additions & 0 deletions conformance/stack-encrypt/query-refusals-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Record query-refusal profile

`record-query-refusals-v1` describes the cases in `v1/query-refusals.json`:
what a query is refused for before any term exists, and the few inputs
that look refusable and are not. It is an experimental test contract for
native Rust and Go WASI under `NoTargets`. Python and TypeScript runners
remain pending.

The scalar and runtime profiles cover a handful of equality refusals. This
profile is the shared rejection corpus: every scalar operator against
every field capability of the plan, and the operand shapes and values
listed below, with the class of each refusal, its structured diagnostic,
and the observation that no key is requested.

The runners are `packages/stack-encrypt/tests/query_refusal_conformance.rs`
and `languages/golang/encrypt/query_refusal_conformance_test.go`.

## Plan and inputs

`fields` declares one plan every case queries, each field under context
`["conformance", <name>]`: equality-only text, match-only text, order-only
text (ORE and block ORE), an integer with equality and order, an order-only
float, a 128-bit integer with equality and OPE, a ciphertext-only bool and
text, a sealed-null and a SQL-null integer, and bytes with an order index.

A case names a `field`, an `operator` and an `input`: a scalar `{type, text}`
in the value profile's form, or `{"type": "list", "items": [...]}` for a
range. `accepted` is mandatory; a refused case carries `expected_error`.

## Refusal classes

| `class` | Engine error | Go sentinel | Diagnostic |
|---|---|---|---|
| `source` | `Error::Source` | `ErrEncoding` | `stack_encrypt::dynamic_source` with `reason` and `field` |
| `plan` | `Error::Plan` | `ErrEncoding` | `stack_encrypt::dynamic_plan` with `reason` and `field` |
| `term` | the index's own refusal of the value | `ErrTerm` | the term error's code (`stack_encrypt::empty_term_text`, `term_normalize`, `ore_block_too_long`, `ore_block_trailing_nul`); no reason or field |

Source and plan refusals cover every scalar operator (`eq`, `lt`, `lte`,
`gt`, `gte`, `between`, `matches`) on every field of the plan whose
indexes do not answer it, one case per pair: the equality-only text and
integer, the match-only text, the ORE and block ORE text, the order-only
float, the ciphertext-only bool and text, and the bytes field (equality
and an order index no operator reads). The 128-bit integer (equality and
OPE) and the 32-bit integer (equality and ORE) answer every operator but
`matches`. The SQL-null integer shares the equality-only
integer's indexes and is not repeated. A JSON document operator on an
integer is refused as well. The remaining source refusals are operand
shapes and values: a range that is not a list of two
(a scalar, one bound, three bounds, text), a value of another kind (text or
a fraction for an integer, a negative for an unsigned, an integer for text,
a bool for an integer, a null bound), a null on a field that does not seal
its nulls beside an equality term (ORE and OPE order indexes included), a field the plan does not name and an
operator name the engine does not know.

Term refusals cover match text that cuts no token (too short, empty, only
separators), text no index can normalize (a code point Unicode 16 does not
assign) under match, equality and order, and the block ORE limits (a fold
over 1023 bytes, a trailing NUL) on either bound of a range.

Accepted cases are the positive controls: an inverted range at 32-bit,
128-bit and float widths (neither path refuses it; as SQL's `BETWEEN`, it
matches nothing), the longest block ORE
text, an interior NUL, empty order text, a null equality query on a
sealed-null field, folded match text and a losslessly widened integer.

## Required observations

Both runners run every case through query-term derivation: `QueryTerm` in
Go, `dynamic::query_term` in Rust, which must refuse before returning a
pending operation. For `matches`, both also run verification of an empty
page (`Verify`, `dynamic::verify`) and require the same outcome. Rust also
runs the binding's key-free check, `dynamic::check_query`, and requires the
same diagnostic, and observes that no keyset lookup, data-key generation or
retrieval happens on any path. Go's deterministic guest makes no network
request; that is not a logical key-call count.

The operator vocabulary is closed in Rust (`OpSpec`), so the unknown
operator case is observed at the binding boundary alone; the Rust runner
checks that the case expects exactly that and continues.

## The typed chain

`typed` records how the typed Rust query chain answers the same query, for
the queries it can spell (text on the text fields, a `uint32` range under
ORE and an `int128` range under OPE): `before_keyset`, `after_keyset` or `none`. The Rust runner
builds a one-field typed plan with the index that answers the operator,
runs the chain under a keyset named for the case, and counts the keyset
load. A refusal before the load is `before_keyset`; one after it is
`after_keyset`. The error code must equal the binding's.

Today the two paths refuse the same inputs with the same codes, but not at
the same stage:

| Refusal | Binding (`check_query`, `query_term`) | Typed chain |
|---|---|---|
| Match text with no token or no normal form | before any keyset | before any keyset |
| Equality text with no normal form | before any keyset | after the keyset is loaded |
| Order text with no normal form (ORE, OPE) | before any keyset | after the keyset is loaded |
| Block ORE text too long or ending in NUL | before any keyset | after the keyset is loaded |

The binding checks every index's value refusals in `check_input`; the
typed chain's `Supports::check` runs before the keyset for the match index
alone, and the other indexes refuse the value when they derive its term.
This profile pins the current stage as a documented difference. Moving the
typed refusals earlier is the query-preparation consolidation tracked
separately; when it lands, the `typed` entries change to `before_keyset`
on purpose, and this table with them.

## Scope and remaining work

The corpus covers scalar operators under `NoTargets`. Match options the
index cannot use (an n-gram length of zero) are a declaration refusal, in
`v1/declarations.json` (`match-invalid-ngram-zero`). JSON document
operators against JSON fields, EQL target refusals, `target_limit` inputs and the match-case
mismatch only the typed chain can express remain with their existing
tests. The Go runner does not exercise generated query methods; those keep
their fixture tests. No existing test is removed.
Loading
Loading