Skip to content

refactor(keys): one WIF-to-identity-key matcher and one home for key enums - #373

Merged
PastaPastaPasta merged 2 commits into
stagingfrom
refactor/key-matching
Sep 7, 2026
Merged

PastaPastaPasta merged 2 commits into
stagingfrom
refactor/key-matching

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Owner

Fourth PR of the anti-slop series (after #370, #371, #372). One matcher for "which identity key does this WIF correspond to", and one home for the key enums.

The five copies

Service Filter order Handled disabledAt Notes
state-transition-service match, then check purpose/level no rejected MASTER by < CRITICAL
dpns-service match, then check yes same logic, different log prefix
token-service filter, then match yes the one that got the ordering right
tip-service filter purpose, match, check key id yes transfer keys
identity-service match, then require MASTER no used by add-key flows

Each also inlined a hex decoder for the wasm key's data getter. The ordering matters: matching first means a MEDIUM key registered from the same private key as a HIGH key (e.g. after a rotation) can win the byte comparison and then fail the level check, masking the valid key. Only token-service filtered first; its own comment explains the bug.

What replaces them

lib/crypto/keys.ts:

  • matchIdentityKey(wif, keys, { network, purpose, allowedSecurityLevels, keyId? }). Filters enabled keys by purpose and level first, then matches, then enforces a pinned key id. Returns { ok: true, key, match } with the original key object so callers hand it straight to createSignerFromWasmKey, or { ok: false, reason } with 'no-candidates' | 'no-match' | 'wrong-key-id'.
  • toKeyInfo(key): reads both shapes an identity key arrives in (the wasm getter object with keyId/purposeNumber/hex data, and the JSON/app shape with id/purpose/base64 data) into the numeric IdentityPublicKeyInfo the matcher compares.

lib/crypto/identity-keys.ts: KeyPurpose, SecurityLevel, KeyType and the name helpers, replacing the copies in signer-service.ts, key-validation.ts (KEY_PURPOSE) and identity-service.ts (IDENTITY_KEY_*), and the bare 0/1/2 comparisons in eight more files (encryption-key-lookup, private-feed-service, direct-message-service, key-validation-service, registration-wizard, identity-update-builder, …).

lib/crypto/keys.test.ts adds ten specs: both key shapes, the level-first ordering with a weaker duplicate present, disabled keys, purpose filtering, the no-candidates/no-match distinction, and the pinned-id path.

Behaviour changes

  • state-transition and dpns now filter before matching (the token-service ordering). For an identity with exactly one key per private key, nothing changes. For an identity that has the same key at two levels, the operation now finds the usable one instead of failing.
  • identity-service.validateKeySecurityLevel keeps matching against every key first so its error can name the wrong level the user supplied, then requires MASTER. Same outward behaviour as before.

Validation

npm run lint, npx tsc --noEmit, npm run test (70 specs), npx knip (empty), npm run build all clean locally. Independent review is running; findings land as a follow-up commit here.

🤖 Generated with Claude Code

…enums

Five services each carried their own copy of 'decode the wasm key's hex data, build IdentityPublicKeyInfo, call findMatchingKeyIndex, then filter by purpose and security level' (state-transition, dpns, token, tip, identity). They filtered in different orders: token-service filtered by level BEFORE matching so a weaker duplicate of the same key could not mask the right one, the others matched first. lib/crypto/keys.ts now has matchIdentityKey(wif, keys, { purpose, allowedSecurityLevels, keyId? }) which always filters first and returns the original key object for the signer, plus toKeyInfo() that reads both the wasm getter shape (keyId/purposeNumber/hex data) and the JSON shape (id/purpose/base64 data). Ten specs cover both shapes, the level-first ordering, disabled keys, and the pinned-key-id path.

KeyPurpose/SecurityLevel/KeyType lived in signer-service (as consts), key-validation (KEY_PURPOSE), identity-service (IDENTITY_KEY_*) and as bare 0/1/2 literals in eight more files. They now live once in lib/crypto/identity-keys.ts with the name helpers, and every comparison against a raw purpose/type/level number goes through them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 7, 2026

Copy link
Copy Markdown

Deploying yappr-v2 with  Cloudflare Pages  Cloudflare Pages

Latest commit: a6fec25
Status:⚡️  Build in progress...

View logs

@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 1821aea8-7521-4d77-9c1c-32c60b83c46d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

🕓 Ready for review — 33 ahead in queue (commit a6fec25)
Queue position: 34/34

… uses the shared matcher

Review findings on the matcher: toKeyInfo now resolves purpose/level/type through the shared name-aware resolvers and returns null when any cannot be resolved, instead of defaulting an unknown purpose to AUTHENTICATION. matchIdentityKey distinguishes 'rejected' (the WIF matches an enabled key that fails the purpose/level filter, returned so the log can name it) from 'no-match', so the state-transition and DPNS logs are as specific as before. allowedSecurityLevels is optional (any level) so tip-service stops enumerating all four. The DPNS registration wizard uses matchIdentityKey too, so its ordering matches dpnsService. identity-service's private enum normalizer is the shared one. Tests pin the returned key by identity and use realistic fixtures (wasm shape with both name and number fields, JSON shape with base64 data).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@PastaPastaPasta
PastaPastaPasta merged commit 917018b into staging Sep 7, 2026
8 of 9 checks passed
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