G4 — §6.2: `tenant_id = null` encoding in `canonical_context` is unspecified
Labels: §6.2 · spec-gap · blocks-vectors · needs-crypto-review
Blocks: the context/ vector family; every envelope vector for a deployment without a tenancy value.
Gap
§6.1 declares tenant_id : bytes | null, but §6.2’s canonical encoding defines omission behavior only for row_id (“omitted entirely if null”). For tenant_id there are three candidate encodings — omit the field entirely, encode u64be(0) with an empty value, or forbid null — and the spec chooses none. Worse, “omitted entirely” as a general strategy creates an aliasing question: if optional fields can be silently absent, can a crafted set of present-field values serialize to the same byte string as a different context with a field omitted? The length-prefixing in §6.2 exists precisely to prevent cross-field-boundary forgery (its own justification cites RFC 7518 §5.2 and Tink); optional-field omission reopens a variant of the same problem at the field-count level.
This is not cosmetic: canonical_context is both the KDF info (§5.3) and part of the AAD (§6.3). Two implementations disagreeing on the null encoding derive different keys from identical inputs; an aliasing ambiguity is a forgery surface.
Proposed direction (starting point, not a decision)
Make field presence explicit rather than positional-and-optional. Candidates for the issue discussion:
- Presence bitmap: prefix
canonical_contextwith one byte whose bits declare which optional fields follow. Unambiguous, cheap, but a format change to §6.2. - Field count prefix: prefix with
u8(count); fields keep a fixed order. Simpler, but does not say which fields are present unless order+count is injective (it is, for the current two optionals — fragile under future extension). - Encode null as a distinguished length: e.g.,
u64be(0xFFFFFFFFFFFFFFFF)for null vsu64be(0)for present-but-empty. No layout change, but overloads the length field.
Option 1 is proposed. Whatever is chosen, tenant_id = null and tenant_id = b"" (present, zero-length) MUST encode differently or zero-length MUST be forbidden — the issue must pick one and say so.
Justification
The same argument §6.2 already makes for length-prefixing: “Unlength-prefixed concatenation is forgeable across field boundaries” (spec §6.2, citing RFC 7518 §5.2 and Tink’s AES-CTR-HMAC encoding). An encoding in which two distinct FieldContext values can produce one byte string violates the injectivity that canonical encodings exist to provide.
What it breaks
Compatibility-breaking for every derived key and every AAD (all ciphertext, all indexes). Nothing exists yet. If option 1 or 2 is adopted, §6.2’s layout changes and row_id’s existing “omitted entirely” rule is subsumed by the same mechanism — the issue must restate §6.2 in full, not patch it.
Vector obligations
context/: canonical encodings for — all fields present;tenant_idnull;row_idnull; both null;tenant_idzero-length (expected: distinct encoding or a defined rejection).- Negative vectors: byte strings exhibiting the aliasing attempt (a present-field encoding equal to an omitted-field encoding) with the expected rejection/impossibility documented.
kdf/vectors deriving keys from null-tenant contexts.
Review flag
Needs cryptographic review — this is canonical-encoding injectivity, a known forgery surface; a reviewer should confirm the chosen encoding is injective over the full (including future-extended) field set.