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>
55 lines
1.8 KiB
Python
55 lines
1.8 KiB
Python
"""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()
|