Skip to content
switch-nzPublic

About

Submission for the Yubico 5.8 Hackathon

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Keybound - hardware-bound browser sessions (YubiKey 5.8 / CTAP 2.3)

A single-server demo showing the benefits of a hardware-bound session: a stolen session cookie - leaked via infostealer malware, XSS exfiltration, phishing, a compromised backup, whatever - is worthless on its own. Every authenticated request, including reads, must carry a live proof that the browser holding the cookie is the same one that the session is hardware-bound to.

Built for the YubiKey 5.8 / CTAP 2.3 developer hackathon. It uses the headline 5.8 capability - hardware-backed signatures via previewSign + ARKG - to mint that binding once per short-lived epoch, then verifies every request against it with a cheap HMAC. The demo app behind the session is a personal account dashboard (SSN, date of birth, phone, address, a saved payment card, recovery email, trusted devices) - deliberately the kind of PII payload an infostealer actually goes after when it steals a cookie. The subject of this submission is the session-binding mechanism, not the app sitting behind it.

Limitation: this defeats the entire class of off-machine attacker - anyone who only has a copy of the cookie. It does not defend against an attacker whose code is already running inside your live browser (XSS, a malicious extension): that code shares the page's JS memory, so it can read the binding key exactly like the app itself and mint valid requests. The demo shows this directly rather than asserting it - see the two attacker scenarios further along in this README.

To run this on a hardware key

In hardware mode the rolling-proof epoch is signed by the YubiKey via previewSign/ARKG.

Prerequisite - enroll the ARKG seed once (needs the key; Windows admin terminal). This produces arkg_enrollment.json; copy it into this folder:

python enroll_arkg.py            # writes arkg_enrollment.json (defaults to --flags unattended;
                                  # run --help for the touch/PIN presence-policy variants)

Run it

pip install cryptography fido2    # the only two dependencies
                                  #   cryptography - always
                                  #   fido2        - talking to the key (--hardware, enrolment)

Terminal 1 - the app server:

python server.py --hardware      # loads arkg_enrollment.json; serves http://localhost:8000

Terminal 2 - the local signing agent (talks to the YubiKey; Windows admin):

python signing_agent.py          # http://localhost:8801, POST /sign

Then open http://localhost:8000, create an account / sign in with the passkey, and the browser will drive prepare → agent /sign → commit to bind the session on the key. The sign-in establishment asks for a touch (up=true); background refreshes are silent (up=false). Expect roughly a one-second pause on each epoch signature - during testing, this 1s pause became apparent on the preview 5.8 YubiKey, and it's why the application signs once per epoch and binds each request with a cheap HMAC rather than signing every request.

Notes:

  • The agent's RP_ID (inertcookie.local) must match what the ARKG credential was enrolled under; the web RP_ID (localhost) is separate and drives the passkey ceremonies. They don't need to agree.
  • If the page runs on a port other than 8000, update PAGE_ORIGIN in signing_agent.py (CORS) and pass --port to match.
  • Verify the whole route with software crypto first - no device needed: python test_hw_route.py runs prepare → sign → commit → bound read end-to-end, so if that passes, only the ARKG-derive and the on-key signature are left to exercise live.

In production:

The manual enroll_arkg.py run above is fine for this hackathon demo, but the fact that it requires - an elevated terminal and a separate command isn't a good end-user experience, and isn't actually necessary long-term.

Why it exists: previewSign/ARKG's generateKey extension isn't yet part of the browser-exposed WebAuthn surface - navigator.credentials.create() doesn't know about it, only raw CTAP does.

The fix: previewSign/ARKG is early-access on 5.8 - once it graduates into the standard WebAuthn extension surface, browsers expose it directly and the native helper requirement is resolved.

How it works: the hardware-bound session

Every authenticated route - including GET /api/profile - requires a rolling-proof binding keyed by bk, a per-epoch key that lives only in browser memory and the server's session state, never in the cookie.

  • Once per epoch (a short window) the client produces one hardware signature over an epoch proof: {session, origin, epoch id, start, lifetime, commitment to bk}. In --hardware mode this is a real previewSign/ARKG signature on the YubiKey.
  • Every request in that epoch carries a cheap HMAC(bk, canonical_request)
    • no hardware round-trip, unlimited rate. canonical.py/canonical.js keep client and server byte-identical on what gets bound: method, path, body hash, origin, session id, timestamp, and a strictly-increasing counter (verifier.py's sliding-window replay check).

A stolen cookie carries none of this. It can't read the SSN, the saved card, the address - let alone hijack the recovery email - the entire infostealer / stolen-cookie / phished-session class of attack is dead against this session, not just login.

Architecture (one server)

server.py          HTTP server: WebAuthn RP + sessions + rolling-proof + UI
webauthn_rp.py     minimal stdlib-only WebAuthn (CBOR/COSE parse, assertion verify)
rolling_proof.py   epoch proofs + MAC-bound request verification (tested)
verifier.py        ECDSA-P256 verify + sliding-window replay (tested)
canonical.py       canonical request contract (byte-identical to canonical.js)
canonical.js       browser port of the request contract
web/index.html     sign-in / sign-up / app / attacker UI
app.js             WebAuthn client + transparent secured-fetch + attacker scenarios
signing_agent.py   (hardware mode) local previewSign/ARKG signer for the epoch
enroll_arkg.py     (hardware mode, one-time) enrolls the ARKG seed -> arkg_enrollment.json
test_hw_route.py   end-to-end test of the hardware epoch route (see Tests below)
users.json         persisted identities (created on first run; gitignored)

State: what survives a restart and what doesn't

users.json (created next to server.py on first registration) persists usernames, WebAuthn public-key credentials, and the synthetic PII profile (SSN, DOB, phone, address, card, recovery email, trusted devices) across server restarts - no private key material is ever in it.

Everything else is deliberately memory-only and dies with the process: sessions, WebAuthn challenges, and - most importantly - the rolling-proof epoch and bk itself, which must never touch disk or they'd defeat their own purpose.

Security notes

  • Default mode uses real WebAuthn with a software epoch signer. --hardware puts the epoch proof on the key.
  • The demo shows the cookie and doesn't set HttpOnly so you can see it get stolen; production would set HttpOnly; Secure; SameSite. The point stands: even a fully-stolen cookie is inert without bk.
  • In-browser attackers are explicitly not defended against, and the demo shows this rather than hiding it. Closing that gap would mean requiring a fresh per-action WebAuthn gesture on top of the session binding - deliberately left out of this submission.

AI disclosure

This project was built with substantial assistance from Claude Code (Anthropic), used throughout as a pair-programmer, code reviewer, and debugging partner:

The concept and security design behind Keybound are my own - the rolling-proof session binding, the per-epoch signing model that works around ARKG's ~1 second signature cost, and the threat model it's built to defeat. I used Claude Code (Anthropic) to turn that design into working software quickly: writing the implementation, debugging the YubiKey and CTAP integration, and iterating on the demo. Every architectural and product decision was mine; the AI accelerated the build, not the thinking.

About

Submission for the Yubico 5.8 Hackathon

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages