From 21d9b705b718da51511b96996549365b9e926517 Mon Sep 17 00:00:00 2001 From: Jason Tudisco Date: Mon, 25 May 2026 14:52:59 -0600 Subject: [PATCH] fix(kez-chat/web): verifiers surface real reasons instead of silent fall-through MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DNS verifier used to say "no envelope found" even when a kez:z1: TXT was sitting there but failed to parse (DNS providers can mangle bytes at 255-char segment boundaries). GitHub verifier said "no proof found" even when the gists API returned 403 — rate-limited from the browser (unauthenticated GitHub allows only 60 req/hr/IP). Now: - DNS: distinguishes "found a kez record but it's corrupted" from "no kez record exists." Calls out provider-side segment mangling and tells the user to re-publish. - GitHub: returns the actual HTTP status and rate-limit reset time when the gists API rejects the request. - Both: when an envelope's primary doesn't match the local key, the error explicitly notes "probably signed with an older build — re-sign and re-publish" (relevant to anything created before cd8dda6). Co-Authored-By: Claude Opus 4.7 --- kez-chat/web/src/lib/verifiers/dns.ts | 61 ++++++++++++--------- kez-chat/web/src/lib/verifiers/github.ts | 68 +++++++++++++++++------- 2 files changed, 86 insertions(+), 43 deletions(-) diff --git a/kez-chat/web/src/lib/verifiers/dns.ts b/kez-chat/web/src/lib/verifiers/dns.ts index ab45674..455e135 100644 --- a/kez-chat/web/src/lib/verifiers/dns.ts +++ b/kez-chat/web/src/lib/verifiers/dns.ts @@ -65,34 +65,47 @@ export const verifyDns: Verifier = async (ctx) => { .join(""), ); + const parseErrors: string[] = []; for (const txt of candidates) { + let fetched; try { - const fetched = await parseAnyEnvelope(txt); - if (fetched.payload.subject !== ctx.subject) continue; - if (fetched.payload.primary !== ctx.primary) { - return fail( - `TXT envelope's primary (${fetched.payload.primary}) doesn't match your key`, - { evidence_url: url, fetched }, - ); + fetched = await parseAnyEnvelope(txt); + } catch (e) { + if (txt.includes("kez:z1:") || txt.includes("```kez")) { + // Looks like it was MEANT to be a kez envelope — record why parse failed + parseErrors.push(` • ${(e as Error).message} — value: ${txt.slice(0, 60)}…`); } - if (!verifyEnvelope(fetched)) { - return fail(`TXT envelope signature is invalid`, { - evidence_url: url, - fetched, - }); - } - return ok(`Verified via DNS TXT at ${queryName}`, { - evidence_url: url, - fetched, - details: `Fetched ${candidates.length} TXT record(s); signature checked against ${ctx.primary}.`, - }); - } catch { - // not a kez envelope — try the next TXT record continue; } + if (fetched.payload.subject !== ctx.subject) continue; + if (fetched.payload.primary !== ctx.primary) { + return fail( + `TXT envelope's primary (${fetched.payload.primary ?? "missing"}) doesn't match your key (${ctx.primary}). The published value was probably signed with an older build — re-publish from your Claims page.`, + { evidence_url: url, fetched }, + ); + } + if (!verifyEnvelope(fetched)) { + return fail(`TXT envelope signature is invalid`, { + evidence_url: url, + fetched, + }); + } + return ok(`Verified via DNS TXT at ${queryName}`, { + evidence_url: url, + fetched, + details: `Fetched ${candidates.length} TXT record(s); signature checked against ${ctx.primary}.`, + }); } - return fail(`Found TXT records at ${queryName} but none contained a valid kez envelope for ${ctx.subject}`, { - evidence_url: url, - details: `Candidates examined:\n${candidates.map((c) => ` • ${c.slice(0, 80)}${c.length > 80 ? "…" : ""}`).join("\n")}`, - }); + return fail( + parseErrors.length > 0 + ? `Found a kez TXT record at ${queryName} but it couldn't be decoded — your DNS provider may have mangled the value at a segment boundary. Try re-publishing.` + : `Found TXT records at ${queryName} but none contained a kez envelope for ${ctx.subject}`, + { + evidence_url: url, + details: + parseErrors.length > 0 + ? `Parse errors:\n${parseErrors.join("\n")}` + : `Candidates examined:\n${candidates.map((c) => ` • ${c.slice(0, 80)}${c.length > 80 ? "…" : ""}`).join("\n")}`, + }, + ); }; diff --git a/kez-chat/web/src/lib/verifiers/github.ts b/kez-chat/web/src/lib/verifiers/github.ts index fbd5d63..21ced4a 100644 --- a/kez-chat/web/src/lib/verifiers/github.ts +++ b/kez-chat/web/src/lib/verifiers/github.ts @@ -29,36 +29,66 @@ async function tryGists( const resp = await fetch(url, { headers: { Accept: "application/vnd.github+json" }, }); - if (!resp.ok) return null; + if (!resp.ok) { + // Surface the failure instead of silently falling through. + // 403 with X-RateLimit-Remaining: 0 is the common case for + // unauthenticated browser callers (60 req/hr/IP). + const reset = resp.headers.get("x-ratelimit-reset"); + const remaining = resp.headers.get("x-ratelimit-remaining"); + return fail( + `GitHub gists API responded ${resp.status}` + + (remaining === "0" ? " (rate-limited)" : ""), + { + evidence_url: url, + details: + `GitHub's unauthenticated rate limit is 60 req/hr/IP.\n` + + (reset + ? `Limit resets at ${new Date(Number(reset) * 1000).toLocaleString()}.\n` + : "") + + `Wait and try again, or check that the user exists.`, + }, + ); + } const gists = (await resp.json()) as Gist[]; for (const g of gists) { const kezFile = Object.values(g.files).find( (f) => f.filename.toLowerCase() === "kez.md", ); if (!kezFile) continue; + let raw: string; try { - const raw = await fetch(kezFile.raw_url).then((r) => r.text()); - const fetched = await parseAnyEnvelope(raw); - if (fetched.payload.subject !== ctx.subject) continue; - if (fetched.payload.primary !== ctx.primary) { - return fail( - `Gist envelope's primary doesn't match your key`, - { evidence_url: g.html_url, fetched }, - ); - } - if (!verifyEnvelope(fetched)) { - return fail(`Gist envelope signature is invalid`, { - evidence_url: g.html_url, - fetched, - }); - } - return ok(`Verified via public gist`, { + raw = await fetch(kezFile.raw_url).then((r) => r.text()); + } catch (e) { + return fail(`Failed to fetch gist raw: ${(e as Error).message}`, { + evidence_url: g.html_url, + }); + } + let fetched; + try { + fetched = await parseAnyEnvelope(raw); + } catch (e) { + return fail( + `Gist ${g.id} has kez.md but it doesn't contain a parseable kez envelope: ${(e as Error).message}`, + { evidence_url: g.html_url }, + ); + } + if (fetched.payload.subject !== ctx.subject) continue; + if (fetched.payload.primary !== ctx.primary) { + return fail( + `Gist envelope's primary (${fetched.payload.primary ?? "missing"}) doesn't match your key (${ctx.primary}). The published value was probably signed with an older build — re-sign on the Claims page and update the gist.`, + { evidence_url: g.html_url, fetched }, + ); + } + if (!verifyEnvelope(fetched)) { + return fail(`Gist envelope signature is invalid`, { evidence_url: g.html_url, fetched, }); - } catch { - continue; } + return ok(`Verified via public gist`, { + evidence_url: g.html_url, + fetched, + }); } return null; }