/** * passkey-utils.ts — WebAuthn relying-party-id validation. * * The native browser enforces that a page may only request a passkey whose * rpId is a registrable-domain-suffix of the page's own origin. On Firefox, * Peach intercepts `navigator.credentials` in a MAIN-world content script and * services the ceremony itself, so Peach — not the browser — must re-impose * that rule. Without it a page at `evil.com` could request (and Peach would * happily mint) a credential scoped to `google.com` (SEV-1-A). */ /** * Validates that `rpId` is equal to, or a registrable-domain-suffix of, the * hostname of `callerOrigin`. Mirrors WebAuthn §7.1 step 6 / §7.2 step 8. * * This is a conservative suffix check, not a full Public-Suffix-List lookup: * it requires the rpId to contain at least one dot (blocking bare-eTLD rpIds * like "com" or "localhost"), and requires the caller host to be exactly the * rpId or to end with "." + rpId. That correctly rejects the classic * suffix-confusion payload "google.com.attacker.com" for rpId "google.com". * * isValidRpId("google.com", "https://accounts.google.com") → true * isValidRpId("google.com", "https://google.com") → true * isValidRpId("com", "https://google.com") → false (bare eTLD) * isValidRpId("google.com", "https://evil.google.com.x.com")→ false * isValidRpId("evil.com", "https://google.com") → false */ export function isValidRpId(rpId: string, callerOrigin: string): boolean { let callerHost: string; try { callerHost = new URL(callerOrigin).hostname; } catch { return false; } if (!rpId || !callerHost) return false; const id = rpId.toLowerCase(); const host = callerHost.toLowerCase(); // Require at least one dot — blocks bare public suffixes ("com", "co.uk" // collapses are not handled here, but a bare TLD is the dangerous case). if (!id.includes(".")) return false; // Exact match. if (host === id) return true; // Registrable-suffix match: host must end with ".". This rejects // "google.com.attacker.com" for rpId "google.com" because that host ends // with ".attacker.com", not ".google.com". if (host.endsWith("." + id)) return true; return false; }