fix(kez-chat/web): signClaim was producing envelopes without primary/key

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 <noreply@anthropic.com>
This commit is contained in:
Jason Tudisco 2026-05-25 14:33:40 -06:00
parent 41f66ae366
commit cd8dda681c
2 changed files with 19 additions and 1 deletions

View File

@ -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 {

View File

@ -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";
}