9b1e359cf8d6
fix: clientDataJSON.origin uses page origin (active tab) instead of https://<rpId> — fixes passkey create/get on Google subdomains (v3.6.105)
- time
- 2026-05-28 17:00 UTC · 5 months ago
- machine
- koh2
- parent
- ccf38e50bdc9
↓ peach-browser-9b1e359c.tar.gzatlas at this save
files changed4 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/passkey.ts+45 −2manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22 "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33 "manifest_version": 3,44 "name": "Peach Password Manager",5- "version": "3.6.104",5+ "version": "3.6.105",66 "description": "Zero-knowledge password manager",77 "browser_specific_settings": {88 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "3.6.104",4+ "version": "3.6.105",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "3.6.104",3+ "version": "3.6.105",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/passkey.ts+45 −2
@@ -101,6 +101,41 @@101101 : `https://${rpId}`;102102 }103103 104+/**105+ * The clientDataJSON sent back to the RP must contain the page's actual106+ * origin — not a synthesized one. `chrome.webAuthenticationProxy` does not107+ * surface the requesting origin in CreateRequest/GetRequest, so we read it108+ * from the active tab and validate that its hostname is a registrable109+ * suffix match for the rpId. WebAuthn requires this relationship anyway, so110+ * a mismatch indicates either a stale active tab or a hostile environment;111+ * we fall back to https://<rpId> in that case to avoid silently signing the112+ * wrong origin.113+ *114+ * Without this, a passkey created on https://myaccount.google.com gets115+ * clientDataJSON.origin = "https://google.com", and Google's RP rejects the116+ * attestation with "Something went wrong" because the origin hash doesn't117+ * match what its own JS captured at create time.118+ */119+async function resolvePageOrigin(rpId: string): Promise<string> {120+ try {121+ const tabs = await new Promise<chrome.tabs.Tab[]>((resolve) =>122+ chrome.tabs.query({ active: true, lastFocusedWindow: true }, resolve)123+ );124+ const url = tabs[0]?.url;125+ if (url) {126+ const parsed = new URL(url);127+ const host = parsed.hostname.toLowerCase();128+ const rp = rpId.toLowerCase();129+ if (host === rp || host.endsWith(`.${rp}`)) {130+ return parsed.origin;131+ }132+ }133+ } catch {134+ // fall through to rpId-derived origin135+ }136+ return originFromRpId(rpId);137+}138+104139 async function setBadge(text: string): Promise<void> {105140 if (!chrome.action?.setBadgeText) return;106141 await chrome.action.setBadgeText({ text });@@ -371,7 +406,11 @@371406 publicKeyCose,372407 });373408 const attestationObject = createAttestationObject(authData);374- const clientDataJSON = buildClientDataJSON("webauthn.create", options.challenge, originFromRpId(rpId));409+ const clientDataJSON = buildClientDataJSON(410+ "webauthn.create",411+ options.challenge,412+ await resolvePageOrigin(rpId)413+ );375414 376415 const passkey: PasskeyData = {377416 rpId,@@ -567,7 +606,11 @@567606 flags,568607 signCount: nextSignCount,569608 });570- const clientDataJSON = buildClientDataJSON("webauthn.get", options.challenge, originFromRpId(credential.rpId));609+ const clientDataJSON = buildClientDataJSON(610+ "webauthn.get",611+ options.challenge,612+ await resolvePageOrigin(credential.rpId)613+ );571614 const clientDataHash = new Uint8Array(572615 await crypto.subtle.digest("SHA-256", new Uint8Array(clientDataJSON))573616 );