Skip to content

feat: authenticate with a hardware security key - #174

Open
epheterson wants to merge 2 commits into
mandarons:mainfrom
epheterson:feat/security-key
Open

feat: authenticate with a hardware security key#174
epheterson wants to merge 2 commits into
mandarons:mainfrom
epheterson:feat/security-key

Conversation

@epheterson

Copy link
Copy Markdown
Contributor

Once security keys are enrolled on an Apple ID, Apple stops offering a 6-digit code and returns an fsaChallenge — a WebAuthn assertion request — from every 2FA endpoint. validate_2fa_code then has nothing to validate, and such an account cannot complete two-factor at all.

New surface:

api.security_key_challenge               # pending challenge, or None
api.confirm_security_key()               # sign with an attached key, submit, trust
api.confirm_security_key(assertion=...)  # submit an assertion built elsewhere
api.sign_security_key_challenge(c, dev)  # sign without submitting
api.fido2_devices                        # attached devices
build_security_key_assertion(...)        # assertion response -> Apple's payload

fido2 is imported lazily and only on the signing path, so it stays optional and callers that supply an assertion don't need it installed.

Two details that are easy to get wrong, both taken from Apple's sign-in bundle:

  • an accepted assertion returns 409, not 2xx (250 is Apple's own "two-factor completed")
  • the challenge in the payload is read back out of clientData, so the value signed and the value declared can't diverge

19 tests added, no network. Verified end to end against a real account and a YubiKey: one touch, hsaTrustedBrowser=True, session trusted.

Note tests/test_account.py::test_storage fails on main already, unrelated to this change.

epheterson and others added 2 commits August 21, 2026 22:06
Apple stops offering a 6-digit code once security keys are enrolled on an
Apple ID and returns an fsaChallenge instead, so validate_2fa_code has
nothing to validate and such an account cannot complete 2FA.

Adds:

  security_key_challenge          the pending challenge, or None
  confirm_security_key(...)       sign with an attached key and submit,
                                  or submit an assertion built elsewhere
  sign_security_key_challenge()   sign without submitting
  build_security_key_assertion()  assertion response -> Apple's payload
  fido2_devices                   attached devices

fido2 is imported lazily and only when signing, so callers that supply an
assertion do not need it.

Two details worth noting: Apple answers an accepted assertion with 409
rather than 2xx, and the challenge in the payload is taken from clientData
so the value signed and the value declared cannot diverge.

Verified against a real account and a YubiKey.

Co-Authored-By: Claude <noreply@anthropic.com>
@mandarons

Copy link
Copy Markdown
Owner

/oc please review this code.

@opencode-agent

Copy link
Copy Markdown

Model not found: opencode/minimax-m3-free. Did you mean: minimax-m3, hy3-free, mimo-v2.5-free?

opencode session  |  github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants