1
0
mirror of https://git.libssh.org/projects/libssh.git synced 2025-12-06 13:20:57 +03:00
Files
libssh/doc/fido2.dox
Praneeth Sarode 47305a2f72 docs(fido2): add FIDO2/U2F security key support chapter to documentation
Signed-off-by: Praneeth Sarode <praneethsarode@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Eshan Kelkar <eshankelkar@galorithm.com>
2025-11-13 15:28:23 +05:30

602 lines
23 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
@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 users 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 <other options> ..
@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 <libssh/libssh.h>
#include <string.h>
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 <libssh/libssh.h>
#include <stdio.h>
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 <libssh/libssh.h>
#include <stdio.h>
#include <string.h>
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 <libssh/callbacks.h> /* For ssh_sk_callbacks_struct */
#include <libssh/sk_api.h> /* 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 <libssh/libssh.h>
#include <libssh/callbacks.h>
#include <libssh/sk_api.h>
/* 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=<device 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
*/