Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6f0b092
feat(stack-encrypt): encrypt and decrypt owned Vec<S> collections as …
coderdan Nov 2, 2026
c2d7c7c
test(stack-encrypt): enroll owned collection lifecycle evidence
coderdan Nov 2, 2026
585f6a8
fix(stack-encrypt): wipe a column's recovered prefix when an element …
coderdan Nov 2, 2026
34120f5
test(stack-encrypt): pin that no vector encrypts into one ciphertext …
coderdan Nov 2, 2026
c762f40
test(stack-encrypt): enroll the owned-column drop witnesses, which se…
coderdan Nov 2, 2026
4b4a5b4
test(stack-encrypt): pin the borrowed element refusal on the trait, n…
coderdan Nov 2, 2026
8c4e938
build(stack-encrypt)!: move to vitaminc 0.9
coderdan Nov 5, 2026
1f81e22
feat(stack-encrypt): byte strings are opaque scalars, through vitamin…
coderdan Nov 5, 2026
f4e5ba2
build(stack-encrypt): move the fuzz crates to vitaminc 0.9, and impor…
coderdan Nov 5, 2026
348a92e
docs(stack-encrypt): plain-language pass over the owned collections docs
coderdan Nov 6, 2026
14de0da
docs(stack-encrypt): hide the fake key source in the collections example
coderdan Nov 6, 2026
72ffaf6
docs(stack-encrypt): say collection, not column, and hide the fake ke…
coderdan Nov 6, 2026
4f6a1de
feat(stack-encrypt)!: frame readers take a ProtectedRef<[u8]>
coderdan Nov 6, 2026
0587c4d
fix(golang): the auth guest's tests use vitaminc-protected 0.9.1
coderdan Nov 6, 2026
51e19eb
docs(stack-encrypt): link the scalar docs to collections, and show nu…
coderdan Nov 6, 2026
203f93f
feat(stack-encrypt): ValueFrame compares in constant time, held in an…
coderdan Nov 6, 2026
feee160
test(stack-encrypt): import AsProtectedRef only where json_index's dy…
coderdan Nov 6, 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
166 changes: 83 additions & 83 deletions Cargo.lock

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,15 @@ cts-common = { version = "0.43.0", default-features = false }
zerokms-protocol = "0.12.31"
recipher = "=0.3.1"
# The two order-preserving schemes Stack Encrypt's order terms encrypt
# through. Both implement vitaminc-ore's `OrderScheme` against vitaminc 0.8:
# through. Both implement vitaminc-ore's `OrderScheme` against vitaminc 0.9:
# `clww-ore` (cllw-ore renamed) for CLWW ORE and OPE, and ore-rs's
# `BlockOre` for block ORE, which routes a value to its 6-bit fixed-width
# engine (up to ten bytes) or its chained one (wider, or variable length),
# the terms EQL v4's `_ord_ore` domains store.
# vitaminc is a plain dependency of both from here, not a feature. Both are
# pinned exactly: a new release can change every stored order term.
clww-ore = { version = "=0.6.0", default-features = false }
ore-rs = { version = "=0.10.0", default-features = false }
clww-ore = { version = "=0.7.0", default-features = false }
ore-rs = { version = "=0.11.0", default-features = false }

