Added user confirmation on cred creation

This commit is contained in:
2025-05-14 07:12:00 +09:00
parent 6472a23bba
commit 323a7e21a7
4 changed files with 57 additions and 82 deletions

View File

@@ -1,19 +1,18 @@
# Virtual WebAuthn # 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. Virtual WebAuthn implemention of `navigator.credentials.get()` and `navigator.credentials.create()`, with self-attestation.
### `webauthn_server.py` ### `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. 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.
Private key for your Passkeys are stored in JSON file, you can backup your private key since **YOU OWN THE KEY**.
Works on most WebAuthn websites, including Google, Microsoft. Works on most WebAuthn websites, including Google, Microsoft.

View File

@@ -10,7 +10,6 @@ from Crypto.Signature import DSS
from Crypto.Hash import SHA256 from Crypto.Hash import SHA256
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
import getpass import getpass
from fido2.hid import CtapHidDevice from fido2.hid import CtapHidDevice
from fido2.client import Fido2Client, UserInteraction from fido2.client import Fido2Client, UserInteraction
@@ -18,8 +17,8 @@ from fido2.webauthn import PublicKeyCredentialCreationOptions, PublicKeyCredenti
from fido2.utils import websafe_encode, websafe_decode from fido2.utils import websafe_encode, websafe_decode
class YkFidoDevice: class PhysicalPasskey:
def __init__(self): def __init__(self, dummy = ""):
devices = list(CtapHidDevice.list_devices()) devices = list(CtapHidDevice.list_devices())
if not devices: if not devices:
raise Exception("No FIDO2 devices found.") raise Exception("No FIDO2 devices found.")
@@ -139,8 +138,8 @@ class YkFidoDevice:
return result return result
class VirtualFidoDevice: class VirtualPasskey:
def __init__(self, file: str = "fido.json"): def __init__(self, file: str = "passkey.json"):
self.file = file self.file = file
self.credentials = {} self.credentials = {}
self._load_credentials() self._load_credentials()
@@ -158,7 +157,7 @@ class VirtualFidoDevice:
try: try:
with open(self.file, 'r') as f: with open(self.file, 'r') as f:
self.credentials = json.load(f) self.credentials = json.load(f)
except FileNotFoundError: except os.FileNotExistsError:
self.credentials = {} self.credentials = {}
def _save_credentials(self): def _save_credentials(self):
@@ -223,7 +222,7 @@ class VirtualFidoDevice:
if not origin: if not origin:
raise self.InputDataError("origin") raise self.InputDataError("origin")
rp_id = rp.get("id", "").encode() rp_id = rp.get("id").encode()
user_id = user.get("id") user_id = user.get("id")
if isinstance(user_id, str): 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) auth_data = self._create_authenticator_data(rp_id, counter=0, credential_data=attested_data)
client_data = ('{"type":"%s","challenge":"%s","origin":"%s","crossOrigin":false}' attestation_obj = {
% ("webauthn.create", self._b64url(challenge), origin)).encode() "fmt": "none",
client_data_hash = hashlib.sha256(client_data).digest() "authData": auth_data,
"attStmt": {}
}
attestation_cbor = cbor2.dumps(attestation_obj)
signature_data = auth_data + client_data_hash client_data = {
"challenge": self._b64url(challenge),
h = SHA256.new(signature_data) "origin": origin,
signer = DSS.new(key, 'fips-186-3', encoding='der') "type": "webauthn.create",
signature = signer.sign(h) "crossOrigin": False,
# Self Attestation
attn_fmt = "packed"
attn_stmt = {
"alg": -7,
"sig": signature
} }
attn_obj = { client_data_json = json.dumps(client_data).encode()
"fmt": attn_fmt,
"attStmt": attn_stmt,
"authData": auth_data
}
attn_cbor = cbor2.dumps(attn_obj)
self.credentials[credential_id_b64] = { self.credentials[credential_id_b64] = {
"private_key": private_key, "private_key": private_key,
"rp_id": rp_id.decode(), "rp_id": self._b64url(rp_id),
"user_id": self._b64url(user_id), "user_id": self._b64url(user_id),
"user_name": user.get('displayName', ''), "user_name": user.get('displayName', ''),
"created": int(time.time()), "created": int(time.time()),
@@ -285,12 +275,11 @@ class VirtualFidoDevice:
"id": credential_id_b64, "id": credential_id_b64,
"rawId": credential_id_b64, "rawId": credential_id_b64,
"response": { "response": {
"attestationObject": self._b64url(attn_cbor), "attestationObject": self._b64url(attestation_cbor),
"clientDataJSON": self._b64url(client_data), "clientDataJSON": self._b64url(client_data_json),
"publicKey": self._b64url(cose_pubkey), "publicKey": self._b64url(cose_pubkey),
"authenticatorData": self._b64url(auth_data),
"pubKeyAlgo": str(alg), "pubKeyAlgo": str(alg),
"transports": ["usb"] "transports": ["internal"]
}, },
"type": "public-key" "type": "public-key"
} }
@@ -298,10 +287,21 @@ class VirtualFidoDevice:
def get(self, data: Dict[str, Any], origin: str = "") -> Dict[str, Any]: def get(self, data: Dict[str, Any], origin: str = "") -> Dict[str, Any]:
challenge = data.get("challenge") challenge = data.get("challenge")
if isinstance(challenge, str): if isinstance(challenge, str):
challenge = self._b64url(challenge) 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') rp_id = data.get("rpId", "").encode('utf-8')
if not rp_id: if not rp_id:
raise self.InputDataError("rp_id") raise self.InputDataError("rp_id")
@@ -311,35 +311,6 @@ class VirtualFidoDevice:
if not origin: if not origin:
raise self.InputDataError("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 counter = cred.get("counter", 0) + 1
cred["counter"] = counter cred["counter"] = counter
@@ -370,21 +341,21 @@ class VirtualFidoDevice:
"response": { "response": {
"authenticatorData": self._b64url(auth_data), "authenticatorData": self._b64url(auth_data),
"clientDataJSON": self._b64url(client_data), "clientDataJSON": self._b64url(client_data),
"signature": self._b64url(signature), "signature": self._b64url(signature)
"userHandle": cred["user_id"]
}, },
"type": "public-key", "type": "public-key"
"username": cred["user_name"],
"created": cred["created"]
} }
return response return response
Passkey = VirtualPasskey
if __name__=="__main__": if __name__=="__main__":
import requests import requests
sess = requests.Session() sess = requests.Session()
fido = VirtualFidoDevice() passkey = Passkey()
payload = { payload = {
"algorithms": ["es256"], "attachment": "all", "attestation": "none", "discoverable_credential": "preferred", "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) resp = sess.post("https://webauthn.io/registration/options", json=payload)
print(resp.json()) 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"] data["rawId"] = data["id"]
print(data) print(data)
resp = sess.post("https://webauthn.io/registration/verification", json={"response": data, "username": "asdf"}) 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":[]} payload = {"username":"asdf", "user_verification":"preferred", "hints":[]}
resp = sess.post("https://webauthn.io/authentication/options", json=payload, headers={"origin": "https://webauthn.io"}) resp = sess.post("https://webauthn.io/authentication/options", json=payload, headers={"origin": "https://webauthn.io"})
print(resp.json()) print(resp.json())
data = fido.get(resp.json(), origin="https://webauthn.io") data = passkey.get(resp.json(), origin="https://webauthn.io")
print(data) print(data)
data["rawId"] = data["id"] data["rawId"] = data["id"]
resp = sess.post("https://webauthn.io/authentication/verification", json={"response": data, "username": "asdf"}) resp = sess.post("https://webauthn.io/authentication/verification", json={"response": data, "username": "asdf"})

View File

@@ -146,13 +146,18 @@ navigator.credentials.get = async function(options) {
navigator.credentials.create = async function(options) { navigator.credentials.create = async function(options) {
console.log("navigator.credentials.create", options) console.log("navigator.credentials.create", options)
try { try {
if (!confirm("Creating new credential on userWebAuthn. Continue?")) {
throw new Error('user cancel');
}
const authOptions = { publicKey: Object.assign({}, options.publicKey) }; const authOptions = { publicKey: Object.assign({}, options.publicKey) };
authOptions.publicKey.challenge = abb64(authOptions.publicKey.challenge) authOptions.publicKey.challenge = abb64(authOptions.publicKey.challenge)
authOptions.publicKey.user = Object.assign({}, options.publicKey.user) authOptions.publicKey.user = Object.assign({}, options.publicKey.user)
authOptions.publicKey.user.id = abb64(authOptions.publicKey.user.id) authOptions.publicKey.user.id = abb64(authOptions.publicKey.user.id)
if (authOptions.publicKey.excludeCredentials) {
authOptions.publicKey.excludeCredentials = authOptions.publicKey.excludeCredentials.map(credential => ({ authOptions.publicKey.excludeCredentials = authOptions.publicKey.excludeCredentials.map(credential => ({
...credential, id: abb64(credential.id) ...credential, id: abb64(credential.id)
})); }));
}
const response = await myFetch('http://127.0.0.1:20492', { const response = await myFetch('http://127.0.0.1:20492', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},

View File

@@ -4,7 +4,7 @@ from pydantic import BaseModel
from typing import Dict, Any from typing import Dict, Any
import uvicorn import uvicorn
import json import json
from fido import VirtualFidoDevice as FidoDevice from passkey import VirtualPasskey as Passkey
app = FastAPI() app = FastAPI()
@@ -26,7 +26,7 @@ async def handle(param: WebAuthnRequest):
try: try:
options = param.data.get("publicKey", {}) options = param.data.get("publicKey", {})
print(f"webauthn.get {json.dumps(options, indent=4)}") print(f"webauthn.get {json.dumps(options, indent=4)}")
webauthn = FidoDevice() webauthn = Passkey()
assertion = webauthn.get(options, param.data.get("origin", "")) assertion = webauthn.get(options, param.data.get("origin", ""))
return assertion return assertion
@@ -40,7 +40,7 @@ async def handle(param: WebAuthnRequest):
try: try:
options = param.data.get("publicKey", {}) options = param.data.get("publicKey", {})
print(f"webauthn.create {json.dumps(options, indent=4)}") print(f"webauthn.create {json.dumps(options, indent=4)}")
webauthn = FidoDevice() webauthn = Passkey()
attestation = webauthn.create(options, param.data.get("origin", "")) attestation = webauthn.create(options, param.data.get("origin", ""))
return attestation return attestation