Skip to content

0.14.3 key binding: an anchor certification outranks a session and any earned binding - #61

Open
sym-bot wants to merge 2 commits into
release/0.14.3from
fix/0.14.3-certification
Open

sym-bot wants to merge 2 commits into
release/0.14.3from
fix/0.14.3-certification

Conversation

@sym-bot

@sym-bot sym-bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Why

A nodeId is self-chosen, so "nodeId X has key K" is always a statement some source makes. The R2-1 fix on release/0.14.3 was right that only an unscoped grant whose whole chain is unscoped admin grants up to the anchor is a key-binding source (bindsKey). It was wrong to make that grant view the weakest source. Under that rule, a squatter that reached a receiver first with its own key under a certified nodeId kept the nodeId. A session proves only that its peer holds some key. The anchor's certification says which key is X's.

The design as built

  1. Precedence, strongest first: the anchor pin and this node's own key, then an operator's out-of-band pin, then an anchor certification (the in-force grant view, restricted by bindsKey), then a proven session (trust on first proven use), then earned and legacy-claim bindings. RosterKeyRegistry.binding() is the one decision, and get, source, expected, _identityKey and _keySource all read it.
  2. While a certification X -> K1 is in force:
    • A session proving X with K2 is refused with 1009. The conflict is recorded in roster-conflicts.jsonl, shown in sym status, and raises the metric identity-conflict.
    • A live session that proved K2 is closed.
    • A record claiming X but signed by K2 is not verified.
    • A stored lower-rank binding X -> K2 is superseded. It stays on disk, is marked as superseded (a persisted line, and a member of a compacted line) and recorded as a conflict of kind superseded. It is never used to verify.
    • A session proving K1 stores nothing on the grant's account.
  3. After a revoke or expiry:
    • A nodeId with no other binding is unbound again, and the next session binds it by first proven use.
    • A superseded binding stays conflicted (fail closed). Nothing verifies as X, and every source except a pin is refused, until the operator runs sym keys resolve or a pin or a new certification settles it. A new certification naming the superseded key clears the mark.
  4. A pin outranks a certification, and the conflict is recorded (kind pin-over-certification). A pin made while a session holds the nodeId under another key closes that session. An invite still never repoints a stored binding that the authority has not superseded (design D5).
  5. Scoped grants, and grants under validator or issuer chains, bind no key. Every role evaluation already required the grant's subject key to equal the key passed in. Attestation weighting now passes the key that verified the signature explicitly (_attesterRole(att, key)) instead of re-reading the binding.
  6. No grant ever binds this node's own nodeId.
  7. R2-9 lifetimes: a certification is never stored, so it never ages out. A superseded binding never expires and is outside the new-binding share.

Also in this PR:

  • Certifications that name two keys for one nodeId certify neither, so nothing verifies as that nodeId (kind certifications-disagree).
  • A grant that is not in force supersedes nothing and records nothing.
  • The node offers every certification in force at start and each one that enters at a settle. It enforces live sessions when a certification enters or leaves.
  • keyBindings() lists the key that decides each nodeId and leaves a conflicted nodeId out. XMesh's authorKeyResolver reads the first row's key.
  • grantRole accepts a certified key.
  • sym keys lists a superseded binding as such.

Tests

  • tests/security-0.14.3-cert.test.js (new), 22 tests. 16 fail on 9f6847c; 6 pass there and pin behaviour that already held.
  • Updated tests, each with its reason in the test:
    • roster-keys rows for proven / pinned / grant, and the persistence row.
    • security-0.14.3 R2-1 "never refuses a proving session".
    • authority-node §6.6.9 row.
  • Mutation check: 28 mutants, one per new guard; all 28 caught.
  • Full suite, macOS, --test-concurrency=1 (the machine is shared): 1673 tests, 1669 pass, 0 fail, 4 skipped.
  • Integration: 6 of 6. scripts/gate-two-stock-nodes.mjs passed. npm audit: 0 vulnerabilities.
  • Linux in Docker (node:22-slim v22.23.3, node:20-slim v20.20.2), on the five changed or new test files: 98 of 98 pass on each.
  • CI on this PR (Node 20 and 22): green.

