fix(kez-chat/web): verifiers surface real reasons instead of silent fall-through

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 <noreply@anthropic.com>
This commit is contained in:
Jason Tudisco 2026-05-25 14:52:59 -06:00
parent cd8dda681c
commit 21d9b705b7
2 changed files with 86 additions and 43 deletions

View File

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

View File

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