/** @page libssh_tutor_fido2 Chapter 11: FIDO2/U2F Keys Support @section fido2_intro Introduction The traditional SSH public key model stores the private key on disk and anyone who obtains that file (and possibly its passphrase) can impersonate the user. FIDO2 authenticators, such as USB security keys, are hardware tokens that generate or securely store private key material within a secure element and may require explicit user interaction such as a touch, PIN, or biometric verification for use. Hence, security keys are far safer from theft or exfiltration than traditional file-based SSH keys. libssh provides support for FIDO2/U2F security keys as hardware-backed SSH authentication credentials. This chapter explains the concepts, build prerequisites, the API, and usage patterns for enrolling (creating) and using security key-backed SSH keys, including resident (discoverable) credentials. @subsection fido2_resident_keys Resident Keys Two credential storage modes exist for security keys: - Non-resident (default): A credential ID (key handle) and metadata are stored on the client-side in a key file. This key handle must be presented to the FIDO2/U2F device while signing. This is somewhat similar to traditional SSH keys, except that the key handle is not the private key itself, but used in combination with the device's master key to derive the actual private key. - Resident (discoverable): The credential (and metadata like user id) is stored on the device. No local file is needed; the device can enumerate or locate the credential internally when queried. Advantages of resident keys include portability (using the same device across hosts) and resilience (no loss if the local machine is destroyed). Although, they may be limited by the storage of the authenticator. @subsection fido2_presence_verification User Presence vs. User Verification FIDO2 distinguishes between: - User Presence (UP): A simple physical interaction (touch) to confirm a human is present. - User Verification (UV): Verification of the user’s identity through biometric authentication or a PIN. Requiring UV provides additional protection if the device is stolen and used without the PIN/biometric. libssh exposes flags controlling these requirements (see below). @subsection fido2_callbacks The Callback Abstraction Different environments may need to access security keys through different transport layers (e.g., USB-HID, NFC, Bluetooth, etc.). To accommodate this variability, libssh does not hard-code a single implementation. Instead, it defines a small callback interface (`ssh_sk_callbacks`) used for all security key operations. Any implementation of this callback interface can be used by higher-level PKI functions to perform enroll/sign/load_resident_keys operations without needing to know the transport specifics. Hence, users can define their own implementations for these callbacks to support different transport protocols or custom hardware. Refer @ref fido2_custom_callbacks for additional details. The callback interface is defined in `libssh/callbacks.h` and the behaviour and return values are specified by `libssh/sk_api.h`, which is the same interface defined by OpenSSH for its security key support. This means that any callback implementations (also called "middleware" in OpenSSH terminology) developed for OpenSSH can be adapted to libssh with minimal changes. The following operations are abstracted by the callback interface: - api_version(): Report the version of the SK API that the callback implementation is based on, so that libssh can check whether this implementation would be compatible with the SK API version that it supports. Refer @ref fido2_custom_callbacks_version for additional details. - enroll(): Create (enroll) a new credential, returning public key, key handle, attestation data. - sign(): Produce a signature for supplied inputs using an existing key handle. - load_resident_keys(): Enumerate resident (discoverable) credentials stored on the authenticator. libssh provides a default implementation of the `ssh_sk_callbacks` using the libfido2 library for the USB-HID transport protocol. Hence, by default, libssh can interact with any FIDO2/U2F device that supports USB-HID and is compatible with libfido2, without requiring any additional modifications. @subsection fido2_build Building with FIDO2 Support To enable FIDO2/U2F support, libssh must be built with the WITH_FIDO2 build option as follows: @verbatim cmake -DWITH_FIDO2=ON .. @endverbatim libssh will also build the default USB-HID `ssh_sk_callbacks`, if the libfido2 library and headers are installed on your system. @warning If built without libfido2, support for interacting with FIDO2/U2F devices over USB-HID will not be available. @subsection fido2_api_overview API Overview Security key operations are configured through the `ssh_pki_ctx` which allows to specify both general PKI options and FIDO2-specific options such as the sk_callbacks, challenge data, application string, flags, etc. The following sections describe the options that can be configured and how the `ssh_pki_ctx` is used in conjunction with `ssh_key` to perform enrollment, signing, and resident key loading operations. @subsection fido2_key_objects Security Key Objects & Metadata Security keys are surfaced as `ssh_key` objects of type `SSH_KEYTYPE_SK_ECDSA` and `SSH_KEYTYPE_SK_ED25519` (corresponding to the OpenSSH public key algorithm names `sk-ecdsa-sha2-nistp256@openssh.com` and `sk-ssh-ed25519@openssh.com`). In addition to standard key handling, libssh exposes the following helper functions to retrieve embedded SK metadata: - ssh_key_get_sk_application(): Returns the relying party / application (RP ID) string. The Relying Party ID (RP ID) is a string that identifies the application or service requesting key enrollment. It ensures that a credential is bound to a specific origin, preventing phishing across sites. During registration, the authenticator associates the credential with this RP ID so that it can later only be used for authentication requests from the same relying party. For SSH keys, the common format is "ssh:user@host". - ssh_key_get_sk_user_id(): Returns a copy of the user ID associated with a key which represents a unique identifier for the user within the relying party (application) context. It is typically a string (such as an email, or a random identifier) that helps distinguish credentials belonging to different users for the same application. Though the user ID can be binary data according to the FIDO2 spec, libssh only supports NUL-terminated strings for enrolling new keys in order to remain compatible with the OpenSSH's sk-api interface. However, libssh does support loading existing resident keys with user IDs containing arbitrary binary data. It does so by using an `ssh_string` to store the loaded key's user_id, and an `ssh_string` can contain arbitrary binary data that can not be stored in a traditional NUL-terminated string (like null bytes). @note The user_id is NOT stored in the key file for non-resident keys. It is only available for resident (discoverable) keys loaded from the authenticator via ssh_sk_resident_keys_load(). For keys imported from files, this function returns NULL. - ssh_key_get_sk_flags(): Returns the flags associated with the key. The following are the supported flags and they can be combined using bitwise OR: - SSH_SK_USER_PRESENCE_REQD : Require user presence (touch). - SSH_SK_USER_VERIFICATION_REQD : Require user verification (PIN/biometric). - SSH_SK_RESIDENT_KEY : Request a resident discoverable credential. - SSH_SK_FORCE_OPERATION : Force resident (discoverable) credential creation even if one with same application and user_id already exists. These functions perform no additional communication with the authenticator, this metadata is captured during enrollment/loading and cached in the `ssh_key`. @subsection fido2_options Setting Security Key Context Options Options are set via ssh_pki_ctx_options_set(). Representative security key options: - SSH_PKI_OPTION_SK_APPLICATION (const char *): Required relying party ID If not set, a default value of "ssh:" is used. - SSH_PKI_OPTION_SK_FLAGS (uint8_t *): Flags described above. If not set, defaults to SSH_SK_USER_PRESENCE_REQD. This is because OpenSSH `sshd` requires user presence for security key authentication by default. - SSH_PKI_OPTION_SK_USER_ID (const char *): Represents a unique identifier for the user within the relying party (application) context. It is typically a string (such as an email, or a random identifier) that helps distinguish credentials belonging to different users for the same application. If not set, defaults to 64 zeros. - SSH_PKI_OPTION_SK_CHALLENGE (ssh_buffer): Custom challenge; if omitted a random 32-byte challenge is generated. - SSH_PKI_OPTION_SK_CALLBACKS (ssh_sk_callbacks): Replace the default callbacks with custom callbacks. PIN callback: Use ssh_pki_ctx_set_sk_pin_callback() to register a function matching `ssh_auth_callback` to prompt for and supply a PIN. The callback may be called multiple times to ask for the pin depending on the authenticator policy. Callback options: Callback implementations may accept additional configuration name/value options such as the path to the fido device. These options can be provided via `ssh_pki_ctx_sk_callbacks_option_set()`. Refer @ref fido2_custom_callbacks_options for additional details. The built-in callback implementation provided by libssh supports additional options, with their names defined in `libssh.h` prefixed with `SSH_SK_OPTION_NAME_*`, such as: SSH_SK_OPTION_NAME_DEVICE_PATH: Used for specifying a device path. If the device path is not specified and multiple devices are connected, then depending upon the operation and the flags set, the callback implementation may automatically select a suitable device, or the user may be prompted to touch the device they want to use. SSH_SK_OPTION_NAME_USER_ID: Used for setting the user ID. Note that the user ID can also be set using the ssh_pki_ctx_options_set() API. @subsection fido2_enrollment Enrollment Example An enrollment operation creates a new credential on the authenticator and returns an ssh_key object representing it. The application and user_id fields are required for creating the credential. The other options are optional. A successful enrollment returns the public key, key handle, and metadata which are stored in the ssh_key object, and may optionally return attestation data which is used for verifying the authenticator model and firmware version. Below is a simple example enrolling an Ed25519 security key (non-resident) requiring user presence only: @code #include #include static int pin_cb(const char *prompt, char *buf, size_t len, int echo, int verify, void *userdata) { (void)prompt; (void)echo; (void)verify; (void)userdata; /* In a real application, the user would be prompted to enter the PIN */ const char *pin = "4242"; size_t l = strlen(pin); if (l + 1 > len) { return SSH_ERROR; } memcpy(buf, pin, l + 1); return SSH_OK; } int enroll_sk_key() { const char *app = "ssh:user@host"; const char *user_id = "alice"; uint8_t flags = SSH_SK_USER_PRESENCE_REQD | SSH_SK_USER_VERIFICATION_REQD; const char *device_path = "/dev/hidraw6"; /* Optional device path */ ssh_pki_ctx pki_ctx = ssh_pki_ctx_new(); ssh_pki_ctx_options_set(pki_ctx, SSH_PKI_OPTION_SK_APPLICATION, app); ssh_pki_ctx_options_set(pki_ctx, SSH_PKI_OPTION_SK_USER_ID, user_id); ssh_pki_ctx_options_set(pki_ctx, SSH_PKI_OPTION_SK_FLAGS, &flags); ssh_pki_ctx_set_sk_pin_callback(pki_ctx, pin_cb, NULL); ssh_pki_ctx_sk_callbacks_option_set(pki_ctx, SSH_SK_OPTION_NAME_DEVICE_PATH, device_path, true); ssh_key enrolled = NULL; int rc = ssh_pki_generate_key(SSH_KEYTYPE_SK_ED25519, pki_ctx, &enrolled); /* produces sk-ed25519 key */ /* Save enrolled key using ssh_pki_export_privkey_file, retrieve attestation * buffer etc. */ /* Free context and key when done */ } @endcode After a successful enrollment, you can retrieve the attestation buffer (if provided by the authenticator) from the PKI context: @code ssh_buffer att_buf = NULL; rc = ssh_pki_ctx_get_sk_attestation_buffer(pki_ctx, &att_buf); if (rc == SSH_OK && att_buf != NULL) { /* att_buf now contains the serialized attestation * ("ssh-sk-attest-v01"). You can inspect, save, or * parse the buffer as needed */ ssh_buffer_free(att_buf); } @endcode Notes: - The attestation buffer is only populated if the enrollment operation succeeds and the authenticator provides attestation data. - `ssh_pki_ctx_get_sk_attestation_buffer()` returns a copy of the attestation buffer; the caller must free it with `ssh_buffer_free()`. @subsection fido2_signing Authenticating with a Stored Security Key Public Key To authenticate using a security key, the application typically loads the previously enrolled sk-* private key, establishes an SSH connection, and calls `ssh_userauth_publickey()`. libssh automatically recognizes security key types and transparently handles the required hardware-backed authentication steps such as prompting for a touch or PIN using the configured security key callbacks. Example: @code #include #include int auth_with_sk_file(const char *host, const char *user, const char *privkey_path) { ssh_session session = NULL; ssh_key privkey = NULL; int rc = SSH_ERROR; session = ssh_new(); ssh_options_set(session, SSH_OPTIONS_HOST, host); ssh_options_set(session, SSH_OPTIONS_USER, user); ssh_connect(session); ssh_pki_import_privkey_file(privkey_path, NULL, NULL, NULL, &privkey); ssh_pki_ctx pki_ctx = ssh_pki_ctx_new(); /* Optionally set PIN callback, device path, etc. */ /* ssh_pki_ctx_set_sk_pin_callback(pki_ctx, pin_cb, NULL); */ ssh_options_set(session, SSH_OPTIONS_PKI_CONTEXT, pki_ctx); rc = ssh_userauth_publickey(session, user, privkey); if (rc == SSH_AUTH_SUCCESS) { printf("Authenticated with security key.\n"); rc = SSH_OK; } else { fprintf(stderr, "Authentication failed rc=%d err=%s\n", rc, ssh_get_error(session)); rc = SSH_ERROR; } /* Free resources */ } @endcode @subsection fido2_resident Resident Key Enumeration Resident keys stored on the device can be discovered and loaded with ssh_sk_resident_keys_load() which takes a PKI context (configured with a PIN callback) and returns each key as an ssh_key and the number of keys loaded. Example: @code #include #include #include static int pin_cb(const char *prompt, char *buf, size_t len, int echo, int verify, void *userdata) { (void)prompt; (void)echo; (void)verify; (void)userdata; const char *pin = "4242"; size_t l = strlen(pin); if (l + 1 > len) { return SSH_ERROR; } memcpy(buf, pin, l + 1); return SSH_OK; } int auth_with_resident(const char *host, const char *user, const char *application, const char *user_id) { ssh_pki_ctx pki_ctx = NULL; size_t num_found = 0; ssh_key *keys = NULL; ssh_key final_key = NULL; int rc = SSH_ERROR; ssh_string cur_application = NULL; ssh_string cur_user_id = NULL; ssh_string expected_application = NULL; ssh_string expected_user_id = NULL; pki_ctx = ssh_pki_ctx_new(); ssh_pki_ctx_set_sk_pin_callback(pki_ctx, pin_cb, NULL); expected_application = ssh_string_from_char(application); expected_user_id = ssh_string_from_char(user_id); rc = ssh_sk_resident_keys_load(pki_ctx, &keys, &num_found); for (size_t i = 0; i < num_found; i++) { cur_application = ssh_key_get_sk_application(keys[i]); cur_user_id = ssh_key_get_sk_user_id(keys[i]); if (ssh_string_cmp(cur_application, expected_application) == 0 && ssh_string_cmp(cur_user_id, expected_user_id) == 0) { SSH_STRING_FREE(cur_application); SSH_STRING_FREE(cur_user_id); final_key = keys[i]; break; } SSH_STRING_FREE(cur_application); SSH_STRING_FREE(cur_user_id); } SSH_STRING_FREE(expected_application); SSH_STRING_FREE(expected_user_id); /* Continue with authentication using the ssh_key with * ssh_userauth_publickey as usual, and free resources when done. */ } @endcode @subsection fido2_sshsig Signing using the sshsig API Security keys can also be used for general-purpose signing of arbitrary data (without SSH authentication) using the existing `sshsig_sign()` and `sshsig_verify()` functions. These functions work seamlessly with security key types (`SSH_KEYTYPE_SK_ECDSA` and `SSH_KEYTYPE_SK_ED25519`) and will automatically invoke the configured security key callbacks to perform hardware-backed signing operations. @subsection fido2_custom_callbacks Implementing Custom Callback Implementations Users may need to implement custom callback implementations to support different transport protocols (e.g., NFC, Bluetooth) beyond the default USB-HID support. This section describes how to implement and integrate custom callback implementations. To implement custom callbacks, you must include the following headers: @code #include /* For ssh_sk_callbacks_struct */ #include /* For SK API constants and data structures */ @endcode The `libssh/sk_api.h` header provides the complete interface specification including request/response structures, flags, and version macros. @subsubsection fido2_custom_callbacks_version API Version Compatibility libssh validates callback implementations by checking the API version returned by the `api_version()` callback. To ensure compatibility, libssh compares the major version (upper 16 bits) of the returned value with `LIBSSH_SK_API_VERSION_MAJOR`. If they don't match, libssh will reject the callback implementation. This ensures that the callbacks' SK API matches the major version expected by libssh, while allowing minor version differences. @subsubsection fido2_custom_callbacks_implementation Implementation Example Here's a minimal example of defining and using custom callbacks: @code #include #include #include /* Your custom API version callback */ static uint32_t my_sk_api_version(void) { /* Match the major version, set your own minor version */ return SSH_SK_VERSION_MAJOR | 0x0001; } /* Your custom enroll callback */ static int my_sk_enroll(uint32_t alg, const uint8_t *challenge, size_t challenge_len, const char *application, uint8_t flags, const char *pin, struct sk_option **options, struct sk_enroll_response **enroll_response) { /* Parse options array to extract custom parameters */ if (options != NULL) { for (size_t i = 0; options[i] != NULL; i++) { if (strcmp(options[i]->name, "my_custom_option") == 0) { /* Use options[i]->value */ } } } /* Implement your enroll logic here */ /* ... */ return SSH_SK_ERR_GENERAL; /* Return appropriate error code */ } /* Implement other required callbacks: sign, load_resident_keys */ /* ... */ /* Define your callback structure */ static struct ssh_sk_callbacks_struct my_sk_callbacks = { .size = sizeof(struct ssh_sk_callbacks_struct), .api_version = my_sk_api_version, .enroll = my_sk_enroll, .sign = my_sk_sign, /* Your implementation */ .load_resident_keys = my_sk_load_resident_keys, /* Your implementation */ }; /* Usage example */ void use_custom_callbacks(void) { ssh_pki_ctx pki_ctx = ssh_pki_ctx_new(); /* Set your custom callbacks */ ssh_pki_ctx_options_set(pki_ctx, SSH_PKI_OPTION_SK_CALLBACKS, &my_sk_callbacks); /* Pass custom options to your callbacks */ ssh_pki_ctx_sk_callbacks_option_set(pki_ctx, "my_custom_option", "my_custom_value", false); /* Use the context for enrollment, signing, etc. */ } @endcode @subsubsection fido2_custom_callbacks_options Passing Custom Options The `ssh_pki_ctx_sk_callbacks_option_set()` function allows you to pass implementation-specific options as name/value string pairs: @code ssh_pki_ctx_sk_callbacks_option_set(pki_ctx, "option_name", "option_value", required); @endcode Parameters: - `option_name`: The name of the option (e.g., "device_path", "my_custom_param") - `option_value`: The string value for this option - `required`: If true, this option must be processed by the callback implementation and cannot be ignored. If false, the option is advisory and can be skipped if the callback implementation does not support it. These options are passed to your callbacks in the `struct sk_option **options` parameter as a NULL-terminated array. Each `sk_option` has the following fields: - `name`: The option name (char *) - `value`: The option value (char *) - `required`: Whether the option must be processed (uint8_t, non-zero = required) @subsubsection fido2_custom_callbacks_openssh OpenSSH Middleware Compatibility Since libssh uses the same SK API as OpenSSH, middleware implementations developed for OpenSSH can be adapted with minimal changes. To adapt an OpenSSH middleware for libssh, create a wrapper that populates `ssh_sk_callbacks_struct` with pointers to the middleware's functions. @subsection fido2_testing Testing and Environment Variables Unit tests covering USB-HID enroll/sign/load_resident_keys operations can be found in the `tests/unittests/torture_sk_usbhid.c` file. To run these tests you must have libfido2 installed and the WITH_FIDO2=ON build option set. Additionally, you must ensure the following: - An actual FIDO2 device must be connected to the test machine. - The TORTURE_SK_USBHID environment variable must be set. - The environment variable TORTURE_SK_PIN= must be set. If these are not set, the tests are skipped. The higher level PKI integration tests can be found in `tests/unittests/torture_pki_sk.c` and the tests related to the sshsig API can be found in `tests/unittests/torture_pki_sshsig.c`. These use the callback implementation provided by OpenSSH's sk-dummy.so, which simulates an authenticator without requiring any hardware. Hence, these tests can be run in the CI environment. However, these tests can also be configured to use the default USB-HID callbacks by setting the same environment variables as described above. The following devices were tested during development: - Yubico Security Key NFC - USB-A */