Not changed

  • The spec is not edited here. MMP §3.4 and §6.6.9 still say that a binding is never replaced and that a grant binds only an unbound nodeId. The spec text should follow once this is tested in release.
  • lib/relay.js is not touched.

Do not merge before review.

…d binding

A nodeId is self-chosen, so "X has key K" is a statement some source
makes. The R2-1 fix made the in-force grant view the weakest source, so
a squatter that reached a receiver first with its own key under a
certified nodeId kept it. Only the identity authority says which key is
a nodeId's; a session proves only that its peer holds some key.

Precedence, strongest first: the anchor pin and this node's own key, an
out-of-band pin, an anchor certification (an in-force grant that
bindsKey: unscoped, its chain unscoped admins up to the anchor), trust
on first proven use, a legacy claim.

- RosterKeyRegistry.binding() is the one decision; get/source/expected
  read it. While a certification of X -> K1 is in force, a session or
  legacy claim naming another key is refused and recorded, and a stored
  binding of another key is superseded: kept on disk, marked (a
  persisted line, and a member of a compacted one), recorded with kind
  'superseded', never used to verify, never expiring and outside the
  new-binding share. A session proving K1 stores nothing.
- Revoked or expired, a nodeId nothing else binds is unbound again; a
  superseded binding stays conflicted (fail closed: it verifies nothing,
  and every source but a pin is refused) until the operator resolves it
  or a pin or a new certification settles it.
- A pin outranks a certification (recorded 'pin-over-certification');
  an invite still never repoints a binding the authority has not
  superseded. Certifications naming two keys for one nodeId certify
  neither ('certifications-disagree').
- A grant not in force supersedes and records nothing.
- AuthorityStore.certifiedKeys/certifications, indexed once per
  resolution; the node offers every certification in force at start and
  each one that enters at a settle, and closes a live session whose key
  no longer decides its nodeId when a certification enters or leaves or
  a pin is made (1009, recorded, metric identity-conflict).
- _onSessionConfirmed/_admitSession/_identityKey/_keySource/_earnBinding
  follow binding(); keyBindings() lists the key that decides each nodeId
  and leaves a conflicted one out; grantRole takes a certified key; an
  attestation's role is judged under the key that verified it.
- sym keys lists a superseded binding as such.

Tests: security-0.14.3-cert.test.js (new) has one or more per rule;
the ones encoding "a session outranks the grant view" are updated, each
with its reason.
…sts, SECURITY, README, operator guide)

The R2-1 entry described the grant view as the weakest source; it now
describes the precedence as built: the anchor and this node's own key,
an out-of-band pin, an anchor certification, trust on first proven use,
a legacy claim; supersession, failing closed after a revoke, pins over
certifications, two certified keys, roles under the verified key. The
Tests section gains the new test file, the updated tests with their
reasons, the 28-mutant check and this branch's suite, integration,
gate and audit results. SECURITY and the README no longer say that no
source overrides a different key; the operator guide says that an
unscoped grant certifies its key, and what a wrong key costs.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T18:36:14.243123Z a1615d1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a1615d1de0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/roster-keys.js
Comment on lines +543 to +546
b.superseded = null;
this._track(nodeId, b);
this._persist({ nodeId, key: b.key, superseded: false });
this._log(`Key registry: ${String(nodeId).slice(0, 8)} — a certification in force names the key that was superseded; it is bound to it again`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear conflict records when a certification settles a binding

When a later certification names the stored key, this branch clears b.superseded and makes that key valid again but leaves the earlier superseded entry in _conflicts and roster-conflicts.jsonl. Consequently status().coreSecure.keyConflicts and sym keys continue reporting the old certified key as bound and the now-valid key as refused, telling the operator to resolve a conflict that no longer exists; repeated settled conflicts also consume the bounded conflict slots and can prevent subsequent real conflicts from being recorded. Remove this nodeId's supersession conflict and rewrite the conflict file when settling it, as resolveConflict does.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant