Files
libssh/src/sk_usbhid.c
Praneeth Sarode 50ee6411f2 fido2: implement the default sk_callbacks for FIDO2/U2F keys using the usb-hid protocol
Signed-off-by: Praneeth Sarode <praneethsarode@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Eshan Kelkar <eshankelkar@galorithm.com>
2025-11-09 05:52:45 +05:30

2240 lines
69 KiB
C

/*
* This file is part of the SSH Library
*
* Copyright (c) 2025 Praneeth Sarode <praneethsarode@gmail.com>
*
* The SSH Library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 2.1 of the License.
*
* The SSH Library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the SSH Library; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
* MA 02111-1307, USA.
*/
#include "config.h"
#include "libssh/callbacks.h"
#include "libssh/misc.h"
#include "libssh/pki.h"
#include "libssh/sk_api.h"
#include "libssh/sk_common.h"
#include "libssh/sk_usbhid.h"
#include <fido.h>
#include <fido/credman.h>
#include <fido/err.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#endif
#define SK_USBHID_API_VERSION 0x000a0000
#define ECDSA_P256_PUBKEY_LEN 64
/* Maximum number of FIDO2/U2F devices that can be connected */
#define MAX_FIDO_DEVICES 8
/* Timeout for touch detection on single FIDO2/U2F device during each polling */
#define FIDO_POLL_MS 50
/* Sleep between each consecutive polling */
#define POLL_SLEEP_NS 200000000
/* The entire timeout for user to touch any of the connected devices */
#define SELECT_MS 15000
/* DER encoding constants */
#define DER_SEQUENCE_TAG 0x30
#define DER_INTEGER_TAG 0x02
#define DER_MAX_LEN_BYTES 2
struct sk_device {
char *path;
fido_dev_t *fido_device;
};
/**
* libfido2 log handler that prints libfido2 debug messages.
*
* @param msg The log message from libfido2
*/
static void fido_log_handler(const char *msg)
{
if (msg == NULL) {
return;
}
SSH_LOG(SSH_LOG_TRACE, "libfido2: %s", msg);
}
/**
* Initialize libfido2 with appropriate logging settings based on
* current libssh log level.
*/
static void sk_fido_init(void)
{
int fido_flags = 0;
int log_level = ssh_get_log_level();
/* Enable libfido2 debug output if libssh is at TRACE level */
if (log_level == SSH_LOG_TRACE) {
fido_flags |= FIDO_DEBUG;
fido_set_log_handler(fido_log_handler);
}
fido_init(fido_flags);
}
/**
* Convert a libfido2 error code to a libssh security key error code.
*
* @param fido_err The FIDO error code to convert
*
* @return The corresponding SSH_SK_ERR_* error code
*/
static int fido_err_to_ssh_sk_err(int fido_err)
{
switch (fido_err) {
case FIDO_ERR_UNSUPPORTED_OPTION:
case FIDO_ERR_UNSUPPORTED_ALGORITHM:
case FIDO_ERR_UNSUPPORTED_EXTENSION:
return SSH_SK_ERR_UNSUPPORTED;
case FIDO_ERR_PIN_REQUIRED:
case FIDO_ERR_PIN_INVALID:
return SSH_SK_ERR_PIN_REQUIRED;
default:
return SSH_SK_ERR_GENERAL;
}
}
static void sk_device_close(struct sk_device *device)
{
int rc;
if (device == NULL) {
return;
}
if (device->fido_device != NULL) {
rc = fido_dev_cancel(device->fido_device);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to cancel device operations: %s",
fido_strerr(rc));
}
rc = fido_dev_close(device->fido_device);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to close device: %s",
fido_strerr(rc));
}
fido_dev_free(&device->fido_device);
}
SAFE_FREE(device->path);
SAFE_FREE(device);
}
static struct sk_device *sk_device_open(const char *device_path)
{
int rc;
struct sk_device *device = NULL;
if (device_path == NULL) {
SSH_LOG(SSH_LOG_WARN, "Device path cannot be NULL");
goto error;
}
device = calloc(1, sizeof(struct sk_device));
if (device == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for sk_device");
goto error;
}
device->fido_device = fido_dev_new();
if (device->fido_device == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create new fido device instance");
goto error;
}
device->path = strdup(device_path);
if (device->path == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for device path");
goto error;
}
rc = fido_dev_open(device->fido_device, device->path);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to open FIDO2/U2F device at %s: %s",
device->path,
fido_strerr(rc));
goto error;
}
return device;
error:
sk_device_close(device);
return NULL;
}
static void sk_device_close_list(struct sk_device **devices, size_t num_devices)
{
size_t i;
if (devices == NULL) {
return;
}
for (i = 0; i < num_devices; i++) {
sk_device_close(devices[i]);
}
SAFE_FREE(devices);
}
static struct sk_device **
sk_device_open_list(const fido_dev_info_t *device_list,
size_t num_devices,
size_t *num_opened)
{
size_t i;
const char *device_path = NULL;
struct sk_device **devices = NULL;
const fido_dev_info_t *device_info = NULL;
*num_opened = 0;
devices = calloc(num_devices, sizeof(struct sk_device *));
if (devices == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for device list");
return NULL;
}
for (i = 0; i < num_devices; i++) {
device_info = fido_dev_info_ptr(device_list, i);
if (device_info == NULL) {
SSH_LOG(SSH_LOG_INFO, "Failed to get device info for index %zu", i);
continue;
}
device_path = fido_dev_info_path(device_info);
devices[*num_opened] = sk_device_open(device_path);
if (devices[*num_opened] == NULL) {
SSH_LOG(SSH_LOG_INFO,
"Failed to open device %zu at %s",
*num_opened,
device_path);
} else {
(*num_opened)++;
}
}
if (*num_opened == 0) {
sk_device_close_list(devices, num_devices);
devices = NULL;
}
return devices;
}
/**
* Check if given device has the credentials corresponding to the given
* key_handle.
*
* @param device The security key device to check
* @param application The application identifier (relying party ID)
* @param key_handle The key handle to check for
* @param key_handle_len The length of the key handle in bytes
*
* @return FIDO_OK if resident key exists, FIDO_ERR_NO_CREDENTIALS if it
* doesn't, other FIDO_ERR_* codes on failure
*/
static int sk_device_check_key_handle(const struct sk_device *device,
const char *application,
const uint8_t *key_handle,
size_t key_handle_len)
{
int ret = FIDO_ERR_INTERNAL;
uint8_t dummy_data[32] = {0};
fido_assert_t *assert = NULL;
bool is_dev_fido2 = false;
/*
* We make use of the pre-flight checking as described in
* https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pre-flight
* to identify whether the device knows of the passed key_handle.
*/
assert = fido_assert_new();
if (assert == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create new FIDO assertion");
return FIDO_ERR_INTERNAL;
}
ret = fido_assert_set_clientdata(assert, dummy_data, sizeof(dummy_data));
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set client data for assertion: %s",
fido_strerr(ret));
goto out;
}
ret = fido_assert_set_rp(assert, application);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set Relying Party for assertion: %s",
fido_strerr(ret));
goto out;
}
ret = fido_assert_set_up(assert, FIDO_OPT_FALSE);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set user presence for assertion: %s",
fido_strerr(ret));
goto out;
}
/* Allow assertions only from this particular key_handle */
ret = fido_assert_allow_cred(assert, key_handle, key_handle_len);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to allow credential for assertion: %s",
fido_strerr(ret));
goto out;
}
is_dev_fido2 = fido_dev_is_fido2(device->fido_device);
ret = fido_dev_get_assert(device->fido_device, assert, NULL);
if (!is_dev_fido2 && ret == FIDO_ERR_USER_PRESENCE_REQUIRED) {
/* U2F devices might return this */
ret = FIDO_OK;
} else if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_INFO,
"Failed to get assertion from device: %s",
fido_strerr(ret));
}
out:
fido_assert_free(&assert);
return ret;
}
/**
* Check if given device has the resident key with given user_id and
* application.
*
* @param device The security key device to check
* @param application The application identifier (relying party ID)
* @param user_id The binary user ID to search for in resident keys
* @param user_id_len Length of binary user_id
* @param pin The PIN for the device (can be NULL if not required)
*
* @return FIDO_OK if resident key exists, FIDO_ERR_NO_CREDENTIALS if it
* doesn't, other FIDO_ERR_* codes on failure
*/
static int sk_device_check_resident_key(const struct sk_device *device,
const char *application,
const uint8_t *user_id,
size_t user_id_len,
const char *pin)
{
int rc, ret = FIDO_ERR_INTERNAL;
bool supports_uv = false;
size_t i, num_asserts = 0, len;
const uint8_t *ptr = NULL;
uint8_t dummy_data[32] = {0};
fido_opt_t user_verification = FIDO_OPT_OMIT;
fido_assert_t *assert = NULL;
/* If no user_id or zero length provided, nothing to compare */
if (user_id == NULL || user_id_len == 0) {
return FIDO_ERR_NO_CREDENTIALS;
}
assert = fido_assert_new();
if (assert == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create new FIDO assertion");
goto out;
}
ret = fido_assert_set_clientdata(assert, dummy_data, sizeof(dummy_data));
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set client data for assertion: %s",
fido_strerr(ret));
goto out;
}
ret = fido_assert_set_rp(assert, application);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set Relying Party for assertion: %s",
fido_strerr(ret));
goto out;
}
/* Check if device supports internal user verification methods such as
* biometric */
supports_uv = fido_dev_supports_uv(device->fido_device);
/*
* Determine user verification strategy for resident key enumeration:
* - If PIN is provided, rely on PIN-based authentication (UV = OMIT)
*
* - If no PIN is provided but device supports internal UV (biometric/etc),
* enable UV to ensure we can access all resident keys regardless of their
* credential protection while minimising user friction.
*
* - If no PIN is provided and device does not support internal UV, we will
* only be able to access resident keys without user-verification
* protection.
*
* Read about credential protection and resident keys:
* (https://developers.yubico.com/WebAuthn/WebAuthn_Developer_Guide/Resident_Keys.html)
*/
user_verification =
(pin == NULL && supports_uv) ? FIDO_OPT_TRUE : FIDO_OPT_OMIT;
ret = fido_assert_set_uv(assert, user_verification);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set user verification for assertion: %s",
fido_strerr(ret));
goto out;
}
ret = fido_dev_get_assert(device->fido_device, assert, pin);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to get assertion from device: %s",
fido_strerr(ret));
goto out;
}
ret = FIDO_ERR_NO_CREDENTIALS;
num_asserts = fido_assert_count(assert);
for (i = 0; i < num_asserts; i++) {
ptr = fido_assert_user_id_ptr(assert, i);
len = fido_assert_user_id_len(assert, i);
if (len != user_id_len) {
continue;
}
rc = memcmp(ptr, user_id, user_id_len);
if (rc == 0) {
SSH_LOG(SSH_LOG_INFO, "Resident key with given user ID exists");
ret = FIDO_OK;
break;
}
}
out:
fido_assert_free(&assert);
return ret;
}
/**
* Begin touch detection on all devices in the provided list.
*
* @param devices Array of device pointers
* @param num_devices Number of devices in the array
*
* @return SSH_OK if at least one device started touch detection successfully,
* SSH_ERROR if all devices failed
*/
static int sk_device_touch_begin(struct sk_device **devices, size_t num_devices)
{
int rc;
size_t i, num_success = 0;
for (i = 0; i < num_devices; i++) {
rc = fido_dev_get_touch_begin(devices[i]->fido_device);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_INFO,
"Failed to begin touch on device %s: %s",
devices[i]->path,
fido_strerr(rc));
} else {
num_success++;
}
}
return (num_success > 0) ? SSH_OK : SSH_ERROR;
}
/**
* Poll the touch status on all devices and return the index of the device
* on which touch was detected.
*
* @param devices Array of device pointers to poll for touch status
* @param num_devices Number of devices in the array
* @param touch_detected Pointer to store whether touch was detected (0 or 1)
* @param chosen_idx Pointer to store the index of the device that was touched
*
* @return SSH_OK on successful polling (regardless of whether touch was
* detected), SSH_ERROR if no devices left to poll.
*
* @warning Automatically closes the device if any error occurs
* while detecting if it was touched.
*/
static int sk_device_touch_poll(struct sk_device **devices,
size_t num_devices,
int *touch_detected,
size_t *chosen_idx)
{
int rc;
size_t i, n_failed = 0;
for (i = 0; i < num_devices; i++) {
if (devices[i] == NULL) {
continue;
}
SSH_LOG(SSH_LOG_DEBUG,
"Polling touch status on device %s",
devices[i]->path);
rc = fido_dev_get_touch_status(devices[i]->fido_device,
touch_detected,
FIDO_POLL_MS);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_INFO,
"Failed to get touch status on device %s: %s",
devices[i]->path,
fido_strerr(rc));
sk_device_close(devices[i]);
devices[i] = NULL;
n_failed++;
if (n_failed == num_devices) {
SSH_LOG(SSH_LOG_WARN, "No devices left to poll");
return SSH_ERROR;
}
} else if (*touch_detected) {
*chosen_idx = i;
return SSH_OK;
}
}
*touch_detected = 0;
return SSH_OK;
}
/**
* Select a device from the list of devices which has the given
* application and key handle.
*
* @param device_list Array of device information structures
* @param num_devices Number of devices in the device_list array
* @param application The application identifier (relying party ID)
* @param key_handle The key handle to look for
* @param key_handle_len The length of the key handle in bytes
*
* @return The selected device on success, NULL if no device found or on error.
*/
static struct sk_device *
sk_device_select_by_credential(const fido_dev_info_t *device_list,
size_t num_devices,
const char *application,
const uint8_t *key_handle,
size_t key_handle_len)
{
int rc;
size_t num_opened = 0, i;
struct sk_device **devices = NULL, *selected_device = NULL;
devices = sk_device_open_list(device_list, num_devices, &num_opened);
if (devices == NULL) {
SSH_LOG(SSH_LOG_WARN, "No FIDO2/U2F devices opened");
return NULL;
}
selected_device = NULL;
for (i = 0; i < num_opened; i++) {
rc = sk_device_check_key_handle(devices[i],
application,
key_handle,
key_handle_len);
if (rc == FIDO_OK) {
selected_device = devices[i];
devices[i] = NULL;
SSH_LOG(SSH_LOG_DEBUG,
"Selected device %s for key handle",
selected_device->path);
break;
}
}
sk_device_close_list(devices, num_opened);
return selected_device;
}
/**
* Select a device by touch, where the user touches the key they want to use.
* The function will block until a touch is detected or the timeout is reached.
*
* @param device_list Array of device information structures
* @param num_devices Number of devices in the device_list array
*
* @return The selected device on success, NULL if no device found or on error.
*/
static struct sk_device *
sk_device_select_by_touch(const fido_dev_info_t *device_list,
size_t num_devices)
{
int rc, touch = 0;
size_t num_opened = 0, chosen_idx;
struct sk_device **devices = NULL, *selected_device = NULL;
struct ssh_timestamp ts;
#ifndef _WIN32
struct timespec poll_sleep = {.tv_sec = 0, .tv_nsec = POLL_SLEEP_NS};
#endif
devices = sk_device_open_list(device_list, num_devices, &num_opened);
if (devices == NULL) {
SSH_LOG(SSH_LOG_WARN, "No FIDO2/U2F devices opened");
return NULL;
}
if (num_opened == 1) {
selected_device = devices[0];
devices[0] = NULL;
SSH_LOG(SSH_LOG_DEBUG,
"Only one device opened, automatically selected %s",
selected_device->path);
goto out;
}
SSH_LOG(SSH_LOG_DEBUG, "%zu FIDO2/U2F device(s) opened", num_opened);
rc = sk_device_touch_begin(devices, num_opened);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to begin touch on any device");
goto out;
}
ssh_timestamp_init(&ts);
do {
rc = sk_device_touch_poll(devices, num_opened, &touch, &chosen_idx);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to poll touch status");
goto out;
} else if (touch) {
selected_device = devices[chosen_idx];
devices[chosen_idx] = NULL;
goto out;
}
if (ssh_timeout_elapsed(&ts, SELECT_MS)) {
SSH_LOG(SSH_LOG_WARN, "Touch selection timed out");
break;
}
#ifdef _WIN32
/* Sleep expects milliseconds; convert nanoseconds (round down). */
Sleep((DWORD)(POLL_SLEEP_NS / 1000000));
#else
nanosleep(&poll_sleep, NULL);
#endif /* _WIN32 */
} while (true);
out:
sk_device_close_list(devices, num_opened);
return selected_device;
}
/**
* Probe for FIDO2/U2F devices and choose one based on the provided application
* and key handle. If application or key handle are NULL, the user will be
* prompted to touch the key they want to use.
*
* @param application The application identifier (relying party ID), can be NULL
* @param key_handle The key handle to look for, can be NULL
* @param key_handle_len The length of the key handle in bytes
* @param probe_resident Whether to probe for resident keys
*
* @return The selected device on success, NULL if no device found or on error.
*/
static struct sk_device *sk_device_probe(const char *application,
const uint8_t *key_handle,
size_t key_handle_len,
bool probe_resident)
{
int rc;
size_t num_devices = 0;
struct sk_device *device = NULL;
fido_dev_info_t *device_list = NULL;
#ifdef _WIN32
if (!probe_resident) {
device = sk_device_open("windows://hello");
if (device == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to open Windows Hello device");
return NULL;
}
SSH_LOG(SSH_LOG_DEBUG, "Using Windows Hello device");
return device;
}
#endif /* _WIN32 */
device_list = fido_dev_info_new(MAX_FIDO_DEVICES);
if (device_list == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create device info list");
return NULL;
}
rc = fido_dev_info_manifest(device_list, MAX_FIDO_DEVICES, &num_devices);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to get device info manifest: %s",
fido_strerr(rc));
goto out;
}
if (num_devices == 0) {
SSH_LOG(SSH_LOG_WARN, "No FIDO2/U2F devices found");
goto out;
}
SSH_LOG(SSH_LOG_DEBUG, "%zu FIDO2/U2F device(s) detected", num_devices);
/*
* If key_handle and application are specified, then we find the key which
* has the corresponding credentials, otherwise, we rely on the user to
* touch the key that they want to use.
*/
if (application != NULL && key_handle != NULL) {
SSH_LOG(SSH_LOG_DEBUG, "Selecting device by credential");
device = sk_device_select_by_credential(device_list,
num_devices,
application,
key_handle,
key_handle_len);
} else {
SSH_LOG(SSH_LOG_DEBUG, "Selecting device by touch");
device = sk_device_select_by_touch(device_list, num_devices);
}
out:
fido_dev_info_free(&device_list, MAX_FIDO_DEVICES);
return device;
}
/**
* Export an ECDSA public key from a FIDO2/U2F credential.
*
* The format returned by libfido2 is different from the expected SEC1 octet
* string representation, so this function performs the necessary conversion.
*
* @param credential The FIDO2/U2F credential containing the ECDSA public key
* @param response The enrollment response structure to fill with the public key
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int export_public_key_ecdsa(const fido_cred_t *credential,
struct sk_enroll_response *response)
{
size_t len;
const uint8_t *ptr = NULL;
response->public_key = NULL;
response->public_key_len = 0;
ptr = fido_cred_pubkey_ptr(credential);
if (ptr == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to get FIDO2/U2F credential public key");
return SSH_ERROR;
}
len = fido_cred_pubkey_len(credential);
if (len != ECDSA_P256_PUBKEY_LEN) {
SSH_LOG(SSH_LOG_WARN,
"Bad FIDO2/U2F credential public key length %zu"
"(expected ecdsa public key length %d)",
len,
ECDSA_P256_PUBKEY_LEN);
return SSH_ERROR;
}
/*
* Convert from libfido2's raw coordinate format to SEC1 octet string
* format.
*
* libfido2 returns: x_coordinate (32 bytes) + y_coordinate (32
* bytes)
*
* SEC1 format expects: 0x04 + x_coordinate (32 bytes) + y_coordinate (32
* bytes)
*/
response->public_key_len = 1 + ECDSA_P256_PUBKEY_LEN;
response->public_key = calloc(response->public_key_len, sizeof(uint8_t));
if (response->public_key == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for public key");
return SSH_ERROR;
}
/* SEC1 uncompressed point format: 0x04 prefix + raw coordinates */
response->public_key[0] = 0x04;
memcpy(response->public_key + 1, ptr, ECDSA_P256_PUBKEY_LEN);
return SSH_OK;
}
/**
* Export an Ed25519 public key from a FIDO2 credential.
*
* @param credential The FIDO2 credential containing the Ed25519 public key
* @param response The enrollment response structure to fill with the public key
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int export_public_key_ed25519(const fido_cred_t *credential,
struct sk_enroll_response *response)
{
size_t len;
const uint8_t *ptr = NULL;
response->public_key = NULL;
response->public_key_len = 0;
ptr = fido_cred_pubkey_ptr(credential);
if (ptr == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to get FIDO2 credential public key");
return SSH_ERROR;
}
len = fido_cred_pubkey_len(credential);
if (len != ED25519_KEY_LEN) {
SSH_LOG(SSH_LOG_WARN,
"Bad FIDO2 credential public key length %zu"
" (expected ed25519 public key length %d)",
len,
ED25519_KEY_LEN);
return SSH_ERROR;
}
response->public_key_len = len;
response->public_key = calloc(response->public_key_len, sizeof(uint8_t));
if (response->public_key == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for public key");
return SSH_ERROR;
}
memcpy(response->public_key, ptr, len);
return SSH_OK;
}
/**
* Export a public key from a FIDO2/U2F credential based on the specified
* algorithm.
*
* @param algorithm The key algorithm (SSH_SK_ECDSA or SSH_SK_ED25519)
* @param credential The FIDO2/U2F credential containing the public key
* @param response The enrollment response structure to fill with the public key
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int export_public_key(int algorithm,
const fido_cred_t *credential,
struct sk_enroll_response *response)
{
int ret;
switch (algorithm) {
case SSH_SK_ECDSA:
ret = export_public_key_ecdsa(credential, response);
break;
case SSH_SK_ED25519:
ret = export_public_key_ed25519(credential, response);
break;
default:
SSH_LOG(SSH_LOG_WARN, "Unsupported algorithm: %d", algorithm);
ret = SSH_ERROR;
}
return ret;
}
/**
* Parse DER length encoding.
*
* @param p Pointer to the current position in DER data (updated on success)
* @param end Pointer to the end of DER data
* @param length Pointer to store the parsed length
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int
parse_der_length(const uint8_t **p, const uint8_t *end, size_t *length)
{
int len_bytes = 0;
if (*p >= end) {
SSH_LOG(SSH_LOG_WARN, "Insufficient data for DER length");
return SSH_ERROR;
}
/* If the MSB is set, it indicates a long form length
* where the lower 7 bits indicate the number of
* subsequent bytes that represent the length.
*
* If the MSB is not set, it indicates a short form
* length where the length is directly represented
* in the the byte itself.
*/
if (**p & 0x80) {
/* Long form length */
len_bytes = **p & 0x7f;
(*p)++;
if (len_bytes > DER_MAX_LEN_BYTES) {
SSH_LOG(
SSH_LOG_WARN,
"Invalid DER length bytes: %d. Should not be greater than %d",
len_bytes,
DER_MAX_LEN_BYTES);
return SSH_ERROR;
}
if (*p + len_bytes > end) {
SSH_LOG(SSH_LOG_WARN, "Insufficient data for length bytes");
return SSH_ERROR;
}
*length = 0;
while (len_bytes--) {
*length = (*length << 8) | **p;
(*p)++;
}
} else {
/* Short form length */
*length = **p;
(*p)++;
}
return SSH_OK;
}
/**
* Parse a single DER-encoded INTEGER.
*
* @param p Pointer to the current position in DER data (updated on success)
* @param end Pointer to the end of DER data
* @param component_name Name of the component for error messages
* @param int_ptr Pointer to store the integer data
* @param int_len Pointer to store the integer length
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int parse_der_integer(const uint8_t **p,
const uint8_t *end,
const char *component_name,
uint8_t **int_ptr,
size_t *int_len)
{
size_t length = 0;
const uint8_t *data_ptr = NULL;
int rc = SSH_ERROR;
if (int_ptr == NULL || int_len == NULL) {
SSH_LOG(SSH_LOG_WARN,
"Invalid arguments provided for %s component",
component_name);
return SSH_ERROR;
}
*int_ptr = NULL;
*int_len = 0;
/* Check for INTEGER tag */
if (*p >= end || **p != DER_INTEGER_TAG) {
SSH_LOG(SSH_LOG_WARN,
"Expected INTEGER tag for %s component",
component_name);
return SSH_ERROR;
}
(*p)++;
/* Parse length */
rc = parse_der_length(p, end, &length);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Invalid %s component length", component_name);
return SSH_ERROR;
}
/* Verify we have enough data */
if (*p + length > end) {
SSH_LOG(SSH_LOG_WARN,
"%s component extends beyond signature",
component_name);
return SSH_ERROR;
}
/* Skip leading zero if present (The leading zero is placed when the MSB of
* the actual number is 1, so that it is not confused as a negative number
* in 2's complement) */
data_ptr = *p;
if (length > 0 && **p == 0x00) {
data_ptr++;
length--;
}
/* Allocate memory for the integer data */
if (length > 0) {
*int_ptr = calloc(length, sizeof(uint8_t));
if (*int_ptr == NULL) {
SSH_LOG(SSH_LOG_WARN,
"Failed to allocate memory for %s component",
component_name);
return SSH_ERROR;
}
memcpy(*int_ptr, data_ptr, length);
}
*int_len = length;
*p = data_ptr + length;
return SSH_OK;
}
/**
* Parse DER-encoded ECDSA signature and extract r and s components.
*
* @param der_sig DER-encoded signature data
* @param der_len Length of DER data
* @param r_ptr Pointer to store r component
* @param r_len Pointer to store r length
* @param s_ptr Pointer to store s component
* @param s_len Pointer to store s length
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int parse_ecdsa_der_signature(const uint8_t *der_sig,
size_t der_len,
uint8_t **r_ptr,
size_t *r_len,
uint8_t **s_ptr,
size_t *s_len)
{
const uint8_t *p = der_sig;
const uint8_t *end = der_sig + der_len;
size_t seq_len = 0;
int rc;
if (r_ptr == NULL || r_len == NULL || s_ptr == NULL || s_len == NULL ||
der_sig == NULL) {
SSH_LOG(SSH_LOG_WARN, "Invalid arguments provided");
return SSH_ERROR;
}
*r_ptr = NULL;
*r_len = 0;
*s_ptr = NULL;
*s_len = 0;
/* Parse SEQUENCE tag */
if (p >= end || *(p++) != DER_SEQUENCE_TAG) {
SSH_LOG(SSH_LOG_WARN, "Expected SEQUENCE tag in DER signature");
return SSH_ERROR;
}
/* Parse sequence length */
rc = parse_der_length(&p, end, &seq_len);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Invalid DER sequence length");
return SSH_ERROR;
}
/* Verify sequence length matches remaining data */
if (p + seq_len != end) {
SSH_LOG(SSH_LOG_WARN, "DER sequence length mismatch");
return SSH_ERROR;
}
/* Parse first INTEGER (r component) */
rc = parse_der_integer(&p, end, "r", r_ptr, r_len);
if (rc != SSH_OK) {
goto error;
}
/* Parse second INTEGER (s component) */
rc = parse_der_integer(&p, end, "s", s_ptr, s_len);
if (rc != SSH_OK) {
goto error;
}
/* Verify we consumed all data */
if (p != end) {
SSH_LOG(SSH_LOG_WARN, "Unexpected data after s component");
goto error;
}
return SSH_OK;
error:
SAFE_FREE(*r_ptr);
*r_len = 0;
SAFE_FREE(*s_ptr);
*s_len = 0;
return SSH_ERROR;
}
/**
* Export an ECDSA signature from a FIDO2/U2F assertion.
*
* @param assert The FIDO2/U2F assertion containing the signature
* @param response The sign response structure to fill with the signature
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int export_signature_ecdsa(fido_assert_t *assert,
struct sk_sign_response *response)
{
size_t len = 0;
const uint8_t *ptr = NULL;
int rc;
len = fido_assert_sig_len(assert, 0);
ptr = fido_assert_sig_ptr(assert, 0);
if (ptr == NULL || len == 0) {
SSH_LOG(SSH_LOG_WARN,
"Invalid signature data from FIDO2/U2F assertion");
return SSH_ERROR;
}
/* This will allocate and populate response->sig_r/_s (+ lengths) */
rc = parse_ecdsa_der_signature(ptr,
len,
&response->sig_r,
&response->sig_r_len,
&response->sig_s,
&response->sig_s_len);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to parse DER ECDSA signature");
return SSH_ERROR;
}
return SSH_OK;
}
/**
* Export an Ed25519 signature from a FIDO2 assertion.
*
* @param assert The FIDO2 assertion containing the signature
* @param response The sign response structure to fill with the signature
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int export_signature_ed25519(fido_assert_t *assert,
struct sk_sign_response *response)
{
const uint8_t *ptr = NULL;
size_t len;
ptr = fido_assert_sig_ptr(assert, 0);
len = fido_assert_sig_len(assert, 0);
if (len != ED25519_SIG_LEN) {
SSH_LOG(SSH_LOG_WARN, "Bad ED25519 signature length %zu", len);
return SSH_ERROR;
}
response->sig_r_len = len;
response->sig_r = calloc(response->sig_r_len, sizeof(uint8_t));
if (response->sig_r == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for signature");
return SSH_ERROR;
}
memcpy(response->sig_r, ptr, len);
response->sig_s = NULL;
response->sig_s_len = 0;
return SSH_OK;
}
/**
* Export a signature from a FIDO2/U2F assertion based on the specified
* algorithm.
*
* @param algorithm The signature algorithm (SSH_SK_ECDSA or SSH_SK_ED25519)
* @param assert The FIDO2/U2F assertion containing the signature
* @param response The sign response structure to fill with the signature
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int export_signature(int algorithm,
fido_assert_t *assert,
struct sk_sign_response *response)
{
int ret;
switch (algorithm) {
case SSH_SK_ECDSA:
ret = export_signature_ecdsa(assert, response);
break;
case SSH_SK_ED25519:
ret = export_signature_ed25519(assert, response);
break;
default:
SSH_LOG(SSH_LOG_WARN, "Unsupported algorithm: %d", algorithm);
ret = SSH_ERROR;
}
return ret;
}
static uint32_t ssh_sk_usbhid_api_version(void)
{
return SK_USBHID_API_VERSION;
}
/**
* Create and configure a new FIDO2/U2F credential for enrollment.
*
* @param device The FIDO2/U2F device to use
* @param alg The algorithm to use (SSH_SK_ECDSA or SSH_SK_ED25519)
* @param challenge The challenge data
* @param challenge_len The length of the challenge data
* @param application The application identifier (relying party ID)
* @param flags The enrollment flags
* @param pin The PIN for the device (can be NULL)
* @param user_id The binary user ID buffer (can be NULL)
* @param user_id_len Length of user_id buffer in bytes (0 if none)
* @param credential_ptr Pointer to store the created credential
*
* @return FIDO_OK on success, FIDO_ERR_* codes on failure
*/
static int create_new_fido_credential(struct sk_device *device,
uint32_t alg,
const uint8_t *challenge,
size_t challenge_len,
const char *application,
uint8_t flags,
const char *pin,
const uint8_t *user_id,
size_t user_id_len,
fido_cred_t **credential_ptr)
{
int ret = FIDO_ERR_INTERNAL;
int cose_algorithm, cred_protection;
bool cred_prot_support = false;
fido_opt_t set_resident_key = FIDO_OPT_OMIT;
fido_cred_t *credential = NULL;
/* Set the COSE algorithm based on the requested algorithm */
switch (alg) {
case SSH_SK_ECDSA:
cose_algorithm = COSE_ES256;
break;
case SSH_SK_ED25519:
cose_algorithm = COSE_EDDSA;
break;
default:
SSH_LOG(SSH_LOG_WARN, "Unsupported algorithm: %u", alg);
return FIDO_ERR_UNSUPPORTED_ALGORITHM;
}
credential = fido_cred_new();
if (credential == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create new FIDO2/U2F credential");
goto error;
}
ret = fido_cred_set_type(credential, cose_algorithm);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set credential type: %s",
fido_strerr(ret));
goto error;
}
ret = fido_cred_set_clientdata(credential, challenge, challenge_len);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set client data: %s",
fido_strerr(ret));
goto error;
}
if (flags & SSH_SK_RESIDENT_KEY) {
set_resident_key = FIDO_OPT_TRUE;
}
ret = fido_cred_set_rk(credential, set_resident_key);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set resident key option: %s",
fido_strerr(ret));
goto error;
}
/* TODO: Add an additional option to set display_name, icon ..etc */
ret =
fido_cred_set_user(credential, user_id, user_id_len, NULL, NULL, NULL);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set user information: %s",
fido_strerr(ret));
goto error;
}
ret = fido_cred_set_rp(credential, application, NULL);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set Relying Party: %s",
fido_strerr(ret));
goto error;
}
if (flags & (SSH_SK_USER_VERIFICATION_REQD | SSH_SK_RESIDENT_KEY)) {
cred_prot_support = fido_dev_supports_cred_prot(device->fido_device);
if (!cred_prot_support) {
SSH_LOG(SSH_LOG_WARN,
"Device does not support credential protection");
ret = FIDO_ERR_UNSUPPORTED_EXTENSION;
goto error;
}
if (flags & SSH_SK_USER_VERIFICATION_REQD) {
cred_protection = FIDO_CRED_PROT_UV_REQUIRED;
} else {
cred_protection = FIDO_CRED_PROT_UV_OPTIONAL_WITH_ID;
}
ret = fido_cred_set_prot(credential, cred_protection);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set credential protection: %s",
fido_strerr(ret));
goto error;
}
}
ret = fido_dev_make_cred(device->fido_device, credential, pin);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to make credential: %s",
fido_strerr(ret));
goto error;
}
*credential_ptr = credential;
return FIDO_OK;
error:
fido_cred_free(&credential);
return ret;
}
/**
* Construct an enrollment response from a FIDO2/U2F credential.
* This function extracts and copies all necessary data from the fido_cred_t
* into the response structure.
*
* @param alg The algorithm used (SSH_SK_ECDSA or SSH_SK_ED25519)
* @param credential The FIDO2/U2F credential containing the enrollment data
* @param flags The enrollment flags
* @param response_ptr Pointer to store the constructed enrollment response
*
* @return SSH_OK on success, SSH_ERROR on failure
*/
static int
fido_cred_export_sk_enroll_response(uint32_t alg,
const fido_cred_t *credential,
uint8_t flags,
struct sk_enroll_response **response_ptr)
{
const uint8_t *ptr = NULL;
const char *fmt = NULL;
struct sk_enroll_response *response = NULL;
int rc;
response = calloc(1, sizeof(struct sk_enroll_response));
if (response == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for enroll response");
return SSH_ERROR;
}
response->flags = flags;
/* Export public key */
rc = export_public_key(alg, credential, response);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to export public key from credential");
goto error;
}
/* Export the key handle */
ptr = fido_cred_id_ptr(credential);
if (ptr == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to get key handle");
goto error;
}
response->key_handle_len = fido_cred_id_len(credential);
response->key_handle = calloc(response->key_handle_len, sizeof(uint8_t));
if (response->key_handle == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for key handle");
goto error;
}
memcpy(response->key_handle, ptr, response->key_handle_len);
/* Export challenge signature */
fmt = fido_cred_fmt(credential);
if (fmt == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to get attestation format");
goto error;
}
ptr = fido_cred_sig_ptr(credential);
if (ptr != NULL) {
response->signature_len = fido_cred_sig_len(credential);
response->signature = calloc(response->signature_len, sizeof(uint8_t));
if (response->signature == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for signature");
goto error;
}
memcpy(response->signature, ptr, response->signature_len);
} else if (strcmp(fmt, "none") == 0) {
/* No signature for "none" attestation format */
response->signature = NULL;
response->signature_len = 0;
} else {
SSH_LOG(SSH_LOG_WARN, "Failed to get signature");
goto error;
}
/* Export attestation information if available */
ptr = fido_cred_x5c_ptr(credential);
if (ptr != NULL) {
response->attestation_cert_len = fido_cred_x5c_len(credential);
response->attestation_cert =
calloc(response->attestation_cert_len, sizeof(uint8_t));
if (response->attestation_cert == NULL) {
SSH_LOG(SSH_LOG_WARN,
"Failed to allocate memory for attestation cert");
goto error;
}
memcpy(response->attestation_cert, ptr, response->attestation_cert_len);
}
/* Export authdata */
ptr = fido_cred_authdata_ptr(credential);
if (ptr == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to get authdata");
goto error;
}
response->authdata_len = fido_cred_authdata_len(credential);
response->authdata = calloc(response->authdata_len, sizeof(uint8_t));
if (response->authdata == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for authdata");
goto error;
}
memcpy(response->authdata, ptr, response->authdata_len);
*response_ptr = response;
response = NULL;
return SSH_OK;
error:
sk_enroll_response_free(response);
return SSH_ERROR;
}
static int ssh_sk_usbhid_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)
{
int rc, ret = SSH_SK_ERR_GENERAL;
const uint8_t *ptr = NULL;
const char *attestation_format = NULL;
const char *supported_options[] = {SSH_SK_OPTION_NAME_DEVICE_PATH,
SSH_SK_OPTION_NAME_USER_ID,
NULL};
char **option_values = NULL;
const char *device_path = NULL;
uint8_t user_id[SK_MAX_USER_ID_LEN] = {0};
size_t user_id_len = 0;
size_t j;
struct sk_device *device = NULL;
struct sk_enroll_response *response = NULL;
fido_cred_t *credential = NULL;
if (enroll_response == NULL) {
SSH_LOG(SSH_LOG_WARN, "enroll_response cannot be NULL");
goto out;
}
*enroll_response = NULL;
switch (alg) {
case SSH_SK_ECDSA:
case SSH_SK_ED25519:
break;
default:
SSH_LOG(SSH_LOG_WARN, "Unsupported algorithm: %u", alg);
ret = SSH_SK_ERR_UNSUPPORTED;
goto out;
}
if (challenge == NULL || challenge_len == 0) {
SSH_LOG(SSH_LOG_WARN, "challenge cannot be NULL or empty");
goto out;
}
if (application == NULL || application[0] == '\0') {
SSH_LOG(SSH_LOG_WARN, "application cannot be NULL or empty");
goto out;
}
/* Extract device path from options if provided */
rc = sk_options_validate_get((const struct sk_option **)options,
supported_options,
&option_values);
if (rc == SSH_OK && option_values != NULL) {
device_path = option_values[0]; /* device path is first in the array */
/*
* The user id is actually binary data according to the FIDO2
* specification, but since we want to remain compatible with OpenSSH
* sk-api, so we are restricted to only obtain the user_id as a char *
* from the sk_option struct.
*/
if (option_values[1] != NULL) {
user_id_len = strlen(option_values[1]);
if (user_id_len > SK_MAX_USER_ID_LEN) {
SSH_LOG(SSH_LOG_WARN,
"user_id length exceeds maximum of %d characters",
SK_MAX_USER_ID_LEN);
goto out;
}
memcpy((char *)user_id, option_values[1], user_id_len);
}
}
sk_fido_init();
if (device_path != NULL) {
device = sk_device_open(device_path);
} else {
device = sk_device_probe(NULL, NULL, 0, false);
}
if (device == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to open FIDO2/U2F device");
ret = SSH_SK_ERR_DEVICE_NOT_FOUND;
goto out;
}
SSH_LOG(SSH_LOG_DEBUG, "Using FIDO2/U2F device: %s", device->path);
/*
* Check whether a resident key with same user_id exists to avoid
* overwriting, unless operation is marked as forceful.
*/
if ((flags & SSH_SK_RESIDENT_KEY) != 0 &&
(flags & SSH_SK_FORCE_OPERATION) == 0) {
rc = sk_device_check_resident_key(device,
application,
(uint8_t *)user_id,
SK_MAX_USER_ID_LEN,
pin);
if (rc == FIDO_OK) {
SSH_LOG(SSH_LOG_INFO, "Resident key already exists");
ret = SSH_SK_ERR_CREDENTIAL_EXISTS;
goto out;
} else if (rc != FIDO_ERR_NO_CREDENTIALS) {
SSH_LOG(SSH_LOG_WARN,
"Failed to check for resident key: %s",
fido_strerr(rc));
ret = fido_err_to_ssh_sk_err(rc);
goto out;
}
}
/* Create and configure the FIDO2/U2F credential */
ret = create_new_fido_credential(device,
alg,
challenge,
challenge_len,
application,
flags,
pin,
(uint8_t *)user_id,
SK_MAX_USER_ID_LEN,
&credential);
if (ret != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to create new FIDO2/U2F credential");
ret = fido_err_to_ssh_sk_err(ret);
goto out;
}
ptr = fido_cred_x5c_ptr(credential);
attestation_format = fido_cred_fmt(credential);
if (attestation_format == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to get attestation format");
goto out;
}
rc = strcmp(attestation_format, "none");
/*
* If the x509 certificate is available, we can assume attestation type to
* be Basic Attestation and verify the attestation using the
* fido_cred_verify function, which checks the attestation signature using
* the attestation key mentioned in the x509 certificate.
*
* If the x509 certificate is not available, we check the attestation format
* to see whether it's type is Self attestation or None. If it
* is Self Attestation, we use fido_cred_verify_self to verify the
* credential, which checks the attestation signature against the public key
* of the credential itself.
*
* For more details, refer:
* https://developers.yubico.com/libfido2/Manuals/fido_cred_verify.html
* https://www.w3.org/TR/webauthn-2/#sctn-attestation
*/
if (ptr != NULL) {
SSH_LOG(SSH_LOG_DEBUG,
"Verifying attestation (type: Basic Attestation)");
rc = fido_cred_verify(credential);
} else if (rc != 0) {
SSH_LOG(SSH_LOG_DEBUG,
"Verifying attestation (type: Self attestation)");
rc = fido_cred_verify_self(credential);
} else {
SSH_LOG(SSH_LOG_DEBUG, "No attestation data available");
rc = FIDO_OK;
}
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to verify credential: %s",
fido_strerr(rc));
ret = fido_err_to_ssh_sk_err(rc);
goto out;
}
/* Construct the enrollment response from the credential data */
rc = fido_cred_export_sk_enroll_response(alg, credential, flags, &response);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to export public key from credential");
goto out;
}
*enroll_response = response;
response = NULL;
ret = SSH_OK;
out:
/* Clean up extracted values */
if (option_values != NULL) {
for (j = 0; supported_options[j] != NULL; j++) {
SAFE_FREE(option_values[j]);
}
SAFE_FREE(option_values);
}
sk_enroll_response_free(response);
sk_device_close(device);
fido_cred_free(&credential);
return ret;
}
static int ssh_sk_usbhid_sign(uint32_t alg,
const uint8_t *data,
size_t data_len,
const char *application,
const uint8_t *key_handle,
size_t key_handle_len,
uint8_t flags,
const char *pin,
struct sk_option **options,
struct sk_sign_response **sign_response)
{
int rc, ret = SSH_SK_ERR_GENERAL;
size_t i;
const char *supported_options[] = {SSH_SK_OPTION_NAME_DEVICE_PATH, NULL};
char **option_values = NULL;
const char *device_path = NULL;
struct sk_device *device = NULL;
struct sk_sign_response *response = NULL;
bool has_internal_uv = false, is_winhello = false;
fido_opt_t user_presence = FIDO_OPT_FALSE;
fido_assert_t *assert = NULL;
if (sign_response == NULL) {
SSH_LOG(SSH_LOG_WARN, "sign_response cannot be NULL");
goto out;
}
*sign_response = NULL;
switch (alg) {
case SSH_SK_ECDSA:
case SSH_SK_ED25519:
break;
default:
SSH_LOG(SSH_LOG_WARN, "Unsupported algorithm: %u", alg);
ret = SSH_SK_ERR_UNSUPPORTED;
goto out;
}
if (data == NULL || data_len == 0) {
SSH_LOG(SSH_LOG_WARN, "data to sign cannot be NULL or empty");
goto out;
}
if (application == NULL || application[0] == '\0') {
SSH_LOG(SSH_LOG_WARN, "application cannot be NULL or empty");
goto out;
}
if (key_handle == NULL || key_handle_len == 0) {
SSH_LOG(SSH_LOG_WARN, "key_handle cannot be NULL or empty");
goto out;
}
/* Extract device path from options if provided */
rc = sk_options_validate_get((const struct sk_option **)options,
supported_options,
&option_values);
if (rc == SSH_OK && option_values != NULL) {
device_path = option_values[0];
}
sk_fido_init();
/*
* We directly open the device if path is given.
*
* Otherwise, If PIN supplied or UV required, we avoid credential probing
* across multiple devices (which could trigger multiple UV prompts).
* Instead, we select by user touch first.
*
* For presence-only (UP) cases, credential-based probing is silent (see the
* comment in the sk_device_check_key_handle function about pre-flight
* checking), so we keep it to reduce touches.
*/
if (device_path != NULL) {
device = sk_device_open(device_path);
} else if (pin != NULL || (flags & SSH_SK_USER_VERIFICATION_REQD)) {
/* Touch based selection */
device = sk_device_probe(NULL, NULL, 0, false);
} else {
/* Credential based selection */
device =
sk_device_probe(application, key_handle, key_handle_len, false);
}
if (device == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to open FIDO2/U2F device");
ret = SSH_SK_ERR_DEVICE_NOT_FOUND;
goto out;
}
assert = fido_assert_new();
if (assert == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create new FIDO2/U2F assertion");
goto out;
}
rc = fido_assert_set_clientdata(assert, data, data_len);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to set client data: %s", fido_strerr(rc));
ret = fido_err_to_ssh_sk_err(rc);
goto out;
}
rc = fido_assert_set_rp(assert, application);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set relying party: %s",
fido_strerr(rc));
ret = fido_err_to_ssh_sk_err(rc);
goto out;
}
rc = fido_assert_allow_cred(assert, key_handle, key_handle_len);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to allow credential: %s",
fido_strerr(rc));
ret = fido_err_to_ssh_sk_err(rc);
goto out;
}
user_presence =
(flags & SSH_SK_USER_PRESENCE_REQD) ? FIDO_OPT_TRUE : FIDO_OPT_FALSE;
rc = fido_assert_set_up(assert, user_presence);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set user presence: %s",
fido_strerr(rc));
ret = fido_err_to_ssh_sk_err(rc);
goto out;
}
/*
* WinHello always requests the pin, unless we explicitly specify that we
* don't expect user verification.
*/
is_winhello = fido_dev_is_winhello(device->fido_device);
if (pin == NULL && is_winhello) {
rc = fido_assert_set_uv(assert, FIDO_OPT_FALSE);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set user verification: %s",
fido_strerr(rc));
ret = fido_err_to_ssh_sk_err(rc);
}
}
/*
* pin can be NULL if device internally has user verification capabilities
* such as biometric.
*/
if (pin == NULL && (flags & SSH_SK_USER_VERIFICATION_REQD)) {
has_internal_uv = fido_dev_has_uv(device->fido_device);
if (!has_internal_uv) {
SSH_LOG(SSH_LOG_WARN,
"User Verification requirement cannot be satisfied as "
"device lacks internal user verification and PIN is also "
"not provided");
ret = SSH_SK_ERR_PIN_REQUIRED;
goto out;
}
rc = fido_assert_set_uv(assert, FIDO_OPT_TRUE);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to set user verification: %s",
fido_strerr(rc));
ret = fido_err_to_ssh_sk_err(rc);
goto out;
}
}
rc = fido_dev_get_assert(device->fido_device, assert, pin);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to get assertion: %s", fido_strerr(rc));
ret = fido_err_to_ssh_sk_err(rc);
goto out;
}
response = calloc(1, sizeof(struct sk_sign_response));
if (response == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate sign response");
goto out;
}
response->flags = fido_assert_flags(assert, 0);
response->counter = fido_assert_sigcount(assert, 0);
rc = export_signature(alg, assert, response);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to export signature");
goto out;
}
*sign_response = response;
response = NULL;
ret = SSH_OK;
out:
if (option_values != NULL) {
for (i = 0; supported_options[i] != NULL; i++) {
SAFE_FREE(option_values[i]);
}
SAFE_FREE(option_values);
}
fido_assert_free(&assert);
sk_device_close(device);
sk_sign_response_free(response);
return ret;
}
/**
* Export a single resident credential into an allocated sk_resident_key.
*/
static int fido_cred_export_sk_resident_key(const fido_cred_t *credential,
const char *relying_party_id,
bool has_internal_uv,
struct sk_resident_key **out_key)
{
struct sk_resident_key *resident_key = NULL;
const uint8_t *ptr = NULL;
size_t len;
int algorithm;
int rc;
resident_key = calloc(1, sizeof(struct sk_resident_key));
if (resident_key == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for resident key");
goto error;
}
/* application */
resident_key->application = strdup(relying_party_id);
if (resident_key->application == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for application");
goto error;
}
/* key handle */
len = fido_cred_id_len(credential);
ptr = fido_cred_id_ptr(credential);
resident_key->key.key_handle_len = len;
resident_key->key.key_handle = calloc(len, sizeof(uint8_t));
if (resident_key->key.key_handle == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for key handle");
goto error;
}
memcpy(resident_key->key.key_handle, ptr, len);
/* user id */
len = fido_cred_user_id_len(credential);
ptr = fido_cred_user_id_ptr(credential);
resident_key->user_id_len = len;
resident_key->user_id = calloc(len, sizeof(uint8_t));
if (resident_key->user_id == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to allocate memory for user ID");
goto error;
}
memcpy(resident_key->user_id, ptr, len);
/* algorithm */
algorithm = fido_cred_type(credential);
switch (algorithm) {
case COSE_ES256:
resident_key->alg = SSH_SK_ECDSA;
break;
case COSE_EDDSA:
resident_key->alg = SSH_SK_ED25519;
break;
default:
SSH_LOG(SSH_LOG_WARN, "Unsupported algorithm %d", algorithm);
goto error;
}
rc = fido_cred_prot(credential);
if (rc == FIDO_CRED_PROT_UV_REQUIRED && !has_internal_uv) {
resident_key->flags |= SSH_SK_USER_VERIFICATION_REQD;
}
rc = export_public_key(resident_key->alg, credential, &resident_key->key);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to export public key for credential: %d",
rc);
goto error;
}
*out_key = resident_key;
return SSH_OK;
error:
SK_RESIDENT_KEY_FREE(resident_key);
return SSH_ERROR;
}
/**
* Load resident keys from a specific security key device.
*
* @param device The security key device to load keys from
* @param pin The PIN for the device (required for loading resident keys)
* @param resident_keys_ptr Pointer to store the array of loaded resident keys
* @param num_keys_found_ptr Pointer to store the number of keys found
*
* @return SSH_SK_ERR_* error code (SSH_OK on success)
*
* @note This function only considers resident keys that belong to
* relying parties starting with "ssh:".
*/
static int
sk_device_load_resident_keys(struct sk_device *device,
const char *pin,
struct sk_resident_key ***resident_keys_ptr,
size_t *num_keys_found_ptr)
{
int ret = SSH_SK_ERR_GENERAL, rc;
bool has_internal_uv = false;
size_t i, j, keys_count, num_relying_parties;
const char *relying_party_id = NULL;
struct sk_resident_key *cur_resident_key = NULL, **temp_ptr = NULL;
fido_credman_metadata_t *metadata = NULL;
fido_credman_rp_t *relying_parties = NULL;
fido_credman_rk_t *resident_keys = NULL;
const fido_cred_t *credential = NULL;
has_internal_uv = fido_dev_has_uv(device->fido_device);
metadata = fido_credman_metadata_new();
if (metadata == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create FIDO2/U2F metadata");
goto out;
}
rc = fido_credman_get_dev_metadata(device->fido_device, metadata, pin);
if (rc != FIDO_OK) {
if (rc == FIDO_ERR_INVALID_COMMAND) {
SSH_LOG(SSH_LOG_WARN, "Device does not support resident keys");
ret = SSH_SK_ERR_UNSUPPORTED;
} else {
SSH_LOG(SSH_LOG_WARN,
"Failed to get device metadata: %s for device at %s",
fido_strerr(rc),
device->path);
ret = fido_err_to_ssh_sk_err(rc);
}
goto out;
}
relying_parties = fido_credman_rp_new();
if (relying_parties == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create relying parties list");
goto out;
}
rc = fido_credman_get_dev_rp(device->fido_device, relying_parties, pin);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_WARN,
"Failed to get relying party: %s",
fido_strerr(rc));
ret = fido_err_to_ssh_sk_err(rc);
goto out;
}
num_relying_parties = fido_credman_rp_count(relying_parties);
SSH_LOG(SSH_LOG_DEBUG,
"Device %s has key(s) for %zu relying party(ies).",
device->path,
num_relying_parties);
/*
* Check all resident keys belonging to relying parties starting with "ssh:"
*/
for (i = 0; i < num_relying_parties; i++) {
relying_party_id = fido_credman_rp_id(relying_parties, i);
if (relying_party_id != NULL) {
rc = strncasecmp(relying_party_id, "ssh:", 4);
if (rc != 0) {
SSH_LOG(SSH_LOG_DEBUG,
"Skipping non-SSH relying party: %s",
relying_party_id);
continue;
}
} else {
SSH_LOG(SSH_LOG_DEBUG,
"Relying party ID is NULL, skipping RP %zu",
i);
continue;
}
fido_credman_rk_free(&resident_keys);
resident_keys = fido_credman_rk_new();
if (resident_keys == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to create FIDO2 resident key");
goto out;
}
rc = fido_credman_get_dev_rk(device->fido_device,
relying_party_id,
resident_keys,
pin);
if (rc != FIDO_OK) {
SSH_LOG(SSH_LOG_INFO,
"Failed to get resident key for RP %s: %s",
relying_party_id,
fido_strerr(rc));
ret = fido_err_to_ssh_sk_err(rc);
continue;
}
keys_count = fido_credman_rk_count(resident_keys);
if (keys_count == 0) {
SSH_LOG(SSH_LOG_INFO,
"No resident keys found for RP %s",
relying_party_id);
continue;
}
SSH_LOG(SSH_LOG_DEBUG,
"Found %zu resident key(s) for RP %s",
keys_count,
relying_party_id);
for (j = 0; j < keys_count; j++) {
credential = fido_credman_rk(resident_keys, j);
if (credential == NULL) {
SSH_LOG(SSH_LOG_INFO, "No resident key in slot %zu", j);
continue;
}
rc = fido_cred_export_sk_resident_key(credential,
relying_party_id,
has_internal_uv,
&cur_resident_key);
if (rc != SSH_OK) {
goto out;
}
temp_ptr = realloc(*resident_keys_ptr,
sizeof(struct sk_resident_key *) *
(*num_keys_found_ptr + 1));
if (temp_ptr == NULL) {
SSH_LOG(SSH_LOG_WARN,
"Failed to allocate memory for resident keys list");
goto out;
}
*resident_keys_ptr = temp_ptr;
(*resident_keys_ptr)[*num_keys_found_ptr] = cur_resident_key;
(*num_keys_found_ptr)++;
cur_resident_key = NULL;
}
}
ret = SSH_OK;
out:
SK_RESIDENT_KEY_FREE(cur_resident_key);
fido_credman_rp_free(&relying_parties);
fido_credman_rk_free(&resident_keys);
fido_credman_metadata_free(&metadata);
return ret;
}
static int
ssh_sk_usbhid_load_resident_keys(const char *pin,
struct sk_option **options,
struct sk_resident_key ***resident_keys_ptr,
size_t *num_keys_found_ptr)
{
int rc, ret = SSH_SK_ERR_GENERAL;
size_t i, j, keys_count = 0;
const char *supported_options[] = {SSH_SK_OPTION_NAME_DEVICE_PATH, NULL};
char **option_values = NULL;
const char *device_path = NULL;
struct sk_resident_key **resident_keys = NULL;
struct sk_device *device = NULL;
if (resident_keys_ptr == NULL || num_keys_found_ptr == NULL) {
SSH_LOG(SSH_LOG_WARN,
"resident_keys_ptr and num_keys_found_ptr cannot be NULL");
return SSH_SK_ERR_GENERAL;
}
/*
* To load device metadata and resident keys, a valid pin must be provided
* regardless of internal uv support.
*/
if (pin == NULL) {
SSH_LOG(SSH_LOG_WARN, "PIN cannot be NULL for loading resident keys");
return SSH_SK_ERR_PIN_REQUIRED;
}
*resident_keys_ptr = NULL;
*num_keys_found_ptr = 0;
sk_fido_init();
rc = sk_options_validate_get((const struct sk_option **)options,
supported_options,
&option_values);
if (rc == SSH_OK && option_values != NULL) {
device_path = option_values[0];
}
if (device_path != NULL) {
device = sk_device_open(device_path);
} else {
device = sk_device_probe(NULL, NULL, 0, 1);
}
if (device == NULL) {
SSH_LOG(SSH_LOG_WARN, "Failed to open FIDO2 device");
ret = SSH_SK_ERR_DEVICE_NOT_FOUND;
goto out;
}
rc = sk_device_load_resident_keys(device, pin, &resident_keys, &keys_count);
if (rc != SSH_OK) {
SSH_LOG(SSH_LOG_WARN, "Failed to load resident keys: %d", rc);
ret = rc;
goto out;
}
*resident_keys_ptr = resident_keys;
*num_keys_found_ptr = keys_count;
resident_keys = NULL;
keys_count = 0;
ret = SSH_OK;
out:
if (option_values != NULL) {
for (j = 0; supported_options[j] != NULL; j++) {
SAFE_FREE(option_values[j]);
}
SAFE_FREE(option_values);
}
sk_device_close(device);
for (i = 0; i < keys_count; i++) {
SK_RESIDENT_KEY_FREE(resident_keys[i]);
}
SAFE_FREE(resident_keys);
return ret;
}
static struct ssh_sk_callbacks_struct sk_usbhid_callbacks = {
.api_version = ssh_sk_usbhid_api_version,
.enroll = ssh_sk_usbhid_enroll,
.sign = ssh_sk_usbhid_sign,
.load_resident_keys = ssh_sk_usbhid_load_resident_keys,
};
const struct ssh_sk_callbacks_struct *ssh_sk_get_usbhid_callbacks(void)
{
ssh_callbacks_init(&sk_usbhid_callbacks);
return &sk_usbhid_callbacks;
}