From cd8dda681c51da5f80de61e777f506eec8b9fe3e Mon Sep 17 00:00:00 2001 From: Jason Tudisco Date: Mon, 25 May 2026 14:33:40 -0600 Subject: [PATCH] fix(kez-chat/web): signClaim was producing envelopes without primary/key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AddClaim.svelte passed session.unlocked (an UnlockedIdentity, shape {handle, server, primary, seed}) to signClaim, which expects an Ed25519Identity ({seed, publicKey, identity}). Different fields: session.unlocked.identity is undefined. Result: payload.primary was undefined → JCS omits it → signature was valid over a payload-without-primary, and signature.key was also undefined. Verifiers correctly rejected these envelopes — and the markdown header read "Primary: undefined". Fix: - AddClaim: derive a real Ed25519Identity via identityFromSeed(session. unlocked.seed) before calling signClaim. The seed is the canonical source of truth — publicKey + identity are derived deterministically. - signWith: throw if signer.identity is missing or seed is malformed. Belt-and-suspenders so a future caller passing the wrong shape gets a loud error instead of producing silently unverifiable envelopes. Note: any claims signed before this fix have invalid signatures and must be re-created. Remove them on the Claims page and re-add. Co-Authored-By: Claude Opus 4.7 --- kez-chat/web/src/lib/kez.ts | 12 ++++++++++++ kez-chat/web/src/routes/AddClaim.svelte | 8 +++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/kez-chat/web/src/lib/kez.ts b/kez-chat/web/src/lib/kez.ts index 4895d08..ac21362 100644 --- a/kez-chat/web/src/lib/kez.ts +++ b/kez-chat/web/src/lib/kez.ts @@ -116,6 +116,18 @@ function signWith( payload: unknown, signer: Ed25519Identity, ): SignatureBlock { + // Defensive: if a caller (Svelte file with loose checks, etc.) passes + // something missing .identity, the envelope would be silently + // unverifiable because both signature.key and payload.primary would + // be undefined. Fail loudly instead. + if (!signer || typeof signer.identity !== "string") { + throw new Error( + "signWith: signer.identity missing — pass an Ed25519Identity (use identityFromSeed)", + ); + } + if (!(signer.seed instanceof Uint8Array) || signer.seed.length !== 32) { + throw new Error("signWith: signer.seed must be a 32-byte Uint8Array"); + } const jcs = canonicalBytes(payload); const sig = ed25519.sign(jcs, signer.seed); return { diff --git a/kez-chat/web/src/routes/AddClaim.svelte b/kez-chat/web/src/routes/AddClaim.svelte index 18dc69c..0939de1 100644 --- a/kez-chat/web/src/routes/AddClaim.svelte +++ b/kez-chat/web/src/routes/AddClaim.svelte @@ -3,6 +3,7 @@ import { push } from "svelte-spa-router"; import { signClaim, + identityFromSeed, toPrettyJson, toMarkdown, toCompact, @@ -166,7 +167,12 @@ alert("Identifier looks empty — please fill in the field."); return; } - envelope = signClaim(session.unlocked, subject); + // UnlockedIdentity has {handle, server, primary, seed} — not the + // Ed25519Identity {seed, publicKey, identity} shape signClaim wants. + // Reconstruct from the seed so signer.identity is defined and + // ends up in payload.primary + signature.key. + const signer = identityFromSeed(session.unlocked.seed); + envelope = signClaim(signer, subject); step = "publish"; }