# External dependencies, with the suite's feature lists.
base64 = "0.22.0"
Expand Down Expand Up @@ -139,18 +139,18 @@ web-time = "1.1"
zeroize = { version = "1.8.1", features = ["derive"] }
# This is needed because lock_api 0.4.6 was breaking things and the branch forces the use of 0.4.12
temp-env = { git = "https://github.com/cipherstash/temp-env", branch = "main" }
vitaminc = { version = "0.8.0", features = ["random", "protected", "encrypt"] }
vitaminc-aead = "0.8.0"
vitaminc = { version = "0.9.0", features = ["random", "protected", "encrypt"] }
vitaminc-aead = "0.9.0"
# The transport frame and ciphertext codec every language binding carries
# across its FFI boundary. Optional in stack-encrypt (the `dynamic` feature).
vitaminc-aead-frame = "0.8.0"
vitaminc-encrypt = "0.8.0"
vitaminc-hmac = "0.8.0"
vitaminc-aead-frame = "0.9.0"
vitaminc-encrypt = "0.9.0"
vitaminc-hmac = "0.9.0"
# Normalized equality terms (Unicode 16 NFC text, one integer encoding for
# every width), which `vitaminc-prf`'s `normalize` feature derives over.
vitaminc-normalize = "0.8.0"
vitaminc-normalize = "0.9.0"
# The text order key: the system ADR-0002's fold, pinned to Unicode 16,
# which text ORE and OPE terms are derived over (ADR-0009).
vitaminc-ore = "0.8.0"
vitaminc-prf = { version = "0.8.0", features = ["normalize"] }
vitaminc-protected = "0.8.0"
vitaminc-ore = "0.9.0"
vitaminc-prf = { version = "0.9.0", features = ["normalize"] }
vitaminc-protected = "0.9.1"
12 changes: 12 additions & 0 deletions conformance/stack-encrypt/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ The [lifecycle contract](lifecycle-contract.md) enrolls native pending/KMS tests
heap observations, guest ABI cleanup, Go cancellation/memory tests and public SDK
diagnostics. New probes cover unpolled and in-flight drops, provider failure,
cleanup after a late authenticated type failure, and chunk concurrency/drop.
The owned `Vec<S>` to `Vec<T>` conversion is enrolled under the same
requirements. The conversion's tests cover:

- dropping a collection before it is polled
- dropping a collection while its key request is running
- one logical batch of key requests for each collection, and none for an
empty collection
- wiping the source `Vec` through its guard
- wiping the elements already decrypted, when a later element fails

The owned conversion is a native Rust API. Go reaches batching through its
record interface, and the runtime profile covers that interface.
Existing runtime/read/context vectors retain their provider-call counters.

This adds required execution and explicit observation limits; it removes no
Expand Down
17 changes: 11 additions & 6 deletions conformance/stack-encrypt/lifecycle-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ process. Python and TypeScript are explicitly not enrolled.

