Skip to content

fix(openssh): decide FIDO host-key coverage for the hostkey-agent sk certificate phases #328

Closed

Description

@inureyes

Problem / Background

Follow-up to #314. The pinned OpenSSH V_10_3_P1 regress test hostkey-agent passes every plain host-key phase, every non-FIDO host-certificate phase, and multiple hostkeys. The two remaining failures are cert type sk-ssh-ed25519-cert-v01@openssh.com and cert type sk-ecdsa-sha2-nistp256-cert-v01@openssh.com.

The test builds its server host keys from ${SSH} -Q key-plain of the client under test (test-exec.sh:731-732), but takes its certificate list from the reference ssh -Q key-sig, which includes the sk-* certificate types when the regress tree is built with the sk-dummy provider (both CI runners and this host). bssh does not list sk-* in -Q key-plain (src/app/query.rs:20), so the server never holds an sk-* host key and a client restricted to -oHostKeyAlgorithms=sk-*-cert-v01@openssh.com cannot negotiate one.

Listing sk-* in -Q key-plain also makes every test's setup add sk-* user keys as IdentityFile and HostKey (test-exec.sh:734-760), and keytype.sh logs in with ed25519-sk and ecdsa-sk user keys, so bssh must sign user authentication through the security-key provider. Host-side support alone cannot pass the suite.

Scope decision (2026-10-22)

FIDO security keys are in scope, matching OpenSSH 10.3p1. This reverses the FIDO half of the #275 exclusion; PKCS#11 stays excluded. OpenSSH behavior, from the pinned source:

  • KEX_DEFAULT_PK_ALG (myproposal.h) lists sk-ssh-ed25519, sk-ecdsa-sha2-nistp256, and webauthn-sk-ecdsa-sha2-nistp256 (plain and -cert-v01@openssh.com) after the ECDSA entries and before RSA in each group; the reference ssh -G prints the same order for hostkeyalgorithms and pubkeyacceptedalgorithms.
  • -Q key-plain lists sk-ssh-ed25519@openssh.com after ssh-ed25519 and sk-ecdsa-sha2-nistp256@openssh.com after ecdsa-sha2-nistp521; -Q key-sig adds the webauthn-sk-ecdsa-sha2-nistp256 forms.
  • SecurityKeyProvider defaults to internal (built-in USB HID) when built with it and to $SSH_SK_PROVIDER otherwise (readconf.c:3014-3018); a leading $ names an environment variable. Signing goes through sshsk_sign (ssh-sk.c) in the ssh-sk-helper subprocess, which loads the provider library (sk-api, SSH_SK_VERSION_MAJOR 0x000a0000).

The built-in internal provider is split into #331, which builds on the provider interface added here.

Proposed Solution

  • Advertise the sk-* types in -Q key-plain, -Q key, -Q key-cert, -Q key-sig, and the default HostKeyAlgorithms and PubkeyAcceptedAlgorithms in OpenSSH's order (diff each against the reference ssh -Q and ssh -G output). Advertise a webauthn-sk-* type only if bssh verifies it; otherwise document the gap.
  • Verify sk-* host-key KEX signatures and sk-* host certificates through the existing known_hosts and host_certificate.rs paths (russh 0.64.1 ALL_KEY_TYPES and ssh-key 0.7 already know both algorithms). Follow OpenSSH's flag checks for host-key signatures.
  • Implement SecurityKeyProvider for user authentication: parse the option (currently Unimplemented in src/ssh/ssh_config/parser/options/support.rs:245), expand $VAR, load an external sk-api library, and sign sk-* identity files (key handle, application, flags) through it, with OpenSSH's user-presence and PIN prompts. Decide in-process loading versus an ssh-sk-helper-style subprocess and record why. Keys held in an agent keep signing through the agent. Define the provider interface so feat(ssh): implement the built-in internal SecurityKeyProvider for USB HID authenticators #331 plugs internal in; until then internal (the default) returns an explicit diagnostic and authentication moves on to the next identity.
  • Static musl builds cannot dlopen: they must still build and report an explicit diagnostic for an external provider.
  • Update docs/openssh-compatibility-status.md, the epic: OpenSSH drop-in compatibility, measured against the OpenSSH regress suite #275 exclusion text, and the PKCS#11 skip reasons in tests/openssh-regress/selection.tsv so they no longer claim FIDO is excluded.

Acceptance Criteria

Reproduction

python3 tests/openssh-regress/run.py --test hostkey-agent --results /tmp/hostkey-agent.json, then inspect target/openssh-regress/logs/hostkey-agent-candidate.log: only the two sk-*-cert-v01@openssh.com phases report failed / bad SSH_CONNECTION key type.

Activity

  1. inureyes commented on Oct 22, 2026

    @inureyes
    MemberAuthor

    Finding from #330 item 4 (certificate-only HostKeyAlgorithms negotiation), recorded here for the implementer of this issue. No code changed for it in #330.

    bssh does not advertise anything outside a certificate-only HostKeyAlgorithms list. resolve_host_keys (src/ssh/tokio_client/algorithms.rs) puts sk-ssh-ed25519-cert-v01@openssh.com into certificates and leaves keys as an explicit empty list, the parser stores both as Some(..), and to_russh_config (src/ssh/tokio_client/connection.rs) passes them to Preferred::key and Preferred::host_key_certificates without falling back to the defaults. russh 0.64.1 writes the client KEXINIT host-key name-list as host_key_certificates (as certificate names) followed by key (negotiation.rs, write_kex), and read_kex selects from the same combined list, so a server cannot negotiate a type the client did not list. prefer_known_host_algorithms only reorders, and ordering is disabled for an explicit list anyway. A probe through the real -o HostKeyAlgorithms=... path on main 5b20ce5 advertised exactly ["sk-ssh-ed25519-cert-v01@openssh.com"], and the same for ssh-ed25519-cert-v01@openssh.com.

    Why hostkey-agent fails its two sk-* cert phases: test-exec.sh builds SSH_KEYTYPES from $SSH -Q key-plain, which for the candidate run is bssh and lists no sk-* types, so the script never creates sk-* host keys or host certificates for sshd. SSH_CERTTYPES comes from the reference ssh -Q key-sig on PATH, which does list the sk-* cert types, so the script still runs those phases. sshd then has no sk-* certificate to offer, there is no common host-key algorithm, and bssh fails the connection before key exchange completes (no kex: host key algorithm line for those invocations in the latest run). That is the correct fail-closed result. The kex: host key algorithm: ssh-ed25519 lines near those phases in failed-ssh.log belong to other invocations: that file concatenates every failing test's client log. Older runs in the same file fail even earlier with invalid label: 'sk-ssh-ed25519', from a binary that predates the certificate_algorithm parser.

    Consequence for this issue: once bssh reports the sk-* types in -Q key-plain (and -Q key), the regress script will generate sk-* host keys and certificates through SSH_SK_PROVIDER, and the two phases should pass only if bssh can verify an sk-* host certificate signature. They are the acceptance signal for the host-key half of this issue.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions