Skip to content

feat: download resident keys from FIDO2 security keys - #162

Open
rschaffar wants to merge 1 commit into
gwitko:masterfrom
rschaffar:feat/resident-keys
Open

feat: download resident keys from FIDO2 security keys#162
rschaffar wants to merge 1 commit into
gwitko:masterfrom
rschaffar:feat/resident-keys

Conversation

@rschaffar

Copy link
Copy Markdown

Closes #154.

I will gladly take and apply any change requests — including splitting
or trimming this if it goes beyond what you want to maintain.

ssh-keygen -K for the phone: the Add hardware key sheet gets a third
source, From key, which reads resident (discoverable) OpenSSH
credentials off a FIDO2 authenticator over USB or NFC and turns them into
the usual *_sk stubs. Motivation is the recovery case from #154: if the
phone is lost, the authenticator alone is enough to restore access — no
desktop with OpenSSH 8.3+ needed.

The private part never leaves the authenticator. A stub contains only the
public key, application string, and credential handle. Downloading
requires the key's PIN; CTAP credential management always does.

Decisions

Own credential-management client instead of the fido2 package's.
The package's CredentialManagement hardcodes the CTAP 2.1 command byte
(0x0A) and its constructor rejects authenticators that only advertise
credentialMgmtPreview — which is what most deployed YubiKeys expose.
The preview command (0x41) shares the wire format, so
_CredentialManagementClient reuses the package's request encoder,
response decoder, and data types, and only issues the command byte
itself. It is also more tolerant where pre-2.1 firmware is quirky:
missing totalRPs/credProtect fields get spec defaults instead of the
package's null-assertion crashes, and CTAP2_ERR_NO_CREDENTIALS is an
empty result, not an error. OpenSSH's sk-usbhid.c does the same preview
fallback. The class can be deleted once the package supports the preview
command.

pinUvAuthParam length depends on the PIN protocol. The package's
authenticate() returns the full 32-byte HMAC for both protocols, but
CTAP requires LEFT(HMAC, 16) for PIN protocol 1 and the full HMAC for
protocol 2. Sending 32 bytes to a protocol-1 key — every
credentialMgmtPreview-era YubiKey — fails with PIN_AUTH_INVALID.
Protocol 2 is preferred when the key advertises it.

PIN is collected before the CTAP session opens. On iOS an NFC session
does not survive a PIN dialog. Reading resident keys always requires the
PIN, so it is collected up front on all platforms — the same pattern as
the existing signing flow, including re-prompts with the remaining-retries
count after a rejection, bounded at 3 attempts.

Stub flags mirror OpenSSH's sk-usbhid.c. Downloaded stubs get
SSH_SK_USER_PRESENCE_REQD | SSH_SK_RESIDENT_KEY (0x21), plus
SSH_SK_USER_VERIFICATION_REQD when the credential was created with
credProtect uvRequired (0x03) — so ssh prompts for UV exactly when the
key will demand it.

Only ssh: applications are enumerated. RPs are filtered to the
ssh: prefix before any credentials are requested, so WebAuthn logins on
the same key are never read. Credentials with unsupported algorithms are
skipped; ed25519-sk and ecdsa-sk (P-256) are supported.

The picker shows SHA256 fingerprints. Two resident keys can share an
application (both bare ssh:), making picker rows otherwise identical.
The fingerprint is computed exactly like ssh-keygen -l (SHA256 over the
public key blob, unpadded base64), so it can be checked against
authorized_keys. The label is pre-filled from the application suffix;
ssh-keygen's fixed user name "openssh" is never suggested as a label.

Testing

  • flutter analyze: 0 issues. dart format: no diff.
  • flutter test: 264/264. 11 new tests drive the downloader against a
    scripted fake authenticator: byte-exact stubs (re-parsed with
    dartssh2), ecdsa point padding, preview command fallback,
    credProtect→UV mapping, non-ssh RP filtering, empty key, PIN re-prompt
    and cancel, and pinUvAuthParam length under both PIN protocols
    (FakeCtapDevice gained a configurable token length for the
    protocol-2 case).
  • Android debug APK builds clean (both flavors).
  • Tested on Android with two of my own YubiKey 5C NFC keys: firmware
    5.7.1 (CTAP 2.1 credMgmt) and firmware 5.4.3
    (credentialMgmtPreview), so both command bytes are verified on real
    hardware — each with multiple resident SSH keys and the picker, the
    5.4.3 run including verify-required credentials (credProtect → UV
    flag). Everything works as expected. Both firmwares advertise PIN
    protocol 2, which is preferred when available; the protocol-1 length
    handling is covered by the unit tests.

Development was assisted by GPT 5.6-sol and Claude Fable 5.

Adds an ssh-keygen -K equivalent to the Add hardware key sheet: a new
"From key" action reads resident (discoverable) OpenSSH credentials
straight off a FIDO2 authenticator over USB or NFC and turns them into
the usual *_sk stubs, so a lost stub no longer requires a desktop to
recover access.

- New FidoResidentKeyDownloader speaks CTAP2 authenticatorCredential-
  Management, including the pre-2.1 credentialMgmtPreview command byte
  that most deployed YubiKeys expose, enumerates RPs filtered to the
  ssh: application prefix, and rebuilds ed25519-sk / ecdsa-sk stubs
  (mirroring OpenSSH flag semantics, incl. credProtect=3 -> UV flag).
- PIN collected up front so iOS NFC sessions survive; incorrect PINs
  re-prompt with remaining retries, mirroring the signing flow.
- Sheet shows live status while talking to the key and offers a picker
  when several resident SSH keys are found; label is pre-filled from
  the application suffix.

Closes gwitko#154
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.

[Feature Request] Download ed25519-SK resident key

1 participant