Skip to content

core: decide how a carried field declares itself uncuttable before the v2.0 wire freeze #1355

Description

@FSM1

blueprint/core.md "Carried unknown fields" now specifies a produce-side truncation: an encode over the block ceiling drops carried unknown envelope fields until the record fits, rather than refusing to publish. The refusal it replaces was a wedge anyone who could publish at a name could drive — including against the rotation that revokes them — so the truncation is right. What it costs is the other half of the D10 promise: an old client no longer always preserves a newer client's field.

crates/core/src/seal/envelope.rs protects the two fields that carry protocol meaning today by name:

const UNCUTTABLE: &[&str] = &[GRANT_SECTION_KEY, WRITE_SEALED_KEY];

That list is frozen at the moment each binary ships. A v2.0 client has no way to learn that a v2.1 revocationPin, a second sealed body, or any other additive top-level or epochTag field is load-bearing — protocol-bearing-ness is expressed by membership in a constant that predates the field. So every already-shipped reader will silently drop it under pressure, and a v2.1 reader that treats absence as "no pin" is downgraded with nothing to detect.

The cut ranks largest first, which bounds the number of fields dropped rather than the bytes. That is a deliberate choice — it relieves the pressure in one pass instead of leaving the next edit to cut again — but it means a party padding a record with fields smaller than an honest one can aim the first cut at that honest field. Combined with the frozen list, the field most likely to be protocol-bearing is often the largest, which is the one largest-first takes.

Why this has to be decided before the v2.0 format freezes

The rule has to ride the bytes rather than the reader's build date, and that is a wire-format reservation. Retrofitting is impossible once v2.0 binaries are in the field: an old binary cannot be taught a new key name.

The cheapest shape consistent with the existing strict det-CBOR profile is a reserved key-name prefix, the same idea as COSE's crit header (RFC 8152 §3.1):

/// A carried field whose key begins with this marker is critical: a rewrite
/// keeps it or refuses, never silently drops it.
const CRITICAL_KEY_PREFIX: &str = "!";

canonical_key_cmp is length-first, so a one-byte prefix does not perturb ordering semantics. UNCUTTABLE then becomes the two grandfathered names plus the marker test.

Wanted

  • A decision recorded on how a future carried field declares itself uncuttable, taken before the v2.0 wire freeze — a key-name marker, a reserved sub-map, or an explicit "no additive field will ever be protocol-bearing" with the reasoning written down.
  • Whatever lands, stated in blueprint/core.md beside the truncation paragraph and covered by a crates/core test asserting a marked field survives a cut that takes everything else.
  • Revisit the largest-first ranking in the same decision. Smallest-first bounds the bytes a single cut destroys but never relieves the pressure, so every later edit cuts again; neither ordering dominates, and the choice should be made against whichever adversary the marker leaves in scope.

Found by the crypto-privacy gate on the PR implementing the truncation.

Part of #655

Metadata

Metadata

Assignees

No one assigned

    Labels

    comp:corecrates/core — wire formats, crypto, KDF, KATsv2-buildv2 rewrite build slice

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions