Initial prototype: passkey PRF → HKDF → Nostr key

Full pipeline proving a WebAuthn PRF assertion can anchor a stable
Nostr keypair. Core derivation in nostr_passkey/derivation.py (pure,
unit-tested), WebAuthn ceremony glue in webauthn_flow.py, FastAPI
surface in app.py, single-page WebAuthn client in web/index.html, and
an end-to-end simulation in scripts/demo.py for running without a real
authenticator.

Verified working against Firefox 149 + macOS Touch ID over HTTPS on
https://localhost:8000 with a self-signed loopback cert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 23:12:27 -04:00
commit 5f35195e72
10 changed files with 1404 additions and 0 deletions

54
scripts/demo.py Normal file
View File

@@ -0,0 +1,54 @@
"""End-to-end simulation of the passkey -> Nostr key derivation flow.
Without a real WebAuthn authenticator and browser handy, this script
stubs the 32-byte PRF output with a deterministic hash so you can watch
the rest of the pipeline work: HKDF stretches the entropy, the result
becomes a Nostr private key, and nsec/npub fall out the other side.
Run it with:
python -m scripts.demo
"""
from __future__ import annotations
import hashlib
from nostr_passkey.derivation import derive_nostr_key
def fake_prf_output(seed: str) -> bytes:
"""Stand-in for ``clientExtensionResults.prf.results.first``."""
return hashlib.sha256(seed.encode("utf-8")).digest()
def main() -> None:
prf = fake_prf_output("pretend-passkey-credential")
print(f"Simulated PRF output (hex): {prf.hex()}")
ident_none = derive_nostr_key(prf)
print("\n-- No salt --")
print(f" privkey_hex: {ident_none.privkey_hex}")
print(f" nsec: {ident_none.nsec}")
print(f" npub: {ident_none.npub}")
ident_work = derive_nostr_key(prf, salt="work")
print("\n-- salt='work' --")
print(f" nsec: {ident_work.nsec}")
print(f" npub: {ident_work.npub}")
ident_personal = derive_nostr_key(prf, salt="personal")
print("\n-- salt='personal' --")
print(f" nsec: {ident_personal.nsec}")
print(f" npub: {ident_personal.npub}")
# Invariants the whole design rests on:
ident_work_again = derive_nostr_key(prf, salt="work")
assert ident_work.nsec == ident_work_again.nsec, "derivation must be deterministic"
assert ident_work.nsec != ident_none.nsec, "salt must change the output"
assert ident_work.nsec != ident_personal.nsec, "different salts must diverge"
print("\nDeterminism and salt separation verified.")
if __name__ == "__main__":
main()