passkey-utils.ts
1/**2 * passkey-utils.ts — WebAuthn relying-party-id validation.3 *4 * The native browser enforces that a page may only request a passkey whose5 * rpId is a registrable-domain-suffix of the page's own origin. On Firefox,6 * Peach intercepts `navigator.credentials` in a MAIN-world content script and7 * services the ceremony itself, so Peach — not the browser — must re-impose8 * that rule. Without it a page at `evil.com` could request (and Peach would9 * happily mint) a credential scoped to `google.com` (SEV-1-A).10 */1112/**13 * Validates that `rpId` is equal to, or a registrable-domain-suffix of, the14 * hostname of `callerOrigin`. Mirrors WebAuthn §7.1 step 6 / §7.2 step 8.15 *16 * This is a conservative suffix check, not a full Public-Suffix-List lookup:17 * it requires the rpId to contain at least one dot (blocking bare-eTLD rpIds18 * like "com" or "localhost"), and requires the caller host to be exactly the19 * rpId or to end with "." + rpId. That correctly rejects the classic20 * suffix-confusion payload "google.com.attacker.com" for rpId "google.com".21 *22 * isValidRpId("google.com", "https://accounts.google.com") → true23 * isValidRpId("google.com", "https://google.com") → true24 * isValidRpId("com", "https://google.com") → false (bare eTLD)25 * isValidRpId("google.com", "https://evil.google.com.x.com")→ false26 * isValidRpId("evil.com", "https://google.com") → false27 */28export function isValidRpId(rpId: string, callerOrigin: string): boolean {29 let callerHost: string;30 try {31 callerHost = new URL(callerOrigin).hostname;32 } catch {33 return false;34 }35 if (!rpId || !callerHost) return false;3637 const id = rpId.toLowerCase();38 const host = callerHost.toLowerCase();3940 // Require at least one dot — blocks bare public suffixes ("com", "co.uk"41 // collapses are not handled here, but a bare TLD is the dangerous case).42 if (!id.includes(".")) return false;4344 // Exact match.45 if (host === id) return true;4647 // Registrable-suffix match: host must end with ".<rpId>". This rejects48 // "google.com.attacker.com" for rpId "google.com" because that host ends49 // with ".attacker.com", not ".google.com".50 if (host.endsWith("." + id)) return true;5152 return false;53}