| Requirement | Native evidence | Go evidence and limits |
|---|---|---|
| `lifecycle-ownership-v1` | Actual record operations release fulfilment captures exactly once on success, provider failure and unpolled drop; an unpolled encrypt or decrypt makes no key request. Zipped fulfilments do not share key material. | Client keys are consumed/redacted; explicit close and unreachable-client cleanup release guest memory; calls on a closed client fail. GC cleanup is eventual, so callers should close explicitly. |
| `lifecycle-cancellation-v1` | Dropping an encrypt future paused in key generation, or a decrypt future paused in key retrieval, drops the provider future and operation captures; subsequent work on the cipher succeeds. Guest ABI abort spends the handle and drops the operation. | Cancellation before a call sends no request; cancellation between steps or during token fetch leaves unrelated operations usable. A guest step that overruns cancellation closes the client. Discarded late output is wiped. |
| `lifecycle-partial-output-v1` | A batch with a valid first row and a later authenticated type mismatch releases its decoded prefix without marked heap residue. Existing context cases reject late authentication failures without returning partial output. | Generated and raw batch APIs return no partial rows on failure; interrupted HTTP bodies fail; cancelled output is wiped. SDK conversion diagnostics contain no decrypted values. This does not assert erasure of partially populated caller-owned Go objects. |
| `lifecycle-buffer-cleanup-v1` | The existing allocator observer checks selected write/read/JSON parse paths and has a negative control. Guest ABI tests check buffer-registry cleanup, error-buffer release and protected text drop. | Request bodies survive until their owner closes them, then wipe; refused requests wipe; heap growth wipes abandoned backing arrays; guest runtime close releases memory. Post-encryption memory scans are supplementary observations. |
| `lifecycle-ownership-v1` | Actual record operations and owned `Vec<S>` to `Vec<T>` conversions release fulfilment captures exactly once on success, provider failure and unpolled drop; an unpolled encrypt or decrypt makes no key request. Zipped fulfilments do not share key material. | Client keys are consumed/redacted; explicit close and unreachable-client cleanup release guest memory; calls on a closed client fail. GC cleanup is eventual, so callers should close explicitly. |
| `lifecycle-cancellation-v1` | Dropping an encrypt future paused in key generation, or a decrypt future paused in key retrieval, drops the provider future and operation captures, for a record operation or an owned collection; subsequent work on the cipher succeeds. Guest ABI abort spends the handle and drops the operation. | Cancellation before a call sends no request; cancellation between steps or during token fetch leaves unrelated operations usable. A guest step that overruns cancellation closes the client. Discarded late output is wiped. |
| `lifecycle-partial-output-v1` | A batch with a valid first row and a later authenticated type mismatch releases its decoded prefix without marked heap residue. Tests check this for the dynamic record path and for the owned `Vec<Ciphertext>` to `Vec<String>` conversion. For owned collections, drop witnesses are test values that record whether they were wiped. They show that the prefix is wiped, not leaked, after a late failure or an element's panic. Existing context cases reject late authentication failures without returning partial output. | Generated and raw batch APIs return no partial rows on failure; interrupted HTTP bodies fail; cancelled output is wiped. SDK conversion diagnostics contain no decrypted values. This does not assert erasure of partially populated caller-owned Go objects. |
| `lifecycle-buffer-cleanup-v1` | The existing allocator observer checks selected write/read/JSON parse paths and has a negative control. One of those paths encrypts an owned `Vec<String>` collection through its guard. Drop witnesses show that an owned collection's source is wiped before its pending request is returned. That includes a refused context, and a context conversion that panics. Guest ABI tests check buffer-registry cleanup, error-buffer release and protected text drop. | Request bodies survive until their owner closes them, then wipe; refused requests wipe; heap growth wipes abandoned backing arrays; guest runtime close releases memory. Post-encryption memory scans are supplementary observations. |
| `lifecycle-diagnostics-v1` | Guest errors discard untrusted foreign cause messages; failed/successful exports do not hand over stale diagnostics. | Structured errors remain available after failures; SDK errors and redacted display/logging expose types or fields rather than plaintext values. |
| `lifecycle-validation-before-io-v1` | Existing runtime/read/context cases count provider calls. Wrong shape, independently detectable context mismatch and oversized descriptors fail before key I/O; lazy assembly stops after local failure. | Host refusals happen before entering the guest, and shared runtime/read cases exercise the uninitialised guest's preflight boundary. Go does not yet expose native provider counters. |
| `lifecycle-logical-batching-v1` | Instrumented `DataKeySource` calls establish one logical generation batch, retrieval grouping by keyset, descriptor ordering and no work for empty batches. | Go uses the same engine, but this profile does not claim independently instrumented Go key-request counts. |
| `lifecycle-logical-batching-v1` | Instrumented `DataKeySource` calls establish one logical generation batch, retrieval grouping by keyset, descriptor ordering and no work for empty batches, for record operations and for an owned `Vec<S>` collection of any length, including an empty one. | Go uses the same engine, but this profile does not claim independently instrumented Go key-request counts. |
| `lifecycle-provider-chunks-v1` | The actual KMS chunking helper preserves order, handles a final short chunk, fails the whole call on a chunk error, bounds in-flight chunks and releases their futures on cancellation. | This is a native algorithm observation, not a measurement of live HTTP request counts. |

The drop witnesses belong to fulfilment closures and provider futures in real
Expand Down Expand Up @@ -59,7 +59,12 @@ These entries in the index are limitations, not successful requirements:
sealing frame by backtrace and reports excluded frees. It scrubs stack residue
before observing heap frees; it does not prove stack erasure. Its JSON query
paths use borrowed/static path text because owned path copies have separate
cleanup limitations. The lifecycle runner preserves the observer's output.
cleanup limitations. The observer only scans blocks that are freed, so it
misses plaintext that is leaked and never freed. For the owned `Vec<S>`
conversion, the drop witnesses in `packages/stack-encrypt/tests/collections.rs`
record the wipe itself. A test there fails if a source or a decrypted prefix
is leaked. The lifecycle
runner preserves the observer's output.
- **`caller-owned-host-values` — outside the guarantee.** Caller-owned strings,
bytes, objects, returned plaintext, aliases and copies remain the caller's
responsibility. Ownership transfer or protected borrowing must be stated by
Expand Down
34 changes: 31 additions & 3 deletions conformance/stack-encrypt/v1/lifecycle-v1.enrollment.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
"binary": "heap_hygiene",
"success_output": "immediate"
},
"collections": {
"language": "rust",
"lane": "fast",
"workspace": ".",
"package": "stack-encrypt",
"features": ["--all-features"],
"binary": "collections"
},
"runtime": {
"language": "rust",
"lane": "fast",
Expand Down Expand Up @@ -90,7 +98,9 @@
"lifecycle": [
"dropping_unpolled_work_releases_captures_without_key_io",
"dropping_unpolled_decrypt_releases_captures_without_key_io",
"success_and_provider_failure_release_captures_once"
"success_and_provider_failure_release_captures_once",
"dropping_an_unpolled_owned_column_releases_captures_without_key_io",
"an_owned_column_is_one_logical_batch_and_releases_captures_once"
],
"pending": [
"target::pending::tests::zipped_fulfilments_never_share_key_material"
Expand All @@ -108,7 +118,8 @@
"evidence": {
"lifecycle": [
"cancelling_in_flight_work_drops_provider_and_captures",
"cancelling_in_flight_decrypt_drops_provider_and_captures"
"cancelling_in_flight_decrypt_drops_provider_and_captures",
"cancelling_an_owned_column_in_flight_drops_provider_and_captures"
],
"guest-abi": [
"step::tests::an_aborted_operation_is_dropped_and_its_handle_spent",
Expand All @@ -127,7 +138,14 @@
{
"id": "lifecycle-partial-output-v1",
"evidence": {
"heap": ["a_late_read_type_failure_leaves_no_plaintext_on_the_heap"],
"heap": [
"a_late_read_type_failure_leaves_no_plaintext_on_the_heap",
"a_late_type_failure_in_an_owned_column_leaves_no_plaintext_on_the_heap"
],
"collections": [
"a_late_element_failure_wipes_the_plaintext_recovered_before_it",
"a_panicking_element_conversion_wipes_the_plaintext_recovered_before_it"
],
"contexts": ["shared_context_boundaries"],
"go": [
"TestContextBoundaries",
Expand All @@ -142,10 +160,16 @@
"evidence": {
"heap": [
"a_record_encrypted_from_a_value_frame_leaves_no_plaintext_on_the_heap",
"an_owned_column_encrypted_leaves_no_plaintext_on_the_heap",
"a_record_decrypted_with_a_nested_field_leaves_no_plaintext_on_the_heap",
"a_json_document_read_from_a_frame_leaves_no_plaintext_on_the_heap",
"the_spy_sees_an_unwiped_free"
],
"collections": [
"owned_elements_are_wiped_before_the_pending_is_returned",
"owned_elements_are_wiped_when_the_context_is_refused",
"owned_elements_are_guarded_during_context_conversion"
],
"guest-abi": [
"buffers::tests::wipe_all_leaves_no_live_buffer",
"last_error::tests::clearing_wipes_and_frees_the_registered_buffer",
Expand Down Expand Up @@ -205,6 +229,10 @@
"id": "lifecycle-logical-batching-v1",
"evidence": {
"runtime": ["shared_record_runtime_profile"],
"lifecycle": [
"an_owned_column_is_one_logical_batch_and_releases_captures_once",
"an_empty_owned_column_makes_no_provider_call"
],
"pending": [
"target::pending::tests::one_pending_asking_for_many_keys_is_one_call",
"target::pending::tests::all_merges_a_column_into_one_call_preserving_order",
Expand Down
Loading
Loading