Skip to content

Verify chain-delegated certs against the xid registry - #434

Merged
MudDev merged 5 commits into
mainfrom
feat/chain-cert-verify
Aug 26, 2026
Merged

Verify chain-delegated certs against the xid registry#434
MudDev merged 5 commits into
mainfrom
feat/chain-cert-verify

Conversation

@MudDev

@MudDev MudDev commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Closes the chain-cert verification TODO at crates/epix-content/src/verify.rs:451 and proves the user-ban path with tests.

Why

When a xite's user_contents.cert_signers names ["chain"], verify_cert returned Ok(true) without checking cert_sign at all. A user directory named by a raw auth address could therefore carry any cert_user_id it liked. For any xite that treats a chain identity as a paid, scarce thing (the EpixSites directory does: it gives vote weight only to chain-registered names), that gap means free identities.

What it does

verify_chain_cert (verify.rs:533) ports the archived Python XidResolverPlugin._verifyXidCert:

  1. Recovers the keccak/ethsecp256k1 signer from cert_sign over {auth_address}#xid/{name}. For a raw-address dir the recovered address must equal the dir address; for an xID-named dir every linked identity is tried as a candidate, matching the Python loop.
  2. Resolves the cert name through the chain via the new VerifyContext::resolve_xid_identities.
  3. Requires the recovered address to be a linked identity of that name. Active passes.
  4. Handles revocation with the 60 second grace window from the Python reference (REVOCATION_GRACE_PERIOD_SECS): content modified at or after revoked_at_time + 60s is rejected, earlier content stays valid, and a revoked identity with no timestamp is rejected outright.

Legacy provider certs (static issuer address, verify_cert_sign) are unchanged.

Failure semantics: fail closed, matching the existing dir-signer pattern. An unresolved name rejects the cert, the same way an unresolved dot-form dir signer already yields "Valid signs: 0/1". Caching reuses the existing epix_chain::xid_signers layer only (30 minute positive TTL, failures never cached), so a transient RPC failure causes temporary rejection and retry, never acceptance. No new cache was added.

Plumbing change: the pre-resolved xid maps now carry full identity records (address, active, revoked_at_time) instead of bare address lists, so XidMap and its call sites in epix-xite, epix-ui, and epix-node are retyped. epix-chain gains resolve_identities_checked; resolve/resolve_checked are unchanged on top of it. Because a raw-address dir's cert name is not among the parent's dir names, every map-building site also resolves chain_cert_xid_name.

Ban path

user_contents.permissions["<id>"] = false was believed unexercised on the Rust node since the Python era. Four new tests prove it works: a banned user's signed content fails verification network wide, the ban works keyed by raw auth address and by cert_user_id, a null permission_rules value zeroes the write quota, and a rule-granted admin can still sign in a banned directory so moderation tombstones keep working. All passed as written, so no fix was needed. Key precedence matches Python's getUserContentRules.

Wrapper null message guard

Separate one line fix in ui/media/all.js. Browser wallet extensions post into every frame, including null payloads. onMessageInner read message.cmd unconditionally and threw a TypeError on each one. It now ignores anything that is not an object with a cmd.

Tests

  • cargo test -p epix-content: 62 passed, 0 failed (11 new).
  • cargo test -p epix-chain -p epix-xite: all passed.
  • cargo test -p epix-ui -p epix-node: full suites, no failures.
  • cargo check --tests clean for every touched crate plus epix-server.

Deliberately left out

  • Cert name to dir name binding for xID-named dirs. A dir named bob.epix carrying a replayed valid alice@xid.epix cert passes the cert check, though the content must still be signed by bob's identity. This matches the Python reference exactly. Tightening it is a semantics change and is flagged here rather than made.
  • Python's freshness refetch (invalidate cache when content.modified is newer than the cache entry). Verification is synchronous and no new caching machinery was added, so revocation data can be up to the existing 30 minute signer-cache TTL stale, bounded further by the 60 second grace design.
  • resolve_xid dir-signer semantics are unchanged: it still returns all linked identities including revoked ones. That is pre-existing behavior outside the cert scope.

MudDev added 2 commits August 25, 2026 21:36
cert_signers ["chain"] used to accept any cert_user_id without checking
cert_sign, so a user directory named by a raw auth address could carry a
forged chain identity. verify_cert now recovers the keccak signer from
cert_sign over "{auth_address}#xid/{name}", resolves the name's linked
identities through the chain, and requires the recovered address to be
one of them. Revoked identities keep their pre-revocation content: a
modified stamp at or past revoked_at_time plus the 60 second grace
window is rejected, matching the archived Python XidResolver behavior.

VerifyContext gains resolve_xid_identities, and the pre-resolved xid
maps now carry full identity records (address, active, revoked_at_time)
instead of bare address lists. Resolution failures stay fail closed: an
unresolved name makes the cert check reject, the same way an unresolved
dot-form dir signer already failed signature counting. Chain lookups
reuse the existing epix-chain signer cache, which now stores the
identity records.

Callers that verify a child manifest also resolve the name from the
child's own cert_user_id (update receiver, child commit, sign path,
stored-manifest walks, promotion recovery, rehome snapshots), since a
raw-address dir's cert name is not among the parent's dir names.

Also adds tests proving the classic ban path: permissions[user] = false
strips the self-signer for bans keyed by auth address or cert_user_id,
permission_rules null zeroes the write quota, and rule-granted admin
signers still verify in a banned user's directory. No fixes were
needed; the checks were already enforced.
Browser extensions (wallet content scripts) post into every frame,
including null and string payloads. onMessageInner read message.cmd
unconditionally, so a null message threw a TypeError on every such
event. Ignore anything that is not an object with a cmd.
Comment thread crates/epix-content/src/verify.rs Fixed
Comment thread crates/epix-content/src/verify.rs Fixed
Comment thread crates/epix-content/src/verify.rs Fixed
Comment thread crates/epix-content/src/verify.rs Fixed
Comment thread crates/epix-content/src/verify.rs Fixed
Comment thread crates/epix-content/src/verify.rs Fixed
MudDev added 3 commits August 26, 2026 13:25
Each test repeated the same fixture, context, verify, and assert block,
which pushed duplication on new code past the quality gate. Those blocks
now go through shared helpers: new_identity, loaded_parent, user_ctx,
ban_parent, user_content, and verify_chain_cert.

The assert helper matches the rejection reason instead of formatting the
error into the panic message. The fixtures are built from signing keys,
so code scanning read those messages as logging key-derived data.

Same coverage: 62 tests pass, and clippy reports the same six pre-existing
warnings as before.
Five contexts implemented resolve_xid with the same body: look the name
up in the pre-resolved map, map the identity records to addresses, fall
back to empty. The trait can do that itself, since resolve_xid_identities
already carries the records.

Each context now implements resolve_xid_identities only, which removes
the repeated blocks that kept duplication on new code above the quality
gate. Behaviour is unchanged: an unresolved name still yields an empty
signer list, so chain-cert and dot-form signer checks still fail closed.
The one remaining override is the file-overlay wrapper, which delegates
to its base rather than reading a map.
sign_user_content_owned picked up the cert hint inline, which pushed its
cognitive complexity past the analyzer limit. The hint is a self
contained step: read the stored content, and add the user's own cert id
when the node signs as them. Moving it out leaves the signing path
reading as a sequence of steps and keeps behaviour identical.
@sonarqubecloud

Copy link
Copy Markdown

@MudDev
MudDev merged commit 77c52b0 into main Aug 26, 2026
12 checks passed
@MudDev
MudDev deleted the feat/chain-cert-verify branch August 26, 2026 20:43
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.

2 participants