From 323a7e21a741caaea80ea98b74a23de552c3ccc8 Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 14 May 2025 07:12:00 +0900 Subject: [PATCH] Added user confirmation on cred creation --- README.md | 9 ++-- fido.py => passkey.py | 113 ++++++++++++++++-------------------------- webauthn_server.js | 11 ++-- webauthn_server.py | 6 +-- 4 files changed, 57 insertions(+), 82 deletions(-) rename fido.py => passkey.py (83%) diff --git a/README.md b/README.md index c239c37..cdb5920 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ # Virtual WebAuthn -### Unsafe implementation of WebAuthn for private key transparency. +### Locally stored WebAuthn Passkey -### `fido.py` +### `passkey.py` Virtual WebAuthn implemention of `navigator.credentials.get()` and `navigator.credentials.create()`, with self-attestation. ### `webauthn_server.py` -Simple FastAPI server that acts as proxy for `fido.py` and browser environment. +Simple FastAPI server that acts as proxy for `passkey.py` on browser environment. Use `webauthn_server.js` in userscript.js (like TamperMonkey), WebAuthn requests will be forwarded to your local script. - -Private key for your Passkeys are stored in JSON file, you can backup your private key since **YOU OWN THE KEY**. +Private key for your Passkeys are stored in JSON file, you can backup your private key. Works on most WebAuthn websites, including Google, Microsoft. \ No newline at end of file diff --git a/fido.py b/passkey.py similarity index 83% rename from fido.py rename to passkey.py index 5557239..64ad112 100644 --- a/fido.py +++ b/passkey.py @@ -10,7 +10,6 @@ from Crypto.Signature import DSS from Crypto.Hash import SHA256 from typing import Dict, Any, Optional - import getpass from fido2.hid import CtapHidDevice from fido2.client import Fido2Client, UserInteraction @@ -18,8 +17,8 @@ from fido2.webauthn import PublicKeyCredentialCreationOptions, PublicKeyCredenti from fido2.utils import websafe_encode, websafe_decode -class YkFidoDevice: - def __init__(self): +class PhysicalPasskey: + def __init__(self, dummy = ""): devices = list(CtapHidDevice.list_devices()) if not devices: raise Exception("No FIDO2 devices found.") @@ -139,8 +138,8 @@ class YkFidoDevice: return result -class VirtualFidoDevice: - def __init__(self, file: str = "fido.json"): +class VirtualPasskey: + def __init__(self, file: str = "passkey.json"): self.file = file self.credentials = {} self._load_credentials() @@ -158,7 +157,7 @@ class VirtualFidoDevice: try: with open(self.file, 'r') as f: self.credentials = json.load(f) - except FileNotFoundError: + except os.FileNotExistsError: self.credentials = {} def _save_credentials(self): @@ -223,7 +222,7 @@ class VirtualFidoDevice: if not origin: raise self.InputDataError("origin") - rp_id = rp.get("id", "").encode() + rp_id = rp.get("id").encode() user_id = user.get("id") if isinstance(user_id, str): @@ -245,34 +244,25 @@ class VirtualFidoDevice: auth_data = self._create_authenticator_data(rp_id, counter=0, credential_data=attested_data) - client_data = ('{"type":"%s","challenge":"%s","origin":"%s","crossOrigin":false}' - % ("webauthn.create", self._b64url(challenge), origin)).encode() - client_data_hash = hashlib.sha256(client_data).digest() - - signature_data = auth_data + client_data_hash - - h = SHA256.new(signature_data) - signer = DSS.new(key, 'fips-186-3', encoding='der') - signature = signer.sign(h) - - # Self Attestation - attn_fmt = "packed" - attn_stmt = { - "alg": -7, - "sig": signature + attestation_obj = { + "fmt": "none", + "authData": auth_data, + "attStmt": {} } + attestation_cbor = cbor2.dumps(attestation_obj) - attn_obj = { - "fmt": attn_fmt, - "attStmt": attn_stmt, - "authData": auth_data - } - attn_cbor = cbor2.dumps(attn_obj) + client_data = { + "challenge": self._b64url(challenge), + "origin": origin, + "type": "webauthn.create", + "crossOrigin": False, + } + client_data_json = json.dumps(client_data).encode() self.credentials[credential_id_b64] = { "private_key": private_key, - "rp_id": rp_id.decode(), + "rp_id": self._b64url(rp_id), "user_id": self._b64url(user_id), "user_name": user.get('displayName', ''), "created": int(time.time()), @@ -285,12 +275,11 @@ class VirtualFidoDevice: "id": credential_id_b64, "rawId": credential_id_b64, "response": { - "attestationObject": self._b64url(attn_cbor), - "clientDataJSON": self._b64url(client_data), + "attestationObject": self._b64url(attestation_cbor), + "clientDataJSON": self._b64url(client_data_json), "publicKey": self._b64url(cose_pubkey), - "authenticatorData": self._b64url(auth_data), "pubKeyAlgo": str(alg), - "transports": ["usb"] + "transports": ["internal"] }, "type": "public-key" } @@ -298,47 +287,29 @@ class VirtualFidoDevice: def get(self, data: Dict[str, Any], origin: str = "") -> Dict[str, Any]: + challenge = data.get("challenge") if isinstance(challenge, str): challenge = self._b64url(challenge) + allowed_credential = data.get("allowCredentials") + + for credential in allowed_credential: + credential_id_b64 = credential["id"] + if self.credentials.get(credential_id_b64): + cred = self.credentials[credential_id_b64] + break + else: + raise self.CredNotFoundError() + rp_id = data.get("rpId", "").encode('utf-8') if not rp_id: raise self.InputDataError("rp_id") - + if not origin: origin = data.get("origin") if not origin: raise self.InputDataError("origin") - - allowed_credential = data.get("allowCredentials") - cred = None - - if allowed_credential: - for credential in allowed_credential: - credential_id_b64 = credential["id"] - if self.credentials.get(credential_id_b64): - cred = self.credentials[credential_id_b64] - break - else: - match_creds = [] - for cid, cr in self.credentials.items(): - if cr["rp_id"] == rp_id.decode(): - match_creds.append(cid) - - if len(match_creds) == 1: - cred = self.credentials[cid] - - else: - results = [] - for cr in match_creds: - current = data.copy() - current["allowCredentials"] = [{"id": cr}] - results.append(self.get(current, origin)) - return results - - if not cred: - raise self.CredNotFoundError() counter = cred.get("counter", 0) + 1 cred["counter"] = counter @@ -370,21 +341,21 @@ class VirtualFidoDevice: "response": { "authenticatorData": self._b64url(auth_data), "clientDataJSON": self._b64url(client_data), - "signature": self._b64url(signature), - "userHandle": cred["user_id"] + "signature": self._b64url(signature) }, - "type": "public-key", - "username": cred["user_name"], - "created": cred["created"] + "type": "public-key" } return response +Passkey = VirtualPasskey + + if __name__=="__main__": import requests sess = requests.Session() - fido = VirtualFidoDevice() + passkey = Passkey() payload = { "algorithms": ["es256"], "attachment": "all", "attestation": "none", "discoverable_credential": "preferred", @@ -392,7 +363,7 @@ if __name__=="__main__": } resp = sess.post("https://webauthn.io/registration/options", json=payload) print(resp.json()) - data = fido.create(resp.json(), origin="https://webauthn.io") + data = passkey.create(resp.json(), origin="https://webauthn.io") data["rawId"] = data["id"] print(data) resp = sess.post("https://webauthn.io/registration/verification", json={"response": data, "username": "asdf"}) @@ -404,7 +375,7 @@ if __name__=="__main__": payload = {"username":"asdf", "user_verification":"preferred", "hints":[]} resp = sess.post("https://webauthn.io/authentication/options", json=payload, headers={"origin": "https://webauthn.io"}) print(resp.json()) - data = fido.get(resp.json(), origin="https://webauthn.io") + data = passkey.get(resp.json(), origin="https://webauthn.io") print(data) data["rawId"] = data["id"] resp = sess.post("https://webauthn.io/authentication/verification", json={"response": data, "username": "asdf"}) diff --git a/webauthn_server.js b/webauthn_server.js index 651df14..c1a444f 100644 --- a/webauthn_server.js +++ b/webauthn_server.js @@ -146,13 +146,18 @@ navigator.credentials.get = async function(options) { navigator.credentials.create = async function(options) { console.log("navigator.credentials.create", options) try { + if (!confirm("Creating new credential on userWebAuthn. Continue?")) { + throw new Error('user cancel'); + } const authOptions = { publicKey: Object.assign({}, options.publicKey) }; authOptions.publicKey.challenge = abb64(authOptions.publicKey.challenge) authOptions.publicKey.user = Object.assign({}, options.publicKey.user) authOptions.publicKey.user.id = abb64(authOptions.publicKey.user.id) - authOptions.publicKey.excludeCredentials = authOptions.publicKey.excludeCredentials.map(credential => ({ - ...credential, id: abb64(credential.id) - })); + if (authOptions.publicKey.excludeCredentials) { + authOptions.publicKey.excludeCredentials = authOptions.publicKey.excludeCredentials.map(credential => ({ + ...credential, id: abb64(credential.id) + })); + } const response = await myFetch('http://127.0.0.1:20492', { method: 'POST', headers: {'Content-Type': 'application/json'}, diff --git a/webauthn_server.py b/webauthn_server.py index 0d53aa4..347ed68 100644 --- a/webauthn_server.py +++ b/webauthn_server.py @@ -4,7 +4,7 @@ from pydantic import BaseModel from typing import Dict, Any import uvicorn import json -from fido import VirtualFidoDevice as FidoDevice +from passkey import VirtualPasskey as Passkey app = FastAPI() @@ -26,7 +26,7 @@ async def handle(param: WebAuthnRequest): try: options = param.data.get("publicKey", {}) print(f"webauthn.get {json.dumps(options, indent=4)}") - webauthn = FidoDevice() + webauthn = Passkey() assertion = webauthn.get(options, param.data.get("origin", "")) return assertion @@ -40,7 +40,7 @@ async def handle(param: WebAuthnRequest): try: options = param.data.get("publicKey", {}) print(f"webauthn.create {json.dumps(options, indent=4)}") - webauthn = FidoDevice() + webauthn = Passkey() attestation = webauthn.create(options, param.data.get("origin", "")) return attestation