fix: derive kid from JWK thumbprint when the key omits one - #35
Open
Karavil wants to merge 2 commits into
Open
Conversation
`kid` is OPTIONAL in a JWK (RFC 7517 §4.5), but every host write path persisted `publicKey.kid ?? null`. A client that legitimately omits the member was stored with `kid = null`, and since the middleware resolves a host by `findHostById(iss) ?? findHostByKid(iss)`, its thumbprint `iss` matched neither lookup. Every authenticated request then failed with AGENT_NOT_FOUND, permanently. RFC 7638 §3.1 blesses the JWK thumbprint as a `kid` value, and the SDK already stamps `kid = thumbprint` at keygen — which is why the gap is invisible to hosts built with it. Deriving the same value server-side when `kid` is absent keeps the stored lookup key aligned with how a host identifies itself. Applies to /agent/register, /agent/claim, /host/create, /host/enroll, /host/update, and /host/rotate-key. A JWK that carries an explicit `kid` keeps it unchanged.
Karavil
marked this pull request as ready for review
August 4, 2026 02:25
Replaces the duplicated getTestInstance setup and the `as any` casts with createTestContext, and adds a getHost helper so host row assertions no longer need non-null assertions. Behaviour and coverage are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The failure
A host generates an Ed25519 keypair, exports the public JWK, and registers dynamically.
kidis OPTIONAL in a JWK (RFC 7517 §4.5), so the exported key does not carry one. The host identifies itself the way §4.2 prescribes, withissset to the RFC 7638 thumbprint. Registration succeeds. Every authenticated request after that returns401 AGENT_NOT_FOUND, and no retry ever recovers.Every host write path stored
publicKey.kid ?? null, whilefindHostByIdOrKidresolvesissagainst theidandkidcolumns. Nothing recomputes a thumbprint at read time, so the read path silently depends on a write-side convention: the SDK stampskid = thumbprintat keygen (packages/sdk/src/crypto.ts), which is what puts the thumbprint in the column for the lookup to find. A host built with the SDK therefore always works, and one that legitimately omits the member never can. That is why the existing suite never caught it --generateTestKeypairproduces kid-less JWKs, but no test asserted the storedkidor re-authenticated afterwards.The change
resolveHostKid()returns an explicitkidwhen the JWK has one and derives the RFC 7638 thumbprint otherwise. RFC 7638 §3.1 blesses the thumbprint as akidvalue, androtate-key.tsalready derived it for the host id under §8.7 -- this applies the same convention consistently at the remaining write sites:/agent/register,/agent/claim,/host/create,/host/enroll,/host/update, and/host/rotate-key.A JWK carrying an explicit
kidkeeps it unchanged, so existing hosts are unaffected.Tests
Four cases in
kidless-host-jwk.test.ts: a kid-less dynamic registration persists the thumbprint and authenticates on a follow-up request, an explicitkidsurvives untouched, and/host/createand/host/enrollderive it too. Reverting theregister.tschange alone fails the first case withAGENT_NOT_FOUNDand leaves the explicit-kid case passing.Full suite: 260 passed, 15 files.
tsc --noEmitclean.Note on an adjacent issue
Visible in the same lookup and sent separately as #36: when
issnames no known host, the middleware raisesAGENT_NOT_FOUNDrather thanHOST_NOT_FOUND, which exists and is used elsewhere. It sends people looking at the agent record when the host row is what is missing. The two are independent and either can merge first.