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
4 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/passkey.ts+45 −2
manifest.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": {

view file

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",

view file

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": {

view file

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   );

view file