Skip to content

feat: integrate Apple hardware security-key 2FA - #267

Open
Olafejs wants to merge 2 commits into
malmeloo:mainfrom
Olafejs:feat/experimental-apple-security-key
Open

Olafejs wants to merge 2 commits into
malmeloo:mainfrom
Olafejs:feat/experimental-apple-security-key

Conversation

@Olafejs

@Olafejs Olafejs commented Sep 9, 2026 •

Copy link
Copy Markdown

Summary

Related to #159 and parawanderer/OpenTagViewer#18.

This update responds to the maintainer's request on the original example PR: the Apple HSA2 security-key flow is now integrated into FindMy.py's existing second-factor model instead of living only in an example-local account subclass.

Native library integration

  • Adds dependency-free SecurityKeyChallenge and SecurityKeyAssertion value types.
  • Adds SecurityKeySecondFactorMethod, AsyncSecurityKeySecondFactor, and SyncSecurityKeySecondFactor alongside the existing SMS/trusted-device methods.
  • get_2fa_methods() can return the native security-key factor when Apple's auth page advertises the supported HSA2 layout.
  • Keeps SMS and trusted-device APIs unchanged.
  • Keeps submit(code: str) code-oriented; security keys use additive authenticate(signer) instead of a dummy code.
  • The signer callback is the dependency boundary, so python-fido2 remains optional and is used only by the Linux USB example.
  • Adds Apple HSA2 challenge parsing, RP/origin and credential validation, assertion encoding, continuation-header handling, exact endpoint statuses, response limits, and post-key GrandSlam/MobileMe state checks to the library.
  • Preserves backwards compatibility for existing BaseAppleAccount implementations by making the new account methods concrete NotImplementedError hooks rather than new abstract requirements.

Example changes

  • The Linux example now uses the normal AsyncAppleAccount and native AsyncSecurityKeySecondFactor.
  • The example only adapts USB CTAP through python-fido2; it no longer duplicates the account, Apple HTTP transport, parser, or authentication state machine.
  • Removed the example-only Apple root certificate, schema-diagnostic implementation, and duplicate protocol/authentication test stack now that those responsibilities are in the library.
  • Kept private one-attempt terminal handling, 0700/0600 session storage, password exclusion, no automatic retries, bounded PIN interaction and secret-free error reporting.
  • Updated README, protocol notes and account documentation with the native API and scope boundaries.

Evidence and limitations

The original adapter successfully completed login on one real Apple Account protected by a Yubico USB Security Key on Linux: assertion accepted, post-key GrandSlam authenticated, MobileMe issued a FindMy session, and the saved session excluded the password. A separate private integration subsequently reused the session and fetched owned accessory locations. Those private integrations and data are not included here.

This public contribution is tested offline against the current upstream source. It does not claim universal hardware/account compatibility, official Apple support, legacy FSA1/U2F support, primary-FSA2/passwordless login, or a second real-account trial of this extracted branch. No account-security changes, key removal, macOS SIP/AMFI changes or weaker fallback were used.

Verification

  • uv run --group test pytest -q: 173 passed
  • The same 173 tests passed on Python 3.10, 3.11, 3.12, 3.13 and 3.14 on Linux.
  • uv run basedpyright: 0 errors, 0 warnings, 0 notes
  • uv run ruff check .: passed
  • uv run ruff format --check .: passed
  • uv run pre-commit run --all-files: passed
  • uv build: source distribution and wheel built; the wheel contains findmy/reports/security_key.py, account.py and twofactor.py.
  • Sphinx documentation build completed without errors; existing upstream documentation warnings remain outside the changed account page.
  • Gitleaks plus exact private-value scans over every staged file: no findings.

Tests use explicitly synthetic accounts, challenges, credentials, assertions, signatures, transports and HTTP responses. The CTAP test exercises real python-fido2 and ECDSA against a synthetic authenticator, not a physical key or Apple. No account data, session, token, exported accessory bundle, location or homelab configuration is included.

API example

from findmy import SyncSecurityKeySecondFactor

state = account.login(email, password)
if state == LoginState.REQUIRE_2FA:
    method = next(
        item for item in account.get_2fa_methods()
        if isinstance(item, SyncSecurityKeySecondFactor)
    )
    state = method.authenticate(signer)

