mirror of
https://github.com/morgan9e/virtual-webauthn
synced 2026-04-13 15:55:06 +09:00
Added user confirmation on cred creation
This commit is contained in:
@@ -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.
|
||||||
@@ -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": {}
|
||||||
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_cbor = cbor2.dumps(attestation_obj)
|
||||||
|
|
||||||
attn_obj = {
|
client_data = {
|
||||||
"fmt": attn_fmt,
|
"challenge": self._b64url(challenge),
|
||||||
"attStmt": attn_stmt,
|
"origin": origin,
|
||||||
"authData": auth_data
|
"type": "webauthn.create",
|
||||||
}
|
"crossOrigin": False,
|
||||||
attn_cbor = cbor2.dumps(attn_obj)
|
}
|
||||||
|
|
||||||
|
client_data_json = json.dumps(client_data).encode()
|
||||||
|
|
||||||
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,47 +287,29 @@ 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")
|
||||||
|
|
||||||
if not origin:
|
if not origin:
|
||||||
origin = data.get("origin")
|
origin = data.get("origin")
|
||||||
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"})
|
||||||
@@ -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)
|
||||||
authOptions.publicKey.excludeCredentials = authOptions.publicKey.excludeCredentials.map(credential => ({
|
if (authOptions.publicKey.excludeCredentials) {
|
||||||
...credential, id: abb64(credential.id)
|
authOptions.publicKey.excludeCredentials = authOptions.publicKey.excludeCredentials.map(credential => ({
|
||||||
}));
|
...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'},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user