Skip to content

Normative · Part

Limitations

Read this before implementing. Every gap here is known; none is a discovered defect, and listing them is cheaper for everyone than letting a reader find them by writing code.

39. The gap that blocks production: convergence is pinned by enumeration, not as a property

Section titled “39. The gap that blocks production: convergence is pinned by enumeration, not as a property”

The SSP/1 conformance corpus now reaches the state a receiver materializes, and reaches it one enumerated event set at a time. The published corpora carry SSP/1 cases, and the count is served as data at /vectors/index.json rather than described here, so it cannot quietly stop being true.

What is pinned is what two implementations can compare byte for byte: the change event payload map, the value conventions CBOR has no native type for, the COSE_Sign1 parts over synthetic keys — including the empty unprotected bucket and the mesh_id bound as external_aad — and the event identifier derived from the exact signed bytes. Pinned alongside them is every row of the fail-closed inventory as an executable negative: the first place the corpus reached past the bytes and pinned a decision rather than a comparison.

The hybrid logical clock is pinned one step further than bytes. The corpus replays tick and observe sequences and pins the reading after every step, so an implementation that arrives at the right final clock by the wrong route fails rather than passes. The skew ceiling is pinned as a negative: an observation past the bound is rejected, and a case pinning a clamped reading in its place is a failure, not an accepted variant.

Receiver behaviour above the clock is pinned in part. The corpus carries scenario cases for the resolutions this specification’s pre-implementation review settled: the per-column covering set, the inclusive cursor bound, the apply disposition for an unknown payload column, the watermark a skew rejection pins, the restart tick from the persisted mark, and the partial resurrection rebuild an eviction horizon truncates.

The merge those resolutions feed is pinned too, and pinned on the state rather than on the route to it. The corpus fixes the row an ordered sequence builds column by column, which write takes a contested cell when the per-column clock and the arrival order disagree, the site_id that breaks an equal HLC, and the row a tombstone holds empty against a lower-clocked write arriving after the delete. Each of those fixtures delivers the losing event last, so an implementation that reads the last arrival as the answer fails rather than passes by luck. A convergence case then takes one such event set and materializes it under every delivery order, pinning both the single row they all reach and the number of orders walked to reach it: a receiver that converges on twenty-three of twenty-four is a failure rather than a variant.

What is still not pinned is generality. Section 13.5 asks an implementation for property-based testing over randomized orders, and a frozen vector is the opposite of randomized — the corpus enumerates every order of one small set, which is a floor under that testing rather than a substitute for it. So two implementations can now agree on every byte on the wire, on the order those bytes carry, on refusing all the same inputs, and on the state they materialize from the event sets the corpus names, and still diverge on a set it does not name. The corpus cannot close that by growing, because the sets are unbounded and the vectors are not; closing it needs a generator that produces event sets rather than a corpus that lists them. That gap is what still blocks production use.

What the inventory’s cases establish is narrower than conformance and worth stating exactly. Each one pins the offending bytes and the harness recomputes the refusal from them, so no row of the inventory can be weakened into an acceptance without the corpus going red. A negative vector that merely recorded “this input is bad” would check nothing, and would be worse than an absent one for reading as coverage.

The SSP/1 corpus is generated from the specification prose by an independent tool rather than extracted from the implementation under test: a vector extracted from the code under test proves only that the code agrees with itself. Vectors added for the remaining sections SHOULD be generated the same way.

Until the rest of the behavioural half exists, implementers SHOULD treat cross-implementation testing of materialized state as their own responsibility and SHOULD publish disagreements.

40. Security properties SSP/1 does not provide

Section titled “40. Security properties SSP/1 does not provide”

Removing a node stops it receiving future data. It does not and cannot reach back and unlearn what the node already read. Any product language implying otherwise is wrong.

operation: "purge" is reserved and unimplemented. Deletion is logical: a tombstone hides a row from queries and clears its column values, but the event log retains the history required for resurrection rebuild to work.

An implementation MUST fail closed on a purge request rather than pretend it succeeded. Where erasure is a legal requirement, the mechanism that satisfies it is key destruction at a layer above this one, not a protocol operation here.

40.3. No revocation announcement on the wire

Section titled “40.3. No revocation announcement on the wire”

A removal is a local trust-store change plus, where SOP/1 is deployed, a group membership operation. SSP/1 defines no record announcing a revocation to peers.

So a peer learns that a node was removed only when it independently receives the policy change. Until then it may continue accepting events from a node the authority has removed, and implementations SHOULD minimize that window by their own means.

The window is not merely unbounded by this specification. The policy change travels the same mailbox as everything else, and a mailbox operator can withhold and delay selectively without reading anything — so it can extend the window for a peer of its choosing. An implementation’s minimization MUST therefore rest on positive confirmation from each node, never on the absence of an objection.

An earlier revision of this page called the gap cheap to close, on the reasoning that the records channel already carries chained control records. The channel does; the obligation does not follow. A peer that receives a removal record still needs a normative instruction to act on it, and that instruction is a capability — so the tier test applies. It fails tier 2, because the guarantee an omitting node weakens belongs to the removing authority rather than to itself, which makes it tier 3. But tier 3 is carried by the capability declaration, which is unavailable to a mesh that claims no object plane and binds at admission rather than continuously — so it cannot carry this one either.

A capability with no carrying mechanism lands mandatory, and the mandatory form is worse than the gap it closes. A record that peers apply on receipt is a remote trust-store mutation: it would give every authorized node — and therefore any single compromised one, which §40.5 already assumes — the ability to evict any other node from every peer’s keyring, mesh-wide, with neither the k vouches nor the human confirmation that admission requires. Removal MUST NOT be structurally cheaper than admission. Trading a bounded window of stale trust for an unbounded remote eviction primitive is not a trade this revision makes.

Two things must therefore land before a revision can close this gap: the membership authorization record of SOP/1 §5.7 must acquire a registered content type and a wire encoding, and tier 3 must acquire a carrying mechanism that reaches a mesh without an MLS control group and binds after admission. A revision that has both SHOULD prefer a receiver effect that quarantines the removed node’s events over one that deletes its keyring entry: a quarantine is reversible and an eviction is not. Neither prerequisite is specific to revocation, and stating them here is cheaper than letting the next reader rediscover the wall.

40.4. Metadata is not protected by this layer

Section titled “40.4. Metadata is not protected by this layer”

SSP/1 defines message semantics, not transport confidentiality. A mailbox operator sees payload sizes, timing, and which site_id is talking to which. Where that matters, the transport binding must address it; nothing on this page does.

40.5. A compromised authorized node is fully trusted

Section titled “40.5. A compromised authorized node is fully trusted”

Every node in a mesh can author events for any table within its scope. There is no intra-mesh privilege separation for mutable state: a node authorized to write a table can write anything to it. Per-event signatures make authorship attributable, not constrained.

40.6. A rollback of node-local state is undetectable

Section titled “40.6. A rollback of node-local state is undetectable”

Restoring a node from a snapshot or backup taken before it released events rolls its HLC high-water mark backwards, and nothing in SSP/1 detects that it has happened. The restored node reads a mark it wrote itself and holds no record of what it did after the snapshot. Its peers hold the events that would reveal it, but a node’s clock floor is not addressable on the wire, and self-echo suppression makes the node drop those events on arrival.

Requiring the mark to be integrity-protected narrows this rather than closing it. Protection catches a mark forged to an arbitrary value; it does not catch one replayed from an earlier authentic write, which is what a restore performs. Only rollback-resistant storage does, and SSP/1 cannot require a facility every platform does not have.

The mark is also a target in its own right. Deleting or corrupting it stops the node stamping under that site_id until an operator intervenes — the fail-closed behaviour this specification wants, and a denial of service, which are the same event seen from two sides. SSP/1 takes the safe side deliberately. Write access to node-local storage already implies substantial compromise, so this widens what such a compromise achieves rather than opening a new way in.

The mitigation — re-key a rolled-back node rather than resume it — is an operational rule the protocol states and cannot enforce. Closing the gap on the wire would mean returning a node its own maximum from a peer, which makes a node’s clock floor a value some other node supplies and can lie about; SSP/1 does not take that trade in this revision, and an implementation that treats node-local durable state as restorable-from-backup infrastructure is the deployment shape this section exists to warn about.

40.7. HLC-anchored subkey validity does not prevent backdated forgeries

Section titled “40.7. HLC-anchored subkey validity does not prevent backdated forgeries”

An attacker who extracts a node’s event-signing subkey can craft change events carrying any HLC inside that subkey’s validity window, including HLCs in the past. Section 11.1’s skew bound rejects timestamps in the future; a past HLC is accepted. So an event whose HLC is at or below prior_revoked_at passes validity checks — which is not a claim that it is authentic, and this specification does not make that claim.

Three properties bound the exposure, and they are what make it acceptable rather than merely acknowledged:

  1. Scope. Forged events are confined to the compromised node’s own rows. A subkey cannot mint membership, assume a role, vouch, bind transport identity, or issue a successor subkey (section 10.2). The subkey itself therefore opens no escalation path. Whether the attacker has one is a separate question that section 10.2 does not answer: an extraction that came from compromising the node’s own process reaches a software IK_priv at rest in that same process and signs all five directly. This property is stated against an adversary holding the subkey alone, and is load-bearing only under §19.1’s element.
  2. Merge semantics. Per-column last-writer-wins means a backdated forgery loses silently for any column where a peer has already applied an honest event at a higher HLC. The columns actually at risk are those no peer has yet seen an event for above the forged HLC.
  3. Time. not_after caps the window without requiring a revocation to propagate, which is why section 10.1 states a normative bound rather than leaving the window to the implementer.

Implementations SHOULD keep validity windows short. A future revision MAY define sequence receipts — a peer-signed record attesting the highest HLC it has applied for a given node — as an optional mechanism that further constrains the retroactive window for high-assurance deployments. SOVM/1 does not define one: it would add a chained record type and a coordination round to close a residual risk already bounded by all three properties above.

Retirement leaves two residuals, and closes neither of the gaps above.

A predecessor’s vouch can be a vacuous quorum leg, and the seating peer cannot tell. A peer that seats a successor before cutover — the ordinary case — holds no succession record yet, so identity §21.3’s predecessor exclusion cannot bite, and a quorum that includes the predecessor’s vouch counts one operator twice with no local way to know it. What stands between that and a bad seating is identity §21.3 item 2’s human confirmation — mandatory, default deny, reasoning from out-of-band fingerprints — and, on the upgrading node, identity §22.1’s cutover precondition. That precondition is not a defence against a hostile upgrading node, and this specification does not describe it as one: a node that ignores it is a node whose software key an attacker already holds, which is the adversary item 2 exists for.

History survives only as far as the subkey chain a peer held at retirement. The freeze of identity §20.4 refuses every sovm/ssp-eventkey-v1 record for a retired site_id, including a genuinely late one, so events signed under a generation whose record a peer never received stop verifying there permanently. That cannot be engineered away: a late record dated below the cutoff is indistinguishable from the backdated one an attacker holding the retired IK_priv would mint, which is the forgery the freeze exists to stop. A node that completes a sync exchange with each peer before cutover leaves no such gap for those peers; a peer partitioned across the cutover keeps it.

Within those bounds the retained row’s exposure is section 40.7’s, unwidened: the row resolves a kid below a cutoff that cannot move, and grants nothing else.

Retirement does not narrow section 40.3. It is authored by the retiring node about itself, not received from a remote authority, so it supplies neither of that section’s two prerequisites, and a removal is still enforced only once each peer independently receives the policy change.

These were considered and cut rather than overlooked.

Excluded Why
Batch bundle format A compressed columnar bundle whose identifier is a hash of its bytes is content-addressed only if the writer settings, compression level, row-group size and encoding rules are pinned too; without them, two conforming senders cannot produce the same identifier. SSP/1 defines no bundle format rather than an underspecified one.
PAKE-based pairing A password-authenticated key exchange is specifiable only with a full ciphersuite — group, seeds, message encoding, key schedule. The short-authentication-string and QR authenticators in identity cover the same use case with fully specified constructions.
Shared-mesh-key transport envelope Sealing relay payloads under a shared symmetric mesh key would put a second transport-security mechanism in SOVM/1: where SOP/1 is deployed, MLS already carries group secrets. SSP/1 specifies the mailbox contract instead and leaves confidentiality to the binding.
Plaintext relay payloads SSP/1 has one profile and no plaintext mode: a payload the mailbox operator can read violates the mailbox contract’s opacity requirement.