The signer callback receives a validated SecurityKeyChallenge and returns a SecurityKeyAssertion. The async API uses the same model with an async callback.

Contributed by Olafejs with AI-assisted implementation and testing. Existing FindMy.py GrandSlam/MobileMe code, python-fido2 and other dependencies retain their authorship and licenses; this contribution follows the repository's MIT license.

@malmeloo

Copy link
Copy Markdown
Owner

This is very cool, thank you very much for working on this! I see a lot of example code and tests, but no additions or changes to the library itself. It's awesome that you got this working, but I feel like it would be more useful for end users if FIDO auth was integrated into the library itself. There are existing base classes for this (e.g. BaseSecondFactorMethod) that are intended to implement 2FA methods.

Do you think you could retrofit your solution into the existing 2FA model? Or would we need API changes / additions to properly implement this?

@Olafejs

Olafejs commented Sep 15, 2026

Copy link
Copy Markdown
Author

Yes, I agree that native integration would be much more useful for end users.

I think the protocol can be retrofitted into the existing 2FA model, but not cleanly through the current request() -> None / submit(code: str) interface alone. That interface assumes a code-based factor, while WebAuthn is a challenge -> local authenticator interaction -> signed assertion flow.

My preferred approach would be a small, backward-compatible API addition:

  • keep the existing SMS and trusted-device methods unchanged;
  • add security-key challenge/assertion types and async/sync security-key factor implementations returned by get_2fa_methods();
  • keep the Apple protocol parsing, assertion encoding, and account state transitions in the library;
  • keep python-fido2 optional by letting the factor accept a signer/authenticator callback, with an optional helper adapter for USB keys.

I would avoid forcing the assertion through submit(code: str) or making that method ignore a dummy string. A security-key-specific authenticate(signer) method seems like the smallest clean extension and leaves room for other authenticator frontends later.

So the short answer is: yes, I can rework this into the library's 2FA model, but I recommend a small additive API extension rather than fitting it into the code-based interface unchanged. If that direction works for you, I can retrofit the PR around it.

Refs malmeloo#159. Opt-in Linux HSA2 FIDO2/WebAuthn adapter, checked authentication transport, private CLI and synthetic tests. No default SDK API changes.

Coding-Model: gpt-6-astra
Move the proven HSA2 challenge, assertion, and authentication flow into FindMy's native BaseSecondFactorMethod model with async and sync APIs. Keep python-fido2 optional through a signer callback, preserve code-based factors, bound the 2FA transport, and update docs/tests.

Coding-Model: gpt-5.6-sol-900k
@Olafejs
Olafejs force-pushed the feat/experimental-apple-security-key branch from 3ad09f3 to 58b5c84 Compare September 15, 2026 20:29
@Olafejs Olafejs changed the title feat: add experimental Apple hardware-security-key login example feat: integrate Apple hardware security-key 2FA Sep 15, 2026
@Olafejs

Olafejs commented Sep 15, 2026

Copy link
Copy Markdown
Author

Thanks — I reworked the PR around the existing second-factor model as suggested.

The native library now exposes AsyncSecurityKeySecondFactor and SyncSecurityKeySecondFactor through get_2fa_methods(), with a small additive authenticate(signer) API. SMS and trusted-device methods remain unchanged, and security-key assertions are not forced through submit(code: str). python-fido2 stays optional and is used only by the Linux USB example adapter.

The Apple HSA2 parser, assertion codec, continuation handling, bounded verified-TLS 2FA transport and post-key GrandSlam/MobileMe transitions are now in findmy/; the example only supplies USB signing and the private CLI boundary. Existing custom BaseAppleAccount implementations are not made abstractly incompatible.

Verification on the rebased branch: 173 tests passed on Python 3.10–3.14, Ruff, basedpyright and pre-commit passed, and the built wheel contains the native security-key modules. I also kept the real-account evidence and universal-compatibility limitations explicit in the PR description; no account data or session material is included.

@malmeloo

Copy link
Copy Markdown
Owner

Thanks, I agree that this is probably the correct approach. I can't review your changes in detail right now so give me some time to look into it. One more thing: I'd suggest that we drop the current example additions entirely. It doesn't add a whole lot to the library (they appear to bypass the library entirely), and while it's certainly interesting, I think it serves more as documentation on how the spec works. It definitely has a purpose somewhere (dedicated FindMy docs, anyone???), but probably not in this library.

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