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:
parent
cd8dda681c
commit
21d9b705b7
@ -65,34 +65,47 @@ export const verifyDns: Verifier = async (ctx) => {
|
|||||||
.join(""),
|
.join(""),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const parseErrors: string[] = [];
|
||||||
for (const txt of candidates) {
|
for (const txt of candidates) {
|
||||||
|
let fetched;
|
||||||
try {
|
try {
|
||||||
const fetched = await parseAnyEnvelope(txt);
|
fetched = await parseAnyEnvelope(txt);
|
||||||
if (fetched.payload.subject !== ctx.subject) continue;
|
} catch (e) {
|
||||||
if (fetched.payload.primary !== ctx.primary) {
|
if (txt.includes("kez:z1:") || txt.includes("```kez")) {
|
||||||
return fail(
|
// Looks like it was MEANT to be a kez envelope — record why parse failed
|
||||||
`TXT envelope's primary (${fetched.payload.primary}) doesn't match your key`,
|
parseErrors.push(` • ${(e as Error).message} — value: ${txt.slice(0, 60)}…`);
|
||||||
{ 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}.`,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// not a kez envelope — try the next TXT record
|
|
||||||
continue;
|
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}`, {
|
return fail(
|
||||||
evidence_url: url,
|
parseErrors.length > 0
|
||||||
details: `Candidates examined:\n${candidates.map((c) => ` • ${c.slice(0, 80)}${c.length > 80 ? "…" : ""}`).join("\n")}`,
|
? `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")}`,
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -29,36 +29,66 @@ async function tryGists(
|
|||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
headers: { Accept: "application/vnd.github+json" },
|
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[];
|
const gists = (await resp.json()) as Gist[];
|
||||||
for (const g of gists) {
|
for (const g of gists) {
|
||||||
const kezFile = Object.values(g.files).find(
|
const kezFile = Object.values(g.files).find(
|
||||||
(f) => f.filename.toLowerCase() === "kez.md",
|
(f) => f.filename.toLowerCase() === "kez.md",
|
||||||
);
|
);
|
||||||
if (!kezFile) continue;
|
if (!kezFile) continue;
|
||||||
|
let raw: string;
|
||||||
try {
|
try {
|
||||||
const raw = await fetch(kezFile.raw_url).then((r) => r.text());
|
raw = await fetch(kezFile.raw_url).then((r) => r.text());
|
||||||
const fetched = await parseAnyEnvelope(raw);
|
} catch (e) {
|
||||||
if (fetched.payload.subject !== ctx.subject) continue;
|
return fail(`Failed to fetch gist raw: ${(e as Error).message}`, {
|
||||||
if (fetched.payload.primary !== ctx.primary) {
|
evidence_url: g.html_url,
|
||||||
return fail(
|
});
|
||||||
`Gist envelope's primary doesn't match your key`,
|
}
|
||||||
{ evidence_url: g.html_url, fetched },
|
let fetched;
|
||||||
);
|
try {
|
||||||
}
|
fetched = await parseAnyEnvelope(raw);
|
||||||
if (!verifyEnvelope(fetched)) {
|
} catch (e) {
|
||||||
return fail(`Gist envelope signature is invalid`, {
|
return fail(
|
||||||
evidence_url: g.html_url,
|
`Gist ${g.id} has kez.md but it doesn't contain a parseable kez envelope: ${(e as Error).message}`,
|
||||||
fetched,
|
{ evidence_url: g.html_url },
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
return ok(`Verified via public gist`, {
|
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,
|
evidence_url: g.html_url,
|
||||||
fetched,
|
fetched,
|
||||||
});
|
});
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
return ok(`Verified via public gist`, {
|
||||||
|
evidence_url: g.html_url,
|
||||||
|
fetched,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user