Skip to content

aead-value: a cross-language conformance corpus for the FFI transport codec #332

Description

@coderdan

Summary

vitaminc-aead-value::transport and Go's vcffi describe the FFI codec as "a transport encoding, not a storage format … not a compatibility commitment". In practice every non-Rust binding has to implement it, fuzz it, and agree with every other binding on it: Go has one implementation, and the native SDK plan in cipherstash-suite (docs/plans/stack-encrypt-ffi-sdks.md) needs C, Python and then JVM, .NET, Ruby and Elixir to have one each. A codec that each language re-derives from Rust source is a compatibility commitment in everything but name.

Scope change (2026-09-12)

The original part 1, a leading version byte, is dropped. The encoding is ephemeral: it exists between a host writing wasm linear memory and the guest decoding it, within one call, and never reaches a column, a file or the network. Host and guest ship together (the Go module embeds the wasm blob; a native library ships with its binding), so the two sides of the codec are version-locked by construction and a version byte would never get to disambiguate anything.

What stays: documenting the codec in one place as the contract (the tag constants, which reuse the frozen leaf tags for scalars plus ARRAY / OBJECT / PASSTHROUGH and the CT_* node kinds; the u32 little-endian length rule; MAX_DEPTH; the passthrough embedding rule), and keeping the existing codec rather than moving to CBOR: it is small, deterministic, already hardened against hostile input (depth, counts vs remaining bytes, duplicate keys), and CBOR's flexibility would reintroduce exactly the parsing concerns it was written to avoid.

Proposal

  1. Document the codec as the contract, as above, in transport.rs and the vcffi README.
  2. Ship a language-neutral conformance corpus in the repo: known-answer vectors for every leaf tag (the sealed [tag] ++ payload encoding), every codec node kind including the empty containers and passthrough, and a set of hostile inputs with the expected refusal. Go's gen_fixture example is the seed. Each binding's test suite reads the corpus directly; a binding passes conformance by decoding and re-encoding every vector byte-for-byte. Scope extended (CIP-4037, decision 15): the corpus also carries the stack-encrypt guest-level objects every binding must build or read (the init config, the record plan, the per-call options object with its tagged keyset selector, and the record and term result shapes), so a Python or JS request builder proves itself against the same vectors Go does, not just against the codec framing.
  3. Make the tag table's governance rule a test. The README says a new tag "requires a defined decode mapping for every supported language before it ships". With a corpus, adding a tag without a vector, or a language without a mapping, fails CI instead of relying on review.

Why now

The Go binding's vcffi README already says "one codec, one hostile-input test suite, one fuzz". That is the right instinct; the corpus is what lets a second and third language inherit those tests rather than rewrite them.

Context

  • cipherstash/cipherstash-suite docs/plans/stack-encrypt-ffi-sdks.md, decisions 3 and Phase 5.
  • packages/aead-value/src/transport.rs, bindings/go/vcffi.

Activity

  1. self-assigned this
    on Sep 8, 2026
  2. changed the title [-]aead-value: make the FFI transport codec a versioned contract with a cross-language conformance corpus[/-] [+]aead-value: a cross-language conformance corpus for the FFI transport codec[/+] on Sep 12, 2026
  3. coderdan commented on Sep 12, 2026

    @coderdan
    ContributorAuthor

    Scope narrowed: the version byte (part 1) is dropped. The transport encoding is ephemeral, living only between a host write into wasm linear memory and the guest decode within one call, and host and guest always ship together, so both sides of the codec are version-locked by construction and a version byte would never disambiguate anything. The conformance corpus and the documented contract stay, and this now sits ahead of the cross-language fixtures (Go bindings phase 5) rather than ahead of the Go module (phase 4).

  4. coderdan commented on Oct 8, 2026

    @coderdan
    ContributorAuthor

    ADR-0002 (cipherstash/stack#1139) widens part 3 of this issue and adds a
    second kind of vector:

    • The conformance rule becomes "every kind exists in every layer, or is
      listed as an exception".
      Every ValueKind must have a ciphertext tag, an
      equality domain (vitaminc-prf) and an order encoding (the new
      vitaminc-ore), or an entry in an explicit exceptions list kept next to the
      test. The exceptions today are Bool (ciphertext only, because a two-value
      domain leaks through any term), containers and null, and text under block
      ORE until ore.rs ships its chained scheme. A kind missing from a layer
      without an entry fails CI.
    • Value-level vectors. For each case: a host-language value, the
      ValueKind it must map to, and the exact [tag] ++ payload leaf bytes,
      including Unicode normalisation cases. Every binding (napi, Go vcffi,
      Stack Encrypt's Go SDK) runs them. This catches two bindings that map the
      same host type to different kinds, which nothing checks today.
    • The transport framing tags move to 0xF0 to 0xF2 (see aead-value 0.6: rename FfiValue to Value, make it Clone and non_exhaustive, move transport's framing tags #371), so the corpus
      vectors move with